package com.eveningoutpost.dexdrip.Services; import android.annotation.TargetApi; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.IBinder; import android.os.ParcelUuid; import android.os.PowerManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.eveningoutpost.dexdrip.GcmActivity; import com.eveningoutpost.dexdrip.GlucoseMeter.CurrentTimeRx; import com.eveningoutpost.dexdrip.GlucoseMeter.GlucoseReadingRx; import com.eveningoutpost.dexdrip.GlucoseMeter.RecordsCmdTx; import com.eveningoutpost.dexdrip.GlucoseMeter.VerioHelper; import com.eveningoutpost.dexdrip.GlucoseMeter.caresens.ContextRx; import com.eveningoutpost.dexdrip.GlucoseMeter.caresens.TimeTx; import com.eveningoutpost.dexdrip.Home; import com.eveningoutpost.dexdrip.Models.BloodTest; import com.eveningoutpost.dexdrip.Models.Calibration; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.xdrip; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import static com.eveningoutpost.dexdrip.GlucoseMeter.VerioHelper.VERIO_F7A1_SERVICE; import static com.eveningoutpost.dexdrip.GlucoseMeter.VerioHelper.VERIO_F7A2_WRITE; import static com.eveningoutpost.dexdrip.GlucoseMeter.VerioHelper.VERIO_F7A3_NOTIFICATION; import static com.eveningoutpost.dexdrip.Models.CalibrationRequest.isSlopeFlatEnough; import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.unitized_string_with_units_static; /** * Created by jamorham on 09/12/2016. * based on code by LadyViktoria * <p> * Should support any bluetooth standard compliant meter * offering the Glucose Service, like the excellent * Contour Next One */ @TargetApi(Build.VERSION_CODES.KITKAT) public class BluetoothGlucoseMeter extends Service { public final static String ACTION_BLUETOOTH_GLUCOSE_METER_SERVICE_UPDATE = "com.eveningoutpost.dexdrip.BLUETOOTH_GLUCOSE_METER_SERVICE_UPDATE"; public final static String ACTION_BLUETOOTH_GLUCOSE_METER_NEW_SCAN_DEVICE = "com.eveningoutpost.dexdrip.BLUETOOTH_GLUCOSE_METER_NEW_SCAN_DEVICE"; public final static String BLUETOOTH_GLUCOSE_METER_TAG = "Bluetooth Glucose Meter"; private static final String GLUCOSE_READING_MARKER = "Glucose Reading From: "; private static final String TAG = BluetoothGlucoseMeter.class.getSimpleName(); private static final UUID GLUCOSE_SERVICE = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb"); private static final UUID CURRENT_TIME_SERVICE = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb"); private static final UUID DEVICE_INFO_SERVICE = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb"); private static final UUID CONTOUR_SERVICE = UUID.fromString("00000000-0002-11e2-9e96-0800200c9a66"); private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private static final UUID GLUCOSE_CHARACTERISTIC = UUID.fromString("00002a18-0000-1000-8000-00805f9b34fb"); private static final UUID CONTEXT_CHARACTERISTIC = UUID.fromString("00002a34-0000-1000-8000-00805f9b34fb"); private static final UUID RECORDS_CHARACTERISTIC = UUID.fromString("00002a52-0000-1000-8000-00805f9b34fb"); private static final UUID TIME_CHARACTERISTIC = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb"); private static final UUID DATE_TIME_CHARACTERISTIC = UUID.fromString("00002a08-0000-1000-8000-00805f9b34fb"); private static final UUID CONTOUR_1022 = UUID.fromString("00001022-0002-11e2-9e96-0800200c9a66"); private static final UUID CONTOUR_1025 = UUID.fromString("00001025-0002-11e2-9e96-0800200c9a66"); private static final UUID CONTOUR_1026 = UUID.fromString("00001026-0002-11e2-9e96-0800200c9a66"); private static final UUID ISENS_TIME_SERVICE = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"); private static final UUID ISENS_TIME_CHARACTERISTIC = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"); private static final UUID MANUFACTURER_NAME = UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"); private static final ConcurrentLinkedQueue<Bluetooth_CMD> queue = new ConcurrentLinkedQueue<>(); private static final Object mLock = new Object(); // ok static? private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; private static final int STATE_CONNECTED = 2; private static final long NO_CLOCK_THRESHOLD = Constants.MINUTE_IN_MS * 70; // +- minutes private static final boolean d = false; private static final boolean ignore_control_solution_tests = true; private static final long SCAN_PERIOD = 10000; private static boolean await_acks = false; public static boolean awaiting_ack = false; public static boolean awaiting_data = false; private static int bondingstate = -1; private static long started_at = -1; private static BluetoothAdapter mBluetoothAdapter; public static String mBluetoothDeviceAddress; private static String mLastConnectedDeviceAddress; private static String mLastManufacturer = ""; private static BluetoothGatt mBluetoothGatt; private static int mConnectionState = STATE_DISCONNECTED; private static int service_discovery_count = 0; private static boolean services_discovered = false; private static Bluetooth_CMD last_queue_command; private static CurrentTimeRx ct; private static int highestSequenceStore = 0; private BloodTest lastBloodTest; private GlucoseReadingRx awaitingContext; // bluetooth gatt callback private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { if (mConnectionState != STATE_CONNECTED) { // TODO sane release PowerManager.WakeLock wl = JoH.getWakeLock("bluetooth-meter-connected", 60000); mConnectionState = STATE_CONNECTED; mLastConnectedDeviceAddress = gatt.getDevice().getAddress(); statusUpdate("Connected to device: " + mLastConnectedDeviceAddress); if ((playSounds() && (JoH.ratelimit("bt_meter_connect_sound", 3)))) { JoH.playResourceAudio(R.raw.bt_meter_connect); } Log.d(TAG, "Delay for settling"); waitFor(600); statusUpdate("Discovering services"); service_discovery_count = 0; // reset as new non retried connnection discover_services(); // Bluetooth_CMD.poll_queue(); // do we poll here or on service discovery - should we clear here? } else { // TODO timeout Log.e(TAG, "Apparently already connected - ignoring"); } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { final int old_connection_state = mConnectionState; mConnectionState = STATE_DISCONNECTED; statusUpdate("Disconnected"); if ((old_connection_state == STATE_CONNECTED) && (playSounds() && (JoH.ratelimit("bt_meter_disconnect_sound", 3)))) { JoH.playResourceAudio(R.raw.bt_meter_disconnect); } close(); refreshDeviceCache(mBluetoothGatt); Bluetooth_CMD.poll_queue(); // attempt reconnect reconnect(); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { services_discovered = true; statusUpdate("Services discovered"); bondingstate = mBluetoothGatt.getDevice().getBondState(); if (bondingstate != BluetoothDevice.BOND_BONDED) { statusUpdate("Attempting to create pairing bond - device must be in pairing mode!"); sendDeviceUpdate(gatt.getDevice()); mBluetoothGatt.getDevice().createBond(); waitFor(1000); bondingstate = mBluetoothGatt.getDevice().getBondState(); if (bondingstate != BluetoothDevice.BOND_BONDED) { statusUpdate("Pairing appeared to fail"); } else { sendDeviceUpdate(gatt.getDevice()); } } else { Log.d(TAG, "Device is already bonded - good"); } if (d) { List<BluetoothGattService> gatts = getSupportedGattServices(); for (BluetoothGattService bgs : gatts) { Log.d(TAG, "DEBUG: " + bgs.getUuid()); } } if (queue.isEmpty()) { statusUpdate("Requesting data from meter"); Bluetooth_CMD.read(DEVICE_INFO_SERVICE, MANUFACTURER_NAME, "get device manufacturer"); Bluetooth_CMD.read(CURRENT_TIME_SERVICE, TIME_CHARACTERISTIC, "get device time"); Bluetooth_CMD.notify(GLUCOSE_SERVICE, GLUCOSE_CHARACTERISTIC, "notify new glucose record"); Bluetooth_CMD.enable_notification_value(GLUCOSE_SERVICE, GLUCOSE_CHARACTERISTIC, "notify new glucose value"); Bluetooth_CMD.enable_notification_value(GLUCOSE_SERVICE, CONTEXT_CHARACTERISTIC, "notify new context value"); Bluetooth_CMD.notify(GLUCOSE_SERVICE, CONTEXT_CHARACTERISTIC, "notify new glucose context"); Bluetooth_CMD.enable_indications(GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, "readings indication request"); Bluetooth_CMD.notify(GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, "notify glucose record"); Bluetooth_CMD.write(GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, RecordsCmdTx.getAllRecords(), "request all readings"); Bluetooth_CMD.notify(GLUCOSE_SERVICE, GLUCOSE_CHARACTERISTIC, "notify new glucose record again"); // dummy Bluetooth_CMD.poll_queue(); } else { Log.e(TAG, "Queue is not empty so not scheduling anything.."); } } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { Log.d(TAG, "Descriptor written to: " + descriptor.getUuid() + " getvalue: " + JoH.bytesToHex(descriptor.getValue()) + " status: " + status); if (status == BluetoothGatt.GATT_SUCCESS) { Bluetooth_CMD.poll_queue(); } else { Log.e(TAG, "Got gatt descriptor write failure: " + status); Bluetooth_CMD.retry_last_command(status); } } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { Log.d(TAG, "Written to: " + characteristic.getUuid() + " getvalue: " + JoH.bytesToHex(characteristic.getValue()) + " status: " + status); if (status == BluetoothGatt.GATT_SUCCESS) { if (ack_blocking()) { if (d) Log.d(TAG, "Awaiting ACK before next command: " + awaiting_ack + ":" + awaiting_data); } else { Bluetooth_CMD.poll_queue(); } } else { Log.e(TAG, "Got gatt write failure: " + status); Bluetooth_CMD.retry_last_command(status); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (characteristic.getUuid().equals(TIME_CHARACTERISTIC)) { UserError.Log.d(TAG, "Got time characteristic read data"); ct = new CurrentTimeRx(characteristic.getValue()); statusUpdate("Device time: " + ct.toNiceString()); } else if (characteristic.getUuid().equals(DATE_TIME_CHARACTERISTIC)) { UserError.Log.d(TAG, "Got date time characteristic read data"); ct = new CurrentTimeRx(characteristic.getValue()); statusUpdate("Device time: " + ct.toNiceString()); } else if (characteristic.getUuid().equals(MANUFACTURER_NAME)) { mLastManufacturer = characteristic.getStringValue(0); UserError.Log.d(TAG, "Manufacturer Name: " + mLastManufacturer); statusUpdate("Device from: " + mLastManufacturer); await_acks = false; // reset // Roche Aviva Connect uses a DateTime characteristic instead if (mLastManufacturer.startsWith("Roche")) { Bluetooth_CMD.transmute_command(CURRENT_TIME_SERVICE, TIME_CHARACTERISTIC, GLUCOSE_SERVICE, DATE_TIME_CHARACTERISTIC); } // Diamond Mobile Mini DM30b firmware v1.2.4 // v1.2.4 has reversed sequence numbers and first item is last item and no clock access if (mLastManufacturer.startsWith("TaiDoc")) { // no time service! Bluetooth_CMD.delete_command(CURRENT_TIME_SERVICE, TIME_CHARACTERISTIC); ct = new CurrentTimeRx(); // implicitly trust meter time stamps!! beware daylight saving time changes ct.noClockAccess = true; ct.sequenceNotReliable = true; // no glucose context! Bluetooth_CMD.delete_command(GLUCOSE_SERVICE, CONTEXT_CHARACTERISTIC); Bluetooth_CMD.delete_command(GLUCOSE_SERVICE, CONTEXT_CHARACTERISTIC); // only request last reading - diamond mini seems to make sequence 0 be the most recent record Bluetooth_CMD.replace_command(GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, "W", new Bluetooth_CMD("W", GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, RecordsCmdTx.getFirstRecord(), "request newest reading")); } // Caresens Dual if (mLastManufacturer.startsWith("i-SENS")) { Bluetooth_CMD.delete_command(CURRENT_TIME_SERVICE, TIME_CHARACTERISTIC); ct = new CurrentTimeRx(); // implicitly trust meter time stamps!! beware daylight saving time changes ct.noClockAccess = true; Bluetooth_CMD.notify(ISENS_TIME_SERVICE, ISENS_TIME_CHARACTERISTIC, "notify isens clock"); Bluetooth_CMD.write(ISENS_TIME_SERVICE, ISENS_TIME_CHARACTERISTIC, new TimeTx(JoH.tsl()).getByteSequence(), "set isens clock"); Bluetooth_CMD.write(ISENS_TIME_SERVICE, ISENS_TIME_CHARACTERISTIC, new TimeTx(JoH.tsl()).getByteSequence(), "set isens clock"); Bluetooth_CMD.replace_command(GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, "W", new Bluetooth_CMD("W", GLUCOSE_SERVICE, RECORDS_CHARACTERISTIC, RecordsCmdTx.getNewerThanSequence(getHighestSequence()), "request reading newer than " + getHighestSequence())); } // LifeScan Verio Flex if (mLastManufacturer.startsWith("LifeScan")) { await_acks = true; Bluetooth_CMD.empty_queue(); // Verio Flex isn't standards compliant Bluetooth_CMD.notify(VERIO_F7A1_SERVICE, VERIO_F7A3_NOTIFICATION, "verio general notification"); Bluetooth_CMD.enable_notification_value(VERIO_F7A1_SERVICE, VERIO_F7A3_NOTIFICATION, "verio general notify value"); Bluetooth_CMD.write(VERIO_F7A1_SERVICE, VERIO_F7A2_WRITE, VerioHelper.getTimeCMD(), "verio ask time"); Bluetooth_CMD.write(VERIO_F7A1_SERVICE, VERIO_F7A2_WRITE, VerioHelper.getTcounterCMD(), "verio T data query"); // don't change order with R Bluetooth_CMD.write(VERIO_F7A1_SERVICE, VERIO_F7A2_WRITE, VerioHelper.getRcounterCMD(), "verio R data query"); // don't change order with T } } else { Log.d(TAG, "Got a different charactersitic! " + characteristic.getUuid().toString()); } Bluetooth_CMD.poll_queue(); } else { Log.e(TAG, "Got gatt read failure: " + status); Bluetooth_CMD.retry_last_command(status); } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { final PowerManager.WakeLock wl = JoH.getWakeLock("bt-meter-characterstic-change", 30000); try { processCharacteristicChange(gatt, characteristic); Bluetooth_CMD.poll_queue(); } finally { JoH.releaseWakeLock(wl); } } }; // end callback private BluetoothManager mBluetoothManager; private BluetoothLeScanner mLEScanner; private ScanSettings settings; private List<ScanFilter> filters; private ScanCallback mScanCallback; private String lastScannedDeviceAddress = ""; // old api private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { if (d) Log.i(TAG, "old: onLeScan " + device.toString() + " c:" + device.getBluetoothClass().toString()); if (d) Log.i(TAG, "" + device.getName()); if ((lastScannedDeviceAddress.equals(device.getAddress())) && (!JoH.ratelimit("bt-scan-repeated-address", 2))) { if (d) Log.d(TAG, "Ignoring repeated address: " + device.getAddress()); } else { lastScannedDeviceAddress = device.getAddress(); sendDeviceUpdate(device); } } }, 0); } }; private static void sendDeviceUpdate(BluetoothDevice device) { sendDeviceUpdate(device, false); } private static void sendDeviceUpdate(BluetoothDevice device, boolean force) { if (device == null) return; broadcastUpdate(ACTION_BLUETOOTH_GLUCOSE_METER_NEW_SCAN_DEVICE, device.getAddress() + "^" + device.getBondState() + "^" + ((device.getName() != null) ? device.getName().replace("^", "") : "") + (force ? " " : "")); } private static boolean isBonded() { return (bondingstate == BluetoothDevice.BOND_BONDED); } private static boolean playSounds() { return Pref.getBoolean("bluetooth_meter_play_sounds", true); } private synchronized static void forgetDevice(String address) { Log.d(TAG, "forgetDevice() start"); try { if ((mBluetoothAdapter == null) || (address == null)) return; final Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); if (pairedDevices.size() > 0) { for (BluetoothDevice device : pairedDevices) { if (device.getName() != null) { if (device.getAddress().equals(address)) { Log.e(TAG, "Unpairing.. " + address); JoH.static_toast_long("Unpairing: " + address); try { Method m = device.getClass().getMethod("removeBond", (Class[]) null); m.invoke(device, (Object[]) null); } catch (Exception e) { Log.e(TAG, e.getMessage(), e); } } } } } Log.d(TAG, "forgetDevice() finished"); } catch (Exception e) { Log.wtf(TAG, "Exception forgetting: " + address + " " + e); } } /** * /** * After using a given BLE device, the app must call this method to ensure resources are * released properly. */ private static synchronized void close() { if (mBluetoothGatt == null) { return; } Log.d(TAG, "Closing gatt"); mBluetoothGatt.close(); mBluetoothGatt = null; } protected static void waitFor(final int millis) { synchronized (mLock) { try { Log.e(TAG, "waiting " + millis + "ms"); mLock.wait(millis); } catch (final InterruptedException e) { Log.e(TAG, "Sleeping interrupted", e); } } } @Override public void onCreate() { super.onCreate(); final IntentFilter pairingRequestFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); pairingRequestFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); registerReceiver(mPairingRequestRecevier, pairingRequestFilter); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null) { stopSelf(); return START_NOT_STICKY; } started_at = JoH.tsl(); initialize(); final String service_action = intent.getStringExtra("service_action"); if (service_action != null) { if (service_action.equals("connect")) { // some reset method neeeded - as we don't appear to have shutdown close(); mLastConnectedDeviceAddress = ""; bondingstate = -1; mConnectionState = STATE_DISCONNECTED; scanLeDevice(false); // stop scanning String connect_address = intent.getStringExtra("connect_address"); connect(connect_address); } else if (service_action.equals("scan")) { beginScan(); } else if (service_action.equals("forget")) { final String forget_address = intent.getStringExtra("forget_address"); forgetDevice(forget_address); beginScan(); } } else { // default action here? } //return super.onStartCommand(intent, flags, startId); return START_STICKY; } @Override public void onDestroy() { super.onDestroy(); close(); try { unregisterReceiver(mPairingRequestRecevier); } catch (Exception e) { Log.e(TAG, "Error unregistering pairing receiver: " + e); } started_at = -1; } public void startup() { UserError.Log.d(TAG, "startup()"); initScanCallback(); } // robustly discover device services private synchronized void discover_services() { UserError.Log.d(TAG, "discover_services()"); awaiting_data = false; // reset awaiting_ack = false; services_discovered = false; service_discovery_count++; if (mBluetoothGatt != null) { if (mConnectionState == STATE_CONNECTED) { mBluetoothGatt.discoverServices(); JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { if ((!services_discovered) && (service_discovery_count < 10)) { Log.d(TAG, "Timeout discovering services - retrying..."); discover_services(); } } }, (5000 + (500 * service_discovery_count))); } else { Log.e(TAG, "Cannot discover services as we are not connected"); } } else { Log.e(TAG, "mBluetoothGatt is null!"); } } private void reconnect() { statusUpdate("Attempting reconnection: " + mBluetoothDeviceAddress); connect(mBluetoothDeviceAddress); } // seriously.. private boolean refreshDeviceCache(BluetoothGatt gatt) { if (gatt == null) return false; try { Method localMethod = gatt.getClass().getMethod("refresh", new Class[0]); if (localMethod != null) { return (Boolean) localMethod.invoke(gatt, new Object[0]); } } catch (Exception localException) { Log.e(TAG, "An exception occured while refreshing device"); } return false; } public static void statusUpdate(String status) { broadcastUpdate(ACTION_BLUETOOTH_GLUCOSE_METER_SERVICE_UPDATE, status); UserError.Log.d(TAG, "StatusUpdate: " + status); } private static void broadcastUpdate(final String action, final String data) { final Intent intent = new Intent(action); intent.putExtra("data", data); LocalBroadcastManager.getInstance(xdrip.getAppContext()).sendBroadcast(intent); } private static boolean ack_blocking() { final boolean result = await_acks && (awaiting_ack || awaiting_data); if (result) { if (d) Log.d(TAG, "Ack blocking: " + awaiting_ack + ":" + awaiting_data); } return result; } private synchronized void markDeviceAsSuccessful(BluetoothGatt gatt) { if (!Pref.getStringDefaultBlank("selected_bluetooth_meter_address").equals(mLastConnectedDeviceAddress)) { Pref.setString("selected_bluetooth_meter_address", mLastConnectedDeviceAddress); Pref.setString("selected_bluetooth_meter_info", mLastManufacturer + " " + mLastConnectedDeviceAddress); Pref.setBoolean("bluetooth_meter_enabled", true); // auto-enable the setting JoH.static_toast_long("Success with: " + mLastConnectedDeviceAddress + " Enabling auto-start"); if (gatt != null) sendDeviceUpdate(gatt.getDevice(), true); // force update } } private void processGlucoseReadingRx(GlucoseReadingRx gtb) { if ((!ignore_control_solution_tests) || (gtb.sampleType != 10)) { if (ct.sequenceNotReliable) { // sequence numbers on some meters are reversed so instead invent one using timestamp with 5 second resolution gtb.sequence = (int) ((gtb.time / 5000) - (1496755620 / 5)); } else { setHighestSequence(gtb.sequence); } if (ct.noClockAccess && Pref.getBooleanDefaultFalse("meter_recent_reading_as_now")) { // for diamond mini we don't know if the clock is correct but if it is within the threshold period then we treat it as if the reading // has just happened. We only do this when we have received at least one synced reading so the first one after pairing isn't munged. if (JoH.absMsSince(gtb.time) < NO_CLOCK_THRESHOLD && PersistentStore.getBoolean(GLUCOSE_READING_MARKER + mLastConnectedDeviceAddress)) { final long saved_time = gtb.time; gtb.time = JoH.tsl() - Constants.SECOND_IN_MS * 30; // when the reading was most likely taken UserError.Log.e(TAG, "Munged meter reading time from: " + JoH.dateTimeText(saved_time) + " to " + JoH.dateTimeText(gtb.time)); } if (JoH.quietratelimit(GLUCOSE_READING_MARKER, 10)) { PersistentStore.setBoolean(GLUCOSE_READING_MARKER + mLastConnectedDeviceAddress, true); } } final BloodTest bt = BloodTest.create((gtb.time - ct.timediff) + gtb.offsetMs(), gtb.mgdl, BLUETOOTH_GLUCOSE_METER_TAG + ":\n" + mLastManufacturer + " " + mLastConnectedDeviceAddress, gtb.getUuid().toString()); if (bt != null) { UserError.Log.d(TAG, "Successfully created new BloodTest: " + bt.toS()); bt.glucoseReadingRx = gtb; // add reference lastBloodTest = bt; UserError.Log.uel(TAG, "New blood test data: " + BgGraphBuilder.unitized_string_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " " + bt.source); Inevitable.task("evaluate-meter-records", 2000, this::evaluateLastRecords); } else { if (d) UserError.Log.d(TAG, "Failed to create BloodTest record"); } } else { UserError.Log.d(TAG, "Ignoring control solution test"); } } private synchronized void processCharacteristicChange(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { // extra debug if (d) { UserError.Log.d(TAG, "charactersiticChanged: " + characteristic.getUuid().toString() + " " + JoH.bytesToHex(characteristic.getValue())); } if (GLUCOSE_CHARACTERISTIC.equals(characteristic.getUuid())) { final GlucoseReadingRx gtb = new GlucoseReadingRx(characteristic.getValue(), gatt.getDevice().getAddress()); UserError.Log.d(TAG, "Result: " + gtb.toString()); if (ct == null) { statusUpdate("Cannot process glucose record as we do not know device time!"); } else { if (JoH.quietratelimit("mark-meter-device-success", 10)) { markDeviceAsSuccessful(gatt); } statusUpdate("Glucose Record: " + JoH.dateTimeText((gtb.time - ct.timediff) + gtb.offsetMs()) + "\n" + unitized_string_with_units_static(gtb.mgdl)); if (playSounds() && JoH.ratelimit("bt_meter_data_in", 1)) JoH.playResourceAudio(R.raw.bt_meter_data_in); if (!gtb.contextInfoFollows) { processGlucoseReadingRx(gtb); } else { UserError.Log.d(TAG, "Record has context information so delaying processing"); awaitingContext = gtb; } } } else if (RECORDS_CHARACTERISTIC.equals(characteristic.getUuid())) { UserError.Log.d(TAG, "Change notification for RECORDS: " + JoH.bytesToHex(characteristic.getValue())); } else if (CONTEXT_CHARACTERISTIC.equals(characteristic.getUuid())) { UserError.Log.d(TAG, "Change notification for CONTEXT: " + JoH.bytesToHex(characteristic.getValue())); processContextData(characteristic.getValue()); } else if (VERIO_F7A3_NOTIFICATION.equals(characteristic.getUuid())) { UserError.Log.d(TAG, "Change notification for VERIO: " + JoH.bytesToHex(characteristic.getValue())); try { final GlucoseReadingRx gtb = VerioHelper.parseMessage(characteristic.getValue()); if (gtb != null) { // if this was a BG reading we could process (offset already pre-calculated in time) - not robust against meter clock changes markDeviceAsSuccessful(gatt); statusUpdate("Glucose Record: " + JoH.dateTimeText((gtb.time + gtb.offsetMs())) + "\n" + unitized_string_with_units_static(gtb.mgdl)); if (playSounds() && JoH.ratelimit("bt_meter_data_in", 1)) JoH.playResourceAudio(R.raw.bt_meter_data_in); final BloodTest bt = BloodTest.create((gtb.time) + gtb.offsetMs(), gtb.mgdl, BLUETOOTH_GLUCOSE_METER_TAG + ":\n" + mLastManufacturer + " " + mLastConnectedDeviceAddress); if (bt != null) { UserError.Log.d(TAG, "Successfully created new BloodTest: " + bt.toS()); bt.glucoseReadingRx = gtb; // add reference lastBloodTest = bt; UserError.Log.uel(TAG, "New verio blood test data: " + BgGraphBuilder.unitized_string_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " " + bt.source); final long record_time = lastBloodTest.timestamp; // TODO better replaced with Inevitable Task JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { if (lastBloodTest.timestamp == record_time) { ct = new CurrentTimeRx(); // zero hack evaluateLastRecords(); } } }, 1000); } else { if (d) UserError.Log.d(TAG, "Failed to create BloodTest record"); } } } catch (Exception e) { UserError.Log.wtf(TAG, "Got exception processing Verio data " + e); } } else { UserError.Log.e(TAG, "Unknown characteristic change: " + characteristic.getUuid().toString() + " " + JoH.bytesToHex(characteristic.getValue())); } } private synchronized void processContextData(byte[] context) { final ContextRx crx = new ContextRx(context); if (awaitingContext != null) { if (awaitingContext.sequence == crx.sequence) { if (crx.ketone()) { UserError.Log.e(TAG, "Received Ketone data: " + awaitingContext.asKetone()); awaitingContext = null; } else { if (crx.normalRecord()) { processGlucoseReadingRx(awaitingContext); awaitingContext = null; } else { UserError.Log.e(TAG, "Received context packet but we're not sure what its for: " + crx.toString()); } } } else { UserError.Log.e(TAG, "Received out of sequence context: " + awaitingContext.sequence + " vs " + crx.toString()); } } else { UserError.Log.d(TAG, "Received context but nothing awaiting context: " + crx.toString()); } } private static final BroadcastReceiver mPairingRequestRecevier = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { JoH.doPairingRequest(context, this, intent, mBluetoothDeviceAddress); } }; public static void verioScheduleRequestBg(int r) { Bluetooth_CMD.write(VERIO_F7A1_SERVICE, VERIO_F7A2_WRITE, VerioHelper.getRecordCMD(r), "verio get record " + r); // wording used to update request counter } // decide what to do with newest data private synchronized void evaluateLastRecords() { if (lastBloodTest != null) { GcmActivity.syncBloodTests(); final boolean delay_calibration = true; final GlucoseReadingRx lastGlucoseRecord = lastBloodTest.glucoseReadingRx; if ((lastGlucoseRecord != null) && (lastGlucoseRecord.device != null) && (ct != null)) { final String sequence_id = "last-btm-sequence-" + lastGlucoseRecord.device; final String timestamp_id = "last-btm-timestamp" + lastGlucoseRecord.device; // sequence numbers start from 0 so we add 1 if ((lastGlucoseRecord.sequence + 1) > PersistentStore.getLong(sequence_id)) { PersistentStore.setLong(sequence_id, lastGlucoseRecord.sequence + 1); // get adjusted timestamp if (lastBloodTest.timestamp > PersistentStore.getLong(timestamp_id)) { PersistentStore.setLong(timestamp_id, lastBloodTest.timestamp); Log.d(TAG, "evaluateLastRecords: appears to be a new record: sequence:" + lastGlucoseRecord.sequence); JoH.runOnUiThreadDelayed(Home::staticRefreshBGCharts, 300); if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations") || Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) { final long time_since = JoH.msSince(lastBloodTest.timestamp); if (time_since >= 0) { if (time_since < (12 * Constants.HOUR_IN_MS)) { final Calibration calibration = Calibration.lastValid(); // check must also be younger than most recent calibration if ((calibration == null) || (lastBloodTest.timestamp > calibration.timestamp)) { UserError.Log.ueh(TAG, "Prompting for calibration for: " + BgGraphBuilder.unitized_string_with_units_static(lastBloodTest.mgdl) + " from: " + JoH.dateTimeText(lastBloodTest.timestamp)); JoH.clearCache(); Home.startHomeWithExtra(getApplicationContext(), Home.HOME_FULL_WAKEUP, "1"); JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { Home.staticRefreshBGCharts(); // requires offset in past if ((Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto") && isSlopeFlatEnough())) { Log.d(TAG, "Slope flat enough for auto calibration"); if (!delay_calibration) { Home.startHomeWithExtra(xdrip.getAppContext(), Home.BLUETOOTH_METER_CALIBRATION, BgGraphBuilder.unitized_string_static(lastBloodTest.mgdl), Long.toString(time_since), "auto"); } else { Log.d(TAG, "Delaying calibration for later"); JoH.static_toast_long("Waiting for 15 minutes more sensor data for calibration"); } } else { if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations")) { // manual calibration Home.startHomeWithExtra(xdrip.getAppContext(), Home.BLUETOOTH_METER_CALIBRATION, BgGraphBuilder.unitized_string_static(lastBloodTest.mgdl), Long.toString(time_since), "manual"); } else { Log.d(TAG, "Not flat enough slope for auto calibration and manual calibration not enabled"); } } } }, 500); } else { UserError.Log.e(TAG, "evaluateLastRecords: meter reading is at least as old as last calibration - ignoring"); } } else { UserError.Log.e(TAG, "evaluateLastRecords: meter reading is too far in the past: " + JoH.dateTimeText(lastBloodTest.timestamp)); } } else { UserError.Log.e(TAG, "evaluateLastRecords: time is in the future - ignoring"); } } } } else { UserError.Log.d(TAG, "evaluateLastRecords: sequence isn't newer"); } } else { UserError.Log.e(TAG, "evaluateLastRecords: Data missing for evaluation"); } } else { UserError.Log.e(TAG, "evaluateLastRecords: lastBloodTest is Null!!"); } } @Override public IBinder onBind(Intent intent) { //return mBinder; throw new UnsupportedOperationException("Not yet implemented"); } /** * Initializes a reference to the local Bluetooth adapter. * * @return Return true if the initialization is successful. */ private boolean initialize() { // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothManager. if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { UserError.Log.e(TAG, "Unable to initialize BluetoothManager."); return false; } } mBluetoothAdapter = mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { UserError.Log.e(TAG, "Unable to obtain a BluetoothAdapter."); return false; } startup(); return true; } /** * Connects to the GATT server hosted on the Bluetooth LE device. * * @param address The device address of the destination device. * @return Return true if the connection is initiated successfully. The connection result * is reported asynchronously through the * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} * callback. */ private synchronized boolean connect(final String address) { if ((address == null) || (address.equals("00:00:00:00:00:00"))) { if (d) Log.d(TAG, "ignoring connect with null address"); return false; } Log.d(TAG, "connect() called with address: " + address); if (mBluetoothAdapter == null) { Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); return false; } // if device address changes we should probably disconnect/close if ((mConnectionState == STATE_CONNECTED) && (mLastConnectedDeviceAddress.equals(address)) && (JoH.ratelimit("bt-meter-connect-repeat", 7))) { Log.e(TAG, "We are already connected - not connecting"); if (service_discovery_count == 0) discover_services(); return false; } // Previously connected device. Try to reconnect. statusUpdate("Trying to connect to: " + address); if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { if (d) Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection."); if (mBluetoothGatt.connect()) { mConnectionState = STATE_CONNECTING; return true; } else { return false; } } final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { statusUpdate("Device not found. Unable to connect."); return false; } mBluetoothGatt = device.connectGatt(this, true, mGattCallback); refreshDeviceCache(mBluetoothGatt); if (d) Log.d(TAG, "Trying to create a new connection."); mBluetoothDeviceAddress = address; mConnectionState = STATE_CONNECTING; return true; } /** * Disconnects an existing connection or cancel a pending connection. The disconnection result * is reported asynchronously through the * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} * callback. */ private void disconnect() { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Log.w(TAG, "BluetoothAdapter not initialized"); return; } mBluetoothGatt.disconnect(); } /** * Retrieves a list of supported GATT services on the connected device. This should be * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. * * @return A {@code List} of supported services. */ private List<BluetoothGattService> getSupportedGattServices() { if (mBluetoothGatt == null) return null; return mBluetoothGatt.getServices(); } // currently not used, will need updating if it is @TargetApi(21) private void initScanCallback() { Log.d(TAG, "init v21 ScanCallback()"); // v21 version if (Build.VERSION.SDK_INT >= 21) { mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { Log.i(TAG, "onScanResult result: " + result.toString()); final BluetoothDevice btDevice = result.getDevice(); scanLeDevice(false); // stop scanning connect(btDevice.getAddress()); } @Override public void onBatchScanResults(List<ScanResult> results) { for (ScanResult sr : results) { Log.i("ScanResult - Results", sr.toString()); } } @Override public void onScanFailed(int errorCode) { Log.e(TAG, "Scan Failed Error Code: " + errorCode); if (errorCode == 1) { Log.e(TAG, "Already Scanning: "); // + isScanning); //isScanning = true; } else if (errorCode == 2) { // reset bluetooth? } } }; } } private void scanLeDevice(final boolean enable) { final boolean force_old = true; statusUpdate(enable ? "Starting Scanning" + "\nMake sure meter is turned on - For pairing hold the meter power button until it flashes blue" : "Stopped Scanning"); if (enable) { JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { if ((Build.VERSION.SDK_INT < 21) || (force_old)) { mBluetoothAdapter.stopLeScan(mLeScanCallback); } else { mLEScanner.stopScan(mScanCallback); } // check scan still running and we haven't been restarted in connect mode // and stop the service if so } }, SCAN_PERIOD); if ((Build.VERSION.SDK_INT < 21) || (force_old)) { Log.d(TAG, "Starting old scan"); mBluetoothAdapter.startLeScan(mLeScanCallback); } else { mLEScanner.startScan(filters, settings, mScanCallback); Log.d(TAG, "Starting api21 scan"); } } else { if ((Build.VERSION.SDK_INT < 21) || (force_old)) { mBluetoothAdapter.stopLeScan(mLeScanCallback); } else { mLEScanner.stopScan(mScanCallback); } } } private void beginScan() { if (Build.VERSION.SDK_INT >= 21) { if (d) Log.d(TAG, "Preparing for scan..."); // set up v21 scanner mLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(); filters = new ArrayList<>(); filters.add(new ScanFilter.Builder().setServiceUuid(new ParcelUuid(GLUCOSE_SERVICE)).build()); } // all api versions scanLeDevice(true); } public static void immortality() { if (started_at == -1) { startIfEnabled(); } else { startIfNoRecentData(); } } public static void startIfNoRecentData() { if (JoH.quietratelimit("bluetooth-recent-check", 1800)) { if (Pref.getBoolean("bluetooth_meter_enabled", false)) { final List<BloodTest> btl = BloodTest.lastMatching(1, BLUETOOTH_GLUCOSE_METER_TAG + "%"); if ((btl == null) || (btl.size() == 0) || (JoH.msSince(btl.get(0).created_timestamp) > Constants.HOUR_IN_MS * 6)) { if (JoH.pratelimit("restart_bluetooth_service", 3600 * 5)) { UserError.Log.uel(TAG, "Restarting Bluetooth Glucose meter service"); startIfEnabled(); } } } } } public static void startIfEnabled() { if (Pref.getBoolean("bluetooth_meter_enabled", false)) { final String meter_address = Pref.getStringDefaultBlank("selected_bluetooth_meter_address"); if (meter_address.length() > 5) { if (JoH.pratelimit("bluetooth-glucose-immortality", 10)) { UserError.Log.d(TAG, "Starting Service"); start_service(meter_address); } else { UserError.Log.e(TAG, "Not starting due to rate limit"); } } } } // remote api for stopping public static void stop_service() { final Intent stop_intent = new Intent(xdrip.getAppContext(), BluetoothGlucoseMeter.class); xdrip.getAppContext().stopService(stop_intent); } // remote api for starting public static void start_service(String connect_address) { stop_service(); // is this right? final Intent start_intent = new Intent(xdrip.getAppContext(), BluetoothGlucoseMeter.class); if ((connect_address != null) && (connect_address.length() > 0)) { if (connect_address.equals("auto")) { connect_address = Pref.getStringDefaultBlank("selected_bluetooth_meter_address"); } start_intent.putExtra("service_action", "connect"); start_intent.putExtra("connect_address", connect_address); } else { start_intent.putExtra("service_action", "scan"); } xdrip.getAppContext().startService(start_intent); } // remote api for forgetting public static void start_forget(String forget_address) { stop_service(); // is this right? final Intent start_intent = new Intent(xdrip.getAppContext(), BluetoothGlucoseMeter.class); if ((forget_address != null) && (forget_address.length() > 0)) { start_intent.putExtra("service_action", "forget"); start_intent.putExtra("forget_address", forget_address); xdrip.getAppContext().startService(start_intent); } } public static void sendImmediateData(UUID service, UUID characteristic, byte[] data, String notes) { Log.d(TAG, "Sending immediate data: " + notes); Bluetooth_CMD.process_queue_entry(Bluetooth_CMD.gen_write(service, characteristic, data, notes)); } private static final String PREF_HIGHEST_SEQUENCE = "bt-glucose-sequence-max-"; private static int getHighestSequence() { return (int) PersistentStore.getLong(PREF_HIGHEST_SEQUENCE + mBluetoothDeviceAddress); } private static void setHighestSequence(int sequence) { highestSequenceStore = sequence; Inevitable.task("set-bt-glucose-highest", 1000, new Runnable() { @Override public void run() { if (highestSequenceStore > 0) { PersistentStore.setLong(PREF_HIGHEST_SEQUENCE + mBluetoothDeviceAddress, highestSequenceStore); } } }); } // jamorham bluetooth queue methodology private static class Bluetooth_CMD { final static long QUEUE_TIMEOUT = 10000; private static final int MAX_RESEND = 3; static long queue_check_scheduled = 0; public long timestamp; public String cmd; public byte[] data; public String note; public UUID service; public UUID characteristic; public int resent; private Bluetooth_CMD(String cmd, UUID service, UUID characteristic, byte[] data, String note) { this.cmd = cmd; this.service = service; this.characteristic = characteristic; this.data = data; this.note = note; this.timestamp = System.currentTimeMillis(); this.resent = 0; } private synchronized static void add_item(String cmd, UUID service, UUID characteristic, byte[] data, String note) { queue.add(gen_item(cmd, service, characteristic, data, note)); } private static Bluetooth_CMD gen_item(String cmd, UUID service, UUID characteristic, byte[] data, String note) { final Bluetooth_CMD btc = new Bluetooth_CMD(cmd, service, characteristic, data, note); return btc; } private static Bluetooth_CMD gen_write(UUID service, UUID characteristic, byte[] data, String note) { return gen_item("W", service, characteristic, data, note); } private static void write(UUID service, UUID characteristic, byte[] data, String note) { add_item("W", service, characteristic, data, note); } private static void read(UUID service, UUID characteristic, String note) { add_item("R", service, characteristic, null, note); } private static void notify(UUID service, UUID characteristic, String note) { add_item("N", service, characteristic, new byte[]{0x01}, note); } private static Bluetooth_CMD gen_notify(UUID service, UUID characteristic, String note) { return gen_item("N", service, characteristic, new byte[]{0x01}, note); } private static void unnotify(UUID service, UUID characteristic, String note) { add_item("U", service, characteristic, new byte[]{0x00}, note); } private static void enable_indications(UUID service, UUID characteristic, String note) { add_item("D", service, characteristic, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE, note); } private static void enable_notification_value(UUID service, UUID characteristic, String note) { add_item("D", service, characteristic, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, note); } private static Bluetooth_CMD gen_enable_notification_value(UUID service, UUID characteristic, String note) { return gen_item("D", service, characteristic, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, note); } private static synchronized void check_queue_age() { queue_check_scheduled = 0; if (!queue.isEmpty()) { final Bluetooth_CMD btc = queue.peek(); if (btc != null) { long queue_age = System.currentTimeMillis() - btc.timestamp; if (d) Log.d(TAG, "check queue age.. " + queue_age + " on " + btc.note); if (queue_age > QUEUE_TIMEOUT) { statusUpdate("Timed out on: " + btc.note + (isBonded() ? "" : "\nYou may need to enable the meter's pairing mode by holding the power button when turning it on until it flashes blue")); queue.clear(); last_queue_command = null; close(); waitFor(3000); //reconnect(); hmm we would like to reconnect here } } } else { if (d) Log.d(TAG, "check queue age - queue is empty"); } } private static synchronized void empty_queue() { queue.clear(); } private static synchronized void delete_command(final UUID fromService, final UUID fromCharacteristic) { try { for (Bluetooth_CMD btc : queue) { if (btc.service.equals(fromService) && btc.characteristic.equals(fromCharacteristic)) { Log.d(TAG, "Removing: " + btc.note); queue.remove(btc); break; // currently we only ever need to do one so break for speed } } } catch (Exception e) { Log.wtf("Got exception in delete: ", e); } } private static synchronized void transmute_command(final UUID fromService, final UUID fromCharacteristic, final UUID toService, final UUID toCharacteristic) { try { for (Bluetooth_CMD btc : queue) { if (btc.service.equals(fromService) && btc.characteristic.equals(fromCharacteristic)) { btc.service = toService; btc.characteristic = toCharacteristic; Log.d(TAG, "Transmuted service: " + fromService + " -> " + toService); Log.d(TAG, "Transmuted charact: " + fromCharacteristic + " -> " + toCharacteristic); break; // currently we only ever need to do one so break for speed } } } catch (Exception e) { Log.wtf("Got exception in transmute: ", e); } } private static synchronized void replace_command(final UUID fromService, final UUID fromCharacteristic, String type, Bluetooth_CMD btc_replacement) { try { for (Bluetooth_CMD btc : queue) { if (btc.service.equals(fromService) && btc.characteristic.equals(fromCharacteristic) && btc.cmd.equals(type)) { btc.service = btc_replacement.service; btc.characteristic = btc_replacement.characteristic; btc.cmd = btc_replacement.cmd; btc.data = btc_replacement.data; btc.note = btc_replacement.note; Log.d(TAG, "Replaced service: " + fromService + " -> " + btc_replacement.service); Log.d(TAG, "Replaced charact: " + fromCharacteristic + " -> " + btc_replacement.characteristic); Log.d(TAG, "Replaced cmd: " + btc_replacement.cmd); break; // currently we only ever need to do one so break for speed } } } catch (Exception e) { Log.wtf("Got exception in replace: ", e); } } private static synchronized void insert_after_command(final UUID fromService, final UUID fromCharacteristic, Bluetooth_CMD btc_replacement) { final ConcurrentLinkedQueue<Bluetooth_CMD> tmp_queue = new ConcurrentLinkedQueue<>(); try { for (Bluetooth_CMD btc : queue) { tmp_queue.add(btc); if (btc.service.equals(fromService) && btc.characteristic.equals(fromCharacteristic)) { if (btc_replacement != null) tmp_queue.add(btc_replacement); btc_replacement = null; // first only item } } queue.clear(); queue.addAll(tmp_queue); } catch (Exception e) { Log.wtf("Got exception in insert_after: ", e); } } private synchronized static void poll_queue() { poll_queue(false); } private synchronized static void poll_queue(boolean startup) { if (mConnectionState == STATE_DISCONNECTED) { Log.e(TAG, "Connection is disconnecting, deleting queue"); last_queue_command = null; queue.clear(); return; } if (mBluetoothGatt == null) { Log.e(TAG, "mBluetoothGatt is null - connect and defer"); // connect? // set timer? return; } if (startup && queue.size() > 1) { Log.d(TAG, "Queue busy deferring poll"); // set timer?? return; } final long time_now = System.currentTimeMillis(); if ((time_now - queue_check_scheduled) > 10000) { JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { check_queue_age(); } }, QUEUE_TIMEOUT + 1000); queue_check_scheduled = time_now; } else { if (d) Log.d(TAG, "Queue check already scheduled"); } if (ack_blocking()) { if (d) Log.d(TAG, "Queue blocked by awaiting ack"); return; } Bluetooth_CMD btc = queue.poll(); if (btc != null) { Log.d(TAG, "Processing queue " + btc.cmd + " :: " + btc.note + " :: " + btc.characteristic.toString() + " " + JoH.bytesToHex(btc.data)); last_queue_command = btc; process_queue_entry(btc); } else { if (d) Log.d(TAG, "Queue empty"); } } private static synchronized void retry_last_command(int status) { if (last_queue_command != null) { if (last_queue_command.resent <= MAX_RESEND) { last_queue_command.resent++; if (d) Log.d(TAG, "Delay before retry"); waitFor(200); Log.d(TAG, "Retrying try:(" + last_queue_command.resent + ") last command: " + last_queue_command.note); process_queue_entry(last_queue_command); } else { Log.e(TAG, "Exceeded max resend for: " + last_queue_command.note); last_queue_command = null; } } else { Log.d(TAG, "No last command to retry"); } } private static void process_queue_entry(Bluetooth_CMD btc) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Log.w(TAG, "BluetoothAdapter not initialized"); return; } BluetoothGattService service = null; final BluetoothGattCharacteristic characteristic; if (btc.service != null) service = mBluetoothGatt.getService(btc.service); if ((service != null) || (btc.service == null)) { if ((service != null) && (btc.characteristic != null)) { characteristic = service.getCharacteristic(btc.characteristic); } else { characteristic = null; } if (characteristic != null) { switch (btc.cmd) { case "W": characteristic.setValue(btc.data); if (await_acks && (characteristic.getValue().length > 1)) { awaiting_ack = true; awaiting_data = true; if (d) Log.d(TAG, "Setting await ack blocker 1"); if (btc.note.startsWith("verio get record")) { // notify which record we are processing VerioHelper.updateRequestedRecord(Integer.parseInt(btc.note.substring(17))); } } JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { try { if (!mBluetoothGatt.writeCharacteristic(characteristic)) { Log.d(TAG, "Failed in write characteristic"); waitFor(150); if (!mBluetoothGatt.writeCharacteristic(characteristic)) { Log.e(TAG, "Failed second time in write charactersitic"); } } } catch (NullPointerException e) { UserError.Log.e(TAG, "Got null pointer exception writing characteristic - probably temporary failure"); } } }, 0); break; case "R": mBluetoothGatt.readCharacteristic(characteristic); break; case "N": mBluetoothGatt.setCharacteristicNotification(characteristic, true); waitFor(100); poll_queue(); // we don't get an event from this break; case "U": mBluetoothGatt.setCharacteristicNotification(characteristic, false); break; case "D": final BluetoothGattDescriptor descriptor = characteristic.getDescriptor( CLIENT_CHARACTERISTIC_CONFIG); descriptor.setValue(btc.data); mBluetoothGatt.writeDescriptor(descriptor); break; default: Log.e(TAG, "Unknown queue cmd: " + btc.cmd); } // end switch } else { Log.e(TAG, "Characteristic was null!!!!"); } } else { Log.e(TAG, "Got null service error on: " + btc.service); } } } }