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
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.os.Message
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.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.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.pair.LTKExchanger
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.SessionEstablisher
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.*
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.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.extensions.toHex
import io.reactivex.Observable
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import javax.inject.Singleton
@ -40,88 +29,50 @@ class OmnipodDashBleManagerImpl @Inject constructor(
private val podState: OmnipodDashPodStateManager
) : OmnipodDashBleManager {
// TODO: add busy AtomicBoolean
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
private var sessionKeys: SessionKeys? = null
private var msgIO: MessageIO? = null
private var gatt: BluetoothGatt? = null
private var connection: Connection? = null
private var status: ConnectionStatus = ConnectionStatus.IDLE
private val myId = Id.fromInt(CONTROLLER_ID)
private val uniqueId = podState.uniqueId
private val podId = uniqueId?.let(Id::fromLong)
?: 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 ->
try {
val keys = sessionKeys
val mIO = msgIO
if (keys == null || mIO == null) {
throw Exception("Not connected")
}
val conn = connection ?: throw NotConnectedException("Not connected")
val session = conn.session ?: throw NotConnectedException("Missing session")
emitter.onNext(PodEvent.CommandSending(cmd))
// TODO switch to RX
emitter.onNext(PodEvent.CommandSent(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 sendResult = session.sendCommand(cmd)
when(sendResult) {
is CommandSendErrorSending -> {
emitter.tryOnError(CouldNotSendCommandException())
return@create
}
is CommandSendSuccess ->
emitter.onNext(PodEvent.CommandSent(cmd))
is CommandSendErrorConfirming ->
emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd))
}
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 {
@ -132,42 +83,23 @@ class OmnipodDashBleManagerImpl @Inject constructor(
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 ->
try {
emitter.onNext(PodEvent.BluetoothConnecting)
val podAddress =
podState.bluetoothAddress
?: throw FailedToConnectException("Missing bluetoothAddress, activate the pod first")
// check if already connected
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT)
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: $connectionState")
if (connectionState == BluetoothProfile.STATE_CONNECTED) {
emitter.onNext(PodEvent.AlreadyConnected(podAddress))
val conn = connection
?: Connection(podDevice, aapsLogger, context)
connection = conn
if (conn.connectionState() is Connected) {
emitter.onNext(PodEvent.Connected)
emitter.onComplete()
return@create
}
emitter.onNext(PodEvent.BluetoothConnecting)
if (msgIO != null) {
disconnect()
}
val bleIO = connect(podDevice)
val mIO = MessageIO(aapsLogger, bleIO)
msgIO = mIO
conn.connect()
emitter.onNext(PodEvent.BluetoothConnected(podAddress))
emitter.onNext(PodEvent.EstablishingSession)
@ -182,23 +114,15 @@ class OmnipodDashBleManagerImpl @Inject constructor(
}
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 uniqueId = podState.uniqueId
val podId = uniqueId?.let { Id.fromLong(uniqueId) }
?: myId.increment() // pod not activated
val eapSqn = podState.increaseEapAkaSequenceNumber()
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, myId, podId, msgSeq)
val keys = eapAkaExchanger.negotiateSessionKeys()
conn.establishSession(ltk, msgSeq, myId, podId, eapSqn)
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 ->
@ -209,7 +133,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(
emitter.onComplete()
return@create
}
aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation")
aapsLogger.info(LTag.PUMPBTCOMM, "Starting new pod activation")
emitter.onNext(PodEvent.Scanning)
val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
@ -221,13 +145,17 @@ class OmnipodDashBleManagerImpl @Inject constructor(
emitter.onNext(PodEvent.BluetoothConnecting)
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val bleIO = connect(podDevice)
val mIO = MessageIO(aapsLogger, bleIO)
msgIO = mIO
val conn = Connection(podDevice, aapsLogger, context)
connection = conn
emitter.onNext(PodEvent.BluetoothConnected(podAddress))
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()
emitter.onNext(PodEvent.Paired(podId))
podState.updateFromPairing(podId, pairResult)
@ -246,16 +174,14 @@ class OmnipodDashBleManagerImpl @Inject constructor(
}
override fun disconnect() {
val localGatt = gatt
localGatt?.close() // TODO: use disconnect?
gatt = null
msgIO = null
sessionKeys = null
if (connection == null) {
aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection")
}
connection?.disconnect()
}
companion object {
private const val CONNECT_TIMEOUT_MS = 7000
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 info.nightscout.androidaps.logging.AAPSLogger
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.IncomingPackets
import info.nightscout.androidaps.utils.extensions.toHex
import java.util.*
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class BleCommCallbacks(
private val aapsLogger: AAPSLogger,
private val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>>
private val incomingPackets: IncomingPackets,
) : BluetoothGattCallback() {
private val serviceDiscoveryComplete: CountDownLatch = CountDownLatch(1)
private val connected: CountDownLatch = CountDownLatch(1)
private val writeQueue: BlockingQueue<CharacteristicWriteConfirmation> = LinkedBlockingQueue(1)
private val descriptorWriteQueue: BlockingQueue<DescriptorWriteConfirmation> = LinkedBlockingQueue(1)
private val writeQueue: BlockingQueue<WriteConfirmation> = LinkedBlockingQueue(1)
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
@ -54,52 +51,32 @@ class BleCommCallbacks(
serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
}
@Throws(InterruptedException::class, TimeoutException::class, CouldNotConfirmWriteException::class)
fun confirmWrite(expectedPayload: ByteArray, timeoutMs: Long) {
val received: CharacteristicWriteConfirmation = writeQueue.poll(timeoutMs, TimeUnit.MILLISECONDS)
?: throw TimeoutException()
when (received) {
is CharacteristicWriteConfirmationPayload -> confirmWritePayload(expectedPayload, received)
is CharacteristicWriteConfirmationError -> throw CouldNotConfirmWriteException(received.status)
}
}
private fun confirmWritePayload(expectedPayload: ByteArray, received: CharacteristicWriteConfirmationPayload) {
if (!expectedPayload.contentEquals(received.payload)) {
fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long) : WriteConfirmation{
try {
return when(val received = writeQueue.poll(timeoutMs, TimeUnit.MILLISECONDS) ) {
null -> return WriteConfirmationError("Timeout waiting for writeConfirmation")
is WriteConfirmationSuccess ->
if (expectedPayload.contentEquals(received.payload) &&
expectedUUID == received.uuid) {
received
} else {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Could not confirm write. Got " + received.payload.toHex() + ".Excepted: " + expectedPayload.toHex()
)
throw CouldNotConfirmWriteException(expectedPayload, received.payload)
WriteConfirmationError("Received incorrect writeConfirmation")
}
is WriteConfirmationError ->
received
}
}catch (e: InterruptedException) {
return WriteConfirmationError("Interrupted waiting for confirmation")
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Confirmed write with value: " + received.payload.toHex())
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status)
val writeConfirmation = if (status == BluetoothGatt.GATT_SUCCESS) {
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")
}
onWrite(status, characteristic.uuid, characteristic.value)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
@ -112,57 +89,52 @@ class BleCommCallbacks(
characteristicType + "/" +
payload.toHex()
)
incomingPackets[characteristicType]!!.add(payload)
}
@Throws(InterruptedException::class, CouldNotConfirmDescriptorWriteException::class)
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)
}
}
incomingPackets.byCharacteristicType(characteristicType).add(payload)
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status)
val writeConfirmation = if (status == BluetoothGatt.GATT_SUCCESS) {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnDescriptor value " + descriptor.value.toHex())
DescriptorWriteConfirmationUUID(descriptor.uuid.toString())
} else {
DescriptorWriteConfirmationError(status)
onWrite(status, descriptor.uuid, descriptor.value)
}
private fun onWrite(status: Int, uuid: UUID?, value: ByteArray?) {
if (uuid == null || value == null) {
return
}
val writeConfirmation = when {
uuid == null || value == null ->
WriteConfirmationError("onWrite received Null: UUID=$uuid, value=${value.toHex()} status=$status")
status == BluetoothGatt.GATT_SUCCESS -> {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnWrite value " + value.toHex())
WriteConfirmationSuccess(uuid.toString(), value)
}
else ->WriteConfirmationError("onDescriptorWrite status is not success: $status")
}
try {
if (descriptorWriteQueue.size > 0) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Descriptor write queue should be empty, found: ${descriptorWriteQueue.size}"
)
descriptorWriteQueue.clear()
}
val offered = descriptorWriteQueue.offer(
flushConfirmationQueue()
val offered = writeQueue.offer(
writeConfirmation,
WRITE_CONFIRM_TIMEOUT_MS.toLong(),
TimeUnit.MILLISECONDS
)
if (!offered) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed descriptor write confirmation")
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed write confirmation")
}
} 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
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.CommandType
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))
@ -36,4 +62,35 @@ open class BleCommand(val data: ByteArray) {
override fun hashCode(): Int {
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()),
SUCCESS(0x04.toByte()),
FAIL(0x05.toByte()),
HELLO(0x06.toByte());
HELLO(0x06.toByte()),
INCORRECT(0x09.toByte());
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
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 {
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
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
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
open class ScanFailException : Exception {
open class ScanException : Exception {
constructor(message: String) : super(message)
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 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)
val discoveredDevices: List<BleDiscoveredDevice>

View file

@ -1,3 +1,3 @@
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.LTag
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.utils.extensions.toHex
import java.util.concurrent.BlockingQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class BleIO(
private val aapsLogger: AAPSLogger,
private val chars: Map<CharacteristicType, BluetoothGattCharacteristic>,
private val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>>,
private val gatt: BluetoothGatt,
private val bleCommCallbacks: BleCommCallbacks
) {
sealed class BleReceiveResult
data class BleReceivePayload(val payload: ByteArray) : BleReceiveResult()
data class BleReceiveError(val msg: String, val cause: Throwable? = null) : BleReceiveResult()
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)
* @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(characteristic: CharacteristicType, timeoutMs:Long = DEFAULT_IO_TIMEOUT_MS): ByteArray {
synchronized(state) {
if (state != IOState.IDLE) {
throw BleIOBusyException()
fun receivePacket(timeoutMs: Long = DEFAULT_IO_TIMEOUT_MS): BleReceiveResult {
try {
val ret = incomingPackets.poll(timeoutMs, TimeUnit.MILLISECONDS)
?: return BleReceiveError("Timeout")
return BleReceivePayload(ret)
} catch (e: InterruptedException) {
return BleReceiveError("Interrupted", cause = e)
}
state = IOState.READING
}
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 payload the data to send
* @throws CouldNotSendBleException
*/
@Throws(
CouldNotSendBleException::class,
BleIOBusyException::class,
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)
fun sendAndConfirmPacket(payload: ByteArray): BleSendResult {
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on ${payload.toHex()}")
val set = characteristic.setValue(payload)
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) {
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.
* The incoming queues should be empty, so we log when they are not.
*/
fun flushIncomingQueues() {
synchronized(state) { state = IOState.IDLE }
for (char in CharacteristicType.values()) {
fun flushIncomingQueue() {
do {
val found = incomingPackets[char]?.poll()?.also {
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: ${char.name} queue not empty, flushing: {${it.toHex()}")
val found = incomingPackets.poll()?.also {
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: 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
* @return
*/
@Throws(
CouldNotSendBleException::class,
CouldNotEnableNotifications::class,
DescriptorNotFoundException::class,
InterruptedException::class,
CouldNotConfirmDescriptorWriteException::class
)
fun readyToRead() {
for (type in CharacteristicType.values()) {
val ch = chars[type]
val notificationSet = gatt.setCharacteristicNotification(ch, true)
fun readyToRead(): BleSendResult {
val notificationSet = gatt.setCharacteristicNotification(characteristic, true)
if (!notificationSet) {
throw CouldNotEnableNotifications(type)
throw CouldNotInitiateConnection("Could not enable notifications")
}
val descriptors = ch!!.descriptors
val descriptors = characteristic.descriptors
if (descriptors.size != 1) {
throw DescriptorNotFoundException()
throw CouldNotInitiateConnection("Expecting one descriptor, found: ${descriptors.size}")
}
val descriptor = descriptors[0]
descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
gatt.writeDescriptor(descriptor)
bleCommCallbacks.confirmWriteDescriptor(descriptor.uuid.toString(), DEFAULT_IO_TIMEOUT_MS)
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 {
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 {
@JvmStatic
fun byValue(value: String): CharacteristicType =
values().firstOrNull { it.value == 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.LTag
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.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.io.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
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>()
var maxTries = 3
var tries = 0
var maxMessageReadTries = 3
var messageReadTries = 0
private fun expectCommandType(actual: BleCommand, expected: BleCommand) {
if (actual.data.isEmpty()) {
throw UnexpectedCommandException(actual)
}
// first byte is the command type
if (actual.data[0] == expected.data[0]) {
return
}
throw UnexpectedCommandException(actual)
fun sendMessage(msg: MessagePacket): MessageSendResult {
cmdBleIO.flushIncomingQueue()
dataBleIO.flushIncomingQueue()
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data)
if (sendResult is BleSendErrorSending) {
return MessageSendErrorSending(sendResult)
}
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())
val expectCTS = cmdBleIO.expectCommandType(BleCommandCTS)
if (expectCTS !is BleConfirmSuccess) {
return MessageSendErrorSending(sendResult)
}
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()
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending message: ${payload.toHex()}")
val splitter = PayloadSplitter(payload)
val packets = splitter.splitInPackets()
for ((index, packet) in packets.withIndex()) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.toByteArray().toHex()}")
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.toByteArray())
peekForNack(index, packets)
// This is implementing the same logic as the PDM.
// I think it wil not work in case of packet lost.
// 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).
// I don't worry too much about this because for commands we have retries implemented at MessagePacket level anyway
// If this will be a problem in the future, the fix might be(pending testing with a real pod) to move back the index
// at the value received in the NACK and make sure don't retry forever.
val sendResult = dataBleIO.sendAndConfirmPacket(packet.toByteArray())
val ret = handleSendResult(sendResult, index, packets)
if (ret !is MessageSendSuccess) {
return ret
}
val peek = peekForNack(index, packets)
if (peek !is MessageSendSuccess) {
return if (index == packets.size - 1)
MessageSendErrorConfirming(peek.toString())
else
MessageSendErrorSending(peek.toString())
}
val expectSuccess = bleIO.receivePacket(CharacteristicType.CMD)
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
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")
}
}
throw TimeoutException()
}
private fun readReset() {
maxTries = 3
tries = 0
receivedOutOfOrder.clear()
}
fun receiveMessage(): MesssageReceiveResult {
cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
fun receiveMessage(): MessagePacket {
val expectRTS = bleIO.receivePacket(CharacteristicType.CMD, MESSAGE_READ_TIMEOUT_MS)
expectCommandType(BleCommand(expectRTS), BleCommandRTS())
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandCTS().data)
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data)
if (sendResult !is BleSendSuccess) {
return MessageReceiveError("Error sending CTS: $sendResult")
}
readReset()
var expected: Byte = 0
try {
val firstPacket = expectBlePacket(0)
val joiner = PayloadJoiner(firstPacket)
maxTries = joiner.fullFragments * 2 + 2
if (firstPacket !is PacketReceiveSuccess) {
return MessageReceiveError(firstPacket)
}
val joiner = PayloadJoiner(firstPacket.payload)
maxMessageReadTries = joiner.fullFragments * 2 + 2
for (i in 1 until joiner.fullFragments + 1) {
expected++
val packet = expectBlePacket(expected)
joiner.accumulate(packet)
if (packet !is PacketReceiveSuccess) {
return MessageReceiveError(packet)
}
joiner.accumulate(packet.payload)
}
if (joiner.oneExtraPacket) {
expected++
joiner.accumulate(expectBlePacket(expected))
val packet = expectBlePacket(expected)
if (packet !is PacketReceiveSuccess) {
return MessageReceiveError(packet)
}
joiner.accumulate(packet.payload)
}
val fullPayload = joiner.finalize()
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandSuccess().data)
return MessagePacket.parse(fullPayload)
cmdBleIO.sendAndConfirmPacket(BleCommandSuccess.data)
return MessageReceiveSuccess(MessagePacket.parse(fullPayload))
} catch (e: IncorrectPacketException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Could not read message: $e")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandAbort().data)
throw MessageIOException(cause = e)
cmdBleIO.sendAndConfirmPacket(BleCommandAbort.data)
return MessageReceiveError("Received incorrect packet: $e", cause = e)
} catch (e: CrcMismatchException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "CRC mismatch: $e")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandFail().data)
throw MessageIOException(cause = e)
cmdBleIO.sendAndConfirmPacket(BleCommandFail.data)
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()
}
companion object {
private const val MAX_PACKET_READ_TRIES = 4
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
import android.app.Notification
import info.nightscout.androidaps.logging.AAPSLogger
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.exceptions.MessageIOException
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.StringLengthPrefixEncoding
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.PairingException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.*
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.X25519KeyGenerator
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
import info.nightscout.androidaps.utils.extensions.toHex
import info.nightscout.androidaps.utils.extensions.waitMillis
internal class LTKExchanger(
private val aapsLogger: AAPSLogger,
@ -25,42 +26,54 @@ internal class LTKExchanger(
private var seq: Byte = 1
fun negotiateLTK(): PairResult {
// send SP1, 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++
val sps1 = sps1()
msgIO.sendMessage(sps1.messagePacket)
// send SPS1
val sp1Result = msgIO.sendMessage(sps1.messagePacket)
if (sp1Result !is MessageSendSuccess) {
throw PairingException("Could not send SP1: $sp1Result")
}
// read SPS1
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
seq++
// send SPS2
val sps2 = sps2()
msgIO.sendMessage(sps2.messagePacket)
// read SPS2
val sp2Result = msgIO.sendMessage(sps2.messagePacket)
if (sp1Result !is MessageSendSuccess) {
throw PairingException("Could not send sps2: ${sp2Result}")
}
val podSps2 = msgIO.receiveMessage()
validatePodSps2(podSps2)
if (podSps2 !is MessageReceiveSuccess) {
throw PairingException("Could not read SPS2: $podSps2")
}
validatePodSps2(podSps2.msg)
seq++
// send SP0GP0
msgIO.sendMessage(sp0gp0().messagePacket)
// read P0
val sp0gp0Result = msgIO.sendMessage(sp0gp0().messagePacket)
if (sp0gp0Result is MessageSendErrorSending) {
throw PairingException("Could not send SP0GP0: $sp0gp0Result")
}
// TODO: failing to read or validate p0 will lead to undefined state
// 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)
// No exception throwing after this point. It is possible that the pod saved the LTK
//
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(
ltk = keyExchange.ltk,
msgSeq = seq
@ -147,7 +160,7 @@ internal class LTKExchanger(
val payload = parseKeys(arrayOf(P0), msg.payload)[0]
aapsLogger.debug(LTag.PUMPBTCOMM, "P0 payload from pod: ${payload.toHex()}")
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 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.ScanException
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 java.util.*
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 {
val scanner = bluetoothAdapter.bluetoothLeScanner
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.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
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ScanException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
@ -25,10 +25,10 @@ class ScanCollector(private val logger: AAPSLogger, private val podID: Long) : S
super.onScanFailed(errorCode)
}
@Throws(ScanFailException::class) fun collect(): List<BleDiscoveredDevice> {
@Throws(ScanException::class) fun collect(): List<BleDiscoveredDevice> {
val ret: MutableList<BleDiscoveredDevice> = ArrayList()
if (scanFailed != 0) {
throw ScanFailException(scanFailed)
throw ScanException(scanFailed)
}
logger.debug(LTag.PUMPBTCOMM, "ScanCollector looking for podID: $podID")
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.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.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.MessageType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.*
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.response.NakResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
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(
private val aapsLogger: AAPSLogger,
@ -24,50 +33,62 @@ class Session(
val enDecrypt: EnDecrypt
) {
/**
* Used for commands:
* -> command with retries
* <- response, ACK TODO: retries?
* -> ACK
*/
fun sendCommand(cmd: Command): Response {
fun sendCommand(cmd: Command): CommandSendResult {
sessionKeys.msgSequenceNumber++
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd")
var tries = 0
var certainFailure = true
for (i in 0..MAX_TRIES) {
try {
val msg = getCmdMessage(cmd)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
msgIO.sendMessage(msg)
} catch (e: TimeoutException) {
aapsLogger.info(LTag.PUMPBTCOMM,"Exception trying to send command: $e. Try: $i/$MAX_TRIES")
} // TODO filter out certain vs uncertain errors
}
certainFailure = false
var response: Response?= null
var possiblyUnconfirmedCommand = false
for (i in 0..MAX_TRIES) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
when (val sendResult = msgIO.sendMessage(msg)) {
is MessageSendSuccess ->
return CommandSendSuccess
is MessageSendErrorConfirming -> {
possiblyUnconfirmedCommand = true
aapsLogger.debug(LTag.PUMPBTCOMM, "Error confirming command: $sendResult")
}
is MessageSendErrorSending ->
aapsLogger.debug(LTag.PUMPBTCOMM, "Error sending command: $sendResult")
}
}
val errMsg = "Maximum number of tries reached. Could not send command\""
return if (possiblyUnconfirmedCommand)
CommandSendErrorConfirming(errMsg)
else
CommandSendErrorSending(errMsg)
}
fun readAndAckCommandResponse(): CommandReceiveResult {
var responseMsgPacket: MessagePacket?= null
for (i in 0..MAX_TRIES) {
try {
val responseMsg = msgIO.receiveMessage()
val decrypted = enDecrypt.decrypt(responseMsg)
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")
response = parseResponse(decrypted)
val response = parseResponse(decrypted)
sessionKeys.msgSequenceNumber++
val ack = getAck(responseMsg)
val ack = getAck(responseMsgPacket)
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")
val sendResult = msgIO.sendMessage(ack)
if (sendResult !is MessageSendSuccess) {
return CommandAckError(response, "Could not ACK the response: $sendResult")
}
return CommandReceiveSuccess(response)
}
response?.let{
return it
}
if (certainFailure) {
throw CertainFailureException("Could not send command")
}
throw UncertainFailureException("Possible failure to send commnd")
}
private fun parseResponse(decrypted: MessagePacket): Response {

View file

@ -4,12 +4,16 @@ import info.nightscout.androidaps.logging.AAPSLogger
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.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.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.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.utils.extensions.toHex
import java.security.SecureRandom
import java.util.*
class SessionEstablisher(
private val aapsLogger: AAPSLogger,
@ -23,7 +27,7 @@ class SessionEstablisher(
private val controllerIV = ByteArray(IV_SIZE)
private var nodeIV = ByteArray(IV_SIZE)
private val identifier = Random().nextInt().toByte()
private val milenage = Milenage(aapsLogger, ltk, eapSqn)
init {
@ -36,13 +40,18 @@ class SessionEstablisher(
}
fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge
msgSeq++
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()
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++
var success = eapSuccess()
@ -67,7 +76,7 @@ class SessionEstablisher(
val eapMsg = EapMessage(
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
)
return MessagePacket(
@ -80,12 +89,14 @@ class SessionEstablisher(
}
private fun processChallengeResponse(challengeResponse: MessagePacket) {
// TODO verify that identifier matches identifier from the Challenge
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) {
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got message: $eapMsg")
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("Expecting two attributes, got: ${eapMsg.attributes.size}")
@ -108,7 +119,7 @@ class SessionEstablisher(
val eapMsg = EapMessage(
code = EapCode.SUCCESS,
attributes = arrayOf(),
identifier = 189.toByte() // TODO: find what value we need here
identifier = identifier.toByte()
)
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 */
class CommandSending(val command: Command) : PodEvent()
class CommandSent(val command: Command) : PodEvent()
class CommandSendNotConfirmed(val command: Command) : PodEvent()
class ResponseReceived(val response: Response) : PodEvent()
}