Merge pull request #27 from 0pen-dash/andrei/retries
retries, error handling, session resynchronization
This commit is contained in:
commit
feed6a2f63
83 changed files with 2115 additions and 912 deletions
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
|
||||
|
||||
class BleCommandNack(idx: Byte) : BleCommand(BleCommandType.NACK, byteArrayOf(idx))
|
|
@ -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")
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
|
||||
|
||||
class BleIOBusyException : Exception()
|
||||
class BusyException : Exception("Bluetooth busy")
|
|
@ -1,3 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
|
||||
|
||||
class CharacteristicNotFoundException(cmdCharacteristicUuid: String) : FailedToConnectException("characteristic not found: $cmdCharacteristicUuid")
|
|
@ -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)
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
|
@ -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()}")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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 ?: ""}")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -10,4 +10,4 @@ class PodAlarmException(val response: AlarmStatusResponse) : Exception(
|
|||
response.alarmType.value.toInt() and 0xff,
|
||||
response.alarmType.name
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
|
||||
|
||||
class ServiceNotFoundException(serviceUuid: String) : FailedToConnectException("service not found: $serviceUuid")
|
|
@ -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")
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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 {
|
|
@ -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="
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
|
||||
|
||||
interface DisconnectHandler {
|
||||
|
||||
fun onConnectionLost(status: Int)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,4 +41,4 @@ object ResponseUtil {
|
|||
ResponseType.StatusResponseType.UNKNOWN -> throw CouldNotParseResponseException("Unrecognized additional status response type: ${payload[2]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> =
|
||||
|
|
|
@ -47,7 +47,7 @@ interface OmnipodDashPodStateManager {
|
|||
|
||||
val tempBasal: TempBasal?
|
||||
val tempBasalActive: Boolean
|
||||
val basalProgram: BasalProgram?
|
||||
var basalProgram: BasalProgram?
|
||||
|
||||
fun increaseMessageSequenceNumber()
|
||||
fun increaseEapAkaSequenceNumber(): ByteArray
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,5 +55,4 @@ class ProgramBasalCommandTest {
|
|||
encoded
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue