improve error handling

This commit is contained in:
Andrei Vereha 2021-03-28 20:48:07 +02:00
parent ae08e43109
commit 4b49392200
38 changed files with 795 additions and 556 deletions

View file

@ -1,34 +1,23 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context import android.content.Context
import android.os.Message
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.LTKExchanger import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.LTKExchanger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Session import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionEstablisher
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import io.reactivex.Observable import io.reactivex.Observable
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -40,88 +29,50 @@ class OmnipodDashBleManagerImpl @Inject constructor(
private val podState: OmnipodDashPodStateManager private val podState: OmnipodDashPodStateManager
) : OmnipodDashBleManager { ) : OmnipodDashBleManager {
// TODO: add busy AtomicBoolean
private val bluetoothManager: BluetoothManager = private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
private var sessionKeys: SessionKeys? = null private var connection: Connection? = null
private var msgIO: MessageIO? = null
private var gatt: BluetoothGatt? = null
private var status: ConnectionStatus = ConnectionStatus.IDLE private var status: ConnectionStatus = ConnectionStatus.IDLE
private val myId = Id.fromInt(CONTROLLER_ID) private val myId = Id.fromInt(CONTROLLER_ID)
private val uniqueId = podState.uniqueId private val uniqueId = podState.uniqueId
private val podId = uniqueId?.let(Id::fromLong) private val podId = uniqueId?.let(Id::fromLong)
?: myId.increment() // pod not activated ?: myId.increment() // pod not activated
@Throws(
FailedToConnectException::class,
CouldNotSendBleException::class,
InterruptedException::class,
BleIOBusyException::class,
TimeoutException::class,
CouldNotConfirmWriteException::class,
CouldNotEnableNotifications::class,
DescriptorNotFoundException::class,
CouldNotConfirmDescriptorWriteException::class
)
private fun connect(podDevice: BluetoothDevice): BleIO {
val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>> =
mapOf(
CharacteristicType.CMD to LinkedBlockingDeque(),
CharacteristicType.DATA to LinkedBlockingDeque()
)
val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets)
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}")
val autoConnect = false // TODO: check what to use here
val gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
bleCommCallbacks.waitForConnection(CONNECT_TIMEOUT_MS)
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT)
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: $connectionState")
if (connectionState != BluetoothProfile.STATE_CONNECTED) {
throw FailedToConnectException(podDevice.address)
}
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val chars = discoverer.discoverServices()
val bleIO = BleIO(aapsLogger, chars, incomingPackets, gattConnection, bleCommCallbacks)
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandHello(CONTROLLER_ID).data)
bleIO.readyToRead()
gatt = gattConnection
return bleIO
}
override fun sendCommand(cmd: Command): Observable<PodEvent> = Observable.create { emitter -> override fun sendCommand(cmd: Command): Observable<PodEvent> = Observable.create { emitter ->
try { val conn = connection ?: throw NotConnectedException("Not connected")
val keys = sessionKeys
val mIO = msgIO val session = conn.session ?: throw NotConnectedException("Missing session")
if (keys == null || mIO == null) {
throw Exception("Not connected") emitter.onNext(PodEvent.CommandSending(cmd))
val sendResult = session.sendCommand(cmd)
when(sendResult) {
is CommandSendErrorSending -> {
emitter.tryOnError(CouldNotSendCommandException())
return@create
} }
emitter.onNext(PodEvent.CommandSending(cmd)) is CommandSendSuccess ->
// TODO switch to RX emitter.onNext(PodEvent.CommandSent(cmd))
emitter.onNext(PodEvent.CommandSent(cmd)) is CommandSendErrorConfirming ->
emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd))
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck
)
val session = Session(
aapsLogger = aapsLogger,
msgIO = mIO,
myId = myId,
podId = podId,
sessionKeys = keys,
enDecrypt = enDecrypt
)
val response = session.sendCommand(cmd)
emitter.onNext(PodEvent.ResponseReceived(response))
emitter.onComplete()
} catch (ex: Exception) {
emitter.tryOnError(ex)
} }
val readResult = session.readAndAckCommandResponse()
when (readResult){
is CommandReceiveSuccess ->
emitter.onNext(PodEvent.ResponseReceived(readResult.result))
is CommandAckError ->
emitter.onNext(PodEvent.ResponseReceived(readResult.result))
is CommandReceiveError -> {
emitter.tryOnError(MessageIOException("Could not read response: $readResult"))
return@create
}
}
emitter.onComplete()
} }
override fun getStatus(): ConnectionStatus { override fun getStatus(): ConnectionStatus {
@ -132,42 +83,23 @@ class OmnipodDashBleManagerImpl @Inject constructor(
return s return s
} }
@Throws(
InterruptedException::class,
ScanFailException::class,
FailedToConnectException::class,
CouldNotSendBleException::class,
BleIOBusyException::class,
TimeoutException::class,
CouldNotConfirmWriteException::class,
CouldNotEnableNotifications::class,
DescriptorNotFoundException::class,
CouldNotConfirmDescriptorWriteException::class
)
override fun connect(): Observable<PodEvent> = Observable.create { emitter -> override fun connect(): Observable<PodEvent> = Observable.create { emitter ->
try { try {
emitter.onNext(PodEvent.BluetoothConnecting)
val podAddress = val podAddress =
podState.bluetoothAddress podState.bluetoothAddress
?: throw FailedToConnectException("Missing bluetoothAddress, activate the pod first") ?: throw FailedToConnectException("Missing bluetoothAddress, activate the pod first")
// check if already connected
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT) val conn = connection
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: $connectionState") ?: Connection(podDevice, aapsLogger, context)
if (connectionState == BluetoothProfile.STATE_CONNECTED) { connection = conn
emitter.onNext(PodEvent.AlreadyConnected(podAddress)) if (conn.connectionState() is Connected) {
emitter.onNext(PodEvent.Connected)
emitter.onComplete() emitter.onComplete()
return@create return@create
} }
conn.connect()
emitter.onNext(PodEvent.BluetoothConnecting)
if (msgIO != null) {
disconnect()
}
val bleIO = connect(podDevice)
val mIO = MessageIO(aapsLogger, bleIO)
msgIO = mIO
emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.BluetoothConnected(podAddress))
emitter.onNext(PodEvent.EstablishingSession) emitter.onNext(PodEvent.EstablishingSession)
@ -182,23 +114,15 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} }
private fun establishSession(msgSeq: Byte) { private fun establishSession(msgSeq: Byte) {
val mIO = msgIO ?: throw FailedToConnectException("connection lost") val conn = connection ?: throw FailedToConnectException("connection lost")
val ltk: ByteArray = podState.ltk ?: throw FailedToConnectException("Missing LTK, activate the pod first") val ltk: ByteArray = podState.ltk ?: throw FailedToConnectException("Missing LTK, activate the pod first")
val uniqueId = podState.uniqueId val uniqueId = podState.uniqueId
val podId = uniqueId?.let { Id.fromLong(uniqueId) } val podId = uniqueId?.let { Id.fromLong(uniqueId) }
?: myId.increment() // pod not activated ?: myId.increment() // pod not activated
val eapSqn = podState.increaseEapAkaSequenceNumber() val eapSqn = podState.increaseEapAkaSequenceNumber()
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, myId, podId, msgSeq) conn.establishSession(ltk, msgSeq, myId, podId, eapSqn)
val keys = eapAkaExchanger.negotiateSessionKeys()
podState.commitEapAkaSequenceNumber() podState.commitEapAkaSequenceNumber()
if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "CK: ${keys.ck.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "msgSequenceNumber: ${keys.msgSequenceNumber}")
aapsLogger.info(LTag.PUMPCOMM, "Nonce: ${keys.nonce}")
}
sessionKeys = keys
} }
override fun pairNewPod(): Observable<PodEvent> = Observable.create { emitter -> override fun pairNewPod(): Observable<PodEvent> = Observable.create { emitter ->
@ -209,7 +133,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(
emitter.onComplete() emitter.onComplete()
return@create return@create
} }
aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation") aapsLogger.info(LTag.PUMPBTCOMM, "Starting new pod activation")
emitter.onNext(PodEvent.Scanning) emitter.onNext(PodEvent.Scanning)
val podScanner = PodScanner(aapsLogger, bluetoothAdapter) val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
@ -221,13 +145,17 @@ class OmnipodDashBleManagerImpl @Inject constructor(
emitter.onNext(PodEvent.BluetoothConnecting) emitter.onNext(PodEvent.BluetoothConnecting)
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress) val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val bleIO = connect(podDevice) val conn = Connection(podDevice, aapsLogger, context)
val mIO = MessageIO(aapsLogger, bleIO) connection = conn
msgIO = mIO
emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.BluetoothConnected(podAddress))
emitter.onNext(PodEvent.Pairing) emitter.onNext(PodEvent.Pairing)
val ltkExchanger = LTKExchanger(aapsLogger, mIO, myId, podId, Id.fromLong(PodScanner.POD_ID_NOT_ACTIVATED)) val ltkExchanger = LTKExchanger(
aapsLogger, conn.msgIO, myId, podId, Id.fromLong(
PodScanner
.POD_ID_NOT_ACTIVATED
)
)
val pairResult = ltkExchanger.negotiateLTK() val pairResult = ltkExchanger.negotiateLTK()
emitter.onNext(PodEvent.Paired(podId)) emitter.onNext(PodEvent.Paired(podId))
podState.updateFromPairing(podId, pairResult) podState.updateFromPairing(podId, pairResult)
@ -246,16 +174,14 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} }
override fun disconnect() { override fun disconnect() {
val localGatt = gatt if (connection == null) {
localGatt?.close() // TODO: use disconnect? aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection")
gatt = null }
msgIO = null connection?.disconnect()
sessionKeys = null
} }
companion object { companion object {
private const val CONNECT_TIMEOUT_MS = 7000
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else. const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
} }
} }

View file

@ -7,26 +7,23 @@ import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotConfirmDescriptorWriteException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotConfirmWriteException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType.Companion.byValue import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType.Companion.byValue
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.IncomingPackets
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.util.*
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class BleCommCallbacks( class BleCommCallbacks(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
private val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>> private val incomingPackets: IncomingPackets,
) : BluetoothGattCallback() { ) : BluetoothGattCallback() {
private val serviceDiscoveryComplete: CountDownLatch = CountDownLatch(1) private val serviceDiscoveryComplete: CountDownLatch = CountDownLatch(1)
private val connected: CountDownLatch = CountDownLatch(1) private val connected: CountDownLatch = CountDownLatch(1)
private val writeQueue: BlockingQueue<CharacteristicWriteConfirmation> = LinkedBlockingQueue(1) private val writeQueue: BlockingQueue<WriteConfirmation> = LinkedBlockingQueue(1)
private val descriptorWriteQueue: BlockingQueue<DescriptorWriteConfirmation> = LinkedBlockingQueue(1)
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState) super.onConnectionStateChange(gatt, status, newState)
@ -54,52 +51,32 @@ class BleCommCallbacks(
serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS) serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
} }
@Throws(InterruptedException::class, TimeoutException::class, CouldNotConfirmWriteException::class) fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long) : WriteConfirmation{
fun confirmWrite(expectedPayload: ByteArray, timeoutMs: Long) { try {
val received: CharacteristicWriteConfirmation = writeQueue.poll(timeoutMs, TimeUnit.MILLISECONDS) return when(val received = writeQueue.poll(timeoutMs, TimeUnit.MILLISECONDS) ) {
?: throw TimeoutException() null -> return WriteConfirmationError("Timeout waiting for writeConfirmation")
is WriteConfirmationSuccess ->
when (received) { if (expectedPayload.contentEquals(received.payload) &&
is CharacteristicWriteConfirmationPayload -> confirmWritePayload(expectedPayload, received) expectedUUID == received.uuid) {
is CharacteristicWriteConfirmationError -> throw CouldNotConfirmWriteException(received.status) received
} else {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Could not confirm write. Got " + received.payload.toHex() + ".Excepted: " + expectedPayload.toHex()
)
WriteConfirmationError("Received incorrect writeConfirmation")
}
is WriteConfirmationError ->
received
}
}catch (e: InterruptedException) {
return WriteConfirmationError("Interrupted waiting for confirmation")
} }
} }
private fun confirmWritePayload(expectedPayload: ByteArray, received: CharacteristicWriteConfirmationPayload) {
if (!expectedPayload.contentEquals(received.payload)) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Could not confirm write. Got " + received.payload.toHex() + ".Excepted: " + expectedPayload.toHex()
)
throw CouldNotConfirmWriteException(expectedPayload, received.payload)
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Confirmed write with value: " + received.payload.toHex())
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status) super.onCharacteristicWrite(gatt, characteristic, status)
val writeConfirmation = if (status == BluetoothGatt.GATT_SUCCESS) { onWrite(status, characteristic.uuid, characteristic.value)
CharacteristicWriteConfirmationPayload(characteristic.value)
} else {
CharacteristicWriteConfirmationError(status)
}
aapsLogger.debug(
LTag.PUMPBTCOMM,
"OnCharacteristicWrite with status/char/value " +
status + "/" + byValue(characteristic.uuid.toString()) + "/" + characteristic.value.toHex()
)
try {
if (writeQueue.size > 0) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Write confirm queue should be empty. found: " + writeQueue.size)
writeQueue.clear()
}
val offered = writeQueue.offer(writeConfirmation, WRITE_CONFIRM_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
if (!offered) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed write confirmation")
}
} catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while sending write confirmation")
}
} }
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
@ -112,57 +89,52 @@ class BleCommCallbacks(
characteristicType + "/" + characteristicType + "/" +
payload.toHex() payload.toHex()
) )
incomingPackets[characteristicType]!!.add(payload)
}
@Throws(InterruptedException::class, CouldNotConfirmDescriptorWriteException::class) incomingPackets.byCharacteristicType(characteristicType).add(payload)
fun confirmWriteDescriptor(descriptorUUID: String, timeoutMs: Long) {
val confirmed: DescriptorWriteConfirmation = descriptorWriteQueue.poll(
timeoutMs,
TimeUnit.MILLISECONDS
)
?: throw TimeoutException()
when (confirmed) {
is DescriptorWriteConfirmationError -> throw CouldNotConfirmWriteException(confirmed.status)
is DescriptorWriteConfirmationUUID ->
if (confirmed.uuid != descriptorUUID) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Could not confirm descriptor write. Got ${confirmed.uuid}. Expected: $descriptorUUID"
)
throw CouldNotConfirmDescriptorWriteException(descriptorUUID, confirmed.uuid)
} else {
aapsLogger.debug(LTag.PUMPBTCOMM, "Confirmed descriptor write : " + confirmed.uuid)
}
}
} }
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status) super.onDescriptorWrite(gatt, descriptor, status)
val writeConfirmation = if (status == BluetoothGatt.GATT_SUCCESS) {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnDescriptor value " + descriptor.value.toHex()) onWrite(status, descriptor.uuid, descriptor.value)
DescriptorWriteConfirmationUUID(descriptor.uuid.toString()) }
} else {
DescriptorWriteConfirmationError(status) private fun onWrite(status: Int, uuid: UUID?, value: ByteArray?) {
if (uuid == null || value == null) {
return
} }
try { val writeConfirmation = when {
if (descriptorWriteQueue.size > 0) { uuid == null || value == null ->
aapsLogger.warn( WriteConfirmationError("onWrite received Null: UUID=$uuid, value=${value.toHex()} status=$status")
LTag.PUMPBTCOMM, status == BluetoothGatt.GATT_SUCCESS -> {
"Descriptor write queue should be empty, found: ${descriptorWriteQueue.size}" aapsLogger.debug(LTag.PUMPBTCOMM, "OnWrite value " + value.toHex())
) WriteConfirmationSuccess(uuid.toString(), value)
descriptorWriteQueue.clear()
} }
val offered = descriptorWriteQueue.offer( else ->WriteConfirmationError("onDescriptorWrite status is not success: $status")
}
try {
flushConfirmationQueue()
val offered = writeQueue.offer(
writeConfirmation, writeConfirmation,
WRITE_CONFIRM_TIMEOUT_MS.toLong(), WRITE_CONFIRM_TIMEOUT_MS.toLong(),
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
) )
if (!offered) { if (!offered) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed descriptor write confirmation") aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed write confirmation")
} }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while sending descriptor write confirmation") aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while sending write confirmation")
}
}
fun flushConfirmationQueue() {
if (writeQueue.size > 0) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Write queue should be empty, found: ${writeQueue.size}"
)
writeQueue.clear()
} }
} }

View file

@ -1,7 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks
sealed class CharacteristicWriteConfirmation
data class CharacteristicWriteConfirmationPayload(val payload: ByteArray) : CharacteristicWriteConfirmation()
data class CharacteristicWriteConfirmationError(val status: Int) : CharacteristicWriteConfirmation()

View file

@ -1,7 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks
sealed class DescriptorWriteConfirmation
data class DescriptorWriteConfirmationUUID(val uuid: String) : DescriptorWriteConfirmation()
data class DescriptorWriteConfirmationError(val status: Int) : DescriptorWriteConfirmation()

View file

@ -0,0 +1,10 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks
sealed class WriteConfirmation
data class WriteConfirmationSuccess(val uuid: String, val payload: ByteArray) : WriteConfirmation()
data class WriteConfirmationError(
val msg: String,
val status: Int = 0
) : WriteConfirmation()

View file

@ -1,18 +1,44 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.CommandType
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.nio.ByteBuffer
class BleCommandRTS : BleCommand(BleCommandType.RTS) object BleCommandRTS : BleCommand(BleCommandType.RTS)
class BleCommandCTS : BleCommand(BleCommandType.CTS) object BleCommandCTS : BleCommand(BleCommandType.CTS)
class BleCommandAbort : BleCommand(BleCommandType.ABORT) object BleCommandAbort : BleCommand(BleCommandType.ABORT)
class BleCommandSuccess : BleCommand(BleCommandType.SUCCESS) object BleCommandSuccess : BleCommand(BleCommandType.SUCCESS)
class BleCommandFail : BleCommand(BleCommandType.FAIL) object BleCommandFail : BleCommand(BleCommandType.FAIL)
open class BleCommand(val data: ByteArray) { data class BleCommandNack(val idx: Byte) : BleCommand(BleCommandType.NACK, byteArrayOf(idx)) {
companion object {
fun parse(payload: ByteArray): BleCommand {
if (payload.size < 2) {
return BleCommandIncorrect("Incorrect NACK payload", payload)
}
if (payload[0] != BleCommandType.NACK.value) {
return BleCommandIncorrect("Incorrect NACK header", payload)
}
return BleCommandNack(payload[1])
}
}
}
data class BleCommandHello(private val controllerId: Int) : BleCommand(
BleCommandType.HELLO,
ByteBuffer.allocate(6)
.put(1.toByte()) // TODO find the meaning of this constant
.put(4.toByte()) // TODO find the meaning of this constant
.putInt(controllerId).array()
)
data class BleCommandIncorrect(val msg:String, val payload: ByteArray): BleCommand(BleCommandType.INCORRECT)
sealed class BleCommand(val data: ByteArray) {
constructor(type: BleCommandType) : this(byteArrayOf(type.value)) constructor(type: BleCommandType) : this(byteArrayOf(type.value))
@ -36,4 +62,35 @@ open class BleCommand(val data: ByteArray) {
override fun hashCode(): Int { override fun hashCode(): Int {
return data.contentHashCode() return data.contentHashCode()
} }
companion object {
fun parse(payload: ByteArray): BleCommand {
if (payload.isEmpty()) {
return BleCommandIncorrect("Incorrect command: empty payload", payload)
}
try {
return when(BleCommandType.byValue(payload[0])) {
BleCommandType.RTS ->
BleCommandRTS
BleCommandType.CTS ->
BleCommandCTS
BleCommandType.NACK ->
BleCommandNack.parse(payload)
BleCommandType.ABORT ->
BleCommandAbort
BleCommandType.SUCCESS ->
BleCommandSuccess
BleCommandType.FAIL ->
BleCommandFail
BleCommandType.HELLO ->
BleCommandIncorrect("Incorrect hello command received", payload)
BleCommandType.INCORRECT ->
BleCommandIncorrect("Incorrect command received", payload)
}
} catch (e: IllegalArgumentException) {
return BleCommandIncorrect("Incorrect command payload", payload)
}
}
}
} }

View file

@ -1,11 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
import java.nio.ByteBuffer
class BleCommandHello(controllerId: Int) : BleCommand(
BleCommandType.HELLO,
ByteBuffer.allocate(6)
.put(1.toByte()) // TODO find the meaning of this constant
.put(4.toByte()) // TODO find the meaning of this constant
.putInt(controllerId).array()
)

View file

@ -1,3 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
class BleCommandNack(idx: Byte) : BleCommand(BleCommandType.NACK, byteArrayOf(idx))

View file

@ -7,7 +7,8 @@ enum class BleCommandType(val value: Byte) {
ABORT(0x03.toByte()), ABORT(0x03.toByte()),
SUCCESS(0x04.toByte()), SUCCESS(0x04.toByte()),
FAIL(0x05.toByte()), FAIL(0x05.toByte()),
HELLO(0x06.toByte()); HELLO(0x06.toByte()),
INCORRECT(0x09.toByte());
companion object { companion object {

View file

@ -0,0 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotConfirmCommandException(val msg: String="Could not confirm command") : Exception(msg)

View file

@ -1,6 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotConfirmDescriptorWriteException(override val message: String?) : Exception(message) {
constructor(sent: String, confirmed: String) : this("Could not confirm write. Sent: {$sent} .Received: $confirmed")
constructor(status: Int) : this("Could not confirm write. Write status: $status")
}

View file

@ -1,10 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotConfirmWriteException(override val message: String?) : Exception(message) {
constructor(
sent: ByteArray,
confirmed: ByteArray
) : this("Could not confirm write. Sent: {$sent} .Received: $confirmed")
constructor(status: Int) : this("Could not confirm write. Write status: $status")
}

View file

@ -1,5 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
class CouldNotEnableNotifications(cmd: CharacteristicType) : Exception(cmd.value)

View file

@ -0,0 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotInitiateConnection(msg: String) : Exception(msg)

View file

@ -0,0 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotSendCommandException (val msg: String="Could not send command") : Exception(msg){
}

View file

@ -1,5 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
open class FailedToConnectException : Exception { open class FailedToConnectException : Exception {
constructor(message: String?) : super("Failed to connect: ${message ?: ""}") constructor(message: String?=null) : super("Failed to connect: ${message ?: ""}")
} }

View file

@ -2,5 +2,4 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.excepti
class MessageIOException : Exception { class MessageIOException : Exception {
constructor(msg: String) : super(msg) constructor(msg: String) : super(msg)
constructor(cause: Throwable) : super("Caught Exception during Message I/O", cause)
} }

View file

@ -1,3 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CouldNotSendBleException(msg: String?) : Exception(msg) class NotConnectedException(val msg: String) : Exception(msg)

View file

@ -1,3 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class BleIOBusyException : Exception() class PairingException(val msg: String) : Exception(msg)

View file

@ -1,6 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
open class ScanFailException : Exception { open class ScanException : Exception {
constructor(message: String) : super(message) constructor(message: String) : super(message)
constructor(errorCode: Int) : super("errorCode$errorCode") constructor(errorCode: Int) : super("errorCode$errorCode")
} }

View file

@ -3,7 +3,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.excepti
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.BleDiscoveredDevice import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.BleDiscoveredDevice
import java.util.* import java.util.*
class ScanFailFoundTooManyException(devices: List<BleDiscoveredDevice>) : ScanFailException("Found more than one Pod") { class ScanFailFoundTooManyException(devices: List<BleDiscoveredDevice>) : ScanException("Found more than one Pod") {
private val devices: List<BleDiscoveredDevice> = ArrayList(devices) private val devices: List<BleDiscoveredDevice> = ArrayList(devices)
val discoveredDevices: List<BleDiscoveredDevice> val discoveredDevices: List<BleDiscoveredDevice>

View file

@ -1,3 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class ScanFailNotFoundException : ScanFailException("No Pod found") class ScanFailNotFoundException : ScanException("No Pod found")

View file

@ -1,5 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommand
class UnexpectedCommandException(val cmd: BleCommand) : Exception("Unexpected command: $cmd")

View file

@ -8,127 +8,122 @@ import android.bluetooth.BluetoothGattDescriptor
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmation
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationError
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.WriteConfirmationSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
class BleIO( sealed class BleReceiveResult
private val aapsLogger: AAPSLogger, data class BleReceivePayload(val payload: ByteArray) : BleReceiveResult()
private val chars: Map<CharacteristicType, BluetoothGattCharacteristic>, data class BleReceiveError(val msg: String, val cause: Throwable? = null) : BleReceiveResult()
private val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>>,
private val gatt: BluetoothGatt,
private val bleCommCallbacks: BleCommCallbacks
) {
private var state: IOState = IOState.IDLE
sealed class BleSendResult
object BleSendSuccess : BleSendResult()
data class BleSendErrorSending(val msg: String, val cause: Throwable? = null) : BleSendResult()
data class BleSendErrorConfirming(val msg: String, val cause: Throwable? = null) : BleSendResult()
abstract class BleIO(
private val aapsLogger: AAPSLogger,
private val characteristic: BluetoothGattCharacteristic,
private val incomingPackets: BlockingQueue<ByteArray>,
private val gatt: BluetoothGatt,
private val bleCommCallbacks: BleCommCallbacks,
private val type: CharacteristicType
) {
/*** /***
* *
* @param characteristic where to read from(CMD or DATA) * @param characteristic where to read from(CMD or DATA)
* @return a byte array with the received data * @return a byte array with the received data or error
*/ */
@Throws(BleIOBusyException::class, InterruptedException::class, TimeoutException::class) fun receivePacket(timeoutMs: Long = DEFAULT_IO_TIMEOUT_MS): BleReceiveResult {
fun receivePacket(characteristic: CharacteristicType, timeoutMs:Long = DEFAULT_IO_TIMEOUT_MS): ByteArray { try {
synchronized(state) { val ret = incomingPackets.poll(timeoutMs, TimeUnit.MILLISECONDS)
if (state != IOState.IDLE) { ?: return BleReceiveError("Timeout")
throw BleIOBusyException() return BleReceivePayload(ret)
} } catch (e: InterruptedException) {
state = IOState.READING return BleReceiveError("Interrupted", cause = e)
} }
val ret = incomingPackets[characteristic]?.poll(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
?: throw TimeoutException()
synchronized(state) { state = IOState.IDLE }
return ret
}
fun peekCommand(): ByteArray? {
return incomingPackets[CharacteristicType.CMD]?.peek()
} }
/*** /***
* *
* @param characteristic where to write to(CMD or DATA) * @param characteristic where to write to(CMD or DATA)
* @param payload the data to send * @param payload the data to send
* @throws CouldNotSendBleException
*/ */
@Throws( fun sendAndConfirmPacket(payload: ByteArray): BleSendResult {
CouldNotSendBleException::class, aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on ${payload.toHex()}")
BleIOBusyException::class, val set = characteristic.setValue(payload)
InterruptedException::class,
CouldNotConfirmWriteException::class,
TimeoutException::class
)
fun sendAndConfirmPacket(characteristic: CharacteristicType, payload: ByteArray) {
synchronized(state) {
if (state != IOState.IDLE) {
throw BleIOBusyException()
}
state = IOState.WRITING
}
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on " + characteristic.name + "/" + payload.toHex())
val ch = chars[characteristic]
val set = ch!!.setValue(payload)
if (!set) { if (!set) {
throw CouldNotSendBleException("setValue") return BleSendErrorSending("Could set setValue on ${type.name}")
} }
val sent = gatt.writeCharacteristic(ch) bleCommCallbacks.flushConfirmationQueue()
val sent = gatt.writeCharacteristic(characteristic)
if (!sent) { if (!sent) {
throw CouldNotSendBleException("writeCharacteristic") return BleSendErrorSending("Could not writeCharacteristic on {$type.name}")
}
return when (val confirmation = bleCommCallbacks.confirmWrite(
payload, type.value,
DEFAULT_IO_TIMEOUT_MS)){
is WriteConfirmationError ->
BleSendErrorConfirming(confirmation.msg)
is WriteConfirmationSuccess ->
BleSendSuccess
} }
bleCommCallbacks.confirmWrite(payload, DEFAULT_IO_TIMEOUT_MS)
synchronized(state) { state = IOState.IDLE }
} }
/** /**
* Called before sending a new message. * Called before sending a new message.
* The incoming queues should be empty, so we log when they are not. * The incoming queues should be empty, so we log when they are not.
*/ */
fun flushIncomingQueues() { fun flushIncomingQueue() {
synchronized(state) { state = IOState.IDLE } do {
val found = incomingPackets.poll()?.also {
for (char in CharacteristicType.values()) { aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: queue not empty, flushing: {${it.toHex()}")
do { }
val found = incomingPackets[char]?.poll()?.also { } while (found != null)
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: ${char.name} queue not empty, flushing: {${it.toHex()}")
}
} while (found != null)
}
} }
/** /**
* Enable intentions on the characteristics. * Enable intentions on the characteristic
* This will signal the pod it can start sending back data * This will signal the pod it can start sending back data
* @return * @return
*/ */
@Throws( fun readyToRead(): BleSendResult {
CouldNotSendBleException::class, val notificationSet = gatt.setCharacteristicNotification(characteristic, true)
CouldNotEnableNotifications::class, if (!notificationSet) {
DescriptorNotFoundException::class, throw CouldNotInitiateConnection("Could not enable notifications")
InterruptedException::class,
CouldNotConfirmDescriptorWriteException::class
)
fun readyToRead() {
for (type in CharacteristicType.values()) {
val ch = chars[type]
val notificationSet = gatt.setCharacteristicNotification(ch, true)
if (!notificationSet) {
throw CouldNotEnableNotifications(type)
}
val descriptors = ch!!.descriptors
if (descriptors.size != 1) {
throw DescriptorNotFoundException()
}
val descriptor = descriptors[0]
descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
gatt.writeDescriptor(descriptor)
bleCommCallbacks.confirmWriteDescriptor(descriptor.uuid.toString(), DEFAULT_IO_TIMEOUT_MS)
} }
val descriptors = characteristic.descriptors
if (descriptors.size != 1) {
throw CouldNotInitiateConnection("Expecting one descriptor, found: ${descriptors.size}")
}
val descriptor = descriptors[0]
descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
val wrote = gatt.writeDescriptor(descriptor)
if (!wrote) {
throw CouldNotInitiateConnection("Could not enable indications on descriptor")
}
val confirmation = bleCommCallbacks.confirmWrite(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE,
descriptor.uuid.toString(),
DEFAULT_IO_TIMEOUT_MS)
if (confirmation is WriteConfirmationError) {
throw CouldNotInitiateConnection(confirmation.msg)
}
return BleSendSuccess
} }
companion object { companion object {
private const val DEFAULT_IO_TIMEOUT_MS = 1000.toLong() const val DEFAULT_IO_TIMEOUT_MS = 1000.toLong()
} }
} }

View file

@ -14,7 +14,6 @@ enum class CharacteristicType(val value: String) {
companion object { companion object {
@JvmStatic
fun byValue(value: String): CharacteristicType = fun byValue(value: String): CharacteristicType =
values().firstOrNull { it.value == value } values().firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown Characteristic Type: $value") ?: throw IllegalArgumentException("Unknown Characteristic Type: $value")

View file

@ -0,0 +1,54 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.OmnipodDashBleManagerImpl
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommand
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello
import java.util.concurrent.BlockingQueue
sealed class BleConfirmResult
object BleConfirmSuccess : BleConfirmResult()
data class BleConfirmIncorrectData(val payload: ByteArray) : BleConfirmResult()
data class BleConfirmError(val msg: String, val cause: Throwable? = null) : BleConfirmResult()
class CmdBleIO(
logger: AAPSLogger,
characteristic: BluetoothGattCharacteristic,
private val incomingPackets: BlockingQueue<ByteArray>,
gatt: BluetoothGatt,
bleCommCallbacks: BleCommCallbacks
) : BleIO(
logger,
characteristic,
incomingPackets,
gatt,
bleCommCallbacks,
CharacteristicType.CMD
) {
init {
}
fun peekCommand(): ByteArray? {
return incomingPackets.peek()
}
fun hello() = sendAndConfirmPacket(BleCommandHello(OmnipodDashBleManagerImpl.CONTROLLER_ID).data)
fun expectCommandType(expected: BleCommand, timeoutMs: Long = DEFAULT_IO_TIMEOUT_MS): BleConfirmResult {
return when (val actual = receivePacket(timeoutMs)) {
is BleReceiveError -> BleConfirmError(actual.toString())
is BleReceivePayload ->
if (actual.payload.isEmpty() || actual.payload[0] != expected.data[0]) {
BleConfirmIncorrectData(actual.payload)
} else {
BleConfirmSuccess
}
}
}
}

View file

@ -0,0 +1,22 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import java.util.concurrent.BlockingQueue
class DataBleIO(
logger: AAPSLogger,
characteristic: BluetoothGattCharacteristic,
incomingPackets: BlockingQueue<ByteArray>,
gatt: BluetoothGatt,
bleCommCallbacks: BleCommCallbacks
) : BleIO(
logger,
characteristic,
incomingPackets,
gatt,
bleCommCallbacks,
CharacteristicType.DATA
)

View file

@ -0,0 +1,17 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingDeque
class IncomingPackets {
val cmdQueue: BlockingQueue<ByteArray> = LinkedBlockingDeque()
val dataQueue: BlockingQueue<ByteArray> = LinkedBlockingDeque()
fun byCharacteristicType(char: CharacteristicType): BlockingQueue<ByteArray> {
return when (char) {
CharacteristicType.DATA -> cmdQueue
CharacteristicType.CMD -> dataQueue
}
}
}

View file

@ -3,154 +3,221 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.UnexpectedCommandException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadJoiner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.util.concurrent.TimeoutException
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) { sealed class MesssageReceiveResult
data class MessageReceiveSuccess(val msg: MessagePacket) : MesssageReceiveResult()
data class MessageReceiveError(val msg: String, val cause: Throwable? = null) : MesssageReceiveResult() {
constructor(e: PacketReceiveResult) : this("Could not read DATA packet: $e")
}
sealed class MessageSendResult
object MessageSendSuccess : MessageSendResult()
data class MessageSendErrorSending(val msg: String, val cause: Throwable? = null) : MessageSendResult() {
constructor(e: BleSendResult): this("Could not send packet: $e")
}
data class MessageSendErrorConfirming(val msg: String, val cause: Throwable? = null) : MessageSendResult() {
constructor(e: BleSendResult): this("Could not confirm packet: $e")
}
sealed class PacketReceiveResult
data class PacketReceiveSuccess(val payload: ByteArray) : PacketReceiveResult()
data class PacketReceiveError(val msg: String) : PacketReceiveResult()
class MessageIO(
private val aapsLogger: AAPSLogger,
private val cmdBleIO: CmdBleIO,
private val dataBleIO: DataBleIO,
) {
val receivedOutOfOrder = LinkedHashMap<Byte, ByteArray>() val receivedOutOfOrder = LinkedHashMap<Byte, ByteArray>()
var maxTries = 3 var maxMessageReadTries = 3
var tries = 0 var messageReadTries = 0
private fun expectCommandType(actual: BleCommand, expected: BleCommand) { fun sendMessage(msg: MessagePacket): MessageSendResult {
if (actual.data.isEmpty()) { cmdBleIO.flushIncomingQueue()
throw UnexpectedCommandException(actual) dataBleIO.flushIncomingQueue()
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data)
if (sendResult is BleSendErrorSending) {
return MessageSendErrorSending(sendResult)
} }
// first byte is the command type
if (actual.data[0] == expected.data[0]) { val expectCTS = cmdBleIO.expectCommandType(BleCommandCTS)
return if (expectCTS !is BleConfirmSuccess) {
return MessageSendErrorSending(sendResult)
} }
throw UnexpectedCommandException(actual)
}
private fun peekForNack(index: Int, packets: List<BlePacket>) {
val peekCmd = bleIO.peekCommand() ?: return
if (peekCmd.isEmpty()) {
throw UnexpectedCommandException(BleCommand(peekCmd))
}
when (BleCommandType.byValue(peekCmd[0])) {
BleCommandType.NACK -> {
if (peekCmd.size < 2) {
throw UnexpectedCommandException(BleCommand(peekCmd))
}
val missingIdx = peekCmd[1]
if (missingIdx > packets.size) {
throw UnexpectedCommandException(BleCommand(peekCmd))
}
bleIO.receivePacket(CharacteristicType.CMD) //consume NACK
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packets[missingIdx.toInt()].toByteArray())
}
BleCommandType.SUCCESS -> {
if (index != packets.size - 1) {
throw UnexpectedCommandException(BleCommand(peekCmd))
}
}
else ->
throw UnexpectedCommandException(BleCommand(peekCmd))
}
}
fun sendMessage(msg: MessagePacket) {
bleIO.flushIncomingQueues()
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandRTS().data)
val expectCTS = bleIO.receivePacket(CharacteristicType.CMD)
expectCommandType(BleCommand(expectCTS), BleCommandCTS())
val payload = msg.asByteArray() val payload = msg.asByteArray()
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending message: ${payload.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Sending message: ${payload.toHex()}")
val splitter = PayloadSplitter(payload) val splitter = PayloadSplitter(payload)
val packets = splitter.splitInPackets() val packets = splitter.splitInPackets()
for ((index, packet) in packets.withIndex()) { for ((index, packet) in packets.withIndex()) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.toByteArray().toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.toByteArray().toHex()}")
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.toByteArray()) val sendResult = dataBleIO.sendAndConfirmPacket(packet.toByteArray())
peekForNack(index, packets) val ret = handleSendResult(sendResult, index, packets)
// This is implementing the same logic as the PDM. if (ret !is MessageSendSuccess) {
// I think it wil not work in case of packet lost. return ret
// This is because each lost packet, we will receive a NACK on the next packet. }
// At the end, we will still be missing the last packet(s). val peek = peekForNack(index, packets)
// I don't worry too much about this because for commands we have retries implemented at MessagePacket level anyway if (peek !is MessageSendSuccess) {
// If this will be a problem in the future, the fix might be(pending testing with a real pod) to move back the index return if (index == packets.size - 1)
// at the value received in the NACK and make sure don't retry forever. MessageSendErrorConfirming(peek.toString())
} else
val expectSuccess = bleIO.receivePacket(CharacteristicType.CMD) MessageSendErrorSending(peek.toString())
expectCommandType(BleCommand(expectSuccess), BleCommandSuccess())
}
private fun expectBlePacket(index: Byte): ByteArray {
receivedOutOfOrder[index]?.let {
return it
}
while (tries < maxTries) {
try {
tries++
val payload = bleIO.receivePacket(CharacteristicType.DATA)
if (payload.isEmpty()) {
throw IncorrectPacketException(payload, index)
}
if (payload[0] == index) {
return payload
}
receivedOutOfOrder[payload[0]] = payload
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandNack(index).data)
} catch (e: TimeoutException) {
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandNack(index).data)
continue
} }
} }
throw TimeoutException()
return when (val expectSuccess = cmdBleIO.expectCommandType(BleCommandSuccess)) {
is BleConfirmSuccess ->
MessageSendSuccess
is BleConfirmError ->
MessageSendErrorConfirming("Error reading message confirmation: $expectSuccess")
is BleConfirmIncorrectData ->
when (val received = (BleCommand.parse((expectSuccess.payload)))) {
is BleCommandFail ->
// this can happen if CRC does not match
MessageSendErrorSending("Received FAIL after sending message")
else ->
MessageSendErrorConfirming("Received confirmation message: $received")
}
}
} }
private fun readReset() { fun receiveMessage(): MesssageReceiveResult {
maxTries = 3 cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
tries = 0
receivedOutOfOrder.clear()
}
fun receiveMessage(): MessagePacket { val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data)
val expectRTS = bleIO.receivePacket(CharacteristicType.CMD, MESSAGE_READ_TIMEOUT_MS) if (sendResult !is BleSendSuccess) {
expectCommandType(BleCommand(expectRTS), BleCommandRTS()) return MessageReceiveError("Error sending CTS: $sendResult")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandCTS().data) }
readReset() readReset()
var expected: Byte = 0 var expected: Byte = 0
try { try {
val firstPacket = expectBlePacket(0) val firstPacket = expectBlePacket(0)
val joiner = PayloadJoiner(firstPacket) if (firstPacket !is PacketReceiveSuccess) {
maxTries = joiner.fullFragments * 2 + 2 return MessageReceiveError(firstPacket)
}
val joiner = PayloadJoiner(firstPacket.payload)
maxMessageReadTries = joiner.fullFragments * 2 + 2
for (i in 1 until joiner.fullFragments + 1) { for (i in 1 until joiner.fullFragments + 1) {
expected++ expected++
val packet = expectBlePacket(expected) val packet = expectBlePacket(expected)
joiner.accumulate(packet) if (packet !is PacketReceiveSuccess) {
return MessageReceiveError(packet)
}
joiner.accumulate(packet.payload)
} }
if (joiner.oneExtraPacket) { if (joiner.oneExtraPacket) {
expected++ expected++
joiner.accumulate(expectBlePacket(expected)) val packet = expectBlePacket(expected)
if (packet !is PacketReceiveSuccess) {
return MessageReceiveError(packet)
}
joiner.accumulate(packet.payload)
} }
val fullPayload = joiner.finalize() val fullPayload = joiner.finalize()
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandSuccess().data) cmdBleIO.sendAndConfirmPacket(BleCommandSuccess.data)
return MessagePacket.parse(fullPayload) return MessageReceiveSuccess(MessagePacket.parse(fullPayload))
} catch (e: IncorrectPacketException) { } catch (e: IncorrectPacketException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Could not read message: $e") aapsLogger.warn(LTag.PUMPBTCOMM, "Could not read message: $e")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandAbort().data) cmdBleIO.sendAndConfirmPacket(BleCommandAbort.data)
throw MessageIOException(cause = e) return MessageReceiveError("Received incorrect packet: $e", cause = e)
} catch (e: CrcMismatchException) { } catch (e: CrcMismatchException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "CRC mismatch: $e") aapsLogger.warn(LTag.PUMPBTCOMM, "CRC mismatch: $e")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandFail().data) cmdBleIO.sendAndConfirmPacket(BleCommandFail.data)
throw MessageIOException(cause = e) return MessageReceiveError("CRC mismatch: $e", cause = e)
} finally {
readReset()
} }
}
private fun handleSendResult(sendResult: BleSendResult, index: Int, packets: List<BlePacket>): MessageSendResult {
return when {
sendResult is BleSendSuccess ->
MessageSendSuccess
index == packets.size - 1 && sendResult is BleSendErrorConfirming ->
return MessageSendErrorConfirming("Error confirming last DATA packet $sendResult")
else ->
return MessageSendErrorSending("Error sending DATA: $sendResult")
}
}
private fun peekForNack(index: Int, packets: List<BlePacket>): MessageSendResult {
val peekCmd = cmdBleIO.peekCommand()
?: return MessageSendSuccess
when (val receivedCmd = BleCommand.parse(peekCmd)) {
is BleCommandNack -> {
//// Consume NACK
val received = cmdBleIO.receivePacket()
if (received !is BleReceivePayload) {
return MessageSendErrorSending(received.toString())
}
val sendResult = dataBleIO.sendAndConfirmPacket(packets[receivedCmd.idx.toInt()].toByteArray())
return handleSendResult(sendResult, index, packets)
}
BleCommandSuccess -> {
if (index != packets.size) {
return MessageSendErrorSending("Received SUCCESS before sending all the data. $index")
}
return MessageSendSuccess
}
else ->
return MessageSendErrorSending("Received unexpected command: ${peekCmd.toHex()}")
}
}
private fun expectBlePacket(index: Byte, nackOnTimeout: Boolean = false): PacketReceiveResult {
receivedOutOfOrder[index]?.let {
return PacketReceiveSuccess(it)
}
var packetTries = 0
while (messageReadTries < maxMessageReadTries && packetTries < MAX_PACKET_READ_TRIES) {
messageReadTries++
packetTries++
when (val received = dataBleIO.receivePacket()) {
is BleReceiveError -> {
if (nackOnTimeout)
cmdBleIO.sendAndConfirmPacket(BleCommandNack(index).data)
aapsLogger.info(LTag.PUMPBTCOMM, "Error receiving DATA packet: $received")
}
is BleReceivePayload -> {
val payload = received.payload
if (payload.isEmpty()) {
aapsLogger.info(LTag.PUMPBTCOMM, "Received empty payload at index $index")
continue
}
if (payload[0] == index) {
return PacketReceiveSuccess(payload)
}
receivedOutOfOrder[payload[0]] = payload
cmdBleIO.sendAndConfirmPacket(BleCommandNack(index).data)
}
}
}
return PacketReceiveError("Reached the maximum number tries to read a packet")
}
private fun readReset() {
maxMessageReadTries = 3
messageReadTries = 0
receivedOutOfOrder.clear() receivedOutOfOrder.clear()
} }
companion object { companion object {
private const val MAX_PACKET_READ_TRIES = 4
private const val MESSAGE_READ_TIMEOUT_MS = 2500.toLong() private const val MESSAGE_READ_TIMEOUT_MS = 2500.toLong()
} }
} }

View file

@ -1,17 +1,18 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
import android.app.Notification
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.PairingException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.RandomByteGenerator import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.RandomByteGenerator
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.X25519KeyGenerator import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.X25519KeyGenerator
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import info.nightscout.androidaps.utils.extensions.waitMillis
internal class LTKExchanger( internal class LTKExchanger(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
@ -25,42 +26,54 @@ internal class LTKExchanger(
private var seq: Byte = 1 private var seq: Byte = 1
fun negotiateLTK(): PairResult { fun negotiateLTK(): PairResult {
// send SP1, SP2
val sp1sp2 = sp1sp2(podId.address, sp2()) val sp1sp2 = sp1sp2(podId.address, sp2())
msgIO.sendMessage(sp1sp2.messagePacket) val sendSp1Sp2Result = msgIO.sendMessage(sp1sp2.messagePacket)
if (sendSp1Sp2Result !is MessageSendSuccess) {
throw PairingException("Could not send SP1SP2: $sendSp1Sp2Result")
}
seq++ seq++
val sps1 = sps1() val sps1 = sps1()
msgIO.sendMessage(sps1.messagePacket) val sp1Result = msgIO.sendMessage(sps1.messagePacket)
// send SPS1 if (sp1Result !is MessageSendSuccess) {
throw PairingException("Could not send SP1: $sp1Result")
}
// read SPS1
val podSps1 = msgIO.receiveMessage() val podSps1 = msgIO.receiveMessage()
processSps1FromPod(podSps1) if (podSps1 !is MessageReceiveSuccess) {
throw PairingException("Could not read SPS1: $podSps1")
}
processSps1FromPod(podSps1.msg)
// now we have all the data to generate: confPod, confPdm, ltk and noncePrefix // now we have all the data to generate: confPod, confPdm, ltk and noncePrefix
seq++ seq++
// send SPS2
val sps2 = sps2() val sps2 = sps2()
msgIO.sendMessage(sps2.messagePacket) val sp2Result = msgIO.sendMessage(sps2.messagePacket)
// read SPS2 if (sp1Result !is MessageSendSuccess) {
throw PairingException("Could not send sps2: ${sp2Result}")
}
val podSps2 = msgIO.receiveMessage() val podSps2 = msgIO.receiveMessage()
validatePodSps2(podSps2) if (podSps2 !is MessageReceiveSuccess) {
throw PairingException("Could not read SPS2: $podSps2")
}
validatePodSps2(podSps2.msg)
seq++ seq++
// send SP0GP0 // send SP0GP0
msgIO.sendMessage(sp0gp0().messagePacket) val sp0gp0Result = msgIO.sendMessage(sp0gp0().messagePacket)
// read P0 if (sp0gp0Result is MessageSendErrorSending) {
throw PairingException("Could not send SP0GP0: $sp0gp0Result")
}
// TODO: failing to read or validate p0 will lead to undefined state // No exception throwing after this point. It is possible that the pod saved the LTK
// It could be that: //
// - the pod answered with p0 and we did not receive/could not process the answer
// - the pod answered with some sort of error. This is very unlikely, because we already received(and validated) SPS2 from the pod
// But if sps2 conf value is incorrect, then we would probablysee this when receiving the pod podSps2(to test)
val p0 = msgIO.receiveMessage() val p0 = msgIO.receiveMessage()
validateP0(p0) if (p0 is MessageReceiveSuccess) {
validateP0(p0.msg)
} else{
aapsLogger.warn(LTag.PUMPBTCOMM, "Could not read P0: $p0")
}
return PairResult( return PairResult(
ltk = keyExchange.ltk, ltk = keyExchange.ltk,
msgSeq = seq msgSeq = seq
@ -147,7 +160,7 @@ internal class LTKExchanger(
val payload = parseKeys(arrayOf(P0), msg.payload)[0] val payload = parseKeys(arrayOf(P0), msg.payload)[0]
aapsLogger.debug(LTag.PUMPBTCOMM, "P0 payload from pod: ${payload.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "P0 payload from pod: ${payload.toHex()}")
if (!payload.contentEquals(UNKNOWN_P0_PAYLOAD)) { if (!payload.contentEquals(UNKNOWN_P0_PAYLOAD)) {
throw MessageIOException("Invalid P0 payload received") aapsLogger.warn(LTag.PUMPBTCOMM, "Reveived invalid P0 payload: ${payload.toHex()}")
} }
} }

View file

@ -6,14 +6,14 @@ import android.bluetooth.le.ScanSettings
import android.os.ParcelUuid import android.os.ParcelUuid
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag 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.ScanException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailFoundTooManyException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailFoundTooManyException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailNotFoundException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailNotFoundException
import java.util.* import java.util.*
class PodScanner(private val logger: AAPSLogger, private val bluetoothAdapter: BluetoothAdapter) { class PodScanner(private val logger: AAPSLogger, private val bluetoothAdapter: BluetoothAdapter) {
@Throws(InterruptedException::class, ScanFailException::class) @Throws(InterruptedException::class, ScanException::class)
fun scanForPod(serviceUUID: String?, podID: Long): BleDiscoveredDevice { fun scanForPod(serviceUUID: String?, podID: Long): BleDiscoveredDevice {
val scanner = bluetoothAdapter.bluetoothLeScanner val scanner = bluetoothAdapter.bluetoothLeScanner
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()

View file

@ -5,7 +5,7 @@ import android.bluetooth.le.ScanResult
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag 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.DiscoveredInvalidPodException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanFailException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -25,10 +25,10 @@ class ScanCollector(private val logger: AAPSLogger, private val podID: Long) : S
super.onScanFailed(errorCode) super.onScanFailed(errorCode)
} }
@Throws(ScanFailException::class) fun collect(): List<BleDiscoveredDevice> { @Throws(ScanException::class) fun collect(): List<BleDiscoveredDevice> {
val ret: MutableList<BleDiscoveredDevice> = ArrayList() val ret: MutableList<BleDiscoveredDevice> = ArrayList()
if (scanFailed != 0) { if (scanFailed != 0) {
throw ScanFailException(scanFailed) throw ScanException(scanFailed)
} }
logger.debug(LTag.PUMPBTCOMM, "ScanCollector looking for podID: $podID") logger.debug(LTag.PUMPBTCOMM, "ScanCollector looking for podID: $podID")
for (result in found.values) { for (result in found.values) {

View file

@ -1,3 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
class CertainFailureException(msg: String) : Exception(msg)

View file

@ -0,0 +1,122 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import com.j256.ormlite.stmt.query.Not
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ServiceDiscoverer
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.FailedToConnectException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleSendSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CmdBleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.DataBleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.IncomingPackets
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.utils.extensions.toHex
import info.nightscout.androidaps.utils.extensions.wait
sealed class ConnectionState
object Connected : ConnectionState()
object NotConnected : ConnectionState()
class Connection(val podDevice: BluetoothDevice, private val aapsLogger: AAPSLogger, private val context: Context) {
private val incomingPackets = IncomingPackets()
private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets)
private val gattConnection: BluetoothGatt
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}")
val autoConnect = false
gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
val state = waitForConnection()
if (state !is Connected){
throw FailedToConnectException(podDevice.address)
}
}
private val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
private val discoveredCharacteristics = discoverer.discoverServices()
private val cmdBleIO = CmdBleIO(aapsLogger, discoveredCharacteristics[CharacteristicType.CMD]!!, incomingPackets
.cmdQueue, gattConnection, bleCommCallbacks)
init {
val sendResult = cmdBleIO.hello()
if (sendResult !is BleSendSuccess) {
throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}")
}
}
private val dataBleIO = DataBleIO(aapsLogger, discoveredCharacteristics[CharacteristicType.DATA]!!, incomingPackets
.dataQueue, gattConnection, bleCommCallbacks)
val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO)
var session: Session? = null
fun connect() {
if (!gattConnection.connect()) {
throw FailedToConnectException("connect() returned false")
}
if (waitForConnection() is NotConnected){
throw FailedToConnectException(podDevice.address)
}
cmdBleIO.hello()
}
fun disconnect() {
gattConnection.disconnect()
}
fun waitForConnection(): ConnectionState {
try {
bleCommCallbacks.waitForConnection(CONNECT_TIMEOUT_MS)
}catch (e: InterruptedException) {
// We are still going to check if connection was successful
aapsLogger.info(LTag.PUMPBTCOMM,"Interruped while waiting for connection")
}
return connectionState()
}
fun connectionState(): ConnectionState {
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT)
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: $connectionState")
if (connectionState != BluetoothProfile.STATE_CONNECTED) {
return NotConnected
}
return Connected
}
fun establishSession(ltk: ByteArray, msgSeq: Byte, myId: Id, podID: Id, eapSqn: ByteArray) {
val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk, eapSqn, myId, podID, msgSeq)
val keys = eapAkaExchanger.negotiateSessionKeys()
if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "CK: ${keys.ck.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "msgSequenceNumber: ${keys.msgSequenceNumber}")
aapsLogger.info(LTag.PUMPCOMM, "Nonce: ${keys.nonce}")
}
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck
)
session = Session(aapsLogger, msgIO, myId, podID, sessionKeys = keys, enDecrypt = enDecrypt)
}
companion object {
private const val CONNECT_TIMEOUT_MS = 7000
}
}

View file

@ -4,16 +4,25 @@ import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.NakResponse import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.NakResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.util.concurrent.TimeoutException
sealed class CommandSendResult
object CommandSendSuccess: CommandSendResult()
data class CommandSendErrorSending(val msg: String): CommandSendResult()
// This error marks the undefined state
data class CommandSendErrorConfirming(val msg: String): CommandSendResult()
sealed class CommandReceiveResult
data class CommandReceiveSuccess(val result: Response): CommandReceiveResult()
data class CommandReceiveError(val msg: String): CommandReceiveResult()
data class CommandAckError(val result: Response, val msg: String): CommandReceiveResult()
class Session( class Session(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
@ -24,51 +33,63 @@ class Session(
val enDecrypt: EnDecrypt val enDecrypt: EnDecrypt
) { ) {
/** fun sendCommand(cmd: Command): CommandSendResult {
* Used for commands:
* -> command with retries
* <- response, ACK TODO: retries?
* -> ACK
*/
fun sendCommand(cmd: Command): Response {
sessionKeys.msgSequenceNumber++ sessionKeys.msgSequenceNumber++
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd") aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd")
var tries = 0 var tries = 0
var certainFailure = true val msg = getCmdMessage(cmd)
var possiblyUnconfirmedCommand = false
for (i in 0..MAX_TRIES) { for (i in 0..MAX_TRIES) {
try { aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
val msg = getCmdMessage(cmd)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}") when (val sendResult = msgIO.sendMessage(msg)) {
msgIO.sendMessage(msg) is MessageSendSuccess ->
} catch (e: TimeoutException) { return CommandSendSuccess
aapsLogger.info(LTag.PUMPBTCOMM,"Exception trying to send command: $e. Try: $i/$MAX_TRIES") is MessageSendErrorConfirming -> {
} // TODO filter out certain vs uncertain errors possiblyUnconfirmedCommand = true
} aapsLogger.debug(LTag.PUMPBTCOMM, "Error confirming command: $sendResult")
certainFailure = false }
var response: Response?= null is MessageSendErrorSending ->
for (i in 0..MAX_TRIES) { aapsLogger.debug(LTag.PUMPBTCOMM, "Error sending command: $sendResult")
try {
val responseMsg = msgIO.receiveMessage()
val decrypted = enDecrypt.decrypt(responseMsg)
aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted")
response = parseResponse(decrypted)
sessionKeys.msgSequenceNumber++
val ack = getAck(responseMsg)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending ACK: ${ack.payload.toHex()} in packet $ack")
msgIO.sendMessage(ack)
} catch (e: TimeoutException) {
aapsLogger.info(LTag.PUMPBTCOMM,"Exception trying to send command: $e. Try: $i/$MAX_TRIES")
} }
} }
response?.let{
return it val errMsg = "Maximum number of tries reached. Could not send command\""
} return if (possiblyUnconfirmedCommand)
if (certainFailure) { CommandSendErrorConfirming(errMsg)
throw CertainFailureException("Could not send command") else
} CommandSendErrorSending(errMsg)
throw UncertainFailureException("Possible failure to send commnd")
} }
fun readAndAckCommandResponse(): CommandReceiveResult {
var responseMsgPacket: MessagePacket?= null
for (i in 0..MAX_TRIES) {
val responseMsg = msgIO.receiveMessage()
if (responseMsg !is MessageReceiveSuccess) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Error receiving response: $responseMsg")
continue
}
responseMsgPacket = responseMsg.msg
}
if (responseMsgPacket == null) {
return CommandReceiveError("Could not read response")
}
val decrypted = enDecrypt.decrypt(responseMsgPacket)
aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted")
val response = parseResponse(decrypted)
sessionKeys.msgSequenceNumber++
val ack = getAck(responseMsgPacket)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending ACK: ${ack.payload.toHex()} in packet $ack")
val sendResult = msgIO.sendMessage(ack)
if (sendResult !is MessageSendSuccess) {
return CommandAckError(response, "Could not ACK the response: $sendResult")
}
return CommandReceiveSuccess(response)
}
private fun parseResponse(decrypted: MessagePacket): Response { private fun parseResponse(decrypted: MessagePacket): Response {
val payload = parseKeys(arrayOf(RESPONSE_PREFIX), decrypted.payload)[0] val payload = parseKeys(arrayOf(RESPONSE_PREFIX), decrypted.payload)[0]

View file

@ -4,12 +4,16 @@ import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotInitiateConnection
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.SessionEstablishmentException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.SessionEstablishmentException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageReceiveSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageSendSuccess
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
class SessionEstablisher( class SessionEstablisher(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
@ -23,7 +27,7 @@ class SessionEstablisher(
private val controllerIV = ByteArray(IV_SIZE) private val controllerIV = ByteArray(IV_SIZE)
private var nodeIV = ByteArray(IV_SIZE) private var nodeIV = ByteArray(IV_SIZE)
private val identifier = Random().nextInt().toByte()
private val milenage = Milenage(aapsLogger, ltk, eapSqn) private val milenage = Milenage(aapsLogger, ltk, eapSqn)
init { init {
@ -36,13 +40,18 @@ class SessionEstablisher(
} }
fun negotiateSessionKeys(): SessionKeys { fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge
msgSeq++ msgSeq++
var challenge = eapAkaChallenge() var challenge = eapAkaChallenge()
msgIO.sendMessage(challenge) val sendResult = msgIO.sendMessage(challenge)
if (sendResult !is MessageSendSuccess) {
throw SessionEstablishmentException("Could not send the EAP AKA challenge: $sendResult")
}
val challengeResponse = msgIO.receiveMessage() val challengeResponse = msgIO.receiveMessage()
processChallengeResponse(challengeResponse) // TODO: what do we have to answer if challenge response does not validate? if (challengeResponse !is MessageReceiveSuccess) {
throw SessionEstablishmentException("Could not establish session: $challengeResponse")
}
processChallengeResponse(challengeResponse.msg)
msgSeq++ msgSeq++
var success = eapSuccess() var success = eapSuccess()
@ -67,7 +76,7 @@ class SessionEstablisher(
val eapMsg = EapMessage( val eapMsg = EapMessage(
code = EapCode.REQUEST, code = EapCode.REQUEST,
identifier = 189.toByte(), // TODO: find what value we need here, it's probably random identifier = identifier, // TODO: find what value we need here, it's probably random
attributes = attributes attributes = attributes
) )
return MessagePacket( return MessagePacket(
@ -80,12 +89,14 @@ class SessionEstablisher(
} }
private fun processChallengeResponse(challengeResponse: MessagePacket) { private fun processChallengeResponse(challengeResponse: MessagePacket) {
// TODO verify that identifier matches identifier from the Challenge
val eapMsg = EapMessage.parse(aapsLogger, challengeResponse.payload) val eapMsg = EapMessage.parse(aapsLogger, challengeResponse.payload)
if (eapMsg.identifier != identifier ) {
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got incorrect identifier ${eapMsg.identifier} expected: $identifier")
throw SessionEstablishmentException("Received incorrect EAP identifier: ${eapMsg.identifier}")
}
if (eapMsg.attributes.size != 2) { if (eapMsg.attributes.size != 2) {
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got message: $eapMsg") aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got message: $eapMsg")
if (eapMsg.attributes.size == 1 && eapMsg.attributes[0] is EapAkaAttributeClientErrorCode) { if (eapMsg.attributes.size == 1 && eapMsg.attributes[0] is EapAkaAttributeClientErrorCode) {
// TODO: special exception for this
throw SessionEstablishmentException("Received CLIENT_ERROR_CODE for EAP-AKA challenge: ${eapMsg.attributes[0].toByteArray().toHex()}") throw SessionEstablishmentException("Received CLIENT_ERROR_CODE for EAP-AKA challenge: ${eapMsg.attributes[0].toByteArray().toHex()}")
} }
throw SessionEstablishmentException("Expecting two attributes, got: ${eapMsg.attributes.size}") throw SessionEstablishmentException("Expecting two attributes, got: ${eapMsg.attributes.size}")
@ -108,7 +119,7 @@ class SessionEstablisher(
val eapMsg = EapMessage( val eapMsg = EapMessage(
code = EapCode.SUCCESS, code = EapCode.SUCCESS,
attributes = arrayOf(), attributes = arrayOf(),
identifier = 189.toByte() // TODO: find what value we need here identifier = identifier.toByte()
) )
return MessagePacket( return MessagePacket(

View file

@ -1,3 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
class UncertainFailureException(msg: String) : Exception(msg)

View file

@ -20,5 +20,7 @@ sealed class PodEvent {
/* Message exchange events */ /* Message exchange events */
class CommandSending(val command: Command) : PodEvent() class CommandSending(val command: Command) : PodEvent()
class CommandSent(val command: Command) : PodEvent() class CommandSent(val command: Command) : PodEvent()
class CommandSendNotConfirmed(val command: Command) : PodEvent()
class ResponseReceived(val response: Response) : PodEvent() class ResponseReceived(val response: Response) : PodEvent()
} }