Implement ble-scanning for a new pod

This commit is contained in:
Andrei Vereha 2021-02-23 21:20:51 +01:00
parent 12ff37123d
commit fa8fdd9816
19 changed files with 550 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import info.nightscout.androidaps.plugins.pump.omnipod.common.dagger.ActivityScope
import info.nightscout.androidaps.plugins.pump.omnipod.common.dagger.OmnipodWizardModule
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.BleManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.DashPodManagementActivity
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOverviewFragment
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity
@ -30,4 +31,6 @@ abstract class OmnipodDashModule {
@ContributesAndroidInjector
abstract fun contributesOmnipodDashOverviewFragment(): OmnipodDashOverviewFragment
@ContributesAndroidInjector
abstract fun contributesBleManager(): BleManager
}

View file

@ -0,0 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm;
import android.bluetooth.BluetoothGattCallback;
public class BleCommCallbacks extends BluetoothGattCallback {
}

View file

@ -0,0 +1,159 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import java.math.BigInteger;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Singleton;
import info.nightscout.androidaps.logging.AAPSLogger;
import info.nightscout.androidaps.logging.LTag;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand.BleCommand;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand.BleCommandHello;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand.BleCommandType;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CharacteristicNotFoundException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotSendBleCmdException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotSendBleException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.FailedToConnectException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ServiceNotFoundException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner;
@Singleton
public class BleManager implements OmnipodDashCommunicationManager {
private static final int CONNECT_TIMEOUT_MS = 5000;
private static final int DISCOVER_SERVICES_TIMEOUT_MS = 5000;
private static final String SERVICE_UUID = "1a7e-4024-e3ed-4464-8b7e-751e03d0dc5f";
private static final String CMD_CHARACTERISTIC_UUID = "1a7e-2441-e3ed-4464-8b7e-751e03d0dc5f";
private static final String DATA_CHARACTERISTIC_UUID = "1a7e-2442-e3ed-4464-8b7e-751e03d0dc5f";
private static final int CONTROLLER_ID = 4242; // TODO read from preferences or somewhere else.
private static BleManager instance = null;
private final Context context;
private final BluetoothAdapter bluetoothAdapter;
private final BluetoothManager bluetoothManager;
@Inject AAPSLogger aapsLogger;
private String podAddress;
private BluetoothGatt gatt;
private BluetoothGattCharacteristic cmdCharacteristic;
private BluetoothGattCharacteristic dataCharacteristic;
@Inject
public BleManager(Context context) {
this.context = context;
this.bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
this.bluetoothAdapter = bluetoothManager.getAdapter();
}
public static BleManager getInstance(Context context) {
BleManager ret;
synchronized (BleManager.class) {
if (instance == null) {
instance = new BleManager(context);
}
ret = instance;
}
return ret;
}
private static UUID uuidFromString(String s) {
return new UUID(
new BigInteger(s.replace("-", "").substring(0, 16), 16).longValue(),
new BigInteger(s.replace("-", "").substring(16), 16).longValue()
);
}
public void activateNewPod()
throws InterruptedException,
ScanFailException,
FailedToConnectException,
CouldNotSendBleException {
this.aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation");
PodScanner podScanner = new PodScanner(this.aapsLogger, this.bluetoothAdapter);
this.podAddress = podScanner.scanForPod(PodScanner.SCAN_FOR_SERVICE_UUID, PodScanner.POD_ID_NOT_ACTIVATED).getScanResult().getDevice().getAddress();
// For tests: this.podAddress = "B8:27:EB:1D:7E:BB";
this.connect();
// do the dance: send SP0, SP1, etc
// get and save LTK
}
public void connect()
throws FailedToConnectException,
CouldNotSendBleException {
// TODO: locking?
BluetoothDevice podDevice = this.bluetoothAdapter.getRemoteDevice(this.podAddress);
BluetoothGattCallback bleCommCallback = new BleCommCallbacks();
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to " + this.podAddress);
gatt = podDevice.connectGatt(this.context, true, bleCommCallback, BluetoothDevice.TRANSPORT_LE);
try {
Thread.sleep(CONNECT_TIMEOUT_MS);
} catch (InterruptedException e) {
// we get interrupted on successful connection
// TODO: interrupt this thread onConnect()
}
int connectionState = this.bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT);
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: " + connectionState);
if (connectionState != BluetoothProfile.STATE_CONNECTED) {
throw new FailedToConnectException(this.podAddress);
}
this.discoverServicesAndSayHello(gatt);
}
private void discoverServicesAndSayHello(BluetoothGatt gatt)
throws FailedToConnectException,
CouldNotSendBleException {
gatt.discoverServices();
try {
Thread.sleep(CONNECT_TIMEOUT_MS);
} catch (InterruptedException e) {
// we get interrupted on successfull connection
// TODO: interrupt this thread onConnect()
}
BluetoothGattService service = gatt.getService(uuidFromString(SERVICE_UUID));
if (service == null) {
throw new ServiceNotFoundException(SERVICE_UUID);
}
BluetoothGattCharacteristic cmdChar = service.getCharacteristic(uuidFromString(CMD_CHARACTERISTIC_UUID));
if (cmdChar == null) {
throw new CharacteristicNotFoundException(CMD_CHARACTERISTIC_UUID);
}
BluetoothGattCharacteristic dataChar = service.getCharacteristic(uuidFromString(DATA_CHARACTERISTIC_UUID));
if (dataChar == null) {
throw new CharacteristicNotFoundException(DATA_CHARACTERISTIC_UUID);
}
this.cmdCharacteristic = cmdChar;
this.dataCharacteristic = dataChar;
BleCommand hello = new BleCommandHello(CONTROLLER_ID);
if (!this.sendCmd(hello.asByteArray())) {
throw new CouldNotSendBleCmdException();
}
aapsLogger.debug(LTag.PUMPBTCOMM, "saying hello to the pod" + hello.asByteArray());
}
private boolean sendCmd(byte[] payload) {
// TODO move out of here
this.cmdCharacteristic.setValue(payload);
boolean ret = this.gatt.writeCharacteristic(cmdCharacteristic);
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command status. data:" + payload.toString() + "status: " + ret);
return ret;
}
}

View file

@ -0,0 +1,22 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand;
import org.jetbrains.annotations.NotNull;
public class BleCommand {
private final byte[] data;
public BleCommand(@NotNull BleCommandType type) {
this.data = new byte[]{type.getValue()};
}
public BleCommand(@NotNull BleCommandType type, @NotNull byte[] payload) {
int n = payload.length + 1;
this.data = new byte[n];
this.data[0] = type.getValue();
System.arraycopy(payload, 0, data, 1, payload.length);
}
public byte[] asByteArray() {
return this.data;
}
}

View file

@ -0,0 +1,14 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand;
import java.nio.ByteBuffer;
public class BleCommandHello extends BleCommand {
public BleCommandHello(int controllerId) {
super(BleCommandType.HELLO,
ByteBuffer.allocate(6)
.put((byte) 1) // TODO find the meaning of this constant
.put((byte) 4) // TODO find the meaning of this constant
.putInt(controllerId).array()
);
}
}

View file

@ -0,0 +1,30 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.blecommand;
public enum BleCommandType {
RTS((byte) 0x00),
CTS((byte) 0x01),
NACK((byte) 0x02),
ABORT((byte) 0x03),
SUCCESS((byte) 0x04),
FAIL((byte) 0x05),
HELLO((byte) 0x06);
public final byte value;
BleCommandType(byte value) {
this.value = value;
}
public static BleCommandType byValue(byte value) {
for (BleCommandType type : values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown BleCommandType: " + value);
}
public byte getValue() {
return this.value;
}
}

View file

@ -0,0 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class CharacteristicNotFoundException extends FailedToConnectException {
public CharacteristicNotFoundException(String cmdCharacteristicUuid) {
super("characteristic not found: " + cmdCharacteristicUuid);
}
}

View file

@ -0,0 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class CouldNotSendBleCmdException extends CouldNotSendBleException {
}

View file

@ -0,0 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class CouldNotSendBleException extends Exception {
}

View file

@ -0,0 +1,11 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
import android.os.ParcelUuid;
import java.util.List;
public class DiscoveredInvalidPodException extends Exception {
public DiscoveredInvalidPodException(String message, List<ParcelUuid> serviceUUIds) {
super(message + " service UUIDs: " + serviceUUIds);
}
}

View file

@ -0,0 +1,11 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class FailedToConnectException extends Exception {
public FailedToConnectException() {
super();
}
public FailedToConnectException(String message) {
super(message);
}
}

View file

@ -0,0 +1,10 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class ScanFailException extends Exception {
public ScanFailException() {
}
public ScanFailException(int errorCode) {
super("errorCode" + errorCode);
}
}

View file

@ -0,0 +1,20 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.BleDiscoveredDevice;
public class ScanFailFoundTooManyException extends ScanFailException {
private final List<BleDiscoveredDevice> devices;
public ScanFailFoundTooManyException(List<BleDiscoveredDevice> devices) {
super();
this.devices = new ArrayList<>(devices);
}
public List<BleDiscoveredDevice> getDiscoveredDevices() {
return Collections.unmodifiableList(this.devices);
}
}

View file

@ -0,0 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class ScanFailNotFoundException extends ScanFailException {
}

View file

@ -0,0 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions;
public class ServiceNotFoundException extends FailedToConnectException {
public ServiceNotFoundException(String serviceUuid) {
super("service not found: " + serviceUuid);
}
}

View file

@ -0,0 +1,94 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.os.ParcelUuid;
import java.util.List;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.DiscoveredInvalidPodException;
public class BleDiscoveredDevice {
private final ScanResult scanResult;
private final long podID;
private final int sequenceNo;
private final long lotNo;
public BleDiscoveredDevice(ScanResult scanResult, long searchPodID)
throws DiscoveredInvalidPodException {
this.scanResult = scanResult;
this.podID = searchPodID;
this.validateServiceUUIDs();
this.validatePodID();
this.lotNo = this.parseLotNo();
this.sequenceNo = this.parseSeqNo();
}
private static String extractUUID16(ParcelUuid uuid) {
return uuid.toString().substring(4, 8);
}
private void validateServiceUUIDs()
throws DiscoveredInvalidPodException {
ScanRecord scanRecord = scanResult.getScanRecord();
List<ParcelUuid> serviceUUIDs = scanRecord.getServiceUuids();
if (serviceUUIDs.size() != 9) {
throw new DiscoveredInvalidPodException("Expected 9 service UUIDs, got" + serviceUUIDs.size(), serviceUUIDs);
}
if (!extractUUID16(serviceUUIDs.get(0)).equals("4024")) {
// this is the service that we filtered for
throw new DiscoveredInvalidPodException("The first exposed service UUID should be 4024, got " + extractUUID16(serviceUUIDs.get(0)), serviceUUIDs);
}
// TODO understand what is serviceUUIDs[1]. 0x2470. Alarms?
if (!extractUUID16(serviceUUIDs.get(2)).equals("000a")) {
// constant?
throw new DiscoveredInvalidPodException("The third exposed service UUID should be 000a, got " + serviceUUIDs.get(2), serviceUUIDs);
}
}
private void validatePodID()
throws DiscoveredInvalidPodException {
ScanRecord scanRecord = scanResult.getScanRecord();
List<ParcelUuid> serviceUUIDs = scanRecord.getServiceUuids();
String hexPodID = extractUUID16(serviceUUIDs.get(3)) + extractUUID16(serviceUUIDs.get(4));
Long podID = Long.parseLong(hexPodID, 16);
if (this.podID != podID) {
throw new DiscoveredInvalidPodException("This is not the POD we are looking for. " + this.podID + " found: " + podID, serviceUUIDs);
}
}
private long parseLotNo() {
ScanRecord scanRecord = scanResult.getScanRecord();
List<ParcelUuid> serviceUUIDs = scanRecord.getServiceUuids();
String lotSeq = extractUUID16(serviceUUIDs.get(5)) +
extractUUID16(serviceUUIDs.get(6)) +
extractUUID16(serviceUUIDs.get(7));
return Long.parseLong(lotSeq.substring(0, 10), 16);
}
private int parseSeqNo() {
ScanRecord scanRecord = scanResult.getScanRecord();
List<ParcelUuid> serviceUUIDs = scanRecord.getServiceUuids();
String lotSeq = extractUUID16(serviceUUIDs.get(7)) +
extractUUID16(serviceUUIDs.get(8));
return Integer.parseInt(lotSeq.substring(2), 16);
}
public ScanResult getScanResult() {
return this.scanResult;
}
@Override public String toString() {
return "BleDiscoveredDevice{" +
"scanResult=" + scanResult +
", podID=" + podID +
", sequenceNo=" + sequenceNo +
", lotNo=" + lotNo +
'}';
}
}

View file

@ -0,0 +1,62 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.os.ParcelUuid;
import java.util.Arrays;
import java.util.List;
import info.nightscout.androidaps.logging.AAPSLogger;
import info.nightscout.androidaps.logging.LTag;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailFoundTooManyException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailNotFoundException;
public class PodScanner {
public static final String SCAN_FOR_SERVICE_UUID = "00004024-0000-1000-8000-00805F9B34FB";
public static final long POD_ID_NOT_ACTIVATED = 4294967294L;
private static final int SCAN_DURATION_MS = 5000;
private final BluetoothAdapter bluetoothAdapter;
private final AAPSLogger logger;
public PodScanner(AAPSLogger logger, BluetoothAdapter bluetoothAdapter) {
this.bluetoothAdapter = bluetoothAdapter;
this.logger = logger;
}
public BleDiscoveredDevice scanForPod(String serviceUUID, long podID)
throws InterruptedException, ScanFailException {
BluetoothLeScanner scanner = this.bluetoothAdapter.getBluetoothLeScanner();
ScanFilter filter = new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString(serviceUUID))
.build();
ScanSettings scanSettings = new ScanSettings.Builder()
.setLegacy(false)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
ScanCollector scanCollector = new ScanCollector(this.logger, podID);
this.logger.debug(LTag.PUMPBTCOMM, "Scanning with filters: "+ filter.toString() + " settings" + scanSettings.toString());
scanner.startScan(Arrays.asList(filter), scanSettings, scanCollector);
Thread.sleep(SCAN_DURATION_MS);
scanner.flushPendingScanResults(scanCollector);
scanner.stopScan(scanCollector);
List<BleDiscoveredDevice> collected = scanCollector.collect();
if (collected.size() == 0) {
throw new ScanFailNotFoundException();
} else if (collected.size() > 1) {
throw new ScanFailFoundTooManyException(collected);
}
return collected.get(0);
}
}

View file

@ -0,0 +1,66 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import info.nightscout.androidaps.logging.AAPSLogger;
import info.nightscout.androidaps.logging.LTag;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.DiscoveredInvalidPodException;
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailException;
public class ScanCollector extends ScanCallback {
private final AAPSLogger logger;
private final long podID;
// there could be different threads calling the onScanResult callback
private final ConcurrentHashMap<String, ScanResult> found;
private int scanFailed;
public ScanCollector(AAPSLogger logger, long podID) {
this.podID = podID;
this.logger = logger;
this.found = new ConcurrentHashMap<String, ScanResult>();
}
@Override
public void onScanResult(int callbackType, ScanResult result) {
// callbackType will be ALL
this.logger.debug(LTag.PUMPBTCOMM, "Scan found: "+result.toString());
this.found.put(result.getDevice().getAddress(), result);
}
@Override
public void onScanFailed(int errorCode) {
this.scanFailed = errorCode;
this.logger.warn(LTag.PUMPBTCOMM, "Scan failed with errorCode: "+errorCode);
super.onScanFailed(errorCode);
}
public List<BleDiscoveredDevice> collect()
throws ScanFailException {
List<BleDiscoveredDevice> ret = new ArrayList<>();
if (this.scanFailed != 0) {
throw new ScanFailException(this.scanFailed);
}
logger.debug(LTag.PUMPBTCOMM, "ScanCollector looking for podID: " + this.podID);
for (ScanResult result : this.found.values()) {
try {
BleDiscoveredDevice device = new BleDiscoveredDevice(result, this.podID);
ret.add(device);
logger.debug(LTag.PUMPBTCOMM, "ScanCollector found: " + result.toString() + "Pod ID: " + this.podID);
} catch (DiscoveredInvalidPodException e) {
logger.debug(LTag.PUMPBTCOMM, "ScanCollector: pod not matching" + e.toString());
// this is not the POD we are looking for
}
}
return Collections.unmodifiableList(ret);
}
}

View file

@ -1,15 +1,21 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.viewmodel.action
import android.os.AsyncTask
import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activation.viewmodel.action.InitializePodViewModel
import info.nightscout.androidaps.plugins.pump.omnipod.dash.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.BleManager
import kotlinx.coroutines.launch
import javax.inject.Inject
class DashInitializePodViewModel @Inject constructor(private val aapsLogger: AAPSLogger, private val injector: HasAndroidInjector) : InitializePodViewModel() {
class DashInitializePodViewModel @Inject constructor(private val aapsLogger: AAPSLogger,
private val injector: HasAndroidInjector,
private val bleManager: BleManager) : InitializePodViewModel() {
override fun isPodInAlarm(): Boolean = false // TODO
@ -19,7 +25,14 @@ class DashInitializePodViewModel @Inject constructor(private val aapsLogger: AAP
override fun doExecuteAction(): PumpEnactResult {
// TODO FIRST STEP OF ACTIVATION
aapsLogger.debug(LTag.PUMP, "started activation part 1")
AsyncTask.execute {
try {
bleManager.activateNewPod()
} catch (e: Exception) {
aapsLogger.error(LTag.PUMP, "TEST ACTIVATE Exception" + e.toString())
}
}
return PumpEnactResult(injector).success(false).comment("not implemented")
}