dash: start using podState for BLE

implement disconnect()
various fixes after testing with a real pod(cmd is 2 bytes, message
joining, etc)
This commit is contained in:
Andrei Vereha 2021-03-06 21:12:20 +01:00
parent 4046828567
commit e7a9e24093
11 changed files with 153 additions and 75 deletions

View file

@ -36,5 +36,8 @@ data class Id(val address: ByteArray) {
fun fromInt(v: Int): Id {
return Id(ByteBuffer.allocate(4).putInt(v).array())
}
fun fromLong(v: Long): Id {
return Id(ByteBuffer.allocate(8).putLong(v).array().copyOfRange(4,8))
}
}
}

View file

@ -2,11 +2,13 @@ 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 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
@ -16,13 +18,13 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.Chara
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.EapSqn
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.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
@ -34,7 +36,8 @@ import javax.inject.Singleton
@Singleton
class OmnipodDashBleManagerImpl @Inject constructor(
private val context: Context,
private val aapsLogger: AAPSLogger
private val aapsLogger: AAPSLogger,
private val podState: OmnipodDashPodStateManager
) : OmnipodDashBleManager {
private val bluetoothManager: BluetoothManager =
@ -42,6 +45,8 @@ class OmnipodDashBleManagerImpl @Inject constructor(
private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
private var sessionKeys: SessionKeys? = null
private var msgIO: MessageIO? = null
private var gatt: BluetoothGatt? = null
private var status: ConnectionStatus = ConnectionStatus.IDLE
@Throws(
FailedToConnectException::class,
@ -54,30 +59,29 @@ class OmnipodDashBleManagerImpl @Inject constructor(
DescriptorNotFoundException::class,
CouldNotConfirmDescriptorWriteException::class
)
private fun connect(podAddress: String): BleIO {
// TODO: locking?
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
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 $podAddress")
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}")
val autoConnect = false // TODO: check what to use here
val gatt = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
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(podAddress)
throw FailedToConnectException(podDevice.address)
}
val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks)
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val chars = discoverer.discoverServices()
val bleIO = BleIO(aapsLogger, chars, incomingPackets, gatt, bleCommCallbacks)
val bleIO = BleIO(aapsLogger, chars, incomingPackets, gattConnection, bleCommCallbacks)
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandHello(CONTROLLER_ID).data)
bleIO.readyToRead()
gatt = gattConnection
return bleIO
}
@ -117,7 +121,11 @@ class OmnipodDashBleManagerImpl @Inject constructor(
}
override fun getStatus(): ConnectionStatus {
TODO("not implemented")
var s: ConnectionStatus
synchronized(status) {
s = status
}
return s
}
@Throws(
@ -132,50 +140,77 @@ class OmnipodDashBleManagerImpl @Inject constructor(
DescriptorNotFoundException::class,
CouldNotConfirmDescriptorWriteException::class
)
override fun connect(): Observable<PodEvent> = Observable.create { emitter ->
// TODO: when we are already connected,
// emit PodEvent.AlreadyConnected, complete the observable and return from this method
try {
aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation")
if (podState.bluetoothAddress == null) {
aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation")
val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
emitter.onNext(PodEvent.Scanning)
val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
emitter.onNext(PodEvent.Scanning)
val podAddress = podScanner.scanForPod(
PodScanner.SCAN_FOR_SERVICE_UUID,
PodScanner.POD_ID_NOT_ACTIVATED
).scanResult.device.address
// For tests: this.podAddress = "B8:27:EB:1D:7E:BB";
val podAddress = podScanner.scanForPod(
PodScanner.SCAN_FOR_SERVICE_UUID,
PodScanner.POD_ID_NOT_ACTIVATED
).scanResult.device.address
// For tests: this.podAddress = "B8:27:EB:1D:7E:BB";
podState.bluetoothAddress = podAddress
}
emitter.onNext(PodEvent.BluetoothConnecting)
val podAddress = podState.bluetoothAddress ?: throw FailedToConnectException("Lost connection")
// check if already connected
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT)
aapsLogger.debug(LTag.PUMPBTCOMM, "GATT connection state: $connectionState")
val bleIO = connect(podAddress)
emitter.onNext(PodEvent.BluetoothConnected(podAddress))
if (connectionState == BluetoothProfile.STATE_CONNECTED) {
podState.uniqueId ?: throw FailedToConnectException("Already connection and uniqueId is missing")
emitter.onNext(PodEvent.AlreadyConnected(podAddress, podState.uniqueId ?: 0))
emitter.onComplete()
return@create
}
if (msgIO != null) {
disconnect()
}
val bleIO = connect(podDevice)
val mIO = MessageIO(aapsLogger, bleIO)
val myId = Id.fromInt(CONTROLLER_ID)
val podId = myId.increment()
var msgSeq = 1.toByte()
val ltkExchanger = LTKExchanger(aapsLogger, mIO, myId, podId, Id.fromLong(PodScanner.POD_ID_NOT_ACTIVATED))
if (podState.ltk == null) {
emitter.onNext(PodEvent.Pairing)
val pairResult = ltkExchanger.negotiateLTK()
podState.ltk = pairResult.ltk
podState.uniqueId = podId.toLong()
msgSeq = pairResult.msgSeq
podState.eapAkaSequenceNumber = 1
if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${pairResult.ltk.toHex()}")
}
}
val ltkExchanger = LTKExchanger(aapsLogger, mIO)
emitter.onNext(PodEvent.Pairing)
val ltk = ltkExchanger.negotiateLTK()
val ltk: ByteArray = podState.ltk!!
emitter.onNext(PodEvent.EstablishingSession)
val eapSqn = EapSqn(1)
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}")
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn)
val eapSqn = podState.increaseEapAkaSequenceNumber()
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, myId, podId, msgSeq)
val keys = eapAkaExchanger.negotiateSessionKeys()
aapsLogger.info(LTag.PUMPCOMM, "CK: ${keys.ck.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "msgSequenceNumber: ${keys.msgSequenceNumber}")
aapsLogger.info(LTag.PUMPCOMM, "Nonce: ${keys.nonce}")
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
msgIO = mIO
emitter.onNext(PodEvent.Connected(ltk.podId.toLong()))
emitter.onNext(PodEvent.Connected(podId.toLong()))
emitter.onComplete()
} catch (ex: Exception) {
@ -184,7 +219,11 @@ class OmnipodDashBleManagerImpl @Inject constructor(
}
override fun disconnect() {
TODO("not implemented")
val localGatt = gatt
localGatt?.close()
gatt = null
msgIO = null
sessionKeys = null
}
companion object {

View file

@ -11,9 +11,10 @@ class BleCommandAbort : BleCommand(BleCommandType.ABORT)
class BleCommandSuccess : BleCommand(BleCommandType.SUCCESS)
class BleCommandFail : BleCommand(BleCommandType.FAIL)
open class BleCommand(val data: ByteArray) {
constructor(type: BleCommandType) : this(byteArrayOf(type.value))
constructor(type: BleCommandType) : this(byteArrayOf(type.value, 0))
constructor(type: BleCommandType, payload: ByteArray) : this(
byteArrayOf(type.value) + payload

View file

@ -32,7 +32,7 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
fullFragments == 0 -> {
crc = ByteBuffer.wrap(firstPacket.copyOfRange(2, 6)).int.toUnsignedLong()
val rest = firstPacket[6]
val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, BlePacket.MAX_SIZE)
val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, firstPacket.size)
oneExtraPacket = rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS > end
if (end > firstPacket.size) {
throw IncorrectPacketException(0, firstPacket)
@ -78,12 +78,12 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
}
crc = ByteBuffer.wrap(packet.copyOfRange(2, 6)).int.toUnsignedLong()
val rest = packet[1].toInt()
val end = min(rest + LastBlePacket.HEADER_SIZE, BlePacket.MAX_SIZE)
val end = min(rest + LastBlePacket.HEADER_SIZE, packet.size)
oneExtraPacket = rest + LastBlePacket.HEADER_SIZE > end
if (packet.size < end) {
throw IncorrectPacketException(idx.toByte(), packet)
}
fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, packet.size))
fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, end))
}
idx > fullFragments -> { // this is the extra fragment

View file

@ -18,7 +18,7 @@ import org.spongycastle.crypto.macs.CMac
import org.spongycastle.crypto.params.KeyParameter
import java.security.SecureRandom
internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO) {
internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO, val myId: Id, val podId: Id, val podAddress: Id) {
private val pdmPrivate = X25519.generatePrivateKey()
private val pdmPublic = X25519.publicFromPrivate(pdmPrivate)
@ -27,8 +27,6 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
private val pdmNonce = ByteArray(NONCE_SIZE)
private val pdmConf = ByteArray(CMAC_SIZE)
private val podConf = ByteArray(CMAC_SIZE)
private val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
val nodeId = controllerId.increment()
private var seq: Byte = 1
private var ltk = ByteArray(CMAC_SIZE)
@ -39,7 +37,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
fun negotiateLTK(): PairResult {
// send SP1, SP2
val sp1sp2 = sp1sp2(nodeId.address, sp2())
val sp1sp2 = sp1sp2(podId.address, sp2())
msgIO.sendMessage(sp1sp2.messagePacket)
seq++
@ -76,7 +74,6 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
return PairResult(
ltk = ltk,
podId = nodeId,
msgSeq = seq
)
}
@ -88,8 +85,8 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
)
return PairMessage(
sequenceNumber = seq,
source = controllerId,
destination = nodeId,
source = myId,
destination = podAddress,
payload = payload
)
}
@ -101,8 +98,8 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
)
return PairMessage(
sequenceNumber = seq,
source = controllerId,
destination = nodeId,
source = myId,
destination = podAddress,
payload = payload
)
}
@ -125,8 +122,8 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
)
return PairMessage(
sequenceNumber = seq,
source = controllerId,
destination = nodeId,
source = myId,
destination = podAddress,
payload = payload
)
}
@ -159,8 +156,8 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
val payload = SP0GP0.toByteArray()
return PairMessage(
sequenceNumber = seq,
source = controllerId,
destination = nodeId,
source = myId,
destination = podAddress,
payload = payload
)
}

View file

@ -3,7 +3,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.utils.extensions.toHex
data class PairResult(val ltk: ByteArray, val podId: Id, val msgSeq: Byte) {
data class PairResult(val ltk: ByteArray, val msgSeq: Byte) {
init {
require(ltk.size == 16) { "LTK length must be 16 bytes. Received LTK: ${ltk.toHex()}" }
}

View file

@ -78,7 +78,7 @@ data class EapMessage(
throw MessageIOException("Invalid eap payload. Expected AKA packet type: ${payload.toHex()}")
}
val attributesPayload = payload.copyOfRange(8, totalSize)
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP attributes: ${attributesPayload.toByteString()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "parsing EAP payload: ${payload.toHex()}")
return EapMessage(
code = EapCode.byValue(payload[0]),
identifier = payload[1],

View file

@ -3,32 +3,33 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
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.OmnipodDashBleManagerImpl
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce
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.MessageType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult
import info.nightscout.androidaps.utils.extensions.toHex
import java.security.SecureRandom
class SessionEstablisher(
private val aapsLogger: AAPSLogger,
private val msgIO: MessageIO,
private val ltk: PairResult,
private val eapSqn: EapSqn
private val ltk: ByteArray,
private val eapSqn: ByteArray,
private val myId: Id,
private val podId: Id,
private var msgSeq: Byte
) {
var sequenceNumber = ltk.msgSeq
private val controllerIV = ByteArray(IV_SIZE)
private var nodeIV = ByteArray(IV_SIZE)
private val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
private val milenage = Milenage(aapsLogger, ltk.ltk, eapSqn.increment())
private val milenage = Milenage(aapsLogger, ltk, eapSqn)
init {
require(eapSqn.size == 6) {"EAP-SQN has to be 6 bytes long"}
require(ltk.size == 16) {"LTK has to be 16 bytes long"}
aapsLogger.debug(LTag.PUMPBTCOMM, "Starting EAP-AKA")
val random = SecureRandom()
random.nextBytes(controllerIV)
@ -36,14 +37,14 @@ class SessionEstablisher(
fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge
sequenceNumber++ // TODO: get from pod state. This only works for activating a new pod
msgSeq++ // TODO: get from pod state. This only works for activating a new pod
var challenge = eapAkaChallenge()
msgIO.sendMessage(challenge)
val challengeResponse = msgIO.receiveMessage()
processChallengeResponse(challengeResponse) // TODO: what do we have to answer if challenge response does not validate?
sequenceNumber++
msgSeq++
var success = eapSuccess()
msgIO.sendMessage(success)
@ -53,7 +54,7 @@ class SessionEstablisher(
prefix = controllerIV + nodeIV,
sqn = 0
),
msgSequenceNumber = sequenceNumber
msgSequenceNumber = msgSeq
)
}
@ -66,14 +67,14 @@ class SessionEstablisher(
val eapMsg = EapMessage(
code = EapCode.REQUEST,
identifier = 42, // TODO: find what value we need here, it's probably random
identifier = 189.toByte(), // TODO: find what value we need here, it's probably random
attributes = attributes
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = sequenceNumber,
source = controllerId,
destination = ltk.podId,
sequenceNumber = msgSeq,
source = myId,
destination = podId,
payload = eapMsg.toByteArray()
)
}
@ -103,14 +104,14 @@ class SessionEstablisher(
val eapMsg = EapMessage(
code = EapCode.SUCCESS,
attributes = arrayOf(),
identifier = 44 // TODO: find what value we need here
identifier = 189.toByte() // TODO: find what value we need here
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = sequenceNumber,
source = controllerId,
destination = ltk.podId,
sequenceNumber = msgSeq,
source = myId,
destination = podId,
payload = eapMsg.toByteArray()
)
}

View file

@ -1,6 +1,10 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status
enum class ConnectionStatus {
CONNECTED,
NOT_CONNECTED;
IDLE,
BUSY,
CONNECTING,
ESTABLISHING_SESSION,
PAIRING,
RUNNING_COMMAND;
}

View file

@ -23,6 +23,8 @@ interface OmnipodDashPodStateManager {
val activationTime: Long?
var uniqueId: Long? // TODO make Int
var bluetoothAddress: String?
var ltk: ByteArray?
var eapAkaSequenceNumber: Long
val bluetoothVersion: SoftwareVersion?
val firmwareVersion: SoftwareVersion?
@ -46,6 +48,8 @@ interface OmnipodDashPodStateManager {
val basalProgram: BasalProgram?
fun increaseMessageSequenceNumber()
fun increaseEapAkaSequenceNumber():ByteArray
fun commitEapAkaSequenceNumber()
fun updateFromDefaultStatusResponse(response: DefaultStatusResponse)
fun updateFromVersionResponse(response: VersionResponse)
fun updateFromSetUniqueIdResponse(response: SetUniqueIdResponse)

View file

@ -13,6 +13,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.VersionResponse
import info.nightscout.androidaps.utils.sharedPreferences.SP
import java.io.Serializable
import java.nio.ByteBuffer
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@ -150,6 +151,32 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
store()
}
override var eapAkaSequenceNumber: Long
get() = podState.eapAkaSequenceNumber
set(value) {
podState.eapAkaSequenceNumber = value
store()
}
override var ltk: ByteArray?
get() = podState.ltk
set(value) {
podState.ltk = value
store()
}
override fun increaseEapAkaSequenceNumber():ByteArray {
podState.eapAkaSequenceNumber++
return ByteBuffer.allocate(8)
.putLong(podState.eapAkaSequenceNumber)
.array()
.copyOfRange(2, 8)
}
override fun commitEapAkaSequenceNumber() {
store()
}
override fun updateFromDefaultStatusResponse(response: DefaultStatusResponse) {
podState.deliveryStatus = response.deliveryStatus
podState.podStatus = response.podStatus
@ -262,6 +289,8 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
var activationTime: Long? = null
var uniqueId: Long? = null
var bluetoothAddress: String? = null
var ltk: ByteArray? = null
var eapAkaSequenceNumber: Long = 1
var bleVersion: SoftwareVersion? = null
var firmwareVersion: SoftwareVersion? = null