Merge pull request #27 from 0pen-dash/andrei/retries

retries, error handling, session resynchronization
This commit is contained in:
Andrei Vereha 2021-04-07 20:55:08 +02:00 committed by GitHub
commit feed6a2f63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 2115 additions and 912 deletions

View file

@ -1,6 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="AUTODETECT_INDENTS" value="false" />
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View file

@ -4,7 +4,7 @@ import org.jetbrains.annotations.NotNull;
import info.nightscout.androidaps.queue.commands.CustomCommand;
public final class CommandAcknowledgeAlerts implements CustomCommand {
public final class CommandSilenceAlerts implements CustomCommand {
@NotNull @Override public String getStatusDescription() {
return "ACKNOWLEDGE ALERTS";
}

View file

@ -11,13 +11,23 @@ import info.nightscout.androidaps.plugins.common.ManufacturerType
import info.nightscout.androidaps.plugins.general.actions.defs.CustomAction
import info.nightscout.androidaps.plugins.general.actions.defs.CustomActionType
import info.nightscout.androidaps.plugins.pump.common.defs.PumpType
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BeepType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.ResponseType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOverviewFragment
import info.nightscout.androidaps.plugins.pump.omnipod.dash.util.mapProfileToBasalProgram
import info.nightscout.androidaps.queue.commands.CustomCommand
import info.nightscout.androidaps.utils.TimeChangeType
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import io.reactivex.Single
import io.reactivex.rxkotlin.blockingSubscribeBy
import io.reactivex.rxkotlin.subscribeBy
import org.json.JSONObject
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@ -25,6 +35,8 @@ import javax.inject.Singleton
class OmnipodDashPumpPlugin @Inject constructor(
private val omnipodManager: OmnipodDashManager,
private val podStateManager: OmnipodDashPodStateManager,
private val sp: SP,
private val profileFunction: ProfileFunction,
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
resourceHelper: ResourceHelper,
@ -51,19 +63,17 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
override fun isSuspended(): Boolean {
// TODO
return false
return podStateManager.isSuspended
}
override fun isBusy(): Boolean {
// prevents the queue from executing commands
// TODO
return true
return podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED)
}
override fun isConnected(): Boolean {
// TODO
return false
return true
}
override fun isConnecting(): Boolean {
@ -93,40 +103,127 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
override fun getPumpStatus(reason: String) {
// TODO
// TODO history
omnipodManager.getStatus(ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE).blockingSubscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in getPumpStatus: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in getPumpStatus", throwable)
},
onComplete = {
aapsLogger.debug("getPumpStatus completed")
}
)
}
override fun setNewBasalProfile(profile: Profile): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(true).enacted(true)
// TODO history
return Single.create<PumpEnactResult> { source ->
omnipodManager.setBasalProgram(mapProfileToBasalProgram(profile)).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in setNewBasalProfile: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in setNewBasalProfile", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).enacted(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("setNewBasalProfile completed")
source.onSuccess(PumpEnactResult(injector).success(true).enacted(true))
}
)
}.blockingGet()
}
override fun isThisProfileSet(profile: Profile): Boolean {
// TODO
return true
}
override fun isThisProfileSet(profile: Profile): Boolean = podStateManager.basalProgram?.let {
it == mapProfileToBasalProgram(profile)
} ?: true
override fun lastDataTime(): Long {
// TODO
return System.currentTimeMillis()
return podStateManager.lastConnection
}
override val baseBasalRate: Double
get() = 0.0 // TODO
get() = podStateManager.basalProgram?.rateAt(Date()) ?: 0.0
override val reservoirLevel: Double
get() = 0.0 // TODO
get() {
if (podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED)) {
return 0.0
}
// Omnipod only reports reservoir level when there's < 1023 pulses left
return podStateManager.pulsesRemaining?.let {
it * 0.05
} ?: 75.0
}
override val batteryLevel: Int
// Omnipod Dash doesn't report it's battery level. We return 0 here and hide related fields in the UI
get() = 0
override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(false).enacted(false).comment("TODO")
// TODO history
// TODO update Treatments (?)
// TODO bolus progress
// TODO report actual delivered amount after Pod Alarm and bolus cancellation
return Single.create<PumpEnactResult> { source ->
val bolusBeeps = sp.getBoolean(R.string.key_omnipod_common_bolus_beeps_enabled, false)
omnipodManager.bolus(
detailedBolusInfo.insulin,
bolusBeeps,
bolusBeeps
).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in deliverTreatment: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in deliverTreatment", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).enacted(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("deliverTreatment completed")
source.onSuccess(
PumpEnactResult(injector).success(true).enacted(true).bolusDelivered(detailedBolusInfo.insulin)
.carbsDelivered(detailedBolusInfo.carbs)
)
}
)
}.blockingGet()
}
override fun stopBolusDelivering() {
// TODO
// TODO history
// TODO update Treatments (?)
omnipodManager.stopBolus().blockingSubscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in stopBolusDelivering: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in stopBolusDelivering", throwable)
},
onComplete = {
aapsLogger.debug("stopBolusDelivering completed")
}
)
}
override fun setTempBasalAbsolute(
@ -135,8 +232,33 @@ class OmnipodDashPumpPlugin @Inject constructor(
profile: Profile,
enforceNew: Boolean
): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(false).enacted(false).comment("TODO")
// TODO history
// TODO update Treatments
return Single.create<PumpEnactResult> { source ->
omnipodManager.setTempBasal(
absoluteRate,
durationInMinutes.toShort()
).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in setTempBasalAbsolute: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in setTempBasalAbsolute", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).enacted(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("setTempBasalAbsolute completed")
source.onSuccess(
PumpEnactResult(injector).success(true).enacted(true).absolute(absoluteRate)
.duration(durationInMinutes)
)
}
)
}.blockingGet()
}
override fun setTempBasalPercent(
@ -157,8 +279,29 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(false).enacted(false).comment("TODO")
// TODO history
// TODO update Treatments
return Single.create<PumpEnactResult> { source ->
omnipodManager.stopTempBasal().subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in cancelTempBasal: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in cancelTempBasal", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).enacted(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("cancelTempBasal completed")
source.onSuccess(
PumpEnactResult(injector).success(true).enacted(true)
)
}
)
}.blockingGet()
}
override fun cancelExtendedBolus(): PumpEnactResult {
@ -183,8 +326,11 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
override fun serialNumber(): String {
// TODO
return "TODO"
return if (podStateManager.uniqueId == null) {
"n/a" // TODO i18n
} else {
podStateManager.uniqueId.toString()
}
}
override fun shortStatus(veryShort: Boolean): String {
@ -214,11 +360,185 @@ class OmnipodDashPumpPlugin @Inject constructor(
}
override fun executeCustomCommand(customCommand: CustomCommand): PumpEnactResult? {
return when (customCommand) {
is CommandSilenceAlerts ->
silenceAlerts()
is CommandSuspendDelivery ->
suspendDelivery()
is CommandResumeDelivery ->
resumeDelivery()
is CommandDeactivatePod ->
deactivatePod()
is CommandHandleTimeChange ->
handleTimeChange()
is CommandUpdateAlertConfiguration ->
updateAlertConfiguration()
is CommandPlayTestBeep ->
playTestBeep()
else -> {
aapsLogger.warn(LTag.PUMP, "Unsupported custom command: " + customCommand.javaClass.name)
PumpEnactResult(injector).success(false).enacted(false).comment(
resourceHelper.gs(
R.string.omnipod_common_error_unsupported_custom_command,
customCommand.javaClass.name
)
)
}
}
}
private fun silenceAlerts(): PumpEnactResult {
// TODO history
// TODO filter alert types
return podStateManager.activeAlerts?.let {
Single.create<PumpEnactResult> { source ->
omnipodManager.silenceAlerts(it).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in silenceAlerts: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in silenceAlerts", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("silenceAlerts completed")
source.onSuccess(PumpEnactResult(injector).success(true))
}
)
}.blockingGet()
} ?: PumpEnactResult(injector).success(false).enacted(false).comment("No active alerts") // TODO i18n
}
private fun suspendDelivery(): PumpEnactResult {
// TODO history
return Single.create<PumpEnactResult> { source ->
omnipodManager.suspendDelivery().subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in suspendDelivery: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in suspendDelivery", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("suspendDelivery completed")
source.onSuccess(PumpEnactResult(injector).success(true))
}
)
}.blockingGet()
}
private fun resumeDelivery(): PumpEnactResult {
// TODO history
return profileFunction.getProfile()?.let {
Single.create<PumpEnactResult> { source ->
omnipodManager.setBasalProgram(mapProfileToBasalProgram(it)).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in resumeDelivery: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in resumeDelivery", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("resumeDelivery completed")
source.onSuccess(PumpEnactResult(injector).success(true))
}
)
}.blockingGet()
} ?: PumpEnactResult(injector).success(false).enacted(false).comment("No profile active") // TODO i18n
}
private fun deactivatePod(): PumpEnactResult {
// TODO history
return Single.create<PumpEnactResult> { source ->
omnipodManager.deactivatePod().subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in deactivatePod: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in deactivatePod", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("deactivatePod completed")
source.onSuccess(PumpEnactResult(injector).success(true))
}
)
}.blockingGet()
}
private fun handleTimeChange(): PumpEnactResult {
// TODO
return null
return PumpEnactResult(injector).success(false).enacted(false).comment("NOT IMPLEMENTED")
}
private fun updateAlertConfiguration(): PumpEnactResult {
// TODO
return PumpEnactResult(injector).success(false).enacted(false).comment("NOT IMPLEMENTED")
}
private fun playTestBeep(): PumpEnactResult {
// TODO history
return Single.create<PumpEnactResult> { source ->
omnipodManager.playBeep(BeepType.LONG_SINGLE_BEEP).subscribeBy(
onNext = { podEvent ->
aapsLogger.debug(
LTag.PUMP,
"Received PodEvent in playTestBeep: $podEvent"
)
},
onError = { throwable ->
aapsLogger.error(LTag.PUMP, "Error in playTestBeep", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).enacted(false).comment(throwable.message))
},
onComplete = {
aapsLogger.debug("playTestBeep completed")
source.onSuccess(
PumpEnactResult(injector).success(true).enacted(true)
)
}
)
}.blockingGet()
}
override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) {
// TODO
val eventHandlingEnabled = sp.getBoolean(R.string.key_omnipod_common_time_change_event_enabled, false)
aapsLogger.info(
LTag.PUMP,
"Time, Date and/or TimeZone changed. [timeChangeType=" + timeChangeType.name + ", eventHandlingEnabled=" + eventHandlingEnabled + "]"
)
if (timeChangeType == TimeChangeType.TimeChanged) {
aapsLogger.info(LTag.PUMP, "Ignoring time change because it is not a DST or TZ change")
return
} else if (!podStateManager.isPodRunning) {
aapsLogger.info(LTag.PUMP, "Ignoring time change because no Pod is active")
return
}
aapsLogger.info(LTag.PUMP, "Handling time change")
commandQueue.customCommand(CommandHandleTimeChange(false), null)
}
}

View file

@ -26,11 +26,11 @@ interface OmnipodDashManager {
fun setTempBasal(rate: Double, durationInMinutes: Short): Observable<PodEvent>
fun cancelTempBasal(): Observable<PodEvent>
fun stopTempBasal(): Observable<PodEvent>
fun bolus(units: Double, confirmationBeeps: Boolean, completionBeeps: Boolean): Observable<PodEvent>
fun cancelBolus(): Observable<PodEvent>
fun stopBolus(): Observable<PodEvent>
fun playBeep(beepType: BeepType): Observable<PodEvent>

View file

@ -170,6 +170,8 @@ class OmnipodDashManagerImpl @Inject constructor(
.build(),
DefaultStatusResponse::class
)
}.doOnComplete {
podStateManager.basalProgram = basalProgram
}
}
@ -246,7 +248,8 @@ class OmnipodDashManagerImpl @Inject constructor(
Observable.defer {
Observable.timer(podStateManager.firstPrimeBolusVolume!!.toLong(), TimeUnit.SECONDS)
.flatMap { Observable.empty() }
})
}
)
observables.add(
Observable.defer {
bleManager.sendCommand(
@ -347,7 +350,8 @@ class OmnipodDashManagerImpl @Inject constructor(
Observable.defer {
Observable.timer(podStateManager.secondPrimeBolusVolume!!.toLong(), TimeUnit.SECONDS)
.flatMap { Observable.empty() }
})
}
)
observables.add(
observeSendProgramBolusCommand(
podStateManager.secondPrimeBolusVolume!! * 0.05,
@ -483,7 +487,7 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io)
}
override fun cancelTempBasal(): Observable<PodEvent> {
override fun stopTempBasal(): Observable<PodEvent> {
return Observable.concat(
observePodRunning,
observeConnectToPod,
@ -512,7 +516,7 @@ class OmnipodDashManagerImpl @Inject constructor(
.subscribeOn(aapsSchedulers.io)
}
override fun cancelBolus(): Observable<PodEvent> {
override fun stopBolus(): Observable<PodEvent> {
return Observable.concat(
observePodRunning,
observeConnectToPod,

View file

@ -3,6 +3,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm
import info.nightscout.androidaps.utils.extensions.toHex
import java.nio.ByteBuffer
data class Id(val address: ByteArray) {
init {
require(address.size == 4)
@ -46,11 +47,12 @@ data class Id(val address: ByteArray) {
companion object {
private const val PERIPHERAL_NODE_INDEX = 1 // TODO: understand the meaning of this value. It comes from preferences
private const val PERIPHERAL_NODE_INDEX = 1
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

@ -0,0 +1,20 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
class Ids(podState: OmnipodDashPodStateManager) {
val myId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
private val uniqueId = podState.uniqueId
val podId = uniqueId?.let(Id::fromLong)
?: myId.increment() // pod not activated
companion object {
fun notActivated(): Id {
return Id.fromLong(
PodScanner
.POD_ID_NOT_ACTIVATED
)
}
}
}

View file

@ -1,26 +1,15 @@
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
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
@ -28,9 +17,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.
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 java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.reflect.KClass
@ -42,91 +29,64 @@ class OmnipodDashBleManagerImpl @Inject constructor(
private val podState: OmnipodDashPodStateManager
) : OmnipodDashBleManager {
private val busy = AtomicBoolean(false)
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
}
private val ids = Ids(podState)
override fun sendCommand(cmd: Command, responseType: KClass<out Response>): Observable<PodEvent> =
Observable.create { emitter ->
if (!busy.compareAndSet(false, true)) {
throw BusyException()
}
try {
val keys = sessionKeys
val mIO = msgIO
if (keys == null || mIO == null) {
throw Exception("Not connected")
}
val session = assertSessionEstablished()
emitter.onNext(PodEvent.CommandSending(cmd))
// TODO switch to RX
emitter.onNext(PodEvent.CommandSent(cmd))
when (session.sendCommand(cmd)) {
is CommandSendErrorSending -> {
emitter.tryOnError(CouldNotSendCommandException())
return@create
}
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck
)
is CommandSendSuccess ->
emitter.onNext(PodEvent.CommandSent(cmd))
is CommandSendErrorConfirming ->
emitter.onNext(PodEvent.CommandSendNotConfirmed(cmd))
}
val session = Session(
aapsLogger = aapsLogger,
msgIO = mIO,
myId = myId,
podId = podId,
sessionKeys = keys,
enDecrypt = enDecrypt
)
val response = session.sendCommand(cmd, responseType)
emitter.onNext(PodEvent.ResponseReceived(cmd, response))
when (val readResult = session.readAndAckResponse(responseType)) {
is CommandReceiveSuccess ->
emitter.onNext(PodEvent.ResponseReceived(cmd, readResult.result))
is CommandAckError ->
emitter.onNext(PodEvent.ResponseReceived(cmd, readResult.result))
is CommandReceiveError -> {
emitter.tryOnError(MessageIOException("Could not read response: $readResult"))
return@create
}
}
emitter.onComplete()
} catch (ex: Exception) {
disconnect()
emitter.tryOnError(ex)
} finally {
busy.set(false)
}
}
private fun assertSessionEstablished(): Session {
val conn = assertConnected()
return conn.session
?: throw NotConnectedException("Missing session")
}
override fun getStatus(): ConnectionStatus {
// TODO is this used?
var s: ConnectionStatus
synchronized(status) {
s = status
@ -134,42 +94,32 @@ 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 ->
if (!busy.compareAndSet(false, true)) {
throw BusyException()
}
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) {
if (conn.session == null) {
emitter.onNext(PodEvent.EstablishingSession)
establishSession(1.toByte())
emitter.onNext(PodEvent.Connected)
} else {
emitter.onNext(PodEvent.AlreadyConnected(podAddress))
}
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)
@ -180,30 +130,46 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} catch (ex: Exception) {
disconnect()
emitter.tryOnError(ex)
} finally {
busy.set(false)
}
}
private fun establishSession(msgSeq: Byte) {
val mIO = msgIO ?: 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 conn = assertConnected()
val eapSqn = podState.increaseEapAkaSequenceNumber()
val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn, myId, podId, msgSeq)
val keys = eapAkaExchanger.negotiateSessionKeys()
podState.commitEapAkaSequenceNumber()
val ltk = assertPaired()
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}")
var eapSqn = podState.increaseEapAkaSequenceNumber()
var newSqn = conn.establishSession(ltk, msgSeq, ids, eapSqn)
if (newSqn != null) {
aapsLogger.info(LTag.PUMPBTCOMM, "Updating EAP SQN to: $newSqn")
podState.eapAkaSequenceNumber = newSqn.toLong()
newSqn = conn.establishSession(ltk, msgSeq, ids, podState.increaseEapAkaSequenceNumber())
if (newSqn != null) {
throw SessionEstablishmentException("Received resynchronization SQN for the second time")
}
}
sessionKeys = keys
podState.commitEapAkaSequenceNumber()
}
private fun assertPaired(): ByteArray {
return podState.ltk
?: throw FailedToConnectException("Missing LTK, activate the pod first")
}
private fun assertConnected(): Connection {
return connection
?: throw FailedToConnectException("connection lost")
}
override fun pairNewPod(): Observable<PodEvent> = Observable.create { emitter ->
if (!busy.compareAndSet(false, true)) {
throw BusyException()
}
try {
if (podState.ltk != null) {
@ -211,7 +177,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)
@ -223,16 +189,19 @@ 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,
ids,
)
val pairResult = ltkExchanger.negotiateLTK()
emitter.onNext(PodEvent.Paired(podId))
podState.updateFromPairing(podId, pairResult)
emitter.onNext(PodEvent.Paired(ids.podId))
podState.updateFromPairing(ids.podId, pairResult)
if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${pairResult.ltk.toHex()}")
}
@ -244,20 +213,18 @@ class OmnipodDashBleManagerImpl @Inject constructor(
} catch (ex: Exception) {
disconnect()
emitter.tryOnError(ex)
} finally {
busy.set(false)
}
}
override fun disconnect() {
val localGatt = gatt
localGatt?.close() // TODO: use disconnect?
gatt = null
msgIO = null
sessionKeys = null
connection?.disconnect()
?: aapsLogger.info(LTag.PUMPBTCOMM, "Trying to disconnect a null connection")
}
companion object {
private const val CONNECT_TIMEOUT_MS = 7000
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
}
}

View file

@ -5,8 +5,7 @@ import android.bluetooth.BluetoothGattCharacteristic
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.exceptions.CharacteristicNotFoundException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ServiceNotFoundException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ConnectException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import java.math.BigInteger
import java.util.*
@ -20,18 +19,20 @@ class ServiceDiscoverer(
/***
* This is first step after connection establishment
*/
@Throws(InterruptedException::class, ServiceNotFoundException::class, CharacteristicNotFoundException::class)
fun discoverServices(): Map<CharacteristicType, BluetoothGattCharacteristic> {
logger.debug(LTag.PUMPBTCOMM, "Discovering services")
gatt.discoverServices()
val discover = gatt.discoverServices()
if (!discover) {
throw ConnectException("Could not start discovering services`")
}
bleCallbacks.waitForServiceDiscovery(DISCOVER_SERVICES_TIMEOUT_MS)
logger.debug(LTag.PUMPBTCOMM, "Services discovered")
val service = gatt.getService(SERVICE_UUID.toUuid())
?: throw ServiceNotFoundException(SERVICE_UUID)
?: throw ConnectException("Service not found: $SERVICE_UUID")
val cmdChar = service.getCharacteristic(CharacteristicType.CMD.uuid)
?: throw CharacteristicNotFoundException(CharacteristicType.CMD.value)
val dataChar = service.getCharacteristic(CharacteristicType.DATA.uuid) // TODO: this is never used
?: throw CharacteristicNotFoundException(CharacteristicType.DATA.value)
?: throw ConnectException("Characteristic not found: ${CharacteristicType.CMD.value}")
val dataChar = service.getCharacteristic(CharacteristicType.DATA.uuid)
?: throw ConnectException("Characteristic not found: ${CharacteristicType.DATA.value}")
var chars = mapOf(
CharacteristicType.CMD to cmdChar,
CharacteristicType.DATA to dataChar

View file

@ -7,93 +7,181 @@ 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.plugins.pump.omnipod.dash.driver.comm.session.DisconnectHandler
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,
private val disconnectHandler: DisconnectHandler,
) : 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)
// Synchronized because they can be:
// - read from various callbacks
// - written from resetConnection that is called onConnectionLost
private var serviceDiscoveryComplete: CountDownLatch = CountDownLatch(1)
@Synchronized get
@Synchronized set
private var connected: CountDownLatch = CountDownLatch(1)
@Synchronized get
@Synchronized set
private val writeQueue: BlockingQueue<WriteConfirmation> = LinkedBlockingQueue()
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
aapsLogger.debug(LTag.PUMPBTCOMM, "OnConnectionStateChange with status/state: $status/$newState")
super.onConnectionStateChange(gatt, status, newState)
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
connected.countDown()
}
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
disconnectHandler.onConnectionLost(status)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnServicesDiscovered with status: $status")
super.onServicesDiscovered(gatt, status)
aapsLogger.debug(LTag.PUMPBTCOMM, "OnServicesDiscovered with status$status")
if (status == BluetoothGatt.GATT_SUCCESS) {
serviceDiscoveryComplete.countDown()
}
}
@Throws(InterruptedException::class)
fun waitForConnection(timeoutMs: Int) {
connected.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
try {
connected.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for Connection")
}
}
@Throws(InterruptedException::class)
fun waitForServiceDiscovery(timeoutMs: Int) {
serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
}
@Throws(InterruptedException::class, TimeoutException::class, CouldNotConfirmWriteException::class)
fun confirmWrite(expectedPayload: ByteArray, timeoutMs: Int) {
val received: CharacteristicWriteConfirmation = writeQueue.poll(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
?: throw TimeoutException()
when (received) {
is CharacteristicWriteConfirmationPayload -> confirmWritePayload(expectedPayload, received)
is CharacteristicWriteConfirmationError -> throw CouldNotConfirmWriteException(received.status)
try {
serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while waiting for ServiceDiscovery")
}
}
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)
fun confirmWrite(expectedPayload: ByteArray, expectedUUID: String, timeoutMs: Long): WriteConfirmation {
try {
return when (val received = writeQueue.poll(timeoutMs, TimeUnit.MILLISECONDS)) {
null -> 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()
)
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()
"OnCharacteristicWrite with char/status " +
"${characteristic.uuid} /" +
"$status"
)
try {
if (writeQueue.size > 0) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Write confirm queue should be empty. found: " + writeQueue.size)
writeQueue.clear()
super.onCharacteristicWrite(gatt, characteristic, status)
onWrite(status, characteristic.uuid, characteristic.value)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
super.onCharacteristicChanged(gatt, characteristic)
val payload = characteristic.value
val characteristicType = byValue(characteristic.uuid.toString())
aapsLogger.debug(
LTag.PUMPBTCOMM,
"OnCharacteristicChanged with char/value " +
characteristicType + "/" +
payload.toHex()
)
val insertResult = incomingPackets.byCharacteristicType(characteristicType).add(payload)
if (!insertResult) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Could not insert read data to the incoming queue: $characteristicType")
}
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status)
aapsLogger.debug(
LTag.PUMPBTCOMM,
"OnDescriptorWrite with descriptor/status " +
descriptor.uuid.toString() + "/" +
status + "/"
)
onWrite(status, descriptor.uuid, descriptor.value)
}
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
aapsLogger.debug(
LTag.PUMPBTCOMM,
"onMtuChanged with MTU/status: $mtu/$status "
)
}
override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) {
super.onReadRemoteRssi(gatt, rssi, status)
aapsLogger.debug(
LTag.PUMPBTCOMM,
"onReadRemoteRssi with rssi/status: $rssi/$status "
)
}
override fun onPhyUpdate(gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, status: Int) {
super.onPhyUpdate(gatt, txPhy, rxPhy, status)
aapsLogger.debug(
LTag.PUMPBTCOMM,
"onPhyUpdate with txPhy/rxPhy/status: $txPhy/$rxPhy/$status "
)
}
private fun onWrite(status: Int, uuid: UUID?, value: ByteArray?) {
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)
}
val offered = writeQueue.offer(writeConfirmation, WRITE_CONFIRM_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
else -> WriteConfirmationError("onDescriptorWrite status is not success: $status")
}
try {
flushConfirmationQueue()
val offered = writeQueue.offer(
writeConfirmation,
WRITE_CONFIRM_TIMEOUT_MS.toLong(),
TimeUnit.MILLISECONDS
)
if (!offered) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed write confirmation")
}
@ -102,70 +190,23 @@ class BleCommCallbacks(
}
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
super.onCharacteristicChanged(gatt, characteristic)
val payload = characteristic.value
val characteristicType = byValue(characteristic.uuid.toString())
aapsLogger.debug(
LTag.PUMPBTCOMM,
"OnCharacteristicChanged with char/value " +
characteristicType + "/" +
payload.toHex()
)
incomingPackets[characteristicType]!!.add(payload)
}
@Throws(InterruptedException::class, CouldNotConfirmDescriptorWriteException::class)
fun confirmWriteDescriptor(descriptorUUID: String, timeoutMs: Int) {
val confirmed: DescriptorWriteConfirmation = descriptorWriteQueue.poll(
timeoutMs.toLong(),
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) {
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)
}
try {
if (descriptorWriteQueue.size > 0) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Descriptor write queue should be empty, found: ${descriptorWriteQueue.size}"
)
descriptorWriteQueue.clear()
}
val offered = descriptorWriteQueue.offer(
writeConfirmation,
WRITE_CONFIRM_TIMEOUT_MS.toLong(),
TimeUnit.MILLISECONDS
fun flushConfirmationQueue() {
if (writeQueue.size > 0) {
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Write queue should be empty, found: ${writeQueue.size}"
)
if (!offered) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received delayed descriptor write confirmation")
}
} catch (e: InterruptedException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Interrupted while sending descriptor write confirmation")
writeQueue.clear()
}
}
fun resetConnection() {
aapsLogger.debug(LTag.PUMPBTCOMM, "Reset connection")
connected = CountDownLatch(1)
serviceDiscoveryComplete = CountDownLatch(1)
flushConfirmationQueue()
}
companion object {
private const val WRITE_CONFIRM_TIMEOUT_MS = 10 // the confirmation queue should be empty anyway

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,45 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
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 {
return when {
payload.size < 2 ->
BleCommandIncorrect("Incorrect NACK payload", payload)
payload[0] != BleCommandType.NACK.value ->
BleCommandIncorrect("Incorrect NACK header", payload)
else ->
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 +63,36 @@ 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)
}
return try {
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) {
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,11 +7,11 @@ 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 {
@JvmStatic
fun byValue(value: Byte): BleCommandType =
values().firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown BleCommandType: $value")

View file

@ -1,3 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class BleIOBusyException : Exception()
class BusyException : Exception("Bluetooth busy")

View file

@ -1,3 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class CharacteristicNotFoundException(cmdCharacteristicUuid: String) : FailedToConnectException("characteristic not found: $cmdCharacteristicUuid")

View file

@ -1,3 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class DescriptorNotFoundException : Exception()
class ConnectException(val msg: String) : 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

@ -2,4 +2,5 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.excepti
import info.nightscout.androidaps.utils.extensions.toHex
class CouldNotParseMessageException(val payload: ByteArray) : Exception("Could not parse message payload: ${payload.toHex()}")
class CouldNotParseMessageException(val payload: ByteArray) :
Exception("Could not parse message payload: ${payload.toHex()}")

View file

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

View file

@ -0,0 +1,3 @@
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,5 @@
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

@ -6,4 +6,4 @@ import kotlin.reflect.KClass
class IllegalResponseException(
expectedResponseType: KClass<out Response>,
actualResponse: Response
) : Exception("Illegal response: expected ${expectedResponseType.simpleName} but got $actualResponse")
) : Exception("Illegal response: expected ${expectedResponseType.simpleName} but got $actualResponse")

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

@ -2,4 +2,5 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.excepti
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.NakResponse
class NakResponseException(val response: NakResponse) : Exception("Received NAK response: ${response.nakErrorType.value} ${response.nakErrorType.name}")
class NakResponseException(val response: NakResponse) :
Exception("Received NAK response: ${response.nakErrorType.value} ${response.nakErrorType.name}")

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 ScanFailNotFoundException : ScanFailException("No Pod found")
class PairingException(val msg: String) : Exception(msg)

View file

@ -10,4 +10,4 @@ class PodAlarmException(val response: AlarmStatusResponse) : Exception(
response.alarmType.value.toInt() and 0xff,
response.alarmType.name
)
)
)

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 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class ServiceNotFoundException(serviceUuid: String) : FailedToConnectException("service not found: $serviceUuid")

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,121 +8,130 @@ 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.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(
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()
open class BleIO(
private val aapsLogger: AAPSLogger,
private val chars: Map<CharacteristicType, BluetoothGattCharacteristic>,
private val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>>,
var characteristic: BluetoothGattCharacteristic,
private val incomingPackets: BlockingQueue<ByteArray>,
private val gatt: BluetoothGatt,
private val bleCommCallbacks: BleCommCallbacks
private val bleCommCallbacks: BleCommCallbacks,
private val type: CharacteristicType
) {
private var state: IOState = IOState.IDLE
/***
*
* @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): ByteArray {
synchronized(state) {
if (state != IOState.IDLE) {
throw BleIOBusyException()
fun receivePacket(timeoutMs: Long = DEFAULT_IO_TIMEOUT_MS): ByteArray? {
return try {
val packet = incomingPackets.poll(timeoutMs, TimeUnit.MILLISECONDS)
if (packet == null) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Timeout reading $type packet")
}
state = IOState.READING
packet
} catch (e: InterruptedException) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Interrupted while reading packet: $e")
null
}
val ret = incomingPackets[characteristic]?.poll(DEFAULT_IO_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
?: throw TimeoutException()
synchronized(state) { state = IOState.IDLE }
return ret
}
/***
*
* @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)
@Suppress("ReturnCount")
fun sendAndConfirmPacket(payload: ByteArray): BleSendResult {
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending on $type: ${payload.toHex()}")
val set = characteristic.setValue(payload)
if (!set) {
throw CouldNotSendBleException("setValue")
return BleSendErrorSending("Could set setValue on $type")
}
val sent = gatt.writeCharacteristic(ch)
bleCommCallbacks.flushConfirmationQueue()
val sent = gatt.writeCharacteristic(characteristic)
if (!sent) {
throw CouldNotSendBleException("writeCharacteristic")
return BleSendErrorSending("Could not writeCharacteristic on $type")
}
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() {
for (char in CharacteristicType.values()) {
do {
val found = incomingPackets[char]?.poll()?.also {
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: ${char.name} queue not empty, flushing: {${it.toHex()}")
}
} while (found != null)
}
fun flushIncomingQueue() {
do {
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)
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)
fun readyToRead(): BleSendResult {
gatt.setCharacteristicNotification(characteristic, true)
.assertTrue("enable notifications")
val descriptors = characteristic.descriptors
if (descriptors.size != 1) {
throw ConnectException("Expecting one descriptor, found: ${descriptors.size}")
}
val descriptor = descriptors[0]
descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
gatt.writeDescriptor(descriptor)
.assertTrue("enable indications on descriptor")
aapsLogger.debug(LTag.PUMPBTCOMM, "Enabling indications for $type")
val confirmation = bleCommCallbacks.confirmWrite(
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE,
descriptor.uuid.toString(),
DEFAULT_IO_TIMEOUT_MS
)
return when (confirmation) {
is WriteConfirmationError ->
throw ConnectException(confirmation.msg)
is WriteConfirmationSuccess ->
BleSendSuccess
}
}
companion object {
private const val DEFAULT_IO_TIMEOUT_MS = 60000
const val DEFAULT_IO_TIMEOUT_MS = 1000.toLong()
}
}
private fun Boolean.assertTrue(operation: String) {
if (!this) {
throw ConnectException("Could not $operation")
}
}

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,48 @@
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) : BleConfirmResult()
class CmdBleIO(
logger: AAPSLogger,
characteristic: BluetoothGattCharacteristic,
private val incomingPackets: BlockingQueue<ByteArray>,
gatt: BluetoothGatt,
bleCommCallbacks: BleCommCallbacks
) : BleIO(
logger,
characteristic,
incomingPackets,
gatt,
bleCommCallbacks,
CharacteristicType.CMD
) {
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 receivePacket(timeoutMs)?.let {
if (it.isNotEmpty() && it[0] == expected.data[0])
BleConfirmSuccess
else
BleConfirmIncorrectData(it)
}
?: BleConfirmError("Error reading packet")
}
}

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 -> dataQueue
CharacteristicType.CMD -> cmdQueue
}
}
}

View file

@ -3,6 +3,6 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import info.nightscout.androidaps.utils.extensions.toHex
class IncorrectPacketException(
val expectedIndex: Byte,
val payload: ByteArray
val payload: ByteArray,
val expectedIndex: Byte? = null
) : Exception("Invalid payload: ${payload.toHex()}. Expected index: $expectedIndex")

View file

@ -3,72 +3,222 @@ 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.plugins.pump.omnipod.dash.driver.comm.packet.PayloadJoiner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.PayloadSplitter
import info.nightscout.androidaps.utils.extensions.toHex
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
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")
}
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)
}
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 maxMessageReadTries = 3
var messageReadTries = 0
@Suppress("ReturnCount")
fun sendMessage(msg: MessagePacket): MessageSendResult {
cmdBleIO.flushIncomingQueue()
dataBleIO.flushIncomingQueue()
val rtsSendResult = cmdBleIO.sendAndConfirmPacket(BleCommandRTS.data)
if (rtsSendResult is BleSendErrorSending) {
return MessageSendErrorSending(rtsSendResult)
}
val expectCTS = cmdBleIO.expectCommandType(BleCommandCTS)
if (expectCTS !is BleConfirmSuccess) {
return MessageSendErrorSending(expectCTS.toString())
}
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 (packet in packets) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.asByteArray().toHex()}")
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.asByteArray())
for ((index, packet) in packets.withIndex()) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.toByteArray().toHex()}")
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())
}
}
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")
}
}
// TODO: peek for NACKs
val expectSuccess = bleIO.receivePacket(CharacteristicType.CMD)
expectCommandType(BleCommand(expectSuccess), BleCommandSuccess())
// TODO: handle NACKS/FAILS/etc
}
// TODO: use higher timeout when receiving the first packet in a message
fun receiveMessage(firstCmd: ByteArray? = null): MessagePacket {
var expectRTS = firstCmd
if (expectRTS == null) {
expectRTS = bleIO.receivePacket(CharacteristicType.CMD)
@Suppress("ReturnCount")
fun receiveMessage(): MessagePacket? {
val expectRTS = cmdBleIO.expectCommandType(BleCommandRTS, MESSAGE_READ_TIMEOUT_MS)
if (expectRTS !is BleConfirmSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading RTS: $expectRTS")
return null
}
expectCommandType(BleCommand(expectRTS), BleCommandRTS())
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandCTS().data)
val sendResult = cmdBleIO.sendAndConfirmPacket(BleCommandCTS.data)
if (sendResult !is BleSendSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error sending CTS: $sendResult")
return null
}
readReset()
var expected: Byte = 0
try {
val joiner = PayloadJoiner(bleIO.receivePacket(CharacteristicType.DATA))
val firstPacket = expectBlePacket(0)
if (firstPacket !is PacketReceiveSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading first packet:$firstPacket")
return null
}
val joiner = PayloadJoiner(firstPacket.payload)
maxMessageReadTries = joiner.fullFragments * 2 + 2
for (i in 1 until joiner.fullFragments + 1) {
joiner.accumulate(bleIO.receivePacket(CharacteristicType.DATA))
expected++
val nackOnTimeout = !joiner.oneExtraPacket && i == joiner.fullFragments // last packet
val packet = expectBlePacket(expected, nackOnTimeout)
if (packet !is PacketReceiveSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading packet:$packet")
return null
}
joiner.accumulate(packet.payload)
}
if (joiner.oneExtraPacket) {
joiner.accumulate(bleIO.receivePacket(CharacteristicType.DATA))
expected++
val packet = expectBlePacket(expected, true)
if (packet !is PacketReceiveSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Error reading packet:$packet")
return null
}
joiner.accumulate(packet.payload)
}
val fullPayload = joiner.finalize()
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandSuccess().data)
cmdBleIO.sendAndConfirmPacket(BleCommandSuccess.data)
return MessagePacket.parse(fullPayload)
} catch (e: IncorrectPacketException) {
aapsLogger.warn(LTag.PUMPBTCOMM, "Received incorrect packet: $e")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandNack(e.expectedIndex).data)
throw MessageIOException(cause = e)
cmdBleIO.sendAndConfirmPacket(BleCommandAbort.data)
return null
} 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 null
} 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 ->
MessageSendErrorConfirming("Error confirming last DATA packet $sendResult")
else ->
MessageSendErrorSending("Error sending DATA: $sendResult")
}
}
private fun peekForNack(index: Int, packets: List<BlePacket>): MessageSendResult {
val peekCmd = cmdBleIO.peekCommand()
?: return MessageSendSuccess
return when (val receivedCmd = BleCommand.parse(peekCmd)) {
is BleCommandNack -> {
// // Consume NACK
val received = cmdBleIO.receivePacket()
if (received == null) {
MessageSendErrorSending(received.toString())
} else {
val sendResult = dataBleIO.sendAndConfirmPacket(packets[receivedCmd.idx.toInt()].toByteArray())
handleSendResult(sendResult, index, packets)
}
}
BleCommandSuccess -> {
if (index != packets.size)
MessageSendErrorSending("Received SUCCESS before sending all the data. $index")
else
MessageSendSuccess
}
else ->
MessageSendErrorSending("Received unexpected command: ${peekCmd.toHex()}")
}
}
@Suppress("ReturnCount")
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++
val received = dataBleIO.receivePacket()
if (received == null || received.isEmpty()) {
if (nackOnTimeout)
cmdBleIO.sendAndConfirmPacket(BleCommandNack(index).data)
aapsLogger.info(
LTag.PUMPBTCOMM,
"Error reading index: $index. Received: $received. NackOnTimeout: " +
"$nackOnTimeout"
)
continue
}
if (received[0] == index) {
return PacketReceiveSuccess(received)
}
receivedOutOfOrder[received[0]] = received
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

@ -15,7 +15,7 @@ data class MessagePacket(
val sequenceNumber: Byte,
val ack: Boolean = false,
val ackNumber: Byte = 0.toByte(),
val eqos: Short = 0.toShort(), // TODO: understand. Seems to be set to 1 for commands
val eqos: Short = 0.toShort(),
val priority: Boolean = false,
val lastMessage: Boolean = false,
val gateway: Boolean = false,
@ -75,9 +75,8 @@ data class MessagePacket(
private const val HEADER_SIZE = 16
fun parse(payload: ByteArray): MessagePacket {
if (payload.size < HEADER_SIZE) {
throw CouldNotParseMessageException(payload)
}
payload.assertSizeAtLeast(HEADER_SIZE)
if (payload.copyOfRange(0, 2).decodeToString() != MAGIC_PATTERN) {
throw CouldNotParseMessageException(payload)
}
@ -100,9 +99,8 @@ data class MessagePacket(
val sequenceNumber = payload[4]
val ackNumber = payload[5]
val size = (payload[6].toInt() shl 3) or (payload[7].toUnsignedInt() ushr 5)
if (size + HEADER_SIZE > payload.size) {
throw CouldNotParseMessageException(payload)
}
payload.assertSizeAtLeast(size + HEADER_SIZE)
val payloadEnd = 16 + size +
if (type == MessageType.ENCRYPTED) 8 // TAG
else 0
@ -146,3 +144,9 @@ private class Flag(var value: Int = 0) {
}
internal fun Byte.toUnsignedInt() = this.toInt() and 0xff
private fun ByteArray.assertSizeAtLeast(size: Int) {
if (this.size < size) {
throw CouldNotParseMessageException(this)
}
}

View file

@ -1,118 +0,0 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.CrcMismatchException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.IncorrectPacketException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.crc32
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.FirstBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastOptionalPlusOneBlePacket
import java.lang.Integer.min
import java.nio.ByteBuffer
import java.util.*
class PayloadJoiner(private val firstPacket: ByteArray) {
var oneExtraPacket: Boolean = false
val fullFragments: Int
var crc: Long = 0
private var expectedIndex = 0
private val fragments: LinkedList<ByteArray> = LinkedList<ByteArray>()
init {
if (firstPacket.size < FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS) {
throw IncorrectPacketException(0, firstPacket)
}
fullFragments = firstPacket[1].toInt()
when {
// Without middle packets
firstPacket.size < FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS ->
throw IncorrectPacketException(0, firstPacket)
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, firstPacket.size)
oneExtraPacket = rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS > end
if (end > firstPacket.size) {
throw IncorrectPacketException(0, firstPacket)
}
fragments.add(firstPacket.copyOfRange(FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, end))
}
// With middle packets
firstPacket.size < BlePacket.MAX_SIZE ->
throw IncorrectPacketException(0, firstPacket)
else -> {
fragments.add(
firstPacket.copyOfRange(
FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS,
BlePacket.MAX_SIZE
)
)
}
}
}
fun accumulate(packet: ByteArray) {
if (packet.size < 3) { // idx, size, at least 1 byte of payload
throw IncorrectPacketException((expectedIndex + 1).toByte(), packet)
}
val idx = packet[0].toInt()
if (idx != expectedIndex + 1) {
throw IncorrectPacketException((expectedIndex + 1).toByte(), packet)
}
expectedIndex++
when {
idx < fullFragments -> { // this is a middle fragment
if (packet.size < BlePacket.MAX_SIZE) {
throw IncorrectPacketException(idx.toByte(), packet)
}
fragments.add(packet.copyOfRange(1, BlePacket.MAX_SIZE))
}
idx == fullFragments -> { // this is the last fragment
if (packet.size < LastBlePacket.HEADER_SIZE) {
throw IncorrectPacketException(idx.toByte(), packet)
}
crc = ByteBuffer.wrap(packet.copyOfRange(2, 6)).int.toUnsignedLong()
val rest = packet[1].toInt()
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, end))
}
idx > fullFragments -> { // this is the extra fragment
val size = packet[1].toInt()
if (packet.size < LastOptionalPlusOneBlePacket.HEADER_SIZE + size) {
throw IncorrectPacketException(idx.toByte(), packet)
}
fragments.add(
packet.copyOfRange(
LastOptionalPlusOneBlePacket.HEADER_SIZE,
LastOptionalPlusOneBlePacket.HEADER_SIZE + size
)
)
}
}
}
fun finalize(): ByteArray {
val totalLen = fragments.fold(0, { acc, elem -> acc + elem.size })
val bb = ByteBuffer.allocate(totalLen)
fragments.map { fragment -> bb.put(fragment) }
bb.flip()
val bytes = bb.array()
if (bytes.crc32() != crc) {
throw CrcMismatchException(bytes.crc32(), crc, bytes)
}
return bytes.copyOfRange(0, bytes.size)
}
}
internal fun Int.toUnsignedLong() = this.toLong() and 0xffffffffL

View file

@ -14,26 +14,22 @@ class StringLengthPrefixEncoding private constructor() {
private const val LENGTH_BYTES = 2
fun parseKeys(keys: Array<String>, payload: ByteArray): Array<ByteArray> {
val ret = Array<ByteArray>(keys.size, { ByteArray(0) })
val ret = Array(keys.size) { ByteArray(0) }
var remaining = payload
for ((index, key) in keys.withIndex()) {
remaining.assertSizeAtLeast(key.length)
when {
remaining.size < key.length ->
throw MessageIOException("Payload too short: ${payload.toHex()} for key: $key")
!(remaining.copyOfRange(0, key.length).decodeToString() == key) ->
remaining.copyOfRange(0, key.length).decodeToString() != key ->
throw MessageIOException("Key not found: $key in ${payload.toHex()}")
// last key can be empty, no length
index == keys.size - 1 && remaining.size == key.length ->
return ret
remaining.size < key.length + LENGTH_BYTES ->
throw MessageIOException("Length not found: for $key in ${payload.toHex()}")
}
remaining.assertSizeAtLeast(key.length + LENGTH_BYTES)
remaining = remaining.copyOfRange(key.length, remaining.size)
val length = (remaining[0].toUnsignedInt() shl 1) or remaining[1].toUnsignedInt()
if (length > remaining.size) {
throw MessageIOException("Payload too short, looking for length $length for $key in ${payload.toHex()}")
}
remaining.assertSizeAtLeast(length)
ret[index] = remaining.copyOfRange(LENGTH_BYTES, LENGTH_BYTES + length)
remaining = remaining.copyOfRange(LENGTH_BYTES + length, remaining.size)
}
@ -50,7 +46,7 @@ class StringLengthPrefixEncoding private constructor() {
val k = keys[idx]
val payload = payloads[idx]
bb.put(k.toByteArray())
if (payload.size > 0) {
if (payload.isNotEmpty()) {
bb.putShort(payload.size.toShort())
bb.put(payload)
}
@ -64,3 +60,9 @@ class StringLengthPrefixEncoding private constructor() {
}
}
}
private fun ByteArray.assertSizeAtLeast(size: Int) {
if (this.size < size) {
throw MessageIOException("Payload too short: ${this.toHex()}")
}
}

View file

@ -1,10 +1,12 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.IncorrectPacketException
import java.nio.ByteBuffer
sealed class BlePacket {
abstract fun asByteArray(): ByteArray
abstract val payload: ByteArray
abstract fun toByteArray(): ByteArray
companion object {
@ -13,17 +15,18 @@ sealed class BlePacket {
}
data class FirstBlePacket(
val totalFragments: Byte,
val payload: ByteArray,
val fullFragments: Int,
override val payload: ByteArray,
val size: Byte? = null,
val crc32: Long? = null
val crc32: Long? = null,
val oneExtraPacket: Boolean = false
) : BlePacket() {
override fun asByteArray(): ByteArray {
override fun toByteArray(): ByteArray {
val bb = ByteBuffer
.allocate(MAX_SIZE)
.put(0) // index
.put(totalFragments) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket
.put(fullFragments.toByte()) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket
crc32?.let {
bb.putInt(crc32.toInt())
}
@ -42,32 +45,88 @@ data class FirstBlePacket(
companion object {
internal const val HEADER_SIZE_WITHOUT_MIDDLE_PACKETS = 7 // we are using all fields
internal const val HEADER_SIZE_WITH_MIDDLE_PACKETS = 2
fun parse(payload: ByteArray): FirstBlePacket {
payload.assertSizeAtLeast(HEADER_SIZE_WITH_MIDDLE_PACKETS, 0)
if (payload[0].toInt() != 0) {
// most likely we lost the first packet.
throw IncorrectPacketException(payload, 0)
}
val fullFragments = payload[1].toInt()
require(fullFragments < MAX_FRAGMENTS) { "Received more than $MAX_FRAGMENTS fragments" }
payload.assertSizeAtLeast(HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, 0)
return when {
fullFragments == 0 -> {
val rest = payload[6]
val end = Integer.min(rest + HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, payload.size)
payload.assertSizeAtLeast(end, 0)
FirstBlePacket(
fullFragments = fullFragments,
payload = payload.copyOfRange(HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, end),
crc32 = ByteBuffer.wrap(payload.copyOfRange(2, 6)).int.toUnsignedLong(),
size = rest,
oneExtraPacket = rest + HEADER_SIZE_WITHOUT_MIDDLE_PACKETS > end
)
}
// With middle packets
payload.size < MAX_SIZE ->
throw IncorrectPacketException(payload, 0)
else -> {
FirstBlePacket(
fullFragments = fullFragments,
payload = payload.copyOfRange(HEADER_SIZE_WITH_MIDDLE_PACKETS, MAX_SIZE)
)
}
}
}
private const val HEADER_SIZE_WITHOUT_MIDDLE_PACKETS = 7 // we are using all fields
private const val HEADER_SIZE_WITH_MIDDLE_PACKETS = 2
internal const val CAPACITY_WITHOUT_MIDDLE_PACKETS =
MAX_SIZE - HEADER_SIZE_WITHOUT_MIDDLE_PACKETS // we are using all fields
internal const val CAPACITY_WITH_MIDDLE_PACKETS =
MAX_SIZE - HEADER_SIZE_WITH_MIDDLE_PACKETS // we are not using crc32 or size
internal const val CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET = 18
private const val MAX_FRAGMENTS = 15 // 15*20=300 bytes
}
}
data class MiddleBlePacket(val index: Byte, val payload: ByteArray) : BlePacket() {
data class MiddleBlePacket(val index: Byte, override val payload: ByteArray) : BlePacket() {
override fun asByteArray(): ByteArray {
override fun toByteArray(): ByteArray {
return byteArrayOf(index) + payload
}
companion object {
fun parse(payload: ByteArray): MiddleBlePacket {
payload.assertSizeAtLeast(MAX_SIZE)
return MiddleBlePacket(
index = payload[0],
payload.copyOfRange(1, MAX_SIZE)
)
}
internal const val CAPACITY = 19
}
}
data class LastBlePacket(val index: Byte, val size: Byte, val payload: ByteArray, val crc32: Long) : BlePacket() {
data class LastBlePacket(
val index: Byte,
val size: Byte,
override val payload: ByteArray,
val crc32: Long,
val oneExtraPacket: Boolean = false
) : BlePacket() {
override fun asByteArray(): ByteArray {
override fun toByteArray(): ByteArray {
val bb = ByteBuffer
.allocate(MAX_SIZE)
.put(index)
@ -83,19 +142,61 @@ data class LastBlePacket(val index: Byte, val size: Byte, val payload: ByteArray
companion object {
internal const val HEADER_SIZE = 6
fun parse(payload: ByteArray): LastBlePacket {
payload.assertSizeAtLeast(HEADER_SIZE)
val rest = payload[1]
val end = Integer.min(rest + HEADER_SIZE, payload.size)
payload.assertSizeAtLeast(end)
return LastBlePacket(
index = payload[0],
crc32 = ByteBuffer.wrap(payload.copyOfRange(2, 6)).int.toUnsignedLong(),
oneExtraPacket = rest + HEADER_SIZE > end,
size = rest,
payload = payload.copyOfRange(HEADER_SIZE, end)
)
}
private const val HEADER_SIZE = 6
internal const val CAPACITY = MAX_SIZE - HEADER_SIZE
}
}
data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray, val size: Byte) : BlePacket() {
data class LastOptionalPlusOneBlePacket(
val index: Byte,
override val payload: ByteArray,
val size: Byte
) : BlePacket() {
override fun asByteArray(): ByteArray {
override fun toByteArray(): ByteArray {
return byteArrayOf(index, size) + payload + ByteArray(MAX_SIZE - payload.size - 2)
}
companion object {
internal const val HEADER_SIZE = 2
fun parse(payload: ByteArray): LastOptionalPlusOneBlePacket {
payload.assertSizeAtLeast(2)
val size = payload[1].toInt()
payload.assertSizeAtLeast(HEADER_SIZE + size)
return LastOptionalPlusOneBlePacket(
index = payload[0],
payload = payload.copyOfRange(
HEADER_SIZE,
HEADER_SIZE + size
),
size = size.toByte(),
)
}
private const val HEADER_SIZE = 2
}
}
private fun ByteArray.assertSizeAtLeast(size: Int, index: Byte? = null) {
if (this.size < size) {
throw IncorrectPacketException(this, index)
}
}

View file

@ -0,0 +1,69 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.CrcMismatchException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.IncorrectPacketException
import java.nio.ByteBuffer
import java.util.*
class PayloadJoiner(private val firstPacket: ByteArray) {
var oneExtraPacket: Boolean
val fullFragments: Int
var crc: Long = 0
private var expectedIndex = 0
private val fragments: MutableList<BlePacket> = LinkedList<BlePacket>()
init {
val firstPacket = FirstBlePacket.parse(firstPacket)
fragments.add(firstPacket)
fullFragments = firstPacket.fullFragments
crc = firstPacket.crc32 ?: 0
oneExtraPacket = firstPacket.oneExtraPacket
}
fun accumulate(packet: ByteArray) {
if (packet.size < 3) { // idx, size, at least 1 byte of payload
throw IncorrectPacketException(packet, (expectedIndex + 1).toByte())
}
val idx = packet[0].toInt()
if (idx != expectedIndex + 1) {
throw IncorrectPacketException(packet, (expectedIndex + 1).toByte())
}
expectedIndex++
when {
idx < fullFragments -> {
fragments.add(MiddleBlePacket.parse(packet))
}
idx == fullFragments -> {
val lastPacket = LastBlePacket.parse(packet)
fragments.add(lastPacket)
crc = lastPacket.crc32
oneExtraPacket = lastPacket.oneExtraPacket
}
idx == fullFragments+1 && oneExtraPacket -> {
fragments.add(LastOptionalPlusOneBlePacket.parse(packet))
}
idx > fullFragments -> {
throw IncorrectPacketException(packet, idx.toByte())
}
}
}
fun finalize(): ByteArray {
val payloads = fragments.map { x -> x.payload }
val totalLen = payloads.fold(0, { acc, elem -> acc + elem.size })
val bb = ByteBuffer.allocate(totalLen)
payloads.map { p -> bb.put(p) }
bb.flip()
val bytes = bb.array()
if (bytes.crc32() != crc) {
throw CrcMismatchException(bytes.crc32(), crc, bytes)
}
return bytes.copyOfRange(0, bytes.size)
}
}
internal fun Int.toUnsignedLong() = this.toLong() and 0xffffffffL

View file

@ -1,60 +1,33 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.FirstBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastOptionalPlusOneBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.MiddleBlePacket
import java.lang.Integer.min
import java.util.zip.CRC32
internal class PayloadSplitter(private val payload: ByteArray) {
fun splitInPackets(): List<BlePacket> {
if (payload.size <= FirstBlePacket.CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET) {
return splitInOnePacket()
}
val ret = ArrayList<BlePacket>()
val crc32 = payload.crc32()
if (payload.size <= FirstBlePacket.CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET) {
val end = min(FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS, payload.size)
ret.add(
FirstBlePacket(
totalFragments = 0,
payload = payload.copyOfRange(0, end),
size = payload.size.toByte(),
crc32 = crc32
)
)
if (payload.size > FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS) {
ret.add(
LastOptionalPlusOneBlePacket(
index = 1,
payload = payload.copyOfRange(end, payload.size),
size = (payload.size - end).toByte()
)
)
}
return ret
}
val middleFragments = (payload.size - FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS) / MiddleBlePacket.CAPACITY
val rest =
((payload.size - middleFragments * MiddleBlePacket.CAPACITY) - FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS).toByte()
(
(payload.size - middleFragments * MiddleBlePacket.CAPACITY) -
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS
).toByte()
ret.add(
FirstBlePacket(
totalFragments = (middleFragments + 1).toByte(),
fullFragments = middleFragments + 1,
payload = payload.copyOfRange(0, FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS)
)
)
for (i in 1..middleFragments) {
val p = if (i == 1) {
payload.copyOfRange(
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS,
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + MiddleBlePacket.CAPACITY
)
} else {
payload.copyOfRange(
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + (i - 1) * MiddleBlePacket.CAPACITY,
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + i * MiddleBlePacket.CAPACITY
)
}
val p = payload.copyOfRange(
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + (i - 1) * MiddleBlePacket.CAPACITY,
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + i * MiddleBlePacket.CAPACITY
)
ret.add(
MiddleBlePacket(
index = i.toByte(),
@ -80,7 +53,9 @@ internal class PayloadSplitter(private val payload: ByteArray) {
index = (middleFragments + 2).toByte(),
size = (rest - LastBlePacket.CAPACITY).toByte(),
payload = payload.copyOfRange(
middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + LastBlePacket.CAPACITY,
middleFragments * MiddleBlePacket.CAPACITY +
FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS +
LastBlePacket.CAPACITY,
payload.size
)
)
@ -88,6 +63,30 @@ internal class PayloadSplitter(private val payload: ByteArray) {
}
return ret
}
private fun splitInOnePacket(): List<BlePacket> {
val ret = ArrayList<BlePacket>()
val crc32 = payload.crc32()
val end = min(FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS, payload.size)
ret.add(
FirstBlePacket(
fullFragments = 0,
payload = payload.copyOfRange(0, end),
size = payload.size.toByte(),
crc32 = crc32
)
)
if (payload.size > FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS) {
ret.add(
LastOptionalPlusOneBlePacket(
index = 1,
payload = payload.copyOfRange(end, payload.size),
size = (payload.size - end).toByte()
)
)
}
return ret
}
}
internal fun ByteArray.crc32(): Long {

View file

@ -3,9 +3,13 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
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.Ids
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.PairingException
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.MessageSendErrorSending
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageSendSuccess
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.pod.util.RandomByteGenerator
@ -16,50 +20,68 @@ import info.nightscout.androidaps.utils.extensions.toHex
internal class LTKExchanger(
private val aapsLogger: AAPSLogger,
private val msgIO: MessageIO,
val myId: Id,
val podId: Id,
val podAddress: Id
private val ids: Ids,
) {
private val podAddress = Ids.notActivated()
private val keyExchange = KeyExchange(aapsLogger, X25519KeyGenerator(), RandomByteGenerator())
private var seq: Byte = 1
@Throws(PairingException::class)
fun negotiateLTK(): PairResult {
// send SP1, SP2
val sp1sp2 = sp1sp2(podId.address, sp2())
msgIO.sendMessage(sp1sp2.messagePacket)
val sp1sp2 = PairMessage(
sequenceNumber = seq,
source = ids.myId,
destination = podAddress,
keys = arrayOf(SP1, SP2),
payloads = arrayOf(ids.podId.address, sp2())
)
throwOnSendError(sp1sp2.messagePacket, SP1+SP2)
seq++
val sps1 = sps1()
msgIO.sendMessage(sps1.messagePacket)
// send SPS1
val sps1 = PairMessage(
sequenceNumber = seq,
source = ids.myId,
destination = podAddress,
keys = arrayOf(SPS1),
payloads = arrayOf(keyExchange.pdmPublic + keyExchange.pdmNonce)
)
throwOnSendError(sps1.messagePacket, SPS1)
// read SPS1
val podSps1 = msgIO.receiveMessage()
val podSps1 = msgIO.receiveMessage() ?: throw PairingException("Could not read SPS1")
processSps1FromPod(podSps1)
// 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 sps2 = PairMessage(
sequenceNumber = seq,
source = ids.myId,
destination = podAddress,
keys = arrayOf(SPS2),
payloads = arrayOf(keyExchange.pdmConf)
)
throwOnSendError(sps2.messagePacket, SPS2)
val podSps2 = msgIO.receiveMessage()
val podSps2 = msgIO.receiveMessage() ?: throw PairingException("Could not read SPS2")
validatePodSps2(podSps2)
// No exception throwing after this point. It is possible that the pod saved the LTK
seq++
// send SP0GP0
msgIO.sendMessage(sp0gp0().messagePacket)
// read P0
val sp0gp0 = PairMessage (
sequenceNumber = seq,
source = ids.myId,
destination = podAddress,
keys = arrayOf(SP0GP0),
payloads = arrayOf(ByteArray(0))
)
val result = msgIO.sendMessage(sp0gp0.messagePacket)
if (result !is MessageSendSuccess) {
aapsLogger.warn(LTag.PUMPBTCOMM,"Error sending SP0GP0: $result")
}
// 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)
val p0 = msgIO.receiveMessage()
validateP0(p0)
msgIO.receiveMessage()
?.let { validateP0(it) }
?: aapsLogger.warn(LTag.PUMPBTCOMM, "Could not read P0")
return PairResult(
ltk = keyExchange.ltk,
@ -67,31 +89,14 @@ internal class LTKExchanger(
)
}
private fun sp1sp2(sp1: ByteArray, sp2: ByteArray): PairMessage {
val payload = StringLengthPrefixEncoding.formatKeys(
arrayOf(SP1, SP2),
arrayOf(sp1, sp2)
)
return PairMessage(
sequenceNumber = seq,
source = myId,
destination = podAddress,
payload = payload
)
@Throws(PairingException::class)
private fun throwOnSendError(msg: MessagePacket, msgType: String) {
val result = msgIO.sendMessage(msg)
if (result !is MessageSendSuccess) {
throw PairingException("Could not send or confirm $msgType: $result")
}
}
private fun sps1(): PairMessage {
val payload = StringLengthPrefixEncoding.formatKeys(
arrayOf("SPS1="),
arrayOf(keyExchange.pdmPublic + keyExchange.pdmNonce)
)
return PairMessage(
sequenceNumber = seq,
source = myId,
destination = podAddress,
payload = payload
)
}
private fun processSps1FromPod(msg: MessagePacket) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS1 from pod: ${msg.payload.toHex()}")
@ -100,19 +105,6 @@ internal class LTKExchanger(
keyExchange.updatePodPublicData(payload)
}
private fun sps2(): PairMessage {
val payload = StringLengthPrefixEncoding.formatKeys(
arrayOf(SPS2),
arrayOf(keyExchange.pdmConf)
)
return PairMessage(
sequenceNumber = seq,
source = myId,
destination = podAddress,
payload = payload
)
}
private fun validatePodSps2(msg: MessagePacket) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS2 from pod: ${msg.payload.toHex()}")
@ -131,30 +123,21 @@ internal class LTKExchanger(
return GET_POD_STATUS_HEX_COMMAND.hexStringToByteArray()
}
private fun sp0gp0(): PairMessage {
val payload = SP0GP0.toByteArray()
return PairMessage(
sequenceNumber = seq,
source = myId,
destination = podAddress,
payload = payload
)
}
private fun validateP0(msg: MessagePacket) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Received P0 from pod: ${msg.payload.toHex()}")
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()}")
}
}
companion object {
private const val GET_POD_STATUS_HEX_COMMAND =
"ffc32dbd08030e0100008a" // TODO for now we are assuming this command is build out of constant parameters, use a proper command builder for that.
"ffc32dbd08030e0100008a"
// This is the binary representation of "GetPodStatus command"
private const val SP1 = "SP1="
private const val SP2 = ",SP2="

View file

@ -3,17 +3,22 @@ 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.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
data class PairMessage(
val sequenceNumber: Byte,
val source: Id,
val destination: Id,
val payload: ByteArray,
private val keys: Array<String>,
private val payloads: Array<ByteArray>,
val messagePacket: MessagePacket = MessagePacket(
type = MessageType.PAIRING,
source = source,
destination = destination,
payload = payload,
payload = StringLengthPrefixEncoding.formatKeys(
keys,
payloads,
),
sequenceNumber = sequenceNumber,
sas = true // TODO: understand why this is true for PairMessages
)

View file

@ -3,7 +3,6 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan
import android.bluetooth.le.ScanRecord
import android.bluetooth.le.ScanResult
import android.os.ParcelUuid
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.DiscoveredInvalidPodException
class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: ScanRecord, private val podId: Long) {
@ -36,7 +35,6 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: Sc
}
@Throws(DiscoveredInvalidPodException::class)
private fun validatePodId() {
val serviceUUIDs = scanRecord.serviceUuids
val hexPodId = extractUUID16(serviceUUIDs[3]) + extractUUID16(serviceUUIDs[4])

View file

@ -1,8 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan
import android.os.ParcelUuid
class DiscoveredInvalidPodException : Exception {
constructor(message: String) : super(message)
constructor(message: String, serviceUUIds: List<ParcelUuid?>) : super("$message service UUIDs: $serviceUUIds")
}

View file

@ -6,14 +6,13 @@ 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()
@ -32,7 +31,7 @@ class PodScanner(private val logger: AAPSLogger, private val bluetoothAdapter: B
scanner.stopScan(scanCollector)
val collected = scanCollector.collect()
if (collected.isEmpty()) {
throw ScanFailNotFoundException()
throw ScanException("Not found")
} else if (collected.size > 1) {
throw ScanFailFoundTooManyException(collected)
}

View file

@ -4,8 +4,7 @@ import android.bluetooth.le.ScanCallback
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 +24,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

@ -0,0 +1,172 @@
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 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.Ids
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
sealed class ConnectionState
object Connected : ConnectionState()
object NotConnected : ConnectionState()
class Connection(
private val podDevice: BluetoothDevice,
private val aapsLogger: AAPSLogger,
context: Context
) : DisconnectHandler {
private val incomingPackets = IncomingPackets()
private val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets, this)
private val gattConnection: BluetoothGatt
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
// The session is Synchronized because we can lose the connection right when establishing it
var session: Session? = null
@Synchronized get
@Synchronized set
private val cmdBleIO: CmdBleIO
private val dataBleIO: DataBleIO
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to ${podDevice.address}")
val autoConnect = false
gattConnection = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
// OnDisconnect can be called after this point!!!
val state = waitForConnection()
if (state !is Connected) {
throw FailedToConnectException(podDevice.address)
}
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val discoveredCharacteristics = discoverer.discoverServices()
cmdBleIO = CmdBleIO(
aapsLogger,
discoveredCharacteristics[CharacteristicType.CMD]!!,
incomingPackets
.cmdQueue,
gattConnection,
bleCommCallbacks
)
dataBleIO = DataBleIO(
aapsLogger,
discoveredCharacteristics[CharacteristicType.DATA]!!,
incomingPackets
.dataQueue,
gattConnection,
bleCommCallbacks
)
val sendResult = cmdBleIO.hello()
if (sendResult !is BleSendSuccess) {
throw FailedToConnectException("Could not send HELLO command to ${podDevice.address}")
}
cmdBleIO.readyToRead()
dataBleIO.readyToRead()
}
val msgIO = MessageIO(aapsLogger, cmdBleIO, dataBleIO)
fun connect() {
disconnect()
if (!gattConnection.connect()) {
throw FailedToConnectException("connect() returned false")
}
if (waitForConnection() is NotConnected) {
throw FailedToConnectException(podDevice.address)
}
val discoverer = ServiceDiscoverer(aapsLogger, gattConnection, bleCommCallbacks)
val discovered = discoverer.discoverServices()
dataBleIO.characteristic = discovered[CharacteristicType.DATA]!!
cmdBleIO.characteristic = discovered[CharacteristicType.CMD]!!
cmdBleIO.hello()
cmdBleIO.readyToRead()
dataBleIO.readyToRead()
}
fun disconnect() {
aapsLogger.debug(LTag.PUMPBTCOMM, "Disconnecting")
gattConnection.disconnect()
bleCommCallbacks.resetConnection()
session = null
}
private 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, "Interrupted 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, ids: Ids, eapSqn: ByteArray): EapSqn? {
val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk, eapSqn, ids, msgSeq)
return when (val keys = eapAkaExchanger.negotiateSessionKeys()) {
is SessionNegotiationResynchronization -> {
if (BuildConfig.DEBUG) {
aapsLogger.info(LTag.PUMPCOMM, "EAP AKA resynchronization: ${keys.synchronizedEapSqn}")
}
keys.synchronizedEapSqn
}
is SessionKeys -> {
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, ids, sessionKeys = keys, enDecrypt = enDecrypt)
null
}
}
}
// This will be called from a different thread !!!
override fun onConnectionLost(status: Int) {
aapsLogger.info(LTag.PUMPBTCOMM, "Lost connection with status: $status")
disconnect()
}
companion object {
private const val CONNECT_TIMEOUT_MS = 7000
}
}

View file

@ -0,0 +1,6 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
interface DisconnectHandler {
fun onConnectionLost(status: Int)
}

View file

@ -9,6 +9,7 @@ enum class EapAkaAttributeType(val type: Byte) {
AT_RAND(1),
AT_AUTN(2),
AT_RES(3),
AT_AUTS(4),
AT_CLIENT_ERROR_CODE(22),
AT_CUSTOM_IV(126);
@ -47,10 +48,19 @@ sealed class EapAkaAttribute {
ret.add(EapAkaAttributeCustomIV.parse(tail.copyOfRange(2, EapAkaAttributeCustomIV.SIZE)))
EapAkaAttributeType.AT_AUTN ->
ret.add(EapAkaAttributeAutn.parse(tail.copyOfRange(2, EapAkaAttributeAutn.SIZE)))
EapAkaAttributeType.AT_AUTS ->
ret.add(EapAkaAttributeAuts.parse(tail.copyOfRange(2, EapAkaAttributeAuts.SIZE)))
EapAkaAttributeType.AT_RAND ->
ret.add(EapAkaAttributeRand.parse(tail.copyOfRange(2, EapAkaAttributeRand.SIZE)))
EapAkaAttributeType.AT_CLIENT_ERROR_CODE ->
ret.add(EapAkaAttributeClientErrorCode.parse(tail.copyOfRange(2, EapAkaAttributeClientErrorCode.SIZE)))
ret.add(
EapAkaAttributeClientErrorCode.parse(
tail.copyOfRange(
2,
EapAkaAttributeClientErrorCode.SIZE
)
)
)
}
tail = tail.copyOfRange(size, tail.size)
}
@ -70,12 +80,14 @@ data class EapAkaAttributeRand(val payload: ByteArray) : EapAkaAttribute() {
}
companion object {
fun parse(payload: ByteArray): EapAkaAttribute {
if (payload.size < 2 + 16) {
throw MessageIOException("Could not parse RAND attribute: ${payload.toHex()}")
}
return EapAkaAttributeRand(payload.copyOfRange(2, 2 + 16))
}
const val SIZE = 20 // type, size, 2 reserved bytes, payload=16
}
}
@ -103,6 +115,29 @@ data class EapAkaAttributeAutn(val payload: ByteArray) : EapAkaAttribute() {
}
}
data class EapAkaAttributeAuts(val payload: ByteArray) : EapAkaAttribute() {
init {
require(payload.size == 14) { "AT_AUTS payload size has to be 14 bytes. Payload: ${payload.toHex()}" }
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_AUTS.type, (SIZE / SIZE_MULTIPLIER).toByte(), 0, 0) + payload
}
companion object {
fun parse(payload: ByteArray): EapAkaAttribute {
if (payload.size < SIZE - 2) {
throw MessageIOException("Could not parse AUTS attribute: ${payload.toHex()}")
}
return EapAkaAttributeAuts(payload)
}
const val SIZE = 16 // type, size, 2 reserved bytes, payload=16
}
}
data class EapAkaAttributeRes(val payload: ByteArray) : EapAkaAttribute() {
init {
@ -110,7 +145,12 @@ data class EapAkaAttributeRes(val payload: ByteArray) : EapAkaAttribute() {
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_RES.type, (SIZE / SIZE_MULTIPLIER).toByte(), 0, PAYLOAD_SIZE_BITS) + payload
return byteArrayOf(
EapAkaAttributeType.AT_RES.type,
(SIZE / SIZE_MULTIPLIER).toByte(),
0,
PAYLOAD_SIZE_BITS
) + payload
}
companion object {
@ -145,6 +185,7 @@ data class EapAkaAttributeCustomIV(val payload: ByteArray) : EapAkaAttribute() {
}
return EapAkaAttributeCustomIV(payload.copyOfRange(2, 2 + 4))
}
const val SIZE = 8 // type, size, 2 reserved bytes, payload=4
}
}
@ -156,7 +197,12 @@ data class EapAkaAttributeClientErrorCode(val payload: ByteArray) : EapAkaAttrib
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_CLIENT_ERROR_CODE.type, (SIZE / SIZE_MULTIPLIER).toByte(), 0, 0) + payload
return byteArrayOf(
EapAkaAttributeType.AT_CLIENT_ERROR_CODE.type,
(SIZE / SIZE_MULTIPLIER).toByte(),
0,
0
) + payload
}
companion object {

View file

@ -23,6 +23,7 @@ enum class EapCode(val code: Byte) {
data class EapMessage(
val code: EapCode,
val identifier: Byte,
val subType: Byte = 0,
val attributes: Array<EapAkaAttribute>
) {
@ -56,16 +57,16 @@ data class EapMessage(
private const val HEADER_SIZE = 8
private const val SUBTYPE_AKA_CHALLENGE = 1.toByte()
const val SUBTYPE_SYNCRONIZATION_FAILURE = 4.toByte()
private const val AKA_PACKET_TYPE = 0x17.toByte()
fun parse(aapsLogger: AAPSLogger, payload: ByteArray): EapMessage {
if (payload.size < 4) {
throw MessageIOException("Invalid eap payload: ${payload.toHex()}")
}
payload.assertSizeAtLeast(4)
val totalSize = (payload[2].toInt() shl 8) or payload[3].toInt()
if (totalSize > payload.size) {
throw MessageIOException("Invalid eap payload. Too short: ${payload.toHex()}")
}
payload.assertSizeAtLeast(totalSize)
if (payload.size == 4) { // SUCCESS/FAILURE
return EapMessage(
code = EapCode.byValue(payload[0]),
@ -81,8 +82,15 @@ data class EapMessage(
return EapMessage(
code = EapCode.byValue(payload[0]),
identifier = payload[1],
attributes = EapAkaAttribute.parseAttributes(aapsLogger, attributesPayload).toTypedArray()
attributes = EapAkaAttribute.parseAttributes(aapsLogger, attributesPayload).toTypedArray(),
subType = payload[5],
)
}
}
}
private fun ByteArray.assertSizeAtLeast(size: Int) {
if (this.size < size) {
throw MessageIOException("Payload too short: ${this.toHex()}")
}
}

View file

@ -0,0 +1,34 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import java.nio.ByteBuffer
class EapSqn(val value: ByteArray) {
constructor(v: Long) : this(fromLong(v))
init {
require(value.size == SIZE) { "Eap SQN is $SIZE bytes long" }
}
fun increment(): EapSqn {
return EapSqn(toLong() + 1)
}
fun toLong(): Long {
return ByteBuffer.wrap(
byteArrayOf(0x00, 0x00) +
value
).long
}
override fun toString(): String {
return "EapSqn(value=${toLong()})"
}
companion object {
private const val SIZE = 6
private fun fromLong(v: Long): ByteArray {
return ByteBuffer.allocate(8).putLong(v).array().copyOfRange(2, 8)
}
}
}

View file

@ -12,12 +12,19 @@ class Milenage(
private val aapsLogger: AAPSLogger,
private val k: ByteArray,
val sqn: ByteArray,
private val randParam: ByteArray? = null
randParam: ByteArray? = null,
val auts: ByteArray = ByteArray(AUTS_SIZE),
val amf: ByteArray = MILENAGE_AMF,
) {
init {
require(k.size == KEY_SIZE) { "Milenage key has to be $KEY_SIZE bytes long. Received: ${k.toHex()}" }
require(sqn.size == SQN) { "Milenage SQN has to be $SQN long. Received: ${sqn.toHex()}" }
require(auts.size == AUTS_SIZE) { "Milenage AUTS has to be $AUTS_SIZE long. Received: ${auts.toHex()}" }
require(amf.size == MILENAGE_AMF.size) {
"Milenage AMF has to be ${MILENAGE_AMF.size} long." +
"Received: ${amf.toHex()}"
}
}
private val secretKeySpec = SecretKeySpec(k, "AES")
@ -61,7 +68,7 @@ class Milenage(
val ck = cipher.doFinal(ckInput) xor opc
private val sqnAmf = sqn + MILENAGE_AMF + sqn + MILENAGE_AMF
private val sqnAmf = sqn + amf + sqn + amf
private val sqnAmfXorOpc = sqnAmf xor opc
private val macAInput = ByteArray(KEY_SIZE)
@ -73,7 +80,26 @@ class Milenage(
private val macAFull = cipher.doFinal(macAInput xor randOpcEncrypted) xor opc
private val macA = macAFull.copyOfRange(0, 8)
val autn = (ak xor sqn) + MILENAGE_AMF + macA
val macS = macAFull.copyOfRange(8, 16)
val autn = (ak xor sqn) + amf + macA
// Used for re-synchronisation AUTS = SQN^AK || MAC-S
private val akStarInput = ByteArray(KEY_SIZE)
init {
for (i in 0..15) {
akStarInput[(i + 4) % 16] = randOpcEncryptedXorOpc[i]
}
akStarInput[15] = (akStarInput[15].toInt() xor 8).toByte()
}
private val akStarFull = cipher.doFinal(akStarInput) xor opc
private val akStar = akStarFull.copyOfRange(0, 6)
private val seqXorAkStar = auts.copyOfRange(0, 6)
val synchronizationSqn = akStar xor seqXorAkStar
val receivedMacS = auts.copyOfRange(6, 14)
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage K: ${k.toHex()}")
@ -83,15 +109,23 @@ class Milenage(
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AUTN: ${autn.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage RES: ${res.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AK: ${ak.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AK STAR: ${akStar.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage OPC: ${opc.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage FullMAC: ${macAFull.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage MacA: ${macA.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage MacS: ${macS.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AUTS: ${auts.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage synchronizationSqn: ${synchronizationSqn.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage receivedMacS: ${receivedMacS.toHex()}")
}
companion object {
val RESYNC_AMF = Hex.decode("0000")
private val MILENAGE_OP = Hex.decode("cdc202d5123e20f62b6d676ac72cb318")
private val MILENAGE_AMF = Hex.decode("b9b9")
private const val KEY_SIZE = 16
const val KEY_SIZE = 16
const val AUTS_SIZE = 14
private const val SQN = 6
}
}

View file

@ -41,4 +41,4 @@ object ResponseUtil {
ResponseType.StatusResponseType.UNKNOWN -> throw CouldNotParseResponseException("Unrecognized additional status response type: ${payload[2]}")
}
}
}
}

View file

@ -3,15 +3,14 @@ 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.Ids
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotParseResponseException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.IllegalResponseException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.NakResponseException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.PodAlarmException
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.*
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.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
@ -20,35 +19,72 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.
import info.nightscout.androidaps.utils.extensions.toHex
import kotlin.reflect.KClass
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,
private val msgIO: MessageIO,
private val myId: Id,
private val podId: Id,
private val ids: Ids,
val sessionKeys: SessionKeys,
val enDecrypt: EnDecrypt
) {
/**
* Used for commands:
* -> command with retries
* <- response, ACK TODO: retries?
* -> ACK
*/
@Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
fun sendCommand(cmd: Command, responseType: KClass<out Response>): Response {
fun sendCommand(cmd: Command): CommandSendResult {
sessionKeys.msgSequenceNumber++
aapsLogger.debug(
LTag.PUMPBTCOMM,
"Sending command: ${cmd.javaClass.simpleName}: ${cmd.encoded.toHex()} in packet $cmd"
)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd")
val msg = getCmdMessage(cmd)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
msgIO.sendMessage(msg)
var possiblyUnconfirmedCommand = false
for (i in 0..MAX_TRIES) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
val responseMsg = msgIO.receiveMessage()
val decrypted = enDecrypt.decrypt(responseMsg)
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)
}
@Suppress("ReturnCount")
fun readAndAckResponse(responseType: KClass<out Response>): CommandReceiveResult {
var responseMsgPacket: MessagePacket? = null
for (i in 0..MAX_TRIES) {
val responseMsg = msgIO.receiveMessage()
if (responseMsg != null) {
responseMsgPacket = responseMsg
break
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Error receiving response: $responseMsg")
}
responseMsgPacket
?: return CommandReceiveError("Could not read response")
val decrypted = enDecrypt.decrypt(responseMsgPacket)
aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted")
val response = parseResponse(decrypted)
@ -64,10 +100,13 @@ class Session(
}
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)
return response
val sendResult = msgIO.sendMessage(ack)
if (sendResult !is MessageSendSuccess) {
return CommandAckError(response, "Could not ACK the response: $sendResult")
}
return CommandReceiveSuccess(response)
}
@Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
@ -92,8 +131,8 @@ class Session(
val msg = MessagePacket(
type = MessageType.ENCRYPTED,
sequenceNumber = sessionKeys.msgSequenceNumber,
source = myId,
destination = podId,
source = ids.myId,
destination = ids.podId,
payload = ByteArray(0),
eqos = 0,
ack = true,
@ -113,8 +152,8 @@ class Session(
val msg = MessagePacket(
type = MessageType.ENCRYPTED,
sequenceNumber = sessionKeys.msgSequenceNumber,
source = myId,
destination = podId,
source = ids.myId,
destination = ids.podId,
payload = wrapped,
eqos = 1
)
@ -127,5 +166,7 @@ class Session(
private const val COMMAND_PREFIX = "S0.0="
private const val COMMAND_SUFFIX = ",G0.0"
private const val RESPONSE_PREFIX = "0.0="
private const val MAX_TRIES = 4
}
}

View file

@ -3,27 +3,29 @@ 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.Ids
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.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,
private val msgIO: MessageIO,
ltk: ByteArray,
eapSqn: ByteArray,
private val myId: Id,
private val podId: Id,
private val ltk: ByteArray,
private val eapSqn: ByteArray,
private val ids: Ids,
private var msgSeq: Byte
) {
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 {
@ -35,14 +37,23 @@ class SessionEstablisher(
random.nextBytes(controllerIV)
}
fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge
fun negotiateSessionKeys(): SessionNegotiationResponse {
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?
?: throw SessionEstablishmentException("Could not establish session")
val newSqn = processChallengeResponse(challengeResponse)
if (newSqn != null) {
return SessionNegotiationResynchronization(
synchronizedEapSqn = newSqn,
msgSequenceNumber = msgSeq
)
}
msgSeq++
var success = eapSuccess()
@ -67,34 +78,49 @@ class SessionEstablisher(
val eapMsg = EapMessage(
code = EapCode.REQUEST,
identifier = 189.toByte(), // TODO: find what value we need here, it's probably random
identifier = identifier,
attributes = attributes
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = msgSeq,
source = myId,
destination = podId,
source = ids.myId,
destination = ids.podId,
payload = eapMsg.toByteArray()
)
}
private fun processChallengeResponse(challengeResponse: MessagePacket) {
// TODO verify that identifier matches identifier from the Challenge
val eapMsg = EapMessage.parse(aapsLogger, challengeResponse.payload)
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}")
private fun assertIdentifier(msg: EapMessage) {
if (msg.identifier != identifier) {
aapsLogger.debug(
LTag.PUMPBTCOMM,
"EAP-AKA: got incorrect identifier ${msg.identifier} expected: $identifier"
)
throw SessionEstablishmentException("Received incorrect EAP identifier: ${msg.identifier}")
}
}
private fun processChallengeResponse(challengeResponse: MessagePacket): EapSqn? {
val eapMsg = EapMessage.parse(aapsLogger, challengeResponse.payload)
assertIdentifier(eapMsg)
val eapSqn = isResynchronization(eapMsg)
if (eapSqn != null) {
return eapSqn
}
assertValidAkaMessage(eapMsg)
for (attr in eapMsg.attributes) {
when (attr) {
is EapAkaAttributeRes ->
if (!milenage.res.contentEquals(attr.payload)) {
throw SessionEstablishmentException("RES missmatch. Expected: ${milenage.res.toHex()} Actual: ${attr.payload.toHex()} ")
throw SessionEstablishmentException(
"RES mismatch." +
"Expected: ${milenage.res.toHex()}." +
"Actual: ${attr.payload.toHex()}."
)
}
is EapAkaAttributeCustomIV ->
nodeIV = attr.payload.copyOfRange(0, IV_SIZE)
@ -102,20 +128,70 @@ class SessionEstablisher(
throw SessionEstablishmentException("Unknown attribute received: $attr")
}
}
return null
}
private fun assertValidAkaMessage(eapMsg: EapMessage) {
if (eapMsg.attributes.size != 2) {
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got incorrect: $eapMsg")
if (eapMsg.attributes.size == 1 && eapMsg.attributes[0] is EapAkaAttributeClientErrorCode) {
throw SessionEstablishmentException(
"Received CLIENT_ERROR_CODE for EAP-AKA challenge: ${
eapMsg.attributes[0].toByteArray().toHex()
}"
)
}
throw SessionEstablishmentException("Expecting two attributes, got: ${eapMsg.attributes.size}")
}
}
private fun isResynchronization(eapMsg: EapMessage): EapSqn? {
if (eapMsg.subType != EapMessage.SUBTYPE_SYNCRONIZATION_FAILURE ||
eapMsg.attributes.size != 1 ||
eapMsg.attributes[0] !is EapAkaAttributeAuts
)
return null
val auts = eapMsg.attributes[0] as EapAkaAttributeAuts
val autsMilenage = Milenage(
aapsLogger = aapsLogger,
k = ltk,
sqn = eapSqn,
randParam = milenage.rand,
auts = auts.payload
)
val newSqnMilenage = Milenage(
aapsLogger = aapsLogger,
k = ltk,
sqn = autsMilenage.synchronizationSqn,
randParam = milenage.rand,
auts = auts.payload,
amf = Milenage.RESYNC_AMF,
)
if (!newSqnMilenage.macS.contentEquals(newSqnMilenage.receivedMacS)) {
throw SessionEstablishmentException(
"MacS mismatch. " +
"Expected: ${newSqnMilenage.macS.toHex()}. " +
"Received: ${newSqnMilenage.receivedMacS.toHex()}"
)
}
return EapSqn(autsMilenage.synchronizationSqn)
}
private fun eapSuccess(): MessagePacket {
val eapMsg = EapMessage(
code = EapCode.SUCCESS,
attributes = arrayOf(),
identifier = 189.toByte() // TODO: find what value we need here
identifier = identifier.toByte()
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = msgSeq,
source = myId,
destination = podId,
source = ids.myId,
destination = ids.podId,
payload = eapMsg.toByteArray()
)
}

View file

@ -2,8 +2,20 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce
data class SessionKeys(val ck: ByteArray, val nonce: Nonce, var msgSequenceNumber: Byte) {
sealed class SessionNegotiationResponse
data class SessionKeys(
val ck: ByteArray,
val nonce: Nonce,
var msgSequenceNumber: Byte
) : SessionNegotiationResponse() {
init {
require(ck.size == 16) { "CK has to be 16 bytes long" }
}
}
data class SessionNegotiationResynchronization(
val synchronizedEapSqn: EapSqn,
val msgSequenceNumber: Byte
) : SessionNegotiationResponse()

View file

@ -50,6 +50,12 @@ sealed class PodEvent {
}
}
class CommandSendNotConfirmed(val command: Command) : PodEvent() {
override fun toString(): String {
return "CommandSentNotConfirmed(command=$command)"
}
}
class ResponseReceived(
val command: Command,
val response: Response
@ -58,6 +64,5 @@ sealed class PodEvent {
override fun toString(): String {
return "ResponseReceived(command=$command, response=$response)"
}
}
}

View file

@ -16,7 +16,8 @@ class DefaultStatusResponse(
val deliveryStatus: DeliveryStatus = byValue((encoded[1].toInt() shr 4 and 0x0f).toByte(), DeliveryStatus.UNKNOWN)
val podStatus: PodStatus = byValue((encoded[1] and 0x0f), PodStatus.UNKNOWN)
val totalPulsesDelivered: Short =
((encoded[2] and 0x0f shl 12 or (encoded[3].toInt() and 0xff shl 1) or (encoded[4].toInt() and 0xff ushr 7)).toShort())
(encoded[2] and 0x0f shl 9 or (encoded[3].toInt() and 0xff shl 1) or (encoded[4].toInt() and 0xff ushr 7)).toShort()
val sequenceNumberOfLastProgrammingCommand: Short = (encoded[4] ushr 3 and 0x0f).toShort()
val bolusPulsesRemaining: Short = ((encoded[4] and 0x07 shl 10 or (encoded[5].toInt() and 0xff) and 2047).toShort())
val activeAlerts: EnumSet<AlertType> =

View file

@ -47,7 +47,7 @@ interface OmnipodDashPodStateManager {
val tempBasal: TempBasal?
val tempBasalActive: Boolean
val basalProgram: BasalProgram?
var basalProgram: BasalProgram?
fun increaseMessageSequenceNumber()
fun increaseEapAkaSequenceNumber(): ByteArray

View file

@ -8,6 +8,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.EventOmnipodDashPump
import info.nightscout.androidaps.plugins.pump.omnipod.dash.R
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.EapSqn
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.DefaultStatusResponse
@ -15,7 +16,6 @@ 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
@ -35,8 +35,8 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override var activationProgress: ActivationProgress
get() = podState.activationProgress
set(value) {
podState.activationProgress = value
set(activationProgress) {
podState.activationProgress = activationProgress
store()
}
@ -55,8 +55,8 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override var lastConnection: Long
get() = podState.lastConnection
set(value) {
podState.lastConnection = value
set(lastConnection) {
podState.lastConnection = lastConnection
store()
}
@ -145,8 +145,12 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override val tempBasalActive: Boolean
get() = tempBasal != null && tempBasal!!.startTime + tempBasal!!.durationInMinutes * 60 * 1000 > System.currentTimeMillis()
override val basalProgram: BasalProgram?
override var basalProgram: BasalProgram?
get() = podState.basalProgram
set(basalProgram) {
podState.basalProgram = basalProgram
store()
}
override fun increaseMessageSequenceNumber() {
podState.messageSequenceNumber = ((podState.messageSequenceNumber.toInt() + 1) and 0x0f).toShort()
@ -155,24 +159,21 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
override var eapAkaSequenceNumber: Long
get() = podState.eapAkaSequenceNumber
set(value) {
podState.eapAkaSequenceNumber = value
set(eapAkaSequenceNumber) {
podState.eapAkaSequenceNumber = eapAkaSequenceNumber
store()
}
override var ltk: ByteArray?
get() = podState.ltk
set(value) {
podState.ltk = value
set(ltk) {
podState.ltk = ltk
store()
}
override fun increaseEapAkaSequenceNumber(): ByteArray {
podState.eapAkaSequenceNumber++
return ByteBuffer.allocate(8)
.putLong(podState.eapAkaSequenceNumber)
.array()
.copyOfRange(2, 8)
return EapSqn(podState.eapAkaSequenceNumber).value
}
override fun commitEapAkaSequenceNumber() {
@ -183,7 +184,9 @@ class OmnipodDashPodStateManagerImpl @Inject constructor(
podState.deliveryStatus = response.deliveryStatus
podState.podStatus = response.podStatus
podState.pulsesDelivered = response.totalPulsesDelivered
podState.pulsesRemaining = response.reservoirPulsesRemaining
if (response.reservoirPulsesRemaining < 1023) {
podState.pulsesRemaining = response.reservoirPulsesRemaining
}
podState.sequenceNumberOfLastProgrammingCommand = response.sequenceNumberOfLastProgrammingCommand
podState.minutesSinceActivation = response.minutesSinceActivation
podState.activeAlerts = response.activeAlerts

View file

@ -20,6 +20,7 @@ import info.nightscout.androidaps.queue.Callback
import info.nightscout.androidaps.queue.events.EventQueueChanged
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.alertDialogs.OKDialog
import info.nightscout.androidaps.utils.extensions.toVisibility
import info.nightscout.androidaps.utils.rx.AapsSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
@ -73,7 +74,6 @@ class DashPodManagementActivity : NoSplashAppCompatActivity() {
}
binding.buttonPlayTestBeep.setOnClickListener {
// TODO
binding.buttonPlayTestBeep.isEnabled = false
binding.buttonPlayTestBeep.setText(R.string.omnipod_common_pod_management_button_playing_test_beep)
@ -114,7 +114,34 @@ class DashPodManagementActivity : NoSplashAppCompatActivity() {
}
private fun refreshButtons() {
// TODO update button state from Pod state
// Only show the discard button to reset a cached unique ID before the unique ID has actually been set
// Otherwise, users should use the Deactivate Pod Wizard. In case proper deactivation fails,
// they will get an option to discard the Pod there
val discardButtonEnabled =
podStateManager.uniqueId != null &&
podStateManager.activationProgress.isBefore(ActivationProgress.SET_UNIQUE_ID)
binding.buttonDiscardPod.visibility = discardButtonEnabled.toVisibility()
binding.buttonActivatePod.isEnabled = podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED)
binding.buttonDeactivatePod.isEnabled =
podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED)
if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED)) {
if (commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class.java)) {
binding.buttonPlayTestBeep.isEnabled = false
binding.buttonPlayTestBeep.setText(R.string.omnipod_common_pod_management_button_playing_test_beep)
} else {
binding.buttonPlayTestBeep.isEnabled = true
binding.buttonPlayTestBeep.setText(R.string.omnipod_common_pod_management_button_play_test_beep)
}
} else {
binding.buttonPlayTestBeep.isEnabled = false
binding.buttonPlayTestBeep.setText(R.string.omnipod_common_pod_management_button_play_test_beep)
}
if (discardButtonEnabled) {
binding.buttonDiscardPod.isEnabled = true
}
}
private fun displayErrorDialog(title: String, message: String, @Suppress("SameParameterValue") withSound: Boolean) {

View file

@ -18,9 +18,9 @@ import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNo
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification
import info.nightscout.androidaps.plugins.pump.omnipod.common.databinding.OmnipodCommonOverviewButtonsBinding
import info.nightscout.androidaps.plugins.pump.omnipod.common.databinding.OmnipodCommonOverviewPodInfoBinding
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandAcknowledgeAlerts
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandHandleTimeChange
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandResumeDelivery
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSilenceAlerts
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSuspendDelivery
import info.nightscout.androidaps.plugins.pump.omnipod.dash.EventOmnipodDashPumpValuesChanged
import info.nightscout.androidaps.plugins.pump.omnipod.dash.OmnipodDashPumpPlugin
@ -130,7 +130,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
buttonBinding.buttonSilenceAlerts.setOnClickListener {
disablePodActionButtons()
commandQueue.customCommand(
CommandAcknowledgeAlerts(),
CommandSilenceAlerts(),
DisplayResultDialogCallback(
resourceHelper.gs(R.string.omnipod_common_error_failed_to_silence_alerts),
false
@ -487,7 +487,7 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
if (isAutomaticallySilenceAlertsEnabled() && podStateManager.isPodRunning &&
(
podStateManager.activeAlerts!!.size > 0 ||
commandQueue.isCustomCommandInQueue(CommandAcknowledgeAlerts::class.java)
commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class.java)
)
) {
buttonBinding.buttonSilenceAlerts.visibility = View.VISIBLE

View file

@ -3,38 +3,30 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivat
import androidx.annotation.StringRes
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.interfaces.CommandQueueProvider
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.common.R
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandDeactivatePod
import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.deactivation.viewmodel.action.DeactivatePodViewModel
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.queue.Callback
import io.reactivex.Single
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
class DashDeactivatePodViewModel @Inject constructor(
private val omnipodManager: OmnipodDashManager,
private val podStateManager: OmnipodDashPodStateManager,
private val commandQueueProvider: CommandQueueProvider,
injector: HasAndroidInjector,
logger: AAPSLogger
) : DeactivatePodViewModel(injector, logger) {
override fun doExecuteAction(): Single<PumpEnactResult> = Single.create { source ->
omnipodManager.deactivatePod().subscribeBy(
onNext = { podEvent ->
logger.debug(
LTag.PUMP,
"Received PodEvent in Pod deactivation: $podEvent"
)
},
onError = { throwable ->
logger.error(LTag.PUMP, "Error in Pod deactivation", throwable)
source.onSuccess(PumpEnactResult(injector).success(false).comment(throwable.message))
},
onComplete = {
logger.debug("Pod deactivation completed")
source.onSuccess(PumpEnactResult(injector).success(true))
commandQueueProvider.customCommand(
CommandDeactivatePod(),
object : Callback() {
override fun run() {
source.onSuccess(result)
}
}
)
}

View file

@ -14,7 +14,6 @@
android:orientation="vertical">
<TextView
android:id="@+id/omnipod_eros_pod_management_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
@ -119,8 +118,7 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<!-- FIXME visible for development -->
<info.nightscout.androidaps.utils.ui.SingleClickButton
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_discard_pod"
style="?android:attr/buttonStyle"
android:layout_width="0dp"
@ -128,7 +126,7 @@
android:drawableTop="@drawable/ic_pod_management_discard_pod"
android:text="@string/omnipod_common_pod_management_button_discard_pod"
android:textAllCaps="false"
android:visibility="visible"
android:visibility="gone"
app:layout_constrainedHeight="@+id/Actions_Row_2_horizontal_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toRightOf="@+id/Actions_Col_1_Row_2_vertical_guideline"

View file

@ -1,7 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import com.google.crypto.tink.subtle.Hex
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadJoiner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.PayloadJoiner
import info.nightscout.androidaps.utils.extensions.toHex
import org.junit.Assert.assertEquals
import org.junit.Test

View file

@ -1,6 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadJoiner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.PayloadJoiner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.PayloadSplitter
import info.nightscout.androidaps.utils.extensions.toHex
import org.junit.Assert.assertEquals
import org.junit.Test
@ -16,9 +17,9 @@ class PayloadSplitJoinTest {
random.nextBytes(payload)
val splitter = PayloadSplitter(payload)
val packets = splitter.splitInPackets()
val joiner = PayloadJoiner(packets.get(0).asByteArray())
val joiner = PayloadJoiner(packets.get(0).toByteArray())
for (p in packets.subList(1, packets.size)) {
joiner.accumulate(p.asByteArray())
joiner.accumulate(p.toByteArray())
}
val got = joiner.finalize()
assertEquals(got.toHex(), payload.toHex())

View file

@ -1,6 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import com.google.crypto.tink.subtle.Hex
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.PayloadSplitter
import info.nightscout.androidaps.utils.extensions.toHex
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@ -17,8 +18,8 @@ class PayloadSplitterTest {
val packets = splitter.splitInPackets()
assertEquals(packets.size, 2)
assertEquals(f1, packets.get(0).asByteArray().toHex())
val p2 = packets.get(1).asByteArray()
assertEquals(f1, packets.get(0).toByteArray().toHex())
val p2 = packets.get(1).toByteArray()
assertTrue(p2.size >= 10)
assertEquals(f2.subSequence(0, 20), p2.copyOfRange(0, 10).toHex())
}

View file

@ -11,10 +11,10 @@ class MilenageTest {
@Test fun testMilenage() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("c0772899720972a314f557de66d571dd"),
byteArrayOf(0, 0, 0, 0, 0, 2),
Hex.decode("c2cd1248451103bd77a6c7ef88c441ba")
aapsLogger = aapsLogger,
k = Hex.decode("c0772899720972a314f557de66d571dd"),
sqn = byteArrayOf(0, 0, 0, 0, 0, 2),
randParam = Hex.decode("c2cd1248451103bd77a6c7ef88c441ba")
)
Assert.assertEquals(m.res.toHex(), "a40bc6d13861447e")
Assert.assertEquals(m.ck.toHex(), "55799fd26664cbf6e476525e2dee52c6")
@ -24,10 +24,10 @@ class MilenageTest {
@Test fun testMilenage2() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("78411ccad0fd0fb6f381a47fb3335ecb"),
byteArrayOf(0, 0, 0, 0, 0, 2), // 1 + 1
Hex.decode("4fc01ac1a94376ae3e052339c07d9e1f")
aapsLogger = aapsLogger,
k = Hex.decode("78411ccad0fd0fb6f381a47fb3335ecb"),
sqn = byteArrayOf(0, 0, 0, 0, 0, 2), // 1 + 1
randParam = Hex.decode("4fc01ac1a94376ae3e052339c07d9e1f")
)
Assert.assertEquals(m.res.toHex(), "ec549e00fa668a19")
Assert.assertEquals(m.ck.toHex(), "ee3dac761fe358a9f476cc5ee81aa3e9")
@ -37,14 +37,28 @@ class MilenageTest {
@Test fun testMilenageIncrementedSQN() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("c0772899720972a314f557de66d571dd"),
// byteArrayOf(0,0,0,0,0x01,0x5d), this is in logs. SQN has to be incremented.
byteArrayOf(0, 0, 0, 0, 0x01, 0x5e),
Hex.decode("d71cc44820e5419f42c62ae97c035988")
aapsLogger = aapsLogger,
k = Hex.decode("c0772899720972a314f557de66d571dd"),
// byteArrayOf(0,0,0,0,0x01,0x5d), this is in logs. SQN has to be incremented.
sqn = byteArrayOf(0, 0, 0, 0, 0x01, 0x5e),
randParam = Hex.decode("d71cc44820e5419f42c62ae97c035988")
)
Assert.assertEquals(m.res.toHex(), "5f807a379a5c5d30")
Assert.assertEquals(m.ck.toHex(), "8dd4b3ceb849a01766e37f9d86045c39")
Assert.assertEquals(m.autn.toHex(), "0e0264d056fcb9b9752227365a090955")
}
@Test fun testMileageSynchronization() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger = aapsLogger,
k = Hex.decode("689b860fde3331dd7e1671ad39985e3b"),
sqn = byteArrayOf(0, 0, 0, 0, 0, 8), // 1 + 1
auts = Hex.decode("84ff173947a67567985de71e4890"),
randParam = Hex.decode("396707041ca3a5931fc0e52d2d7b9ecf"),
amf = byteArrayOf(0, 0),
)
Assert.assertEquals(m.receivedMacS.toHex(), m.macS.toHex())
Assert.assertEquals(m.sqn.toHex(), m.synchronizationSqn.toHex())
}
}

View file

@ -25,4 +25,82 @@ class DefaultStatusResponseTest {
Assert.assertEquals(280.toShort(), response.minutesSinceActivation)
Assert.assertEquals(1023.toShort(), response.reservoirPulsesRemaining)
}
/**
* response (hex) 08202EAA0C0A1D1905281000004387D3039A
Status response: 29
Pod status: RUNNING_BELOW_MIN_VOLUME
Basal active: true
Temp Basal active: false
Immediate bolus active: false
Extended bolus active: false
Bolus pulses remaining: 0
sequence number of last programing command: 2
Total full pulses delivered: 2640
Full reservoir pulses remaining: 979
Time since activation: 4321
Alert 1 is InActive
Alert 2 is InActive
Alert 3 is InActive
Alert 4 is InActive
Alert 5 is InActive
Alert 6 is InActive
Alert 7 is InActive
Occlusion alert active false
*/
@Test @Throws(DecoderException::class) fun testValidResponseBelowMin() {
val encoded = Hex.decodeHex("1D1905281000004387D3039A")
val response = DefaultStatusResponse(encoded)
Assert.assertArrayEquals(encoded, response.encoded)
Assert.assertNotSame(encoded, response.encoded)
Assert.assertEquals(ResponseType.DEFAULT_STATUS_RESPONSE, response.responseType)
Assert.assertEquals(ResponseType.DEFAULT_STATUS_RESPONSE.value, response.messageType)
Assert.assertEquals(DeliveryStatus.BASAL_ACTIVE, response.deliveryStatus)
Assert.assertEquals(PodStatus.RUNNING_BELOW_MIN_VOLUME, response.podStatus)
Assert.assertEquals(2.toShort(), response.sequenceNumberOfLastProgrammingCommand)
Assert.assertEquals(0.toShort(), response.bolusPulsesRemaining)
Assert.assertEquals(0, response.activeAlerts.size)
Assert.assertEquals(4321.toShort(), response.minutesSinceActivation)
Assert.assertEquals(979.toShort(), response.reservoirPulsesRemaining)
Assert.assertEquals(2640.toShort(), response.totalPulsesDelivered)
}
/**
* response (hex) 08202EAA080A1D180519C00E0039A7FF8085
Status response: 29
Pod status: RUNNING_ABOVE_MIN_VOLUME
Basal active: true
Temp Basal active: false
Immediate bolus active: false
Extended bolus active: false
Bolus pulses remaining: 14
sequence number of last programing command: 8
Total full pulses delivered: 2611
Full reservoir pulses remaining: 1023
Time since activation: 3689
Alert 1 is InActive
Alert 2 is InActive
Alert 3 is InActive
Alert 4 is InActive
Alert 5 is InActive
Alert 6 is InActive
Alert 7 is InActive
Occlusion alert active false
*/
@Test @Throws(DecoderException::class) fun testValidResponseBolusPulsesRemaining() {
val encoded = Hex.decodeHex("1D180519C00E0039A7FF8085")
val response = DefaultStatusResponse(encoded)
Assert.assertArrayEquals(encoded, response.encoded)
Assert.assertNotSame(encoded, response.encoded)
Assert.assertEquals(ResponseType.DEFAULT_STATUS_RESPONSE, response.responseType)
Assert.assertEquals(ResponseType.DEFAULT_STATUS_RESPONSE.value, response.messageType)
Assert.assertEquals(DeliveryStatus.BASAL_ACTIVE, response.deliveryStatus)
Assert.assertEquals(PodStatus.RUNNING_ABOVE_MIN_VOLUME, response.podStatus)
Assert.assertEquals(8.toShort(), response.sequenceNumberOfLastProgrammingCommand)
Assert.assertEquals(14.toShort(), response.bolusPulsesRemaining)
Assert.assertEquals(0, response.activeAlerts.size)
Assert.assertEquals(3689.toShort(), response.minutesSinceActivation)
Assert.assertEquals(1023.toShort(), response.reservoirPulsesRemaining)
Assert.assertEquals(2611.toShort(), response.totalPulsesDelivered)
}
}

View file

@ -65,7 +65,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLin
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData;
import info.nightscout.androidaps.plugins.pump.common.utils.DateTimeUtil;
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType;
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandAcknowledgeAlerts;
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSilenceAlerts;
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandDeactivatePod;
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandHandleTimeChange;
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandPlayTestBeep;
@ -253,7 +253,7 @@ public class OmnipodErosPumpPlugin extends PumpPluginBase implements PumpInterfa
}
if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled() && podStateManager.isPodActivationCompleted() && !podStateManager.isPodDead() &&
podStateManager.getActiveAlerts().size() > 0 && !getCommandQueue().isCustomCommandInQueue(CommandAcknowledgeAlerts.class)) {
podStateManager.getActiveAlerts().size() > 0 && !getCommandQueue().isCustomCommandInQueue(CommandSilenceAlerts.class)) {
queueAcknowledgeAlertsCommand();
}
} else {
@ -410,7 +410,7 @@ public class OmnipodErosPumpPlugin extends PumpPluginBase implements PumpInterfa
rxBus.send(new EventNewNotification(notification));
nsUpload.uploadError(notificationText);
if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled() && !getCommandQueue().isCustomCommandInQueue(CommandAcknowledgeAlerts.class)) {
if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled() && !getCommandQueue().isCustomCommandInQueue(CommandSilenceAlerts.class)) {
queueAcknowledgeAlertsCommand();
}
}
@ -437,7 +437,7 @@ public class OmnipodErosPumpPlugin extends PumpPluginBase implements PumpInterfa
}
private void queueAcknowledgeAlertsCommand() {
getCommandQueue().customCommand(new CommandAcknowledgeAlerts(), new Callback() {
getCommandQueue().customCommand(new CommandSilenceAlerts(), new Callback() {
@Override public void run() {
if (result != null) {
aapsLogger.debug(LTag.PUMP, "Acknowledge alerts result: {} ({})", result.success, result.comment);
@ -827,7 +827,7 @@ public class OmnipodErosPumpPlugin extends PumpPluginBase implements PumpInterfa
@Override
public PumpEnactResult executeCustomCommand(@NonNull CustomCommand command) {
if (command instanceof CommandAcknowledgeAlerts) {
if (command instanceof CommandSilenceAlerts) {
return executeCommand(OmnipodCommandType.ACKNOWLEDGE_ALERTS, aapsOmnipodErosManager::acknowledgeAlerts);
}
if (command instanceof CommandGetPodStatus) {

View file

@ -24,7 +24,7 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.defs.RileyLin
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.service.RileyLinkServiceData
import info.nightscout.androidaps.plugins.pump.omnipod.common.databinding.OmnipodCommonOverviewButtonsBinding
import info.nightscout.androidaps.plugins.pump.omnipod.common.databinding.OmnipodCommonOverviewPodInfoBinding
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandAcknowledgeAlerts
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSilenceAlerts
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandHandleTimeChange
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandResumeDelivery
import info.nightscout.androidaps.plugins.pump.omnipod.common.queue.command.CommandSuspendDelivery
@ -148,7 +148,8 @@ class OmnipodErosOverviewFragment : DaggerFragment() {
buttonBinding.buttonSilenceAlerts.setOnClickListener {
disablePodActionButtons()
commandQueue.customCommand(CommandAcknowledgeAlerts(),
commandQueue.customCommand(
CommandSilenceAlerts(),
DisplayResultDialogCallback(resourceHelper.gs(R.string.omnipod_common_error_failed_to_silence_alerts), false)
.messageOnSuccess(resourceHelper.gs(R.string.omnipod_common_confirmation_silenced_alerts))
.actionOnSuccess { rxBus.send(EventDismissNotification(Notification.OMNIPOD_POD_ALERTS)) })
@ -513,7 +514,8 @@ class OmnipodErosOverviewFragment : DaggerFragment() {
}
private fun updateSilenceAlertsButton() {
if (!omnipodManager.isAutomaticallyAcknowledgeAlertsEnabled && podStateManager.isPodRunning && (podStateManager.hasActiveAlerts() || commandQueue.isCustomCommandInQueue(CommandAcknowledgeAlerts::class.java))) {
if (!omnipodManager.isAutomaticallyAcknowledgeAlertsEnabled && podStateManager.isPodRunning && (podStateManager.hasActiveAlerts() || commandQueue.isCustomCommandInQueue(
CommandSilenceAlerts::class.java))) {
buttonBinding.buttonSilenceAlerts.visibility = View.VISIBLE
buttonBinding.buttonSilenceAlerts.isEnabled = rileyLinkServiceData.rileyLinkServiceState.isReady && isQueueEmpty()
} else {

View file

@ -14,7 +14,6 @@
android:orientation="vertical">
<TextView
android:id="@+id/omnipod_eros_pod_management_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
@ -125,7 +124,7 @@
android:orientation="horizontal"
app:layout_constraintGuide_percent="0" />
<info.nightscout.androidaps.utils.ui.SingleClickButton
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_play_test_beep"
style="?android:attr/buttonStyle"
android:layout_width="0dp"
@ -256,7 +255,7 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<info.nightscout.androidaps.utils.ui.SingleClickButton
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_pulse_log"
style="?android:attr/buttonStyle"
android:layout_width="0dp"