Merge branch 'dash' into bart/improvements
This commit is contained in:
commit
82d2f9ae36
37 changed files with 950 additions and 150 deletions
|
@ -23,6 +23,7 @@ buildscript {
|
||||||
commonscodec_version = '1.15'
|
commonscodec_version = '1.15'
|
||||||
jodatime_version = '2.10.10'
|
jodatime_version = '2.10.10'
|
||||||
work_version = '2.5.0'
|
work_version = '2.5.0'
|
||||||
|
tink_version = '1.5.0'
|
||||||
|
|
||||||
junit_version = '4.13.2'
|
junit_version = '4.13.2'
|
||||||
mockitoVersion = '3.7.7'
|
mockitoVersion = '3.7.7'
|
||||||
|
|
|
@ -46,6 +46,7 @@ dependencies {
|
||||||
|
|
||||||
//CryptoUtil
|
//CryptoUtil
|
||||||
api 'com.madgag.spongycastle:core:1.58.0.0'
|
api 'com.madgag.spongycastle:core:1.58.0.0'
|
||||||
|
api "com.google.crypto.tink:tink-android:$tink_version"
|
||||||
|
|
||||||
// Graphview cannot be upgraded
|
// Graphview cannot be upgraded
|
||||||
api "com.jjoe64:graphview:4.0.1"
|
api "com.jjoe64:graphview:4.0.1"
|
||||||
|
|
|
@ -13,9 +13,16 @@ dependencies {
|
||||||
testImplementation "org.skyscreamer:jsonassert:1.5.0"
|
testImplementation "org.skyscreamer:jsonassert:1.5.0"
|
||||||
testImplementation "org.hamcrest:hamcrest-all:1.3"
|
testImplementation "org.hamcrest:hamcrest-all:1.3"
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidx_junit"
|
// newer integration test libraries might not work
|
||||||
androidTestImplementation "androidx.test:rules:$androidx_rules"
|
// https://stackoverflow.com/questions/64700104/attribute-androidforcequeryable-not-found-in-android-studio-when-running-espres
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||||
|
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,9 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation project(':omnipod-common')
|
implementation project(':omnipod-common')
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
implementation "androidx.room:room-rxjava2:$room_version"
|
||||||
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
implementation 'com.github.guepardoapps:kulid:1.1.2.0'
|
||||||
}
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.github.guepardoapps.kulid.ULID
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.DashHistoryDatabase
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DashHistoryTest {
|
||||||
|
|
||||||
|
private lateinit var dao: HistoryRecordDao
|
||||||
|
private lateinit var database: DashHistoryDatabase
|
||||||
|
private lateinit var dashHistory: DashHistory
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val schedulerRule = RxSchedulerRule(Schedulers.trampoline())
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
database = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, DashHistoryDatabase::class.java).build()
|
||||||
|
dao = database.historyRecordDao()
|
||||||
|
dashHistory = DashHistory(dao, HistoryMapper())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInsertionAndConverters() {
|
||||||
|
dashHistory.getRecords().test().apply {
|
||||||
|
assertValue { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
dashHistory.createRecord(commandType = OmnipodCommandType.CANCEL_BOLUS, 0L).test().apply {
|
||||||
|
assertValue { ULID.isValid(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
dashHistory.getRecords().test().apply {
|
||||||
|
assertValue { it.size == 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExceptionOnBolusWithoutRecord() {
|
||||||
|
dashHistory.getRecords().test().apply {
|
||||||
|
assertValue { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
dashHistory.createRecord(commandType = OmnipodCommandType.SET_BOLUS, 0L).test().apply {
|
||||||
|
assertError(IllegalArgumentException::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
dashHistory.getRecords().test().apply {
|
||||||
|
assertValue { it.isEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
|
||||||
|
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.plugins.RxJavaPlugins
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import org.junit.runners.model.Statement
|
||||||
|
|
||||||
|
// TODO: move to core before the big merge
|
||||||
|
class RxSchedulerRule(val scheduler: Scheduler) : TestRule {
|
||||||
|
|
||||||
|
override fun apply(base: Statement, description: Description) =
|
||||||
|
object : Statement() {
|
||||||
|
override fun evaluate() {
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||||
|
|
||||||
|
try {
|
||||||
|
base.evaluate()
|
||||||
|
} finally {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.dagger
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.Reusable
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.DashHistory
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.DashHistoryDatabase
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
class OmnipodDashHistoryModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
internal fun provideDatabase(context: Context): DashHistoryDatabase = DashHistoryDatabase.build(context)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
internal fun provideHistoryRecordDao(dashHistoryDatabase: DashHistoryDatabase): HistoryRecordDao = dashHistoryDatabase.historyRecordDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Reusable // no state, let system decide when to reuse or create new.
|
||||||
|
internal fun provideHistoryMapper() = HistoryMapper()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
internal fun provideDashHistory(dao: HistoryRecordDao, historyMapper: HistoryMapper) =
|
||||||
|
DashHistory(dao, historyMapper)
|
||||||
|
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.OmnipodDashOvervi
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.DashPodActivationWizardActivity
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivation.DashPodDeactivationWizardActivity
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.deactivation.DashPodDeactivationWizardActivity
|
||||||
|
|
||||||
@Module
|
@Module(includes = [OmnipodDashHistoryModule::class])
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
abstract class OmnipodDashModule {
|
abstract class OmnipodDashModule {
|
||||||
// ACTIVITIES
|
// ACTIVITIES
|
||||||
|
|
|
@ -13,12 +13,13 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
|
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.BleIO
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk.LTKExchanger
|
|
||||||
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.pair.LTKExchanger
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
|
||||||
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.utils.extensions.toHex
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import org.apache.commons.lang3.NotImplementedException
|
import org.apache.commons.lang3.NotImplementedException
|
||||||
import java.util.concurrent.BlockingQueue
|
import java.util.concurrent.BlockingQueue
|
||||||
|
@ -96,8 +97,10 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context
|
||||||
val ltkExchanger = LTKExchanger(aapsLogger, msgIO)
|
val ltkExchanger = LTKExchanger(aapsLogger, msgIO)
|
||||||
emitter.onNext(PodEvent.Pairing)
|
emitter.onNext(PodEvent.Pairing)
|
||||||
|
|
||||||
val ltk = ltkExchanger.negotiateLTKAndNonce()
|
val ltk = ltkExchanger.negotiateLTK()
|
||||||
aapsLogger.info(LTag.PUMPCOMM, "Got LTK and Nonce Prefix: ${ltk}")
|
|
||||||
|
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}")
|
||||||
|
|
||||||
emitter.onNext(PodEvent.Connected(PodScanner.POD_ID_NOT_ACTIVATED)) // TODO supply actual pod id
|
emitter.onNext(PodEvent.Connected(PodScanner.POD_ID_NOT_ACTIVATED)) // TODO supply actual pod id
|
||||||
|
|
||||||
emitter.onComplete()
|
emitter.onComplete()
|
||||||
|
@ -112,7 +115,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val CONNECT_TIMEOUT_MS = 5000
|
private const val CONNECT_TIMEOUT_MS = 7000
|
||||||
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
|
const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
|
||||||
open class BleCommand(val data: ByteArray) {
|
open class BleCommand(val data: ByteArray) {
|
||||||
|
|
||||||
constructor(type: BleCommandType) : this(byteArrayOf(type.value))
|
constructor(type: BleCommandType) : this(byteArrayOf(type.value))
|
||||||
|
@ -17,6 +19,10 @@ open class BleCommand(val data: ByteArray) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Raw command: [${data.toHex()}]"
|
||||||
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return data.contentHashCode()
|
return data.contentHashCode()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
|
||||||
|
class CouldNotParseMessageException(val payload: ByteArray) : Exception("Could not parse message payload: ${payload.toHex()}")
|
|
@ -0,0 +1,6 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
|
||||||
|
|
||||||
|
class MessageIOException : Exception {
|
||||||
|
constructor(msg: String) : super(msg)
|
||||||
|
constructor(cause: Throwable) : super("Caught Exception during Message I/O", cause)
|
||||||
|
}
|
|
@ -50,7 +50,6 @@ class BleIO(private val aapsLogger: AAPSLogger, private val chars: Map<Character
|
||||||
state = IOState.WRITING
|
state = IOState.WRITING
|
||||||
}
|
}
|
||||||
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on " + characteristic.name + "/" + payload.toHex())
|
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on " + characteristic.name + "/" + payload.toHex())
|
||||||
aapsLogger.debug(LTag.PUMPBTCOMM, "BleIO: Sending data on " + characteristic.name + "/" + payload.toHex())
|
|
||||||
val ch = chars[characteristic]
|
val ch = chars[characteristic]
|
||||||
val set = ch!!.setValue(payload)
|
val set = ch!!.setValue(payload)
|
||||||
if (!set) {
|
if (!set) {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk
|
|
||||||
|
|
||||||
data class LTK(val ltk: ByteArray, val noncePrefix: ByteArray) {
|
|
||||||
init {
|
|
||||||
require(ltk.size == 16)
|
|
||||||
require(noncePrefix.size == 16)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk
|
|
||||||
|
|
||||||
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.Id
|
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
|
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding
|
|
||||||
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
|
|
||||||
|
|
||||||
internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO) {
|
|
||||||
|
|
||||||
fun negotiateLTKAndNonce(): LTK? {
|
|
||||||
// send SP1, SP2
|
|
||||||
// TODO: get this from somewhere(preferences?)
|
|
||||||
var seq: Byte = 1
|
|
||||||
val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
|
|
||||||
val nodeId = controllerId.increment()
|
|
||||||
|
|
||||||
var sp1sp2 = sp1sp2(nodeId.address, sp2(), seq, controllerId, nodeId)
|
|
||||||
msgIO.sendMesssage(sp1sp2.messagePacket)
|
|
||||||
|
|
||||||
/*
|
|
||||||
var sps1 =
|
|
||||||
msgIO.sendMesssage(sps1.messagePacket)
|
|
||||||
// send SPS1
|
|
||||||
// read SPS1
|
|
||||||
val podSps1 = msgIO.receiveMessage()
|
|
||||||
|
|
||||||
// send SPS2
|
|
||||||
var sps2 = PairMessage()
|
|
||||||
msgIO.sendMesssage(sps2.messagePacket)
|
|
||||||
// read SPS2
|
|
||||||
val podSps2 = msgIO.receiveMessage()
|
|
||||||
|
|
||||||
// send SP0GP0
|
|
||||||
msgIO.sendMesssage(sps2.messagePacket)
|
|
||||||
// read P0
|
|
||||||
val p0 = msgIO.receiveMessage()
|
|
||||||
*/
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sp2(): ByteArray {
|
|
||||||
// This is GetPodStatus command, with page 0 parameter.
|
|
||||||
// We could replace that in the future with the serialized GetPodStatus()
|
|
||||||
return GET_POD_STATUS_HEX_COMMAND.hexStringToByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sp1sp2(sp1: ByteArray, sp2: ByteArray, seq: Byte, controllerId: Id, nodeId: Id): PairMessage {
|
|
||||||
val payload = StringLengthPrefixEncoding.formatKeys(
|
|
||||||
arrayOf("SP1=", ",SP2="),
|
|
||||||
arrayOf(sp1, sp2),
|
|
||||||
)
|
|
||||||
return PairMessage(
|
|
||||||
sequenceNumber = seq,
|
|
||||||
source = controllerId,
|
|
||||||
destination = nodeId,
|
|
||||||
payload = payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private 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.
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
|
||||||
|
class CrcMismatchException(val expected: Long, val actual: Long, val payload: ByteArray) :
|
||||||
|
Exception("CRC mismatch. Actual: ${actual}. Expected: ${expected}. Payload: ${payload.toHex()}")
|
|
@ -0,0 +1,5 @@
|
||||||
|
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) : Exception("Invalid payload: ${payload.toHex()}. Expected index: ${expectedIndex}")
|
|
@ -3,12 +3,11 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
||||||
import info.nightscout.androidaps.logging.AAPSLogger
|
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.command.*
|
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.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.BleIO
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
|
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.PayloadJoiner
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadJoinerActionAccept
|
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadJoinerActionReject
|
|
||||||
import info.nightscout.androidaps.utils.extensions.toHex
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
|
||||||
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
|
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
|
||||||
|
@ -21,10 +20,11 @@ class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
|
||||||
throw UnexpectedCommandException(BleCommand(expectCTS))
|
throw UnexpectedCommandException(BleCommand(expectCTS))
|
||||||
}
|
}
|
||||||
val payload = msg.asByteArray()
|
val payload = msg.asByteArray()
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending message: ${payload.toHex()}")
|
||||||
val splitter = PayloadSplitter(payload)
|
val splitter = PayloadSplitter(payload)
|
||||||
val packets = splitter.splitInPackets()
|
val packets = splitter.splitInPackets()
|
||||||
for (packet in packets) {
|
for (packet in packets) {
|
||||||
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ", packet.asByteArray().toHex())
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending DATA: ${packet.asByteArray().toHex()}")
|
||||||
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.asByteArray())
|
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.asByteArray())
|
||||||
}
|
}
|
||||||
// TODO: peek for NACKs
|
// TODO: peek for NACKs
|
||||||
|
@ -38,33 +38,29 @@ class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
|
||||||
|
|
||||||
fun receiveMessage(): MessagePacket {
|
fun receiveMessage(): MessagePacket {
|
||||||
val expectRTS = bleIO.receivePacket(CharacteristicType.CMD)
|
val expectRTS = bleIO.receivePacket(CharacteristicType.CMD)
|
||||||
if (BleCommand(expectRTS) != BleCommandCTS()) {
|
if (BleCommand(expectRTS) != BleCommandRTS()) {
|
||||||
throw UnexpectedCommandException(BleCommand(expectRTS))
|
throw UnexpectedCommandException(BleCommand(expectRTS))
|
||||||
}
|
}
|
||||||
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandCTS().data)
|
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandCTS().data)
|
||||||
val joiner = PayloadJoiner()
|
try {
|
||||||
var data = bleIO.receivePacket(CharacteristicType.DATA)
|
val joiner = PayloadJoiner(bleIO.receivePacket(CharacteristicType.DATA))
|
||||||
val fragments = joiner.start(data)
|
for (i in 1 until joiner.fullFragments + 1) {
|
||||||
for (i in 1 until fragments) {
|
joiner.accumulate(bleIO.receivePacket(CharacteristicType.DATA))
|
||||||
data = bleIO.receivePacket(CharacteristicType.DATA)
|
|
||||||
val accumlateAction = joiner.accumulate(data)
|
|
||||||
if (accumlateAction is PayloadJoinerActionReject) {
|
|
||||||
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandNack(accumlateAction.idx).data)
|
|
||||||
}
|
}
|
||||||
}
|
if (joiner.oneExtraPacket) {
|
||||||
if (joiner.oneExtra) {
|
joiner.accumulate(bleIO.receivePacket(CharacteristicType.DATA))
|
||||||
var data = bleIO.receivePacket(CharacteristicType.DATA)
|
|
||||||
val accumulateAction = joiner.accumulate(data)
|
|
||||||
if (accumulateAction is PayloadJoinerActionReject) {
|
|
||||||
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandNack(accumulateAction.idx).data)
|
|
||||||
}
|
}
|
||||||
|
val fullPayload = joiner.finalize()
|
||||||
|
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, 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)
|
||||||
|
} catch (e: CrcMismatchException) {
|
||||||
|
aapsLogger.warn(LTag.PUMPBTCOMM, "CRC mismatch: $e")
|
||||||
|
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandFail().data)
|
||||||
|
throw MessageIOException(cause = e)
|
||||||
}
|
}
|
||||||
val finalCmd = when (joiner.finalize()) {
|
|
||||||
is PayloadJoinerActionAccept -> BleCommandSuccess()
|
|
||||||
is PayloadJoinerActionReject -> BleCommandFail()
|
|
||||||
}
|
|
||||||
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, finalCmd.data)
|
|
||||||
val fullPayload = joiner.bytes()
|
|
||||||
return MessagePacket.parse(fullPayload)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
||||||
|
|
||||||
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.exceptions.CouldNotParseMessageException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
@ -69,9 +70,56 @@ data class MessagePacket(
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val MAGIC_PATTERN = "TW" // all messages start with this string
|
private val MAGIC_PATTERN = "TW" // all messages start with this string
|
||||||
|
private val HEADER_SIZE = 16
|
||||||
|
|
||||||
fun parse(payload: ByteArray): MessagePacket {
|
fun parse(payload: ByteArray): MessagePacket {
|
||||||
TODO("implement message header parsing")
|
if (payload.size < HEADER_SIZE) {
|
||||||
|
throw CouldNotParseMessageException(payload)
|
||||||
|
}
|
||||||
|
if (payload.copyOfRange(0, 2).decodeToString() != MAGIC_PATTERN) {
|
||||||
|
throw CouldNotParseMessageException(payload)
|
||||||
|
}
|
||||||
|
val f1 = Flag(payload[2].toInt())
|
||||||
|
val sas = f1.get(3) != 0
|
||||||
|
val tfs = f1.get(4) != 0
|
||||||
|
val version = ((f1.get(0) shl 2) or (f1.get(1) shl 1) or (f1.get(2) shl 0)).toShort()
|
||||||
|
val eqos = (f1.get(7) or (f1.get(6) shl 1) or (f1.get(5) shl 2)).toShort()
|
||||||
|
|
||||||
|
val f2 = Flag(payload[3].toInt())
|
||||||
|
val ack = f2.get(0) != 0
|
||||||
|
val priority = f2.get(1) != 0
|
||||||
|
val lastMessage = f2.get(2) != 0
|
||||||
|
val gateway = f2.get(3) != 0
|
||||||
|
val type = MessageType.byValue((f1.get(7) or (f1.get(6) shl 1) or (f1.get(5) shl 2) or (f1.get(4) shl 3)).toByte())
|
||||||
|
if (version.toInt() != 0) {
|
||||||
|
throw CouldNotParseMessageException(payload)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
val payloadEnd = 16 + size +
|
||||||
|
if (type == MessageType.ENCRYPTED) 8
|
||||||
|
else 0
|
||||||
|
|
||||||
|
return MessagePacket(
|
||||||
|
type = type,
|
||||||
|
ack = ack,
|
||||||
|
eqos = eqos,
|
||||||
|
priority = priority,
|
||||||
|
lastMessage = lastMessage,
|
||||||
|
gateway = gateway,
|
||||||
|
sas = sas,
|
||||||
|
tfs = tfs,
|
||||||
|
version = version,
|
||||||
|
sequenceNumber = sequenceNumber,
|
||||||
|
ackNumber = ackNumber,
|
||||||
|
source = Id(payload.copyOfRange(8, 12)),
|
||||||
|
destination = Id(payload.copyOfRange(12, 16)),
|
||||||
|
payload = payload.copyOfRange(16, payloadEnd),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,8 +133,14 @@ private class Flag(var value: Int = 0) {
|
||||||
value = value or mask
|
value = value or mask
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(idx: Byte): Boolean {
|
fun get(idx: Byte): Int {
|
||||||
val mask = 1 shl (7 - idx)
|
val mask = 1 shl (7 - idx)
|
||||||
return value and mask != 0
|
if (value and mask == 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun Byte.toUnsignedInt() = this.toInt() and 0xff
|
|
@ -1,31 +1,109 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
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.*
|
||||||
|
|
||||||
sealed class PayloadJoinerAction
|
class PayloadJoiner(private val firstPacket: ByteArray) {
|
||||||
|
|
||||||
class PayloadJoinerActionAccept : PayloadJoinerAction()
|
var oneExtraPacket: Boolean = false
|
||||||
class PayloadJoinerActionReject(val idx: Byte) : PayloadJoinerAction()
|
val fullFragments: Int
|
||||||
|
var crc: Long = 0
|
||||||
|
private var expectedIndex = 0
|
||||||
|
private val fragments: LinkedList<ByteArray> = LinkedList<ByteArray>()
|
||||||
|
|
||||||
class PayloadJoiner {
|
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)
|
||||||
|
|
||||||
var oneExtra: Boolean = false
|
fullFragments == 0 -> {
|
||||||
|
crc = ByteBuffer.wrap(firstPacket.copyOfRange(2, 6)).int.toUnsignedLong()
|
||||||
|
val rest = firstPacket[6]
|
||||||
|
val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, BlePacket.MAX_SIZE)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
private val payload = ByteArrayOutputStream()
|
// With middle packets
|
||||||
|
firstPacket.size < BlePacket.MAX_SIZE ->
|
||||||
|
throw IncorrectPacketException(0, firstPacket)
|
||||||
|
|
||||||
fun start(payload: ByteArray): Int {
|
else -> {
|
||||||
TODO("not implemented")
|
fragments.add(firstPacket.copyOfRange(FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS, BlePacket.MAX_SIZE))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun accumulate(payload: ByteArray): PayloadJoinerAction {
|
fun accumulate(packet: ByteArray) {
|
||||||
TODO("not implemented")
|
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, BlePacket.MAX_SIZE)
|
||||||
|
oneExtraPacket = rest + LastBlePacket.HEADER_SIZE > end
|
||||||
|
if (packet.size < end) {
|
||||||
|
throw IncorrectPacketException(idx.toByte(), packet)
|
||||||
|
}
|
||||||
|
fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, packet.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(): PayloadJoinerAction {
|
fun finalize(): ByteArray {
|
||||||
TODO("not implemented")
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bytes(): ByteArray {
|
|
||||||
TODO("not implemented")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun Int.toUnsignedLong() = this.toLong() and 0xffffffffL
|
|
@ -25,6 +25,7 @@ internal class PayloadSplitter(private val payload: ByteArray) {
|
||||||
ret.add(LastOptionalPlusOneBlePacket(
|
ret.add(LastOptionalPlusOneBlePacket(
|
||||||
index = 1,
|
index = 1,
|
||||||
payload = payload.copyOfRange(end, payload.size),
|
payload = payload.copyOfRange(end, payload.size),
|
||||||
|
size = (payload.size - end).toByte(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
@ -53,9 +54,10 @@ internal class PayloadSplitter(private val payload: ByteArray) {
|
||||||
payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS, middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + end),
|
payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS, middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + end),
|
||||||
crc32 = crc32,
|
crc32 = crc32,
|
||||||
))
|
))
|
||||||
if (rest > 14) {
|
if (rest > LastBlePacket.CAPACITY) {
|
||||||
ret.add(LastOptionalPlusOneBlePacket(
|
ret.add(LastOptionalPlusOneBlePacket(
|
||||||
index = (middleFragments + 2).toByte(),
|
index = (middleFragments + 2).toByte(),
|
||||||
|
size = (rest - LastBlePacket.CAPACITY).toByte(),
|
||||||
payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + LastBlePacket.CAPACITY, payload.size),
|
payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + LastBlePacket.CAPACITY, payload.size),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ internal class PayloadSplitter(private val payload: ByteArray) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ByteArray.crc32(): Long {
|
internal fun ByteArray.crc32(): Long {
|
||||||
val crc = CRC32()
|
val crc = CRC32()
|
||||||
crc.update(this)
|
crc.update(this)
|
||||||
return crc.value
|
return crc.value
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
@ -9,8 +11,33 @@ class StringLengthPrefixEncoding {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun parseKeys(keys: List<String>): List<ByteArray> {
|
private val LENGTH_BYTES = 2
|
||||||
TODO("not implemented")
|
|
||||||
|
fun parseKeys(keys: Array<String>, payload: ByteArray): Array<ByteArray> {
|
||||||
|
val ret = Array<ByteArray>(keys.size, { ByteArray(0) })
|
||||||
|
var remaining = payload
|
||||||
|
for ((index, key) in keys.withIndex()) {
|
||||||
|
when {
|
||||||
|
remaining.size < key.length ->
|
||||||
|
throw MessageIOException("Payload too short: ${payload.toHex()} for key: ${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 = 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()}")
|
||||||
|
}
|
||||||
|
ret[index] = remaining.copyOfRange(LENGTH_BYTES, LENGTH_BYTES + length)
|
||||||
|
remaining = remaining.copyOfRange(LENGTH_BYTES + length, remaining.size)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatKeys(keys: Array<String>, payloads: Array<ByteArray>): ByteArray {
|
fun formatKeys(keys: Array<String>, payloads: Array<ByteArray>): ByteArray {
|
||||||
|
|
|
@ -7,8 +7,8 @@ sealed class BlePacket {
|
||||||
abstract fun asByteArray(): ByteArray
|
abstract fun asByteArray(): ByteArray
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_BLE_PACKET_LEN = 20
|
|
||||||
const val MAX_BLE_BUFFER_LEN = MAX_BLE_PACKET_LEN + 1 // we use this as the size allocated for the ByteBuffer
|
const val MAX_SIZE = 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ data class FirstBlePacket(val totalFragments: Byte, val payload: ByteArray, val
|
||||||
|
|
||||||
override fun asByteArray(): ByteArray {
|
override fun asByteArray(): ByteArray {
|
||||||
val bb = ByteBuffer
|
val bb = ByteBuffer
|
||||||
.allocate(MAX_BLE_BUFFER_LEN)
|
.allocate(MAX_SIZE)
|
||||||
.put(0) // index
|
.put(0) // index
|
||||||
.put(totalFragments) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket
|
.put(totalFragments) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket
|
||||||
crc32?.let {
|
crc32?.let {
|
||||||
|
@ -26,15 +26,22 @@ data class FirstBlePacket(val totalFragments: Byte, val payload: ByteArray, val
|
||||||
bb.put(size)
|
bb.put(size)
|
||||||
}
|
}
|
||||||
bb.put(payload)
|
bb.put(payload)
|
||||||
val ret = ByteArray(bb.position())
|
|
||||||
|
val pos = bb.position()
|
||||||
|
val ret = ByteArray(MAX_SIZE)
|
||||||
bb.flip()
|
bb.flip()
|
||||||
bb.get(ret)
|
bb.get(ret, 0, pos)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val CAPACITY_WITHOUT_MIDDLE_PACKETS = 13 // we are using all fields
|
|
||||||
internal const val CAPACITY_WITH_MIDDLE_PACKETS = 18 // we are not using crc32 or size
|
internal const val HEADER_SIZE_WITHOUT_MIDDLE_PACKETS = 7 // we are using all fields
|
||||||
|
internal 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
|
internal const val CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET = 18
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +53,7 @@ data class MiddleBlePacket(val index: Byte, val payload: ByteArray) : BlePacket(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
internal const val CAPACITY = 19
|
internal const val CAPACITY = 19
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,26 +62,34 @@ data class LastBlePacket(val index: Byte, val size: Byte, val payload: ByteArray
|
||||||
|
|
||||||
override fun asByteArray(): ByteArray {
|
override fun asByteArray(): ByteArray {
|
||||||
val bb = ByteBuffer
|
val bb = ByteBuffer
|
||||||
.allocate(MAX_BLE_BUFFER_LEN)
|
.allocate(MAX_SIZE)
|
||||||
.put(index)
|
.put(index)
|
||||||
.put(size)
|
.put(size)
|
||||||
.putInt(crc32.toInt())
|
.putInt(crc32.toInt())
|
||||||
.put(payload)
|
.put(payload)
|
||||||
val ret = ByteArray(bb.position())
|
val pos = bb.position()
|
||||||
|
val ret = ByteArray(MAX_SIZE)
|
||||||
bb.flip()
|
bb.flip()
|
||||||
bb.get(ret)
|
bb.get(ret, 0, pos)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val CAPACITY = 14
|
|
||||||
|
internal const val HEADER_SIZE = 6
|
||||||
|
internal const val CAPACITY = MAX_SIZE - HEADER_SIZE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray) : BlePacket() {
|
data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray, val size: Byte) : BlePacket() {
|
||||||
|
|
||||||
override fun asByteArray(): ByteArray {
|
override fun asByteArray(): ByteArray {
|
||||||
return byteArrayOf(index) + payload
|
return byteArrayOf(index, size) + payload + ByteArray(MAX_SIZE - payload.size - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
internal const val HEADER_SIZE = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,256 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
|
||||||
|
|
||||||
|
import com.google.crypto.tink.subtle.X25519
|
||||||
|
import info.nightscout.androidaps.logging.AAPSLogger
|
||||||
|
import info.nightscout.androidaps.logging.LTag
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.OmnipodDashBleManagerImpl
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
|
||||||
|
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
import org.spongycastle.crypto.engines.AESEngine
|
||||||
|
import org.spongycastle.crypto.macs.CMac
|
||||||
|
import org.spongycastle.crypto.params.KeyParameter
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO) {
|
||||||
|
|
||||||
|
private val pdmPrivate = X25519.generatePrivateKey()
|
||||||
|
private val pdmPublic = X25519.publicFromPrivate(pdmPrivate)
|
||||||
|
private var podPublic = ByteArray(PUBLIC_KEY_SIZE)
|
||||||
|
private var podNonce = ByteArray(NONCE_SIZE)
|
||||||
|
private val pdmNonce = ByteArray(NONCE_SIZE)
|
||||||
|
private val pdmConf = ByteArray(CMAC_SIZE)
|
||||||
|
private val podConf = ByteArray(CMAC_SIZE)
|
||||||
|
private val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
|
||||||
|
val nodeId = controllerId.increment()
|
||||||
|
private var seq: Byte = 1
|
||||||
|
private var ltk = ByteArray(CMAC_SIZE)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val random = SecureRandom()
|
||||||
|
random.nextBytes(pdmNonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun negotiateLTK(): PairResult {
|
||||||
|
// send SP1, SP2
|
||||||
|
var sp1sp2 = sp1sp2(nodeId.address, sp2())
|
||||||
|
msgIO.sendMesssage(sp1sp2.messagePacket)
|
||||||
|
|
||||||
|
seq++
|
||||||
|
var sps1 = sps1()
|
||||||
|
msgIO.sendMesssage(sps1.messagePacket)
|
||||||
|
// send SPS1
|
||||||
|
|
||||||
|
// read SPS1
|
||||||
|
val podSps1 = msgIO.receiveMessage()
|
||||||
|
processSps1FromPod(podSps1)
|
||||||
|
// now we have all the data to generate: confPod, confPdm, ltk and noncePrefix
|
||||||
|
generateKeys()
|
||||||
|
seq++
|
||||||
|
// send SPS2
|
||||||
|
val sps2 = sps2()
|
||||||
|
msgIO.sendMesssage(sps2.messagePacket)
|
||||||
|
// read SPS2
|
||||||
|
|
||||||
|
val podSps2 = msgIO.receiveMessage()
|
||||||
|
validatePodSps2(podSps2)
|
||||||
|
|
||||||
|
seq++
|
||||||
|
// send SP0GP0
|
||||||
|
msgIO.sendMesssage(sp0gp0().messagePacket)
|
||||||
|
// read P0
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
return PairResult(
|
||||||
|
ltk = ltk
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sp1sp2(sp1: ByteArray, sp2: ByteArray): PairMessage {
|
||||||
|
val payload = StringLengthPrefixEncoding.formatKeys(
|
||||||
|
arrayOf(SP1, SP2),
|
||||||
|
arrayOf(sp1, sp2),
|
||||||
|
)
|
||||||
|
return PairMessage(
|
||||||
|
sequenceNumber = seq,
|
||||||
|
source = controllerId,
|
||||||
|
destination = nodeId,
|
||||||
|
payload = payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sps1(): PairMessage {
|
||||||
|
val payload = StringLengthPrefixEncoding.formatKeys(
|
||||||
|
arrayOf("SPS1="),
|
||||||
|
arrayOf(pdmPublic + pdmNonce),
|
||||||
|
)
|
||||||
|
return PairMessage(
|
||||||
|
sequenceNumber = seq,
|
||||||
|
source = controllerId,
|
||||||
|
destination = nodeId,
|
||||||
|
payload = payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSps1FromPod(msg: MessagePacket) {
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS1 from pod: ${msg.payload.toHex()}")
|
||||||
|
|
||||||
|
val payload = parseKeys(arrayOf(SPS1), msg.payload)[0]
|
||||||
|
if (payload.size != 48) {
|
||||||
|
throw MessageIOException("Invalid payload size")
|
||||||
|
}
|
||||||
|
podPublic = payload.copyOfRange(0, PUBLIC_KEY_SIZE)
|
||||||
|
podNonce = payload.copyOfRange(PUBLIC_KEY_SIZE, PUBLIC_KEY_SIZE + NONCE_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sps2(): PairMessage {
|
||||||
|
val payload = StringLengthPrefixEncoding.formatKeys(
|
||||||
|
arrayOf(SPS2),
|
||||||
|
arrayOf(pdmConf),
|
||||||
|
)
|
||||||
|
return PairMessage(
|
||||||
|
sequenceNumber = seq,
|
||||||
|
source = controllerId,
|
||||||
|
destination = nodeId,
|
||||||
|
payload = payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validatePodSps2(msg: MessagePacket) {
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS2 from pod: ${msg.payload.toHex()}")
|
||||||
|
|
||||||
|
val payload = parseKeys(arrayOf(SPS2), msg.payload)[0]
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "SPS2 payload from pod: ${payload.toHex()}")
|
||||||
|
|
||||||
|
if (payload.size != CMAC_SIZE) {
|
||||||
|
throw MessageIOException("Invalid payload size")
|
||||||
|
}
|
||||||
|
if (!podConf.contentEquals(payload)) {
|
||||||
|
aapsLogger.warn(LTag.PUMPBTCOMM, "Received invalid podConf. Expected: ${podConf.toHex()}. Got: ${payload.toHex()}")
|
||||||
|
throw MessageIOException("Invalid podConf value received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sp2(): ByteArray {
|
||||||
|
// This is GetPodStatus command, with page 0 parameter.
|
||||||
|
// We could replace that in the future with the serialized GetPodStatus()
|
||||||
|
return GET_POD_STATUS_HEX_COMMAND.hexStringToByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sp0gp0(): PairMessage {
|
||||||
|
val payload = SP0GP0.toByteArray()
|
||||||
|
return PairMessage(
|
||||||
|
sequenceNumber = seq,
|
||||||
|
source = controllerId,
|
||||||
|
destination = nodeId,
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeys() {
|
||||||
|
val curveLTK = X25519.computeSharedSecret(pdmPrivate, podPublic)
|
||||||
|
|
||||||
|
val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) +
|
||||||
|
pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) +
|
||||||
|
podNonce.copyOfRange(podNonce.size - 4, podNonce.size) +
|
||||||
|
pdmNonce.copyOfRange(pdmNonce.size - 4, pdmNonce.size)
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "LTK, first key: ${firstKey.toHex()}")
|
||||||
|
|
||||||
|
val intermediateKey = ByteArray(CMAC_SIZE)
|
||||||
|
aesCmac(firstKey, curveLTK, intermediateKey)
|
||||||
|
|
||||||
|
val ltkData = byteArrayOf(2.toByte()) +
|
||||||
|
INTERMEDIAR_KEY_MAGIC_STRING +
|
||||||
|
podNonce +
|
||||||
|
pdmNonce +
|
||||||
|
byteArrayOf(0.toByte(), 1.toByte())
|
||||||
|
aesCmac(intermediateKey, ltkData, ltk)
|
||||||
|
|
||||||
|
val confData = byteArrayOf(1.toByte()) +
|
||||||
|
INTERMEDIAR_KEY_MAGIC_STRING +
|
||||||
|
podNonce +
|
||||||
|
pdmNonce +
|
||||||
|
byteArrayOf(0.toByte(), 1.toByte())
|
||||||
|
val confKey = ByteArray(CMAC_SIZE)
|
||||||
|
aesCmac(intermediateKey, confData, confKey)
|
||||||
|
|
||||||
|
val pdmConfData = PDM_CONF_MAGIC_PREFIX +
|
||||||
|
pdmNonce +
|
||||||
|
podNonce
|
||||||
|
aesCmac(confKey, pdmConfData, pdmConf)
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "pdmConf: ${pdmConf.toHex()}")
|
||||||
|
|
||||||
|
val podConfData = POD_CONF_MAGIC_PREFIX +
|
||||||
|
podNonce +
|
||||||
|
pdmNonce
|
||||||
|
aesCmac(confKey, podConfData, podConf)
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "podConf: ${podConf.toHex()}")
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPrivate: ${pdmPrivate.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPublic: ${pdmPublic.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "podPublic: ${podPublic.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "pdmNonce: ${pdmNonce.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "podNonce: ${podNonce.toHex()}")
|
||||||
|
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "LTK, donna key: ${curveLTK.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Intermediate key: ${intermediateKey.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "LTK: ${ltk.toHex()}")
|
||||||
|
aapsLogger.debug(LTag.PUMPBTCOMM, "Conf KEY: ${confKey.toHex()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val PUBLIC_KEY_SIZE = 32
|
||||||
|
private val NONCE_SIZE = 16
|
||||||
|
private val CONF_SIZE = 16
|
||||||
|
|
||||||
|
private val CMAC_SIZE = 16
|
||||||
|
|
||||||
|
private val INTERMEDIAR_KEY_MAGIC_STRING = "TWIt".toByteArray()
|
||||||
|
private val PDM_CONF_MAGIC_PREFIX = "KC_2_U".toByteArray()
|
||||||
|
private val POD_CONF_MAGIC_PREFIX = "KC_2_V".toByteArray()
|
||||||
|
|
||||||
|
private 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.
|
||||||
|
|
||||||
|
private val SP1 = "SP1="
|
||||||
|
private val SP2 = ",SP2="
|
||||||
|
private val SPS1 = "SPS1="
|
||||||
|
private val SPS2 = "SPS2="
|
||||||
|
private val SP0GP0 = "SP0,GP0"
|
||||||
|
private val P0 = "P0="
|
||||||
|
private val UNKNOWN_P0_PAYLOAD = byteArrayOf(0xa5.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aesCmac(key: ByteArray, data: ByteArray, result: ByteArray) {
|
||||||
|
val aesEngine = AESEngine()
|
||||||
|
val mac = CMac(aesEngine)
|
||||||
|
mac.init(KeyParameter(key))
|
||||||
|
mac.update(data, 0, data.size)
|
||||||
|
mac.doFinal(result, 0)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk
|
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.Id
|
||||||
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket
|
|
@ -0,0 +1,9 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.utils.extensions.toHex
|
||||||
|
|
||||||
|
data class PairResult(val ltk: ByteArray) {
|
||||||
|
init {
|
||||||
|
require(ltk.size == 16) { "LTK length must be 16 bytes. Received LTK: ${ltk.toHex()}" }
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: Sc
|
||||||
@Throws(DiscoveredInvalidPodException::class)
|
@Throws(DiscoveredInvalidPodException::class)
|
||||||
|
|
||||||
private fun validatePodId() {
|
private fun validatePodId() {
|
||||||
val scanRecord = scanResult.scanRecord
|
|
||||||
val serviceUUIDs = scanRecord.serviceUuids
|
val serviceUUIDs = scanRecord.serviceUuids
|
||||||
val hexPodId = extractUUID16(serviceUUIDs[3]) + extractUUID16(serviceUUIDs[4])
|
val hexPodId = extractUUID16(serviceUUIDs[3]) + extractUUID16(serviceUUIDs[4])
|
||||||
val podId = hexPodId.toLong(16)
|
val podId = hexPodId.toLong(16)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history
|
||||||
|
|
||||||
|
import com.github.guepardoapps.kulid.ULID
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_BOLUS
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType.SET_TEMPORARY_BASAL
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.HistoryRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordDao
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordEntity
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper.HistoryMapper
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import java.lang.System.currentTimeMillis
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DashHistory @Inject constructor(
|
||||||
|
private val dao: HistoryRecordDao,
|
||||||
|
private val historyMapper: HistoryMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun markSuccess(id: String, date: Long): Completable = dao.markResolved(id, ResolvedResult.SUCCESS, currentTimeMillis())
|
||||||
|
|
||||||
|
fun markFailure(id: String, date: Long): Completable = dao.markResolved(id, ResolvedResult.FAILURE, currentTimeMillis())
|
||||||
|
|
||||||
|
fun createRecord(
|
||||||
|
commandType: OmnipodCommandType,
|
||||||
|
date: Long,
|
||||||
|
initialResult: InitialResult = InitialResult.UNCONFIRMED,
|
||||||
|
tempBasalRecord: TempBasalRecord? = null,
|
||||||
|
bolusRecord: BolusRecord? = null,
|
||||||
|
resolveResult: ResolvedResult? = null,
|
||||||
|
resolvedAt: Long? = null
|
||||||
|
): Single<String> {
|
||||||
|
val id = ULID.random()
|
||||||
|
|
||||||
|
when {
|
||||||
|
commandType == SET_BOLUS && bolusRecord == null ->
|
||||||
|
return Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS"))
|
||||||
|
commandType == SET_TEMPORARY_BASAL && tempBasalRecord == null ->
|
||||||
|
return Single.error(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.save(HistoryRecordEntity(
|
||||||
|
id = id,
|
||||||
|
date = date,
|
||||||
|
createdAt = currentTimeMillis(),
|
||||||
|
commandType = commandType,
|
||||||
|
tempBasalRecord = tempBasalRecord,
|
||||||
|
bolusRecord = bolusRecord,
|
||||||
|
initialResult = initialResult,
|
||||||
|
resolvedResult = resolveResult,
|
||||||
|
resolvedAt = resolvedAt)
|
||||||
|
).toSingle { id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRecords(): Single<List<HistoryRecord>> =
|
||||||
|
dao.all().map { list -> list.map(historyMapper::entityToDomain) }
|
||||||
|
|
||||||
|
fun getRecordsAfter(time: Long): Single<List<HistoryRecordEntity>> = dao.allSince(time)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
|
||||||
|
|
||||||
|
data class HistoryRecord(
|
||||||
|
val id: String, // ULID
|
||||||
|
val createdAt: Long, // creation date of the record
|
||||||
|
val date: Long, // when event actually happened
|
||||||
|
val commandType: OmnipodCommandType,
|
||||||
|
val initialResult: InitialResult,
|
||||||
|
val record: Record?,
|
||||||
|
val resolvedResult: ResolvedResult?,
|
||||||
|
val resolvedAt: Long?
|
||||||
|
)
|
|
@ -0,0 +1,11 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
|
||||||
|
|
||||||
|
sealed class Record
|
||||||
|
|
||||||
|
data class BolusRecord(val amout: Double, val bolusType: BolusType): Record()
|
||||||
|
|
||||||
|
data class TempBasalRecord(val duration: Long, val rate: Double): Record()
|
||||||
|
|
||||||
|
enum class BolusType {
|
||||||
|
DEFAULT, SMB
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data
|
||||||
|
|
||||||
|
enum class InitialResult {
|
||||||
|
SUCCESS, FAILURE, UNCONFIRMED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ResolvedResult {
|
||||||
|
SUCCESS, FAILURE
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusType
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toBolusType(s: String) = enumValueOf<BolusType>(s)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromBolusType(bolusType: BolusType) = bolusType.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toInitialResult(s: String) = enumValueOf<InitialResult>(s)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromInitialResult(initialResult: InitialResult) = initialResult.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toResolvedResult(s: String?) = s?.let { enumValueOf<ResolvedResult>(it) }
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromResolvedResult(resolvedResult: ResolvedResult?) = resolvedResult?.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toOmnipodCommandType(s: String) = enumValueOf<OmnipodCommandType>(s)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromOmnipodCommandType(omnipodCommandType: OmnipodCommandType) = omnipodCommandType.name
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [HistoryRecordEntity::class],
|
||||||
|
exportSchema = false,
|
||||||
|
version = DashHistoryDatabase.VERSION
|
||||||
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
abstract class DashHistoryDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun historyRecordDao() : HistoryRecordDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val VERSION = 1
|
||||||
|
|
||||||
|
fun build(context: Context) =
|
||||||
|
Room.databaseBuilder(context.applicationContext, DashHistoryDatabase::class.java, "omnipod_dash_history_database.db")
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class HistoryRecordDao {
|
||||||
|
|
||||||
|
@Query("SELECT * from historyrecords")
|
||||||
|
abstract fun all(): Single<List<HistoryRecordEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * from historyrecords")
|
||||||
|
abstract fun allBlocking(): List<HistoryRecordEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * from historyrecords WHERE createdAt <= :since")
|
||||||
|
abstract fun allSince(since: Long): Single<List<HistoryRecordEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun saveBlocking(historyRecordEntity: HistoryRecordEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun save(historyRecordEntity: HistoryRecordEntity): Completable
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
abstract fun delete(historyRecordEntity: HistoryRecordEntity): Completable
|
||||||
|
|
||||||
|
@Query("UPDATE historyrecords SET resolvedResult = :resolvedResult, resolvedAt = :resolvedAt WHERE id = :id ")
|
||||||
|
abstract fun markResolved(id: String, resolvedResult: ResolvedResult, resolvedAt: Long): Completable
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database
|
||||||
|
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.common.definition.OmnipodCommandType
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.InitialResult
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.ResolvedResult
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
|
||||||
|
|
||||||
|
@Entity(tableName = "historyrecords")
|
||||||
|
data class HistoryRecordEntity(
|
||||||
|
@PrimaryKey val id: String, // ULID
|
||||||
|
val createdAt: Long, // creation date of the record
|
||||||
|
val date: Long, // when event actually happened
|
||||||
|
val commandType: OmnipodCommandType,
|
||||||
|
val initialResult: InitialResult,
|
||||||
|
@Embedded(prefix = "tempBasalRecord_") val tempBasalRecord: TempBasalRecord?,
|
||||||
|
@Embedded(prefix = "bolusRecord_") val bolusRecord: BolusRecord?,
|
||||||
|
val resolvedResult: ResolvedResult?,
|
||||||
|
val resolvedAt: Long?)
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package info.nightscout.androidaps.plugins.pump.omnipod.dash.history.mapper
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.BolusRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.HistoryRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.data.TempBasalRecord
|
||||||
|
import info.nightscout.androidaps.plugins.pump.omnipod.dash.history.database.HistoryRecordEntity
|
||||||
|
|
||||||
|
class HistoryMapper {
|
||||||
|
|
||||||
|
fun domainToEntity(historyRecord: HistoryRecord): HistoryRecordEntity =
|
||||||
|
HistoryRecordEntity(
|
||||||
|
id = historyRecord.id,
|
||||||
|
createdAt = historyRecord.createdAt,
|
||||||
|
date = historyRecord.date,
|
||||||
|
commandType = historyRecord.commandType,
|
||||||
|
initialResult = historyRecord.initialResult,
|
||||||
|
tempBasalRecord = historyRecord.record as? TempBasalRecord,
|
||||||
|
bolusRecord = historyRecord.record as? BolusRecord,
|
||||||
|
resolvedResult = historyRecord.resolvedResult,
|
||||||
|
resolvedAt = historyRecord.resolvedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun entityToDomain(entity: HistoryRecordEntity): HistoryRecord =
|
||||||
|
HistoryRecord(id = entity.id,
|
||||||
|
createdAt = entity.createdAt,
|
||||||
|
date = entity.date,
|
||||||
|
initialResult = entity.initialResult,
|
||||||
|
commandType = entity.commandType,
|
||||||
|
record = entity.bolusRecord ?: entity.tempBasalRecord,
|
||||||
|
resolvedResult = entity.resolvedResult,
|
||||||
|
resolvedAt = entity.resolvedAt
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ fun mapProfileToBasalProgram(profile: Profile): BasalProgram {
|
||||||
if (previousBasalValue != null) {
|
if (previousBasalValue != null) {
|
||||||
entries.add(
|
entries.add(
|
||||||
BasalProgram.Segment(
|
BasalProgram.Segment(
|
||||||
(previousBasalValue!!.timeAsSeconds / 1800).toShort(),
|
(previousBasalValue.timeAsSeconds / 1800).toShort(),
|
||||||
startSlotIndex,
|
startSlotIndex,
|
||||||
(PumpType.Omnipod_Dash.determineCorrectBasalSize(previousBasalValue.value) * 100).roundToInt()
|
(PumpType.Omnipod_Dash.determineCorrectBasalSize(previousBasalValue.value) * 100).roundToInt()
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue