diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt index 0385a74619..335ee0f372 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt @@ -3,6 +3,10 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.OmnipodDashBleManager import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEventType +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.GetVersionCommand +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.GetVersionCommand.Companion.DEFAULT_UNIQUE_ID +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.ActivationProgress import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.AlertConfiguration import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.AlertSlot import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BasalProgram @@ -19,9 +23,36 @@ class OmnipodDashManagerImpl @Inject constructor( private val bleManager: OmnipodDashBleManager ) : OmnipodDashManager { + private val observePodReadyForActivationPart1: Observable + get() { + if (podStateManager.activationProgress.isBefore(ActivationProgress.PHASE_1_COMPLETED)) { + return Observable.empty() + } + return Observable.error(IllegalStateException("Pod is in an incorrect state")) + } + + private val observeConnectToPod: Observable = Observable.defer { + // TODO + // send CONNECTING event here + bleManager.connect() + Observable.just(PodEvent(PodEventType.CONNECTED, null)) + } + override fun activatePodPart1(): Observable { // TODO - return Observable.empty() + return Observable.concat( + observePodReadyForActivationPart1, + observeConnectToPod, + Observable.defer { + bleManager.sendCommand(GetVersionCommand.Builder() // + .setSequenceNumber(podStateManager.messageSequenceNumber) // + .setUniqueId(DEFAULT_UNIQUE_ID) // + .build() + ) + Observable.just(PodEvent(PodEventType.COMMAND_SENT, null)) + } + // ... Send more commands + ) } override fun activatePodPart2(): Observable { diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt index 998e599805..45bbc93b3d 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/definition/BasalProgram.kt @@ -19,7 +19,11 @@ class BasalProgram( fun rateAt(date: Date): Double = 0.0 // TODO - class Segment(val startSlotIndex: Short, val endSlotIndex: Short, val basalRateInHundredthUnitsPerHour: Int) { + class Segment( + val startSlotIndex: Short, + val endSlotIndex: Short, + val basalRateInHundredthUnitsPerHour: Int + ) { fun getPulsesPerHour(): Short { return (basalRateInHundredthUnitsPerHour * PULSES_PER_UNIT / 100).toShort() @@ -37,6 +41,26 @@ class BasalProgram( '}' } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Segment + + if (startSlotIndex != other.startSlotIndex) return false + if (endSlotIndex != other.endSlotIndex) return false + if (basalRateInHundredthUnitsPerHour != other.basalRateInHundredthUnitsPerHour) return false + + return true + } + + override fun hashCode(): Int { + var result: Int = startSlotIndex.toInt() + result = 31 * result + endSlotIndex + result = 31 * result + basalRateInHundredthUnitsPerHour + return result + } + companion object { private const val PULSES_PER_UNIT: Byte = 20 @@ -48,4 +72,19 @@ class BasalProgram( "segments=" + segments + '}' } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BasalProgram + + if (segments != other.segments) return false + + return true + } + + override fun hashCode(): Int { + return segments.hashCode() + } } \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt index 396d56d009..94e9db497d 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/wizard/activation/viewmodel/action/DashInitializePodViewModel.kt @@ -1,6 +1,5 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.ui.wizard.activation.viewmodel.action -import android.os.AsyncTask import androidx.annotation.StringRes import dagger.android.HasAndroidInjector import info.nightscout.androidaps.data.PumpEnactResult @@ -8,12 +7,12 @@ import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.activation.viewmodel.action.InitializePodViewModel import info.nightscout.androidaps.plugins.pump.omnipod.dash.R -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.OmnipodDashBleManager +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager import javax.inject.Inject class DashInitializePodViewModel @Inject constructor(private val aapsLogger: AAPSLogger, private val injector: HasAndroidInjector, - private val bleManager: OmnipodDashBleManager) : InitializePodViewModel() { + private val omnipodManager: OmnipodDashManager) : InitializePodViewModel() { override fun isPodInAlarm(): Boolean = false // TODO @@ -23,13 +22,11 @@ class DashInitializePodViewModel @Inject constructor(private val aapsLogger: AAP override fun doExecuteAction(): PumpEnactResult { // TODO FIRST STEP OF ACTIVATION - AsyncTask.execute { - try { - bleManager.activateNewPod() - } catch (e: Exception) { - aapsLogger.error(LTag.PUMP, "TEST ACTIVATE Exception" + e.toString()) - } - } + val disposable = omnipodManager.activatePodPart1().subscribe( + { podEvent -> aapsLogger.debug(LTag.PUMP, "Received PodEvent in Pod activation part 1: $podEvent") }, + { throwable -> aapsLogger.error(LTag.PUMP, "Error in Pod activation part 1: $throwable") }, + { aapsLogger.debug("Pod activation part 1 completed") } + ) return PumpEnactResult(injector).success(false).comment("not implemented") } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt new file mode 100644 index 0000000000..96d8fbf667 --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.util + +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.plugins.pump.common.defs.PumpType +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BasalProgram +import java.util.* +import kotlin.math.roundToInt + +fun mapProfileToBasalProgram(profile: Profile): BasalProgram { + val basalValues = profile.basalValues + ?: throw IllegalArgumentException("Basal values can not be null") + if (basalValues.isEmpty()) { + throw IllegalArgumentException("Basal values should contain values") + } + + val entries: MutableList = ArrayList() + + var previousBasalValue: Profile.ProfileValue? = null + + for (basalValue in basalValues) { + if (basalValue.timeAsSeconds >= 86_400) { + throw IllegalArgumentException("Basal segment start time can not be greater than 86400") + } + if (basalValue.timeAsSeconds < 0) { + throw IllegalArgumentException("Basal segment start time can not be less than 0") + } + if (basalValue.timeAsSeconds % 1_800 != 0) { + throw IllegalArgumentException("Basal segment time should be dividable by 30 minutes") + } + + val startSlotIndex = (basalValue.timeAsSeconds / 1800).toShort() + + if (previousBasalValue != null) { + entries.add( + BasalProgram.Segment( + (previousBasalValue!!.timeAsSeconds / 1800).toShort(), + startSlotIndex, + (PumpType.Omnipod_Dash.determineCorrectBasalSize(previousBasalValue.value) * 100).roundToInt() + ) + ) + } + + if (entries.size == 0 && basalValue.timeAsSeconds != 0) { + throw java.lang.IllegalArgumentException("First basal segment start time should be 0") + } + + if (entries.size > 0 && entries[entries.size - 1].endSlotIndex != startSlotIndex) { + throw IllegalArgumentException("Illegal start time for basal segment: does not match previous previous segment's end time") + } + + previousBasalValue = basalValue + } + + entries.add( + BasalProgram.Segment( + (previousBasalValue!!.timeAsSeconds / 1800).toShort(), + 48, + (PumpType.Omnipod_Dash.determineCorrectBasalSize(previousBasalValue.value) * 100).roundToInt() + ) + ) + + return BasalProgram(entries) +} \ No newline at end of file diff --git a/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/FunctionsTest.kt b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/FunctionsTest.kt new file mode 100644 index 0000000000..a046e21e17 --- /dev/null +++ b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/FunctionsTest.kt @@ -0,0 +1,118 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.util + +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.data.Profile.ProfileValue +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.definition.BasalProgram +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.mockito.Mockito +import org.powermock.api.mockito.PowerMockito + +class FunctionsTest { + + @Rule + @JvmField var thrown = ExpectedException.none() + + @Test fun validProfile() { + val profile = Mockito.mock(Profile::class.java) + val value1 = Mockito.mock(ProfileValue::class.java) + value1.timeAsSeconds = 0 + value1.value = 0.5 + val value2 = Mockito.mock(ProfileValue::class.java) + value2.timeAsSeconds = 18000 + value2.value = 1.0 + val value3 = Mockito.mock(ProfileValue::class.java) + value3.timeAsSeconds = 50400 + value3.value = 3.05 + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOf( + value1, + value2, + value3 + )) + val basalProgram: BasalProgram = mapProfileToBasalProgram(profile) + val entries: List = basalProgram.segments + assertEquals(3, entries.size) + val entry1: BasalProgram.Segment = entries[0] + assertEquals(0.toShort(), entry1.startSlotIndex) + assertEquals(50, entry1.basalRateInHundredthUnitsPerHour) + assertEquals(10.toShort(), entry1.endSlotIndex) + val entry2: BasalProgram.Segment = entries[1] + assertEquals(10.toShort(), entry2.startSlotIndex) + assertEquals(100, entry2.basalRateInHundredthUnitsPerHour) + assertEquals(28.toShort(), entry2.endSlotIndex) + val entry3: BasalProgram.Segment = entries[2] + assertEquals(28.toShort(), entry3.startSlotIndex) + assertEquals(305, entry3.basalRateInHundredthUnitsPerHour) + assertEquals(48.toShort(), entry3.endSlotIndex) + } + + @Test fun invalidProfileNullEntries() { + thrown.expect(IllegalArgumentException::class.java) + thrown.expectMessage("Basal values can not be null") + mapProfileToBasalProgram(Mockito.mock(Profile::class.java)) + } + + @Test fun invalidProfileZeroEntries() { + thrown.expect(IllegalArgumentException::class.java) + thrown.expectMessage("Basal values should contain values") + val profile = Mockito.mock(Profile::class.java) + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOfNulls(0)) + mapProfileToBasalProgram(profile) + } + + @Test fun invalidProfileNonZeroOffset() { + thrown.expect(IllegalArgumentException::class.java) + thrown.expectMessage("First basal segment start time should be 0") + val profile = Mockito.mock(Profile::class.java) + val value = Mockito.mock(ProfileValue::class.java) + value.timeAsSeconds = 1800 + value.value = 0.5 + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOf( + value)) + mapProfileToBasalProgram(profile) + } + + @Test fun invalidProfileMoreThan24Hours() { + thrown.expect(IllegalArgumentException::class.java) + thrown.expectMessage("Basal segment start time can not be greater than 86400") + + val profile = Mockito.mock(Profile::class.java) + val value1 = Mockito.mock(ProfileValue::class.java) + value1.timeAsSeconds = 0 + value1.value = 0.5 + val value2 = Mockito.mock(ProfileValue::class.java) + value2.timeAsSeconds = 86400 + value2.value = 0.5 + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOf( + value1, + value2 + )) + mapProfileToBasalProgram(profile) + } + + @Test fun invalidProfileNegativeOffset() { + thrown.expect(IllegalArgumentException::class.java) + thrown.expectMessage("Basal segment start time can not be less than 0") + val profile = Mockito.mock(Profile::class.java) + val value = Mockito.mock(ProfileValue::class.java) + value.timeAsSeconds = -1 + value.value = 0.5 + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOf( + value)) + mapProfileToBasalProgram(profile) + } + + @Test fun roundsToSupportedPrecision() { + val profile = Mockito.mock(Profile::class.java) + val value = Mockito.mock(ProfileValue::class.java) + value.timeAsSeconds = 0 + value.value = 0.04 + PowerMockito.`when`(profile.basalValues).thenReturn(arrayOf( + value)) + val basalProgram: BasalProgram = mapProfileToBasalProgram(profile) + val basalProgramElement: BasalProgram.Segment = basalProgram.segments[0] + assertEquals(5, basalProgramElement.basalRateInHundredthUnitsPerHour) + } +} \ No newline at end of file