Omnipod Dash: provide and verify expected response type

This commit is contained in:
Bart Sopers 2021-03-14 21:25:15 +01:00
parent aff38851e1
commit 10c316edd9
8 changed files with 92 additions and 51 deletions

View file

@ -10,7 +10,6 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definitio
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.* 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.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.rx.AapsSchedulers
import info.nightscout.androidaps.utils.rx.retryWithBackoff
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.Action import io.reactivex.functions.Action
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
@ -72,19 +71,29 @@ class OmnipodDashManagerImpl @Inject constructor(
.setNumberOfUnits(units) .setNumberOfUnits(units)
.setDelayBetweenPulsesInEighthSeconds(rateInEighthPulsesPerSeconds) .setDelayBetweenPulsesInEighthSeconds(rateInEighthPulsesPerSeconds)
.setProgramReminder(ProgramReminder(confirmationBeeps, completionBeeps, 0)) .setProgramReminder(ProgramReminder(confirmationBeeps, completionBeeps, 0))
.build() .build(),
DefaultStatusResponse::class
) )
} }
} }
private fun observeSendGetPodStatusCommand(type: ResponseType.StatusResponseType = ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE): Observable<PodEvent> { private fun observeSendGetPodStatusCommand(type: ResponseType.StatusResponseType = ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE): Observable<PodEvent> {
// TODO move somewhere else
val expectedResponseType = when (type) {
ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE -> DefaultStatusResponse::class
ResponseType.StatusResponseType.ALARM_STATUS -> AlarmStatusResponse::class
else -> return Observable.error(UnsupportedOperationException("No response type to class mapping for ${type.name}"))
}
return Observable.defer { return Observable.defer {
bleManager.sendCommand( bleManager.sendCommand(
GetStatusCommand.Builder() GetStatusCommand.Builder()
.setUniqueId(podStateManager.uniqueId!!.toInt()) .setUniqueId(podStateManager.uniqueId!!.toInt())
.setSequenceNumber(podStateManager.messageSequenceNumber) .setSequenceNumber(podStateManager.messageSequenceNumber)
.setStatusResponseType(type) .setStatusResponseType(type)
.build() .build(),
expectedResponseType
) )
} }
} }
@ -115,7 +124,8 @@ class OmnipodDashManagerImpl @Inject constructor(
.setSequenceNumber(podStateManager.messageSequenceNumber) .setSequenceNumber(podStateManager.messageSequenceNumber)
.setNonce(1229869870) // TODO .setNonce(1229869870) // TODO
.setAlertConfigurations(alertConfigurations) .setAlertConfigurations(alertConfigurations)
.build() .build(),
DefaultStatusResponse::class
) )
} }
} }
@ -130,7 +140,8 @@ class OmnipodDashManagerImpl @Inject constructor(
.setProgramReminder(ProgramReminder(atStart = false, atEnd = false, atInterval = 0)) .setProgramReminder(ProgramReminder(atStart = false, atEnd = false, atInterval = 0))
.setBasalProgram(basalProgram) .setBasalProgram(basalProgram)
.setCurrentTime(Date()) .setCurrentTime(Date())
.build() .build(),
DefaultStatusResponse::class
) )
} }
} }
@ -159,7 +170,8 @@ class OmnipodDashManagerImpl @Inject constructor(
.setLotNumber(podStateManager.lotNumber!!.toInt()) // .setLotNumber(podStateManager.lotNumber!!.toInt()) //
.setPodSequenceNumber(podStateManager.podSequenceNumber!!.toInt()) .setPodSequenceNumber(podStateManager.podSequenceNumber!!.toInt())
.setInitializationTime(Date()) .setInitializationTime(Date())
.build() .build(),
SetUniqueIdResponse::class
) // ) //
} }
@ -169,7 +181,8 @@ class OmnipodDashManagerImpl @Inject constructor(
GetVersionCommand.Builder() // GetVersionCommand.Builder() //
.setSequenceNumber(podStateManager.messageSequenceNumber) // .setSequenceNumber(podStateManager.messageSequenceNumber) //
.setUniqueId(DEFAULT_UNIQUE_ID) // .setUniqueId(DEFAULT_UNIQUE_ID) //
.build() .build(),
VersionResponse::class
) // ) //
} }

View file

@ -3,11 +3,13 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import io.reactivex.Observable import io.reactivex.Observable
import kotlin.reflect.KClass
interface OmnipodDashBleManager { interface OmnipodDashBleManager {
fun sendCommand(cmd: Command): Observable<PodEvent> fun sendCommand(cmd: Command, responseType: KClass<out Response>): Observable<PodEvent>
fun getStatus(): ConnectionStatus fun getStatus(): ConnectionStatus

View file

@ -24,6 +24,7 @@ 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.comm.status.ConnectionStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.state.OmnipodDashPodStateManager
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import io.reactivex.Observable import io.reactivex.Observable
@ -32,6 +33,7 @@ import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.reflect.KClass
@Singleton @Singleton
class OmnipodDashBleManagerImpl @Inject constructor( class OmnipodDashBleManagerImpl @Inject constructor(
@ -90,40 +92,39 @@ class OmnipodDashBleManagerImpl @Inject constructor(
return bleIO return bleIO
} }
@Throws(IllegalResponseException::class, UnsupportedOperationException::class) override fun sendCommand(cmd: Command, responseType: KClass<out Response>): Observable<PodEvent> =
override fun sendCommand(cmd: Command): Observable<PodEvent> = Observable.create { emitter -> Observable.create { emitter ->
try { try {
val keys = sessionKeys val keys = sessionKeys
val mIO = msgIO val mIO = msgIO
if (keys == null || mIO == null) { if (keys == null || mIO == null) {
throw Exception("Not connected") throw Exception("Not connected")
}
emitter.onNext(PodEvent.CommandSending(cmd))
// TODO switch to RX
emitter.onNext(PodEvent.CommandSent(cmd))
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck
)
val session = Session(
aapsLogger = aapsLogger,
msgIO = mIO,
myId = myId,
podId = podId,
sessionKeys = keys,
enDecrypt = enDecrypt
)
val response = session.sendCommand(cmd, responseType)
emitter.onNext(PodEvent.ResponseReceived(response))
emitter.onComplete()
} catch (ex: Exception) {
emitter.tryOnError(ex)
} }
emitter.onNext(PodEvent.CommandSending(cmd))
// TODO switch to RX
emitter.onNext(PodEvent.CommandSent(cmd))
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck
)
val session = Session(
aapsLogger = aapsLogger,
msgIO = mIO,
myId = myId,
podId = podId,
sessionKeys = keys,
enDecrypt = enDecrypt
)
val response = session.sendCommand(cmd)
emitter.onNext(PodEvent.ResponseReceived(response))
emitter.onComplete()
} catch (ex: Exception) {
emitter.tryOnError(ex)
} }
}
override fun getStatus(): ConnectionStatus { override fun getStatus(): ConnectionStatus {
var s: ConnectionStatus var s: ConnectionStatus

View file

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

View file

@ -1,3 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class IllegalResponseException(message: String?) : Exception(message) import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import kotlin.reflect.KClass
class IllegalResponseException(
expectedResponseType: KClass<out Response>,
actualResponse: Response
) : Exception("Illegal response: expected ${expectedResponseType.simpleName} but got $actualResponse")

View file

@ -0,0 +1,5 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
class PodAlarmException(val response: AlarmStatusResponse) : Exception("Pod is in alarm: ${response.alarmType.value} ${response.alarmType.name}")

View file

@ -1,32 +1,32 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.IllegalResponseException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotParseResponseException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.byValue import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.byValue
object ResponseUtil { object ResponseUtil {
@Throws(IllegalResponseException::class, UnsupportedOperationException::class) @Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
fun parseResponse(payload: ByteArray): Response { fun parseResponse(payload: ByteArray): Response {
return when (val responseType = byValue(payload[0], ResponseType.UNKNOWN)) { return when (val responseType = byValue(payload[0], ResponseType.UNKNOWN)) {
ResponseType.ACTIVATION_RESPONSE -> parseActivationResponse(payload) ResponseType.ACTIVATION_RESPONSE -> parseActivationResponse(payload)
ResponseType.DEFAULT_STATUS_RESPONSE -> DefaultStatusResponse(payload) ResponseType.DEFAULT_STATUS_RESPONSE -> DefaultStatusResponse(payload)
ResponseType.ADDITIONAL_STATUS_RESPONSE -> parseAdditionalStatusResponse(payload) ResponseType.ADDITIONAL_STATUS_RESPONSE -> parseAdditionalStatusResponse(payload)
ResponseType.NAK_RESPONSE -> NakResponse(payload) ResponseType.NAK_RESPONSE -> NakResponse(payload)
ResponseType.UNKNOWN -> throw IllegalResponseException("Unrecognized message type: $responseType") ResponseType.UNKNOWN -> throw CouldNotParseResponseException("Unrecognized message type: $responseType")
} }
} }
@Throws(IllegalResponseException::class) @Throws(CouldNotParseResponseException::class)
private fun parseActivationResponse(payload: ByteArray): Response { private fun parseActivationResponse(payload: ByteArray): Response {
return when (val activationResponseType = byValue(payload[1], ResponseType.ActivationResponseType.UNKNOWN)) { return when (val activationResponseType = byValue(payload[1], ResponseType.ActivationResponseType.UNKNOWN)) {
ResponseType.ActivationResponseType.GET_VERSION_RESPONSE -> VersionResponse(payload) ResponseType.ActivationResponseType.GET_VERSION_RESPONSE -> VersionResponse(payload)
ResponseType.ActivationResponseType.SET_UNIQUE_ID_RESPONSE -> SetUniqueIdResponse(payload) ResponseType.ActivationResponseType.SET_UNIQUE_ID_RESPONSE -> SetUniqueIdResponse(payload)
ResponseType.ActivationResponseType.UNKNOWN -> throw IllegalResponseException("Unrecognized activation response type: $activationResponseType") ResponseType.ActivationResponseType.UNKNOWN -> throw CouldNotParseResponseException("Unrecognized activation response type: $activationResponseType")
} }
} }
@Throws(IllegalResponseException::class, UnsupportedOperationException::class) @Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
private fun parseAdditionalStatusResponse(payload: ByteArray): Response { private fun parseAdditionalStatusResponse(payload: ByteArray): Response {
return when (val additionalStatusResponseType = byValue(payload[2], ResponseType.StatusResponseType.UNKNOWN)) { return when (val additionalStatusResponseType = byValue(payload[2], ResponseType.StatusResponseType.UNKNOWN)) {
ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE -> DefaultStatusResponse(payload) // Unreachable; this response type is only used for requesting a default status response ResponseType.StatusResponseType.DEFAULT_STATUS_RESPONSE -> DefaultStatusResponse(payload) // Unreachable; this response type is only used for requesting a default status response
@ -38,7 +38,7 @@ object ResponseUtil {
ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_70 -> throw UnsupportedOperationException("Status response page 70 is not (yet) implemented") ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_70 -> throw UnsupportedOperationException("Status response page 70 is not (yet) implemented")
ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_80 -> throw UnsupportedOperationException("Status response page 80 is not (yet) implemented") ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_80 -> throw UnsupportedOperationException("Status response page 80 is not (yet) implemented")
ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_81 -> throw UnsupportedOperationException("Status response page 81 is not (yet) implemented") ResponseType.StatusResponseType.STATUS_RESPONSE_PAGE_81 -> throw UnsupportedOperationException("Status response page 81 is not (yet) implemented")
ResponseType.StatusResponseType.UNKNOWN -> throw IllegalResponseException("Unrecognized additional status response type: $additionalStatusResponseType") ResponseType.StatusResponseType.UNKNOWN -> throw CouldNotParseResponseException("Unrecognized additional status response type: $additionalStatusResponseType")
} }
} }
} }

View file

@ -4,15 +4,19 @@ import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.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.IllegalResponseException
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.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType 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
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.AlarmStatusResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import kotlin.reflect.KClass
class Session( class Session(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
@ -29,8 +33,8 @@ class Session(
* <- response, ACK TODO: retries? * <- response, ACK TODO: retries?
* -> ACK * -> ACK
*/ */
@Throws(IllegalResponseException::class, UnsupportedOperationException::class) @Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
fun sendCommand(cmd: Command): Response { fun sendCommand(cmd: Command, responseType: KClass<out Response>): Response {
sessionKeys.msgSequenceNumber++ sessionKeys.msgSequenceNumber++
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd") aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()} in packet $cmd")
@ -43,6 +47,13 @@ class Session(
aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted") aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted")
val response = parseResponse(decrypted) val response = parseResponse(decrypted)
if (!responseType.isInstance(response)) {
if (response is AlarmStatusResponse) {
throw PodAlarmException(response)
}
throw IllegalResponseException(responseType, response)
}
sessionKeys.msgSequenceNumber++ sessionKeys.msgSequenceNumber++
val ack = getAck(responseMsg) val ack = getAck(responseMsg)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending ACK: ${ack.payload.toHex()} in packet $ack") aapsLogger.debug(LTag.PUMPBTCOMM, "Sending ACK: ${ack.payload.toHex()} in packet $ack")
@ -50,7 +61,7 @@ class Session(
return response return response
} }
@Throws(IllegalResponseException::class, UnsupportedOperationException::class) @Throws(CouldNotParseResponseException::class, UnsupportedOperationException::class)
private fun parseResponse(decrypted: MessagePacket): Response { private fun parseResponse(decrypted: MessagePacket): Response {
val payload = parseKeys(arrayOf(RESPONSE_PREFIX), decrypted.payload)[0] val payload = parseKeys(arrayOf(RESPONSE_PREFIX), decrypted.payload)[0]