Merge pull request #2623 from jbr7rr/medtrum-stage

Medtrum Nano
This commit is contained in:
Milos Kozak 2023-08-09 13:46:46 +02:00 committed by GitHub
commit ac333f6968
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
164 changed files with 11045 additions and 13 deletions

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<g id="Layer_7">
<g id="Medtrum_2">
<path fill="#FFFFFF" d="M94.383,15.26H34.367c-16.327,0-29.563,13.3-29.563,29.706v38.687c0,16.406,13.236,29.706,29.563,29.706
h60.017c16.327,0,29.563-13.3,29.563-29.706V44.965C123.946,28.559,110.71,15.26,94.383,15.26z M56.935,106.393
c-1.272,0-2.303-1.031-2.303-2.303c0-1.272,1.031-2.303,2.303-2.303c1.272,0,2.303,1.031,2.303,2.303
C59.238,105.362,58.207,106.393,56.935,106.393z M56.935,26.83c-1.272,0-2.303-1.031-2.303-2.303c0-1.272,1.031-2.303,2.303-2.303
c1.272,0,2.303,1.031,2.303,2.303C59.238,25.801,58.207,26.83,56.935,26.83z M112.167,73.321V55.296
c2.428,2.241,3.95,5.448,3.95,9.013S114.595,71.081,112.167,73.321z"/>
<g>
<path fill="#E0E0E0" d="M88.268,99.547c5.196-0.229,9.454-0.602,11.926-1.147c8.841-1.951,12.243-6.224,12.243-13.385v-1.263
V44.865v-1.263c0-7.162-3.402-11.436-12.243-13.385c-2.472-0.546-6.73-0.918-11.926-1.147"/>
</g>
<g>
<path fill="#AAAAAA" d="M88.531,29.079c-19.865-0.79-52.154,0.131-58.579,1.565c-8.285,1.848-12.095,5.194-13.418,12.916
c-0.606,3.544-0.627,12.306-0.473,20.748c-0.154,8.442-0.134,17.204,0.473,20.75c1.323,7.721,5.133,11.066,13.418,12.916
c6.425,1.434,38.715,2.355,58.579,1.565"/>
</g>
<g>
<path fill="#EAEAEA" d="M88.268,98.819c5.011-0.224,9.117-0.59,11.502-1.123c8.527-1.911,11.808-6.095,11.808-13.109v-1.237
V45.267V44.03c0-7.014-3.281-11.199-11.808-13.109c-2.384-0.535-6.491-0.899-11.502-1.123"/>
</g>
<path fill="#B2B2B2" d="M88.531,29.807c-19.567-0.774-51.374,0.128-57.702,1.533c-8.161,1.81-11.914,5.087-13.217,12.649
c-0.597,3.471-0.618,12.052-0.466,20.32c-0.152,8.268-0.132,16.849,0.466,20.321c1.303,7.562,5.056,10.837,13.217,12.649
c6.329,1.405,38.135,2.306,57.702,1.533"/>
<g>
<path fill="#F0F0F0" d="M88.268,97.88c4.751-0.218,8.643-0.573,10.904-1.092c8.084-1.859,11.194-5.929,11.194-12.752v-1.203
V45.785v-1.203c0-6.823-3.11-10.895-11.194-12.752c-2.261-0.52-6.153-0.874-10.904-1.092"/>
</g>
<path fill="#B9B9B9" d="M88.531,30.745c-19.148-0.753-50.274,0.124-56.467,1.491c-7.986,1.761-11.659,4.949-12.934,12.305
c-0.585,3.377-0.604,11.724-0.456,19.767c-0.149,8.043-0.129,16.39,0.456,19.768c1.275,7.356,4.948,10.542,12.934,12.305
c6.193,1.367,37.319,2.244,56.467,1.491"/>
<g>
<path fill="#F4F4F4" d="M88.268,96.224c4.241-0.207,7.717-0.545,9.735-1.039c7.217-1.767,9.994-5.637,9.994-12.123v-1.144V46.698
v-1.144c0-6.486-2.777-10.357-9.994-12.123c-2.018-0.495-5.494-0.831-9.735-1.039"/>
</g>
<path fill="#BEBEBE" d="M88.531,32.401c-18.728-0.715-49.17,0.118-55.227,1.417c-7.811,1.674-11.403,4.705-12.65,11.698
c-0.572,3.21-0.591,11.146-0.446,18.792c-0.145,7.646-0.126,15.582,0.446,18.793c1.247,6.993,4.839,10.022,12.65,11.698
c6.057,1.299,36.499,2.133,55.227,1.417"/>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="157.3581" y1="-156.2687" x2="184.8192" y2="-156.2687" gradientTransform="matrix(1 0 0 -1 -97.66 -91.96)">
<stop offset="0" style="stop-color:#BBBDBF"/>
<stop offset="0.1232" style="stop-color:#B5B7B9"/>
<stop offset="0.5123" style="stop-color:#B5B7B9"/>
<stop offset="0.5764" style="stop-color:#B5B7B9"/>
<stop offset="1" style="stop-color:#929497"/>
</linearGradient>
<polyline fill="url(#SVGID_1_)" stroke="#9D9DA0" stroke-width="0.4252" stroke-miterlimit="10" points="59.698,75.534
76.854,75.534 76.854,74.24 87.159,74.24 87.159,54.379 76.854,54.379 76.854,53.083 59.698,53.083 "/>
</g>
<g>
<path fill="#D0D2D3" stroke="#A3A3A3" stroke-width="0.4252" stroke-miterlimit="10" d="M90.39,68.37
c0,1.835-1.432,3.322-3.198,3.322H72.747c-1.766,0-3.198-1.487-3.198-3.322v-8.12c0-1.835,1.432-3.322,3.198-3.322h14.446
c1.766,0,3.198,1.487,3.198,3.322V68.37z"/>
</g>
<g>
<path fill="#F4F4F4" stroke="#C1C1C1" stroke-width="0.2835" stroke-miterlimit="10" d="M84.165,74.54
c0,4.387-0.05,4.448,3.627,4.448l0,0c4.387,0,7.945-3.557,7.945-7.945v-13.47c0-4.387-3.557-7.945-7.945-7.945l0,0
c-3.678,0-3.627,0.102-3.627,4.489V74.54z"/>
</g>
<g>
<g>
<path fill="#F1F1F2" stroke="#B2B2B2" stroke-width="0.2835" stroke-miterlimit="10" d="M94.571,67.677
c0,0.834-0.677,1.511-1.511,1.511l0,0c-0.834,0-1.511-0.677-1.511-1.511v-6.735c0-0.834,0.677-1.511,1.511-1.511l0,0
c0.834,0,1.511,0.677,1.511,1.511V67.677z"/>
</g>
<path fill="#231F20" d="M94.291,61.74c0,0.679-0.551,1.231-1.231,1.231l0,0c-0.679,0-1.231-0.551-1.231-1.231l0,0
c0-0.679,0.551-1.231,1.231-1.231l0,0C93.74,60.51,94.291,61.06,94.291,61.74L94.291,61.74z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -213,6 +213,7 @@ dependencies {
implementation project(':pump:danar')
implementation project(':pump:diaconn')
implementation project(':pump:eopatch')
implementation project(':pump:medtrum')
implementation project(':insight')
implementation project(':pump:medtronic')
implementation project(':pump:pump-common')

View file

@ -54,6 +54,7 @@ import info.nightscout.plugins.sync.xdrip.XdripPlugin
import info.nightscout.pump.combo.ComboPlugin
import info.nightscout.pump.combov2.ComboV2Plugin
import info.nightscout.pump.diaconn.DiaconnG8Plugin
import info.nightscout.pump.medtrum.MedtrumPlugin
import info.nightscout.pump.virtual.VirtualPumpPlugin
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventPreferenceChange
@ -122,6 +123,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
@Inject lateinit var wearPlugin: WearPlugin
@Inject lateinit var maintenancePlugin: MaintenancePlugin
@Inject lateinit var eopatchPumpPlugin: EopatchPumpPlugin
@Inject lateinit var medtrumPlugin: MedtrumPlugin
@Inject lateinit var passwordCheck: PasswordCheck
@Inject lateinit var nsSettingStatus: NSSettingsStatus
@ -212,6 +214,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
addPreferencesFromResourceIfEnabled(medtronicPumpPlugin, rootKey, config.PUMPDRIVERS)
addPreferencesFromResourceIfEnabled(diaconnG8Plugin, rootKey, config.PUMPDRIVERS)
addPreferencesFromResourceIfEnabled(eopatchPumpPlugin, rootKey, config.PUMPDRIVERS)
addPreferencesFromResourceIfEnabled(medtrumPlugin, rootKey, config.PUMPDRIVERS)
addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS)
addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey)
addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, rootKey)

View file

@ -31,6 +31,7 @@ import info.nightscout.pump.common.di.PumpCommonModule
import info.nightscout.pump.dana.di.DanaHistoryModule
import info.nightscout.pump.dana.di.DanaModule
import info.nightscout.pump.danars.di.DanaRSModule
import info.nightscout.pump.medtrum.di.MedtrumModule
import info.nightscout.pump.diaconn.di.DiaconnG8Module
import info.nightscout.pump.virtual.di.VirtualPumpModule
import info.nightscout.rx.di.RxModule
@ -87,6 +88,7 @@ import javax.inject.Singleton
OmnipodErosModule::class,
PumpCommonModule::class,
RileyLinkModule::class,
MedtrumModule::class,
VirtualPumpModule::class
]
)

View file

@ -46,6 +46,7 @@ import info.nightscout.plugins.sync.tidepool.TidepoolPlugin
import info.nightscout.plugins.sync.xdrip.XdripPlugin
import info.nightscout.pump.combo.ComboPlugin
import info.nightscout.pump.combov2.ComboV2Plugin
import info.nightscout.pump.medtrum.MedtrumPlugin
import info.nightscout.pump.diaconn.DiaconnG8Plugin
import info.nightscout.pump.virtual.VirtualPumpPlugin
import info.nightscout.sensitivity.SensitivityAAPSPlugin
@ -209,6 +210,12 @@ abstract class PluginsListModule {
@IntKey(156)
abstract fun bindEopatchPumpPlugin(plugin: EopatchPumpPlugin): PluginBase
@Binds
@PumpDriver
@IntoMap
@IntKey(160)
abstract fun bindMedtrumPlugin(plugin: MedtrumPlugin): PluginBase
@Binds
@AllConfigs
@IntoMap

View file

@ -131,9 +131,12 @@ open class Notification {
const val IDENTIFICATION_NOT_SET = 77
const val PERMISSION_BT = 78
const val EOELOW_PATCH_ALERTS = 79
const val COMBO_PUMP_SUSPENDED = 80
const val PUMP_SUSPENDED = 80
const val COMBO_UNKNOWN_TBR = 81
const val BLUETOOTH_NOT_ENABLED = 82
const val PATCH_NOT_ACTIVE = 83
const val PUMP_SETTINGS_FAILED = 84
const val PUMP_TIMEZONE_UPDATE_FAILED = 85
const val USER_MESSAGE = 1000

View file

@ -0,0 +1,13 @@
package info.nightscout.interfaces.pump
/**
* Functionality supported by Medtrum* pumps only
*/
interface Medtrum {
fun loadEvents(): PumpEnactResult // events history to build treatments from
fun setUserOptions(): PumpEnactResult // set user settings
fun clearAlarms(): PumpEnactResult // clear alarms
fun deactivate(): PumpEnactResult // deactivate patch
fun updateTime(): PumpEnactResult // update time
}

View file

@ -2,6 +2,7 @@ package info.nightscout.interfaces.pump.defs
enum class ManufacturerType(val description: String) {
AAPS("AAPS"),
Medtrum("Medtrum"),
Medtronic("Medtronic"),
Sooil("SOOIL"),
Tandem("Tandem"),

View file

@ -26,6 +26,7 @@ enum class PumpCapability {
YpsomedCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, Refill, ReplaceBattery, TDD, ManualTDDLoad)), // BasalRates (separately grouped)
DiaconnCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, Refill, ReplaceBattery, TDD, ManualTDDLoad)), //
EopatchCapabilities(arrayOf(Bolus, ExtendedBolus, TempBasal, BasalProfileSet, BasalRate30min)),
MedtrumCapabilities(arrayOf(Bolus, TempBasal, BasalProfileSet, BasalRate30min, TDD)), // Technically the pump supports ExtendedBolus, but not implemented (yet)
BasalRate_Duration15minAllowed,
BasalRate_Duration30minAllowed,
BasalRate_Duration15and30minAllowed(arrayOf(BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed)),

View file

@ -391,7 +391,29 @@ enum class PumpType {
isPatchPump = true,
maxReservoirReading = 50,
source = Source.EOPatch2
);
),
//Medtrum Nano Pump
MEDTRUM_NANO(
description = "Medtrum Nano",
manufacturer = ManufacturerType.Medtrum,
model = "Nano",
bolusSize = 0.05,
specialBolusSize = null,
extendedBolusSettings = DoseSettings(0.05, 30, 8 * 60, 0.05, 30.0),
pumpTempBasalType = PumpTempBasalType.Absolute,
tbrSettings = DoseSettings(0.05, 30, 12 * 60, 0.0, 25.0),
specialBasalDurations = PumpCapability.BasalRate_Duration30minAllowed,
baseBasalMinValue = 0.05,
baseBasalMaxValue = 25.0,
baseBasalStep = 0.05,
baseBasalSpecialSteps = null,
pumpCapability = PumpCapability.MedtrumCapabilities,
isPatchPump = true,
maxReservoirReading = 400,
source = Source.Medtrum
),
MEDTRUM_UNTESTED(description = "Medtrum untested", model = "untested", parent = MEDTRUM_NANO);
val description: String
var manufacturer: ManufacturerType? = null
@ -458,6 +480,7 @@ enum class PumpType {
OmnipodEros,
OmnipodDash,
EOPatch2,
Medtrum,
MDI,
VirtualPump,
Unknown

View file

@ -23,11 +23,14 @@ abstract class Command(
BASAL_PROFILE,
READSTATUS,
LOAD_HISTORY, // TDDs and so far only Dana specific
LOAD_EVENTS, // so far only Dana specific
LOAD_EVENTS,
LOAD_TDD,
SET_USER_SETTINGS, // so far only Dana specific,
START_PUMP,
STOP_PUMP,
CLEAR_ALARMS, // so far only Medtrum specific
DEACTIVATE, // so far only Medtrum specific
UPDATE_TIME, // so far only Medtrum specific
INSIGHT_SET_TBR_OVER_ALARM, // insight only
CUSTOM_COMMAND
}

View file

@ -31,6 +31,9 @@ interface CommandQueue {
fun setUserOptions(callback: Callback?): Boolean
fun loadTDDs(callback: Callback?): Boolean
fun loadEvents(callback: Callback?): Boolean
fun clearAlarms(callback: Callback?): Boolean
fun deactivate(callback: Callback?): Boolean
fun updateTime(callback: Callback?): Boolean
fun customCommand(customCommand: CustomCommand, callback: Callback?): Boolean
fun isCustomCommandRunning(customCommandType: Class<out CustomCommand>): Boolean
fun isCustomCommandInQueue(customCommandType: Class<out CustomCommand>): Boolean

View file

@ -59,6 +59,8 @@ fun PumpType.Companion.fromDbPumpType(pt: InterfaceIDs.PumpType): PumpType =
InterfaceIDs.PumpType.USER -> PumpType.USER
InterfaceIDs.PumpType.DIACONN_G8 -> PumpType.DIACONN_G8
InterfaceIDs.PumpType.EOPATCH2 -> PumpType.EOFLOW_EOPATCH2
InterfaceIDs.PumpType.MEDTRUM -> PumpType.MEDTRUM_NANO
InterfaceIDs.PumpType.MEDTRUM_UNTESTED -> PumpType.MEDTRUM_UNTESTED
InterfaceIDs.PumpType.CACHE -> PumpType.CACHE
}
@ -117,5 +119,7 @@ fun PumpType.toDbPumpType(): InterfaceIDs.PumpType =
PumpType.USER -> InterfaceIDs.PumpType.USER
PumpType.DIACONN_G8 -> InterfaceIDs.PumpType.DIACONN_G8
PumpType.EOFLOW_EOPATCH2 -> InterfaceIDs.PumpType.EOPATCH2
PumpType.MEDTRUM_NANO -> InterfaceIDs.PumpType.MEDTRUM
PumpType.MEDTRUM_UNTESTED -> InterfaceIDs.PumpType.MEDTRUM_UNTESTED
PumpType.CACHE -> InterfaceIDs.PumpType.CACHE
}

View file

@ -0,0 +1,71 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M94.38,15.26H34.37c-16.33,0 -29.56,13.3 -29.56,29.71v38.69c0,16.41 13.24,29.71 29.56,29.71h60.02c16.33,0 29.56,-13.3 29.56,-29.71V44.97C123.95,28.56 110.71,15.26 94.38,15.26zM56.94,106.39c-1.27,0 -2.3,-1.03 -2.3,-2.3c0,-1.27 1.03,-2.3 2.3,-2.3c1.27,0 2.3,1.03 2.3,2.3C59.24,105.36 58.21,106.39 56.94,106.39zM56.94,26.83c-1.27,0 -2.3,-1.03 -2.3,-2.3c0,-1.27 1.03,-2.3 2.3,-2.3c1.27,0 2.3,1.03 2.3,2.3C59.24,25.8 58.21,26.83 56.94,26.83zM112.17,73.32V55.3c2.43,2.24 3.95,5.45 3.95,9.01S114.6,71.08 112.17,73.32z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M88.27,99.55c5.2,-0.23 9.45,-0.6 11.93,-1.15c8.84,-1.95 12.24,-6.22 12.24,-13.39v-1.26V44.87v-1.26c0,-7.16 -3.4,-11.44 -12.24,-13.39c-2.47,-0.55 -6.73,-0.92 -11.93,-1.15"
android:fillColor="#E0E0E0"/>
<path
android:pathData="M88.53,29.08c-19.86,-0.79 -52.15,0.13 -58.58,1.57c-8.28,1.85 -12.1,5.19 -13.42,12.92c-0.61,3.54 -0.63,12.31 -0.47,20.75c-0.15,8.44 -0.13,17.2 0.47,20.75c1.32,7.72 5.13,11.07 13.42,12.92c6.43,1.43 38.72,2.36 58.58,1.57"
android:fillColor="#AAAAAA"/>
<path
android:pathData="M88.27,98.82c5.01,-0.22 9.12,-0.59 11.5,-1.12c8.53,-1.91 11.81,-6.09 11.81,-13.11v-1.24V45.27V44.03c0,-7.01 -3.28,-11.2 -11.81,-13.11c-2.38,-0.54 -6.49,-0.9 -11.5,-1.12"
android:fillColor="#EAEAEA"/>
<path
android:pathData="M88.53,29.81c-19.57,-0.77 -51.37,0.13 -57.7,1.53c-8.16,1.81 -11.91,5.09 -13.22,12.65c-0.6,3.47 -0.62,12.05 -0.47,20.32c-0.15,8.27 -0.13,16.85 0.47,20.32c1.3,7.56 5.06,10.84 13.22,12.65c6.33,1.4 38.13,2.31 57.7,1.53"
android:fillColor="#B2B2B2"/>
<path
android:pathData="M88.27,97.88c4.75,-0.22 8.64,-0.57 10.9,-1.09c8.08,-1.86 11.19,-5.93 11.19,-12.75v-1.2V45.78v-1.2c0,-6.82 -3.11,-10.9 -11.19,-12.75c-2.26,-0.52 -6.15,-0.87 -10.9,-1.09"
android:fillColor="#F0F0F0"/>
<path
android:pathData="M88.53,30.75c-19.15,-0.75 -50.27,0.12 -56.47,1.49c-7.99,1.76 -11.66,4.95 -12.93,12.31c-0.58,3.38 -0.6,11.72 -0.46,19.77c-0.15,8.04 -0.13,16.39 0.46,19.77c1.27,7.36 4.95,10.54 12.93,12.31c6.19,1.37 37.32,2.24 56.47,1.49"
android:fillColor="#B9B9B9"/>
<path
android:pathData="M88.27,96.22c4.24,-0.21 7.72,-0.55 9.73,-1.04c7.22,-1.77 9.99,-5.64 9.99,-12.12v-1.14V46.7v-1.14c0,-6.49 -2.78,-10.36 -9.99,-12.12c-2.02,-0.5 -5.49,-0.83 -9.73,-1.04"
android:fillColor="#F4F4F4"/>
<path
android:pathData="M88.53,32.4c-18.73,-0.71 -49.17,0.12 -55.23,1.42c-7.81,1.67 -11.4,4.7 -12.65,11.7c-0.57,3.21 -0.59,11.15 -0.45,18.79c-0.14,7.65 -0.13,15.58 0.45,18.79c1.25,6.99 4.84,10.02 12.65,11.7c6.06,1.3 36.5,2.13 55.23,1.42"
android:fillColor="#BEBEBE"/>
<path
android:pathData="M59.7,75.53l17.16,0l0,-1.29l10.31,0l0,-19.86l-10.31,0l0,-1.3l-17.16,0"
android:strokeWidth="0.4252"
android:strokeColor="#9D9DA0">
<aapt:attr name="android:fillColor">
<gradient
android:startX="59.7"
android:startY="64.31"
android:endX="87.16"
android:endY="64.31"
android:type="linear">
<item android:offset="0" android:color="#FFBBBDBF"/>
<item android:offset="0.12" android:color="#FFB5B7B9"/>
<item android:offset="0.51" android:color="#FFB5B7B9"/>
<item android:offset="0.58" android:color="#FFB5B7B9"/>
<item android:offset="1" android:color="#FF929497"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M90.39,68.37c0,1.84 -1.43,3.32 -3.2,3.32H72.75c-1.77,0 -3.2,-1.49 -3.2,-3.32v-8.12c0,-1.84 1.43,-3.32 3.2,-3.32h14.45c1.77,0 3.2,1.49 3.2,3.32V68.37z"
android:strokeWidth="0.4252"
android:fillColor="#D0D2D3"
android:strokeColor="#A3A3A3"/>
<path
android:pathData="M84.17,74.54c0,4.39 -0.05,4.45 3.63,4.45l0,0c4.39,0 7.95,-3.56 7.95,-7.95v-13.47c0,-4.39 -3.56,-7.95 -7.95,-7.95l0,0c-3.68,0 -3.63,0.1 -3.63,4.49V74.54z"
android:strokeWidth="0.2835"
android:fillColor="#F4F4F4"
android:strokeColor="#C1C1C1"/>
<path
android:pathData="M94.57,67.68c0,0.83 -0.68,1.51 -1.51,1.51l0,0c-0.83,0 -1.51,-0.68 -1.51,-1.51v-6.74c0,-0.83 0.68,-1.51 1.51,-1.51l0,0c0.83,0 1.51,0.68 1.51,1.51V67.68z"
android:strokeWidth="0.2835"
android:fillColor="#F1F1F2"
android:strokeColor="#B2B2B2"/>
<path
android:pathData="M94.29,61.74c0,0.68 -0.55,1.23 -1.23,1.23l0,0c-0.68,0 -1.23,-0.55 -1.23,-1.23l0,0c0,-0.68 0.55,-1.23 1.23,-1.23l0,0C93.74,60.51 94.29,61.06 94.29,61.74L94.29,61.74z"
android:fillColor="#231F20"/>
</vector>

View file

@ -393,6 +393,9 @@
<string name="carbs_g">CARBS %1$d g</string>
<string name="extended_bolus_u_min">EXTENDED BOLUS %1$.2f U %2$d min</string>
<string name="load_events">LOAD EVENTS</string>
<string name="clear_alarms">CLEAR ALARMS</string>
<string name="deactivate">DEACTIVATE</string>
<string name="update_time">UPDATE TIME</string>
<string name="load_history">LOAD HISTORY %1$d</string>
<string name="load_tdds">LOAD TDDs</string>
<string name="set_profile">SET PROFILE</string>

View file

@ -43,6 +43,8 @@ data class InterfaceIDs(
MDI,
DIACONN_G8,
EOPATCH2,
MEDTRUM,
MEDTRUM_UNTESTED,
USER,
CACHE;

View file

@ -14,12 +14,15 @@ import info.nightscout.implementation.queue.commands.CommandTempBasalPercent
import info.nightscout.implementation.queue.commands.CommandBolus
import info.nightscout.implementation.queue.commands.CommandCancelExtendedBolus
import info.nightscout.implementation.queue.commands.CommandCancelTempBasal
import info.nightscout.implementation.queue.commands.CommandClearAlarms
import info.nightscout.implementation.queue.commands.CommandCustomCommand
import info.nightscout.implementation.queue.commands.CommandDeactivate
import info.nightscout.implementation.queue.commands.CommandExtendedBolus
import info.nightscout.implementation.queue.commands.CommandInsightSetTBROverNotification
import info.nightscout.implementation.queue.commands.CommandLoadEvents
import info.nightscout.implementation.queue.commands.CommandLoadHistory
import info.nightscout.implementation.queue.commands.CommandLoadTDDs
import info.nightscout.implementation.queue.commands.CommandUpdateTime
@Module
@Suppress("unused")
@ -32,6 +35,9 @@ abstract class CommandQueueModule {
@ContributesAndroidInjector abstract fun commandExtendedBolusInjector(): CommandExtendedBolus
@ContributesAndroidInjector abstract fun commandInsightSetTBROverNotificationInjector(): CommandInsightSetTBROverNotification
@ContributesAndroidInjector abstract fun commandLoadEventsInjector(): CommandLoadEvents
@ContributesAndroidInjector abstract fun commandClearAlarmsInjector(): CommandClearAlarms
@ContributesAndroidInjector abstract fun commandDeactivateInjector(): CommandDeactivate
@ContributesAndroidInjector abstract fun commandUpdateTimeInjector(): CommandUpdateTime
@ContributesAndroidInjector abstract fun commandLoadHistoryInjector(): CommandLoadHistory
@ContributesAndroidInjector abstract fun commandLoadTDDsInjector(): CommandLoadTDDs
@ContributesAndroidInjector abstract fun commandReadStatusInjector(): CommandReadStatus

View file

@ -1,11 +1,15 @@
package info.nightscout.implementation.pump
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import info.nightscout.androidaps.annotations.OpenForTesting
import info.nightscout.implementation.R
import info.nightscout.interfaces.pump.DetailedBolusInfo
import info.nightscout.interfaces.pump.DetailedBolusInfoStorage
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.shared.sharedPreferences.SP
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.utils.T
import javax.inject.Inject
import javax.inject.Singleton
@ -14,10 +18,12 @@ import kotlin.math.abs
@OpenForTesting
@Singleton
class DetailedBolusInfoStorageImpl @Inject constructor(
val aapsLogger: AAPSLogger
val aapsLogger: AAPSLogger,
val sp: SP,
val rh: ResourceHelper
) : DetailedBolusInfoStorage {
val store = ArrayList<DetailedBolusInfo>()
val store = loadStore()
fun DetailedBolusInfo.toJsonString(): String = Gson().toJson(this)
@ -25,6 +31,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor(
override fun add(detailedBolusInfo: DetailedBolusInfo) {
aapsLogger.debug("Stored bolus info: ${detailedBolusInfo.toJsonString()}")
store.add(detailedBolusInfo)
saveStore()
}
@Synchronized
@ -36,6 +43,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor(
if (bolusTime > d.timestamp - T.mins(1).msecs() && bolusTime < d.timestamp + T.mins(1).msecs() && abs(store[i].insulin - bolus) < 0.01) {
aapsLogger.debug(LTag.PUMP, "Using & removing bolus info for time $bolusTime: ${store[i]}")
store.removeAt(i)
saveStore()
return d
}
}
@ -45,6 +53,7 @@ class DetailedBolusInfoStorageImpl @Inject constructor(
if (bolusTime > d.timestamp - T.mins(1).msecs() && bolusTime < d.timestamp + T.mins(1).msecs() && bolus <= store[i].insulin + 0.01) {
aapsLogger.debug(LTag.PUMP, "Using TIME-ONLY & removing bolus info for time $bolusTime: ${store[i]}")
store.removeAt(i)
saveStore()
return d
}
}
@ -61,4 +70,24 @@ class DetailedBolusInfoStorageImpl @Inject constructor(
aapsLogger.debug(LTag.PUMP, "Bolus info not found for time $bolusTime")
return null
}
private fun saveStore() {
var lastTwoEntries = store
// Only save last two entries, to avoid too much data in preferences
if (store.size > 2) {
lastTwoEntries = ArrayList(store.subList(store.size - 2, store.size))
}
val jsonString = Gson().toJson(lastTwoEntries)
sp.putString(rh.gs(R.string.key_bolus_storage), jsonString)
}
private fun loadStore(): ArrayList<DetailedBolusInfo> {
val jsonString = sp.getString(rh.gs(R.string.key_bolus_storage), "")
return if (jsonString != null && jsonString.isNotEmpty()) {
val type = object : TypeToken<List<DetailedBolusInfo>>() {}.type
Gson().fromJson(jsonString, type)
} else {
ArrayList()
}
}
}

View file

@ -22,7 +22,9 @@ import info.nightscout.implementation.R
import info.nightscout.implementation.queue.commands.CommandBolus
import info.nightscout.implementation.queue.commands.CommandCancelExtendedBolus
import info.nightscout.implementation.queue.commands.CommandCancelTempBasal
import info.nightscout.implementation.queue.commands.CommandClearAlarms
import info.nightscout.implementation.queue.commands.CommandCustomCommand
import info.nightscout.implementation.queue.commands.CommandDeactivate
import info.nightscout.implementation.queue.commands.CommandExtendedBolus
import info.nightscout.implementation.queue.commands.CommandInsightSetTBROverNotification
import info.nightscout.implementation.queue.commands.CommandLoadEvents
@ -36,6 +38,7 @@ import info.nightscout.implementation.queue.commands.CommandStartPump
import info.nightscout.implementation.queue.commands.CommandStopPump
import info.nightscout.implementation.queue.commands.CommandTempBasalAbsolute
import info.nightscout.implementation.queue.commands.CommandTempBasalPercent
import info.nightscout.implementation.queue.commands.CommandUpdateTime
import info.nightscout.interfaces.AndroidPermission
import info.nightscout.interfaces.Config
import info.nightscout.interfaces.constraints.Constraint
@ -535,6 +538,46 @@ class CommandQueueImplementation @Inject constructor(
return true
}
// returns true if command is queued
override fun clearAlarms(callback: Callback?): Boolean {
if (isRunning(CommandType.CLEAR_ALARMS)) {
callback?.result(executingNowError())?.run()
return false
}
// remove all unfinished
removeAll(CommandType.CLEAR_ALARMS)
// add new command to queue
add(CommandClearAlarms(injector, callback))
notifyAboutNewCommand()
return true
}
override fun deactivate(callback: Callback?): Boolean {
if (isRunning(CommandType.DEACTIVATE)) {
callback?.result(executingNowError())?.run()
return false
}
// remove all unfinished
removeAll(CommandType.DEACTIVATE)
// add new command to queue
add(CommandDeactivate(injector, callback))
notifyAboutNewCommand()
return true
}
override fun updateTime(callback: Callback?): Boolean {
if (isRunning(CommandType.UPDATE_TIME)) {
callback?.result(executingNowError())?.run()
return false
}
// remove all unfinished
removeAll(CommandType.UPDATE_TIME)
// add new command to queue
add(CommandUpdateTime(injector, callback))
notifyAboutNewCommand()
return true
}
override fun customCommand(customCommand: CustomCommand, callback: Callback?): Boolean {
if (isCustomCommandInQueue(customCommand.javaClass)) {
callback?.result(executingNowError())?.run()

View file

@ -0,0 +1,39 @@
package info.nightscout.implementation.queue.commands
import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.pump.Dana
import info.nightscout.interfaces.pump.Diaconn
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.Command
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class CommandClearAlarms(
injector: HasAndroidInjector,
callback: Callback?
) : Command(injector, CommandType.CLEAR_ALARMS, callback) {
@Inject lateinit var activePlugin: ActivePlugin
override fun execute() {
val pump = activePlugin.activePump
if (pump is Medtrum) {
val medtrumPump = pump as Medtrum
val r = medtrumPump.clearAlarms()
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
}
override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.clear_alarms)
override fun log(): String = "CLEAR ALARMS"
override fun cancel() {
aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel")
callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run()
}
}

View file

@ -0,0 +1,39 @@
package info.nightscout.implementation.queue.commands
import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.pump.Dana
import info.nightscout.interfaces.pump.Diaconn
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.Command
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class CommandDeactivate(
injector: HasAndroidInjector,
callback: Callback?
) : Command(injector, CommandType.DEACTIVATE, callback) {
@Inject lateinit var activePlugin: ActivePlugin
override fun execute() {
val pump = activePlugin.activePump
if (pump is Medtrum) {
val medtrumPump = pump as Medtrum
val r = medtrumPump.deactivate()
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
}
override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.deactivate)
override fun log(): String = "DEACTIVATE"
override fun cancel() {
aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel")
callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run()
}
}

View file

@ -4,6 +4,7 @@ import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.pump.Dana
import info.nightscout.interfaces.pump.Diaconn
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.Command
@ -32,6 +33,13 @@ class CommandLoadEvents(
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
if (pump is Medtrum) {
val medtrumPump = pump as Medtrum
val r = medtrumPump.loadEvents()
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
}
override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.load_events)

View file

@ -4,6 +4,7 @@ import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.pump.Dana
import info.nightscout.interfaces.pump.Diaconn
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.Command
@ -30,6 +31,12 @@ class CommandSetUserSettings(
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
if (pump is Medtrum) {
val r = pump.setUserOptions()
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
}
override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.set_user_settings)

View file

@ -0,0 +1,39 @@
package info.nightscout.implementation.queue.commands
import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.pump.Dana
import info.nightscout.interfaces.pump.Diaconn
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.Command
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class CommandUpdateTime(
injector: HasAndroidInjector,
callback: Callback?
) : Command(injector, CommandType.UPDATE_TIME, callback) {
@Inject lateinit var activePlugin: ActivePlugin
override fun execute() {
val pump = activePlugin.activePump
if (pump is Medtrum) {
val medtrumPump = pump as Medtrum
val r = medtrumPump.updateTime()
aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}")
callback?.result(r)?.run()
}
}
override fun status(): String = rh.gs(info.nightscout.core.ui.R.string.update_time)
override fun log(): String = "UPDATE TIME"
override fun cancel() {
aapsLogger.debug(LTag.PUMPQUEUE, "Result cancel")
callback?.result(PumpEnactResult(injector).success(false).comment(info.nightscout.core.ui.R.string.connectiontimedout))?.run()
}
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="key_bolus_storage" translatable="false">key_bolus_storage</string>
<string name="bg_label">BG</string>
<string name="executing_right_now">Command is executed right now</string>

View file

@ -2,13 +2,19 @@ package info.nightscout.implementation.pump
import info.nightscout.androidaps.TestBase
import info.nightscout.interfaces.pump.DetailedBolusInfo
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mock
class DetailedBolusInfoStorageTest : TestBase() {
@Mock lateinit var sp: SP
@Mock lateinit var rh: ResourceHelper
private val info1 = DetailedBolusInfo()
private val info2 = DetailedBolusInfo()
private val info3 = DetailedBolusInfo()
@ -26,7 +32,7 @@ class DetailedBolusInfoStorageTest : TestBase() {
@BeforeEach
fun prepare() {
detailedBolusInfoStorage = DetailedBolusInfoStorageImpl(aapsLogger)
detailedBolusInfoStorage = DetailedBolusInfoStorageImpl(aapsLogger, sp, rh)
}
private fun setUp() {

View file

@ -238,6 +238,19 @@ class CommandQueueImplementationTest : TestBaseWithProfile() {
// add loadEvents
commandQueue.loadEvents(null)
Assertions.assertEquals(4, commandQueue.size())
// add clearAlarms
commandQueue.clearAlarms(null)
Assertions.assertEquals(5, commandQueue.size())
// add deactivate
commandQueue.deactivate(null)
Assertions.assertEquals(6, commandQueue.size())
// add updateTime
commandQueue.updateTime(null)
Assertions.assertEquals(7, commandQueue.size())
commandQueue.clear()
commandQueue.tempBasalAbsolute(0.0, 30, true, validProfile, PumpSync.TemporaryBasalType.NORMAL, null)
commandQueue.pickup()
@ -354,6 +367,54 @@ class CommandQueueImplementationTest : TestBaseWithProfile() {
Assertions.assertEquals(1, commandQueue.size())
}
@Test
fun isClearAlarmsCommandInQueue() {
// given
Assertions.assertEquals(0, commandQueue.size())
// when
commandQueue.clearAlarms(null)
// then
Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.CLEAR_ALARMS))
Assertions.assertEquals(1, commandQueue.size())
// next should be ignored
commandQueue.clearAlarms(null)
Assertions.assertEquals(1, commandQueue.size())
}
@Test
fun isDeactivateCommandInQueue() {
// given
Assertions.assertEquals(0, commandQueue.size())
// when
commandQueue.deactivate(null)
// then
Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.DEACTIVATE))
Assertions.assertEquals(1, commandQueue.size())
// next should be ignored
commandQueue.deactivate(null)
Assertions.assertEquals(1, commandQueue.size())
}
@Test
fun isUpdateTimeCommandInQueue() {
// given
Assertions.assertEquals(0, commandQueue.size())
// when
commandQueue.updateTime(null)
// then
Assertions.assertTrue(commandQueue.isLastScheduled(Command.CommandType.UPDATE_TIME))
Assertions.assertEquals(1, commandQueue.size())
// next should be ignored
commandQueue.updateTime(null)
Assertions.assertEquals(1, commandQueue.size())
}
@Test
fun isLoadTDDsCommandInQueue() {
// given

View file

@ -35,6 +35,7 @@ import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.plugin.PluginBase
import info.nightscout.interfaces.plugin.PluginType
import info.nightscout.interfaces.profile.ProfileFunction
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.OmnipodDash
import info.nightscout.interfaces.pump.OmnipodEros
import info.nightscout.interfaces.queue.CommandQueue
@ -319,23 +320,24 @@ class SWDefinition @Inject constructor(
.text(R.string.readstatus)
.action { commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.clicked_connect_to_pump), null) }
.visibility {
// Hide for Omnipod, because as we don't require a Pod to be paired in the setup wizard,
// Hide for Omnipod and Medtrum, because as we don't require a Pod/Patch to be paired in the setup wizard,
// Getting the status might not be possible
activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash
activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash && activePlugin.activePump !is Medtrum
})
.add(SWEventListener(injector, EventPumpStatusChanged::class.java)
.visibility { activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash })
.visibility { activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash && activePlugin.activePump !is Medtrum })
.validator { isPumpInitialized() }
private fun isPumpInitialized(): Boolean {
val activePump = activePlugin.activePump
// For Omnipod, activating a Pod can be done after setup through the Omnipod fragment
// For the Eros model, consider the pump initialized when a RL has been configured successfully
// For Dash model, consider the pump setup without any extra conditions
// For Omnipod and Medtrum, activating a Pod/Patch can be done after setup through the pump fragment
// For the Eros, consider the pump initialized when a RL has been configured successfully
// For all others, consider the pump setup without any extra conditions
return activePump.isInitialized()
|| (activePump is OmnipodEros && activePump.isRileyLinkReady())
|| activePump is OmnipodDash
|| activePump is Medtrum
}
private val screenAps

View file

@ -2226,7 +2226,7 @@ class ComboV2Plugin @Inject constructor (
// only shows up in the Combo fragment.
if (newState == DriverState.Suspended) {
uiInteraction.addNotification(
Notification.COMBO_PUMP_SUSPENDED,
Notification.PUMP_SUSPENDED,
text = rh.gs(R.string.combov2_pump_is_suspended),
level = Notification.NORMAL
)

1
pump/medtrum/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

33
pump/medtrum/build.gradle Normal file
View file

@ -0,0 +1,33 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-allopen'
}
apply from: "${project.rootDir}/core/main/android_dependencies.gradle"
apply from: "${project.rootDir}/core/main/android_module_dependencies.gradle"
apply from: "${project.rootDir}/core/main/allopen_dependencies.gradle"
apply from: "${project.rootDir}/core/main/test_dependencies.gradle"
apply from: "${project.rootDir}/core/main/jacoco_global.gradle"
android {
namespace 'info.nightscout.pump.medtrum'
dataBinding {
enabled = true
}
}
dependencies {
implementation project(':app-wear-shared:shared')
implementation project(':database:entities')
implementation project(':core:libraries')
implementation project(':core:interfaces')
implementation project(':core:main')
implementation project(':core:ui')
implementation project(':core:validators')
implementation project(':pump:pump-common')
implementation project(':core:utils')
testImplementation project(':core:main')
}

View file

21
pump/medtrum/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<application>
<activity android:name=".ui.MedtrumActivity" />
<service
android:name=".services.MedtrumService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View file

@ -0,0 +1,472 @@
package info.nightscout.pump.medtrum
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.text.format.DateFormat
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import dagger.android.HasAndroidInjector
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.core.utils.fabric.FabricPrivacy
import info.nightscout.interfaces.constraints.Constraint
import info.nightscout.interfaces.constraints.Constraints
import info.nightscout.interfaces.notifications.Notification
import info.nightscout.interfaces.plugin.PluginDescription
import info.nightscout.interfaces.plugin.PluginType
import info.nightscout.interfaces.profile.Profile
import info.nightscout.interfaces.pump.DetailedBolusInfo
import info.nightscout.interfaces.pump.Medtrum
import info.nightscout.interfaces.pump.Pump
import info.nightscout.interfaces.pump.PumpEnactResult
import info.nightscout.interfaces.pump.PumpPluginBase
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.interfaces.pump.TemporaryBasalStorage
import info.nightscout.interfaces.pump.actions.CustomAction
import info.nightscout.interfaces.pump.actions.CustomActionType
import info.nightscout.interfaces.pump.defs.ManufacturerType
import info.nightscout.interfaces.pump.defs.PumpDescription
import info.nightscout.interfaces.pump.defs.PumpType
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.CommandQueue
import info.nightscout.interfaces.queue.CustomCommand
import info.nightscout.interfaces.ui.UiInteraction
import info.nightscout.interfaces.utils.DecimalFormatter
import info.nightscout.interfaces.utils.TimeChangeType
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment
import info.nightscout.pump.medtrum.services.MedtrumService
import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventAppExit
import info.nightscout.rx.events.EventDismissNotification
import info.nightscout.rx.events.EventOverviewBolusProgress
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.utils.DateUtil
import info.nightscout.shared.utils.T
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.json.JSONException
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
@Singleton class MedtrumPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
rh: ResourceHelper,
commandQueue: CommandQueue,
private val constraintChecker: Constraints,
private val aapsSchedulers: AapsSchedulers,
private val rxBus: RxBus,
private val context: Context,
private val fabricPrivacy: FabricPrivacy,
private val dateUtil: DateUtil,
private val medtrumPump: MedtrumPump,
private val uiInteraction: UiInteraction,
private val pumpSync: PumpSync,
private val temporaryBasalStorage: TemporaryBasalStorage
) : PumpPluginBase(
PluginDescription()
.mainType(PluginType.PUMP)
.fragmentClass(MedtrumOverviewFragment::class.java.name)
.pluginIcon(info.nightscout.core.ui.R.drawable.ic_medtrum_128)
.pluginName(R.string.medtrum)
.shortName(R.string.medtrum_pump_shortname)
.preferencesId(R.xml.pref_medtrum_pump)
.description(R.string.medtrum_pump_description), injector, aapsLogger, rh, commandQueue
), Pump, Medtrum {
private val disposable = CompositeDisposable()
private var medtrumService: MedtrumService? = null
override fun onStart() {
super.onStart()
aapsLogger.debug(LTag.PUMP, "MedtrumPlugin onStart()")
val intent = Intent(context, MedtrumService::class.java)
context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
disposable += rxBus
.toObservable(EventAppExit::class.java)
.observeOn(aapsSchedulers.io)
.subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException)
}
override fun onStop() {
aapsLogger.debug(LTag.PUMP, "MedtrumPlugin onStop()")
context.unbindService(mConnection)
disposable.clear()
super.onStop()
}
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName) {
aapsLogger.debug(LTag.PUMP, "Service is disconnected")
medtrumService = null
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
aapsLogger.debug(LTag.PUMP, "Service is connected")
val mLocalBinder = service as MedtrumService.LocalBinder
medtrumService = mLocalBinder.serviceInstance
}
}
fun getService(): MedtrumService? {
return medtrumService
}
override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) {
super.preprocessPreferences(preferenceFragment)
val serialSetting = preferenceFragment.findPreference(rh.gs(R.string.key_sn_input)) as EditTextPreference?
serialSetting?.isEnabled = !isInitialized()
}
override fun isInitialized(): Boolean {
return medtrumPump.pumpState > MedtrumPumpState.EJECTED && medtrumPump.pumpState < MedtrumPumpState.STOPPED
}
override fun isSuspended(): Boolean {
return medtrumPump.pumpState < MedtrumPumpState.ACTIVE || medtrumPump.pumpState > MedtrumPumpState.ACTIVE_ALT
}
override fun isBusy(): Boolean {
return false
}
override fun isConnected(): Boolean {
// This is a workaround to prevent AAPS to trigger connects when we have no patch activated
return if (!isInitialized()) true else medtrumService?.isConnected ?: false
}
override fun isConnecting(): Boolean = medtrumService?.isConnecting ?: false
override fun isHandshakeInProgress(): Boolean = false
override fun finishHandshaking() {
}
override fun connect(reason: String) {
if (isInitialized()) {
aapsLogger.debug(LTag.PUMP, "Medtrum connect - reason:$reason")
if (medtrumService != null) {
aapsLogger.debug(LTag.PUMP, "Medtrum connect - Attempt connection!")
val success = medtrumService?.connect(reason) ?: false
if (!success) ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_supported_or_not_paired)
}
}
}
override fun disconnect(reason: String) {
if (isInitialized()) {
aapsLogger.debug(LTag.PUMP, "Medtrum disconnect from: $reason")
medtrumService?.disconnect(reason)
}
}
override fun stopConnecting() {
if (isInitialized()) {
aapsLogger.debug(LTag.PUMP, "Medtrum stopConnecting")
medtrumService?.stopConnecting()
}
}
override fun getPumpStatus(reason: String) {
aapsLogger.debug(LTag.PUMP, "Medtrum getPumpStatus - reason:$reason")
if (isInitialized()) {
val connectionOK = medtrumService?.readPumpStatus() ?: false
if (connectionOK == false) {
aapsLogger.error(LTag.PUMP, "Medtrum getPumpStatus failed")
}
}
}
override fun setNewBasalProfile(profile: Profile): PumpEnactResult {
// New profile will be set when patch is activated
if (!isInitialized()) return PumpEnactResult(injector).success(true).enacted(true)
return if (medtrumService?.updateBasalsInPump(profile) == true) {
rxBus.send(EventDismissNotification(Notification.FAILED_UPDATE_PROFILE))
uiInteraction.addNotificationValidFor(Notification.PROFILE_SET_OK, rh.gs(info.nightscout.core.ui.R.string.profile_set_ok), Notification.INFO, 60)
PumpEnactResult(injector).success(true).enacted(true)
} else {
uiInteraction.addNotification(Notification.FAILED_UPDATE_PROFILE, rh.gs(info.nightscout.core.ui.R.string.failed_update_basal_profile), Notification.URGENT)
PumpEnactResult(injector)
}
}
override fun isThisProfileSet(profile: Profile): Boolean {
if (!isInitialized()) return true
var result = false
val profileBytes = medtrumPump.buildMedtrumProfileArray(profile)
if (profileBytes?.size == medtrumPump.actualBasalProfile.size) {
result = true
for (i in profileBytes.indices) {
if (profileBytes[i] != medtrumPump.actualBasalProfile[i]) {
result = false
break
}
}
}
return result
}
override fun lastDataTime(): Long = medtrumPump.lastConnection
override val baseBasalRate: Double
get() = medtrumPump.baseBasalRate
override val reservoirLevel: Double
get() = medtrumPump.reservoir
override val batteryLevel: Int
get() = 0 // We cannot determine battery level (yet)
@Synchronized
override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult {
aapsLogger.debug(LTag.PUMP, "deliverTreatment: " + detailedBolusInfo.insulin + "U")
if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false)
detailedBolusInfo.insulin = constraintChecker.applyBolusConstraints(Constraint(detailedBolusInfo.insulin)).value()
return if (detailedBolusInfo.insulin > 0 && detailedBolusInfo.carbs == 0.0) {
aapsLogger.debug(LTag.PUMP, "deliverTreatment: Delivering bolus: " + detailedBolusInfo.insulin + "U")
val t = EventOverviewBolusProgress.Treatment(0.0, 0, detailedBolusInfo.bolusType == DetailedBolusInfo.BolusType.SMB, detailedBolusInfo.id)
val connectionOK = medtrumService?.setBolus(detailedBolusInfo, t) ?: false
val result = PumpEnactResult(injector)
result.success = connectionOK && abs(detailedBolusInfo.insulin - t.insulin) < pumpDescription.bolusStep
result.bolusDelivered = t.insulin
if (!result.success) {
// Note: There are no error codes
result.comment = "failed"
} else {
result.comment = "ok"
}
aapsLogger.debug(LTag.PUMP, "deliverTreatment: OK. Success: ${result.success} Asked: ${detailedBolusInfo.insulin} Delivered: ${result.bolusDelivered}")
result
} else {
aapsLogger.debug(LTag.PUMP, "deliverTreatment: Invalid input")
val result = PumpEnactResult(injector)
result.success = false
result.bolusDelivered = 0.0
result.comment = rh.gs(info.nightscout.core.ui.R.string.invalid_input)
aapsLogger.error("deliverTreatment: Invalid input")
result
}
}
override fun stopBolusDelivering() {
if (!isInitialized()) return
aapsLogger.info(LTag.PUMP, "stopBolusDelivering")
medtrumService?.stopBolus()
}
@Synchronized
override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult {
if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false)
aapsLogger.info(LTag.PUMP, "setTempBasalAbsolute - absoluteRate: $absoluteRate, durationInMinutes: $durationInMinutes, enforceNew: $enforceNew")
// round rate to pump rate
val pumpRate = constraintChecker.applyBasalConstraints(Constraint(absoluteRate), profile).value()
temporaryBasalStorage.add(PumpSync.PumpState.TemporaryBasal(dateUtil.now(), T.mins(durationInMinutes.toLong()).msecs(), pumpRate, true, tbrType, 0L, 0L))
val connectionOK = medtrumService?.setTempBasal(pumpRate, durationInMinutes) ?: false
if (connectionOK
&& medtrumPump.tempBasalInProgress
&& Math.abs(medtrumPump.tempBasalAbsoluteRate - pumpRate) <= 0.05
) {
return PumpEnactResult(injector).success(true).enacted(true).duration(durationInMinutes).absolute(medtrumPump.tempBasalAbsoluteRate)
.isPercent(false)
.isTempCancel(false)
} else {
aapsLogger.error(
LTag.PUMP,
"setTempBasalAbsolute failed, connectionOK: $connectionOK, tempBasalInProgress: ${medtrumPump.tempBasalInProgress}, tempBasalAbsoluteRate: ${medtrumPump.tempBasalAbsoluteRate}"
)
return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum setTempBasalAbsolute failed")
}
}
override fun setTempBasalPercent(percent: Int, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult {
aapsLogger.info(LTag.PUMP, "setTempBasalPercent - percent: $percent, durationInMinutes: $durationInMinutes, enforceNew: $enforceNew")
return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum driver does not support percentage temp basals")
}
override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult {
aapsLogger.info(LTag.PUMP, "setExtendedBolus - insulin: $insulin, durationInMinutes: $durationInMinutes")
return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum driver does not support extended boluses")
}
override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult {
if (!isInitialized()) return PumpEnactResult(injector).success(false).enacted(false)
aapsLogger.info(LTag.PUMP, "cancelTempBasal - enforceNew: $enforceNew")
val connectionOK = medtrumService?.cancelTempBasal() ?: false
if (connectionOK && !medtrumPump.tempBasalInProgress) {
return PumpEnactResult(injector).success(true).enacted(true).isTempCancel(true)
} else {
aapsLogger.error(LTag.PUMP, "cancelTempBasal failed, connectionOK: $connectionOK, tempBasalInProgress: ${medtrumPump.tempBasalInProgress}")
return PumpEnactResult(injector).success(false).enacted(false).comment("Medtrum cancelTempBasal failed")
}
}
override fun cancelExtendedBolus(): PumpEnactResult {
return PumpEnactResult(injector)
}
override fun getJSONStatus(profile: Profile, profileName: String, version: String): JSONObject {
val now = System.currentTimeMillis()
if (medtrumPump.lastConnection + 60 * 60 * 1000L < System.currentTimeMillis()) {
return JSONObject()
}
val pumpJson = JSONObject()
val status = JSONObject()
val extended = JSONObject()
try {
status.put(
"status", if (!isSuspended()) "normal"
else if (isInitialized() && isSuspended()) "suspended"
else "no active patch"
)
status.put("timestamp", dateUtil.toISOString(medtrumPump.lastConnection))
if (medtrumPump.lastBolusTime != 0L) {
extended.put("lastBolus", dateUtil.dateAndTimeString(medtrumPump.lastBolusTime))
extended.put("lastBolusAmount", medtrumPump.lastBolusAmount)
}
val tb = pumpSync.expectedPumpState().temporaryBasal
if (tb != null) {
extended.put("TempBasalAbsoluteRate", tb.convertedToAbsolute(now, profile))
extended.put("TempBasalStart", dateUtil.dateAndTimeString(tb.timestamp))
extended.put("TempBasalRemaining", tb.plannedRemainingMinutes)
}
extended.put("BaseBasalRate", baseBasalRate)
try {
extended.put("ActiveProfile", profileName)
} catch (ignored: Exception) {
}
pumpJson.put("status", status)
pumpJson.put("extended", extended)
pumpJson.put("reservoir", medtrumPump.reservoir.toInt())
pumpJson.put("clock", dateUtil.toISOString(now))
} catch (e: JSONException) {
aapsLogger.error(LTag.PUMP, "Unhandled exception: $e")
}
return pumpJson
}
override fun manufacturer(): ManufacturerType {
return ManufacturerType.Medtrum
}
override fun model(): PumpType {
return medtrumPump.pumpType()
}
override fun serialNumber(): String {
// Load from SP here, because this value will be get before pump is initialized
return medtrumPump.pumpSNFromSP.toString(radix = 16)
}
override val pumpDescription: PumpDescription
get() = PumpDescription(medtrumPump.pumpType())
override fun shortStatus(veryShort: Boolean): String {
var ret = ""
if (medtrumPump.lastConnection != 0L) {
val agoMillis = System.currentTimeMillis() - medtrumPump.lastConnection
val agoMin = (agoMillis / 60.0 / 1000.0).toInt()
ret += "LastConn: $agoMin minAgo\n"
}
if (medtrumPump.lastBolusTime != 0L)
ret += "LastBolus: ${DecimalFormatter.to2Decimal(medtrumPump.lastBolusAmount)}U @${DateFormat.format("HH:mm", medtrumPump.lastBolusTime)}\n"
if (medtrumPump.tempBasalInProgress)
ret += "Temp: ${medtrumPump.temporaryBasalToString()}\n"
ret += "Res: ${DecimalFormatter.to0Decimal(medtrumPump.reservoir)}U\n"
return ret
}
override val isFakingTempsByExtendedBoluses: Boolean = false
override fun loadTDDs(): PumpEnactResult {
return PumpEnactResult(injector) // Note: Can implement this if we implement history fully (no priority)
}
override fun getCustomActions(): List<CustomAction>? {
return null
}
override fun executeCustomAction(customActionType: CustomActionType) {
}
override fun executeCustomCommand(customCommand: CustomCommand): PumpEnactResult? {
return null
}
override fun canHandleDST(): Boolean {
return true
}
override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) {
medtrumPump.needCheckTimeUpdate = true
if (isInitialized()) {
commandQueue.updateTime(object : Callback() {
override fun run() {
if (this.result.success == false) {
aapsLogger.error(LTag.PUMP, "Medtrum time update failed")
// Only notify here on failure (connection may be failed), service will handle success
medtrumService?.timeUpdateNotification(false)
}
}
})
}
}
// Medtrum interface
override fun loadEvents(): PumpEnactResult {
if (!isInitialized()) {
val result = PumpEnactResult(injector).success(false)
result.comment = "pump not initialized"
return result
}
val connectionOK = medtrumService?.loadEvents() ?: false
return PumpEnactResult(injector).success(connectionOK)
}
override fun setUserOptions(): PumpEnactResult {
if (!isInitialized()) {
val result = PumpEnactResult(injector).success(false)
result.comment = "pump not initialized"
return result
}
val connectionOK = medtrumService?.setUserSettings() ?: false
return PumpEnactResult(injector).success(connectionOK)
}
override fun clearAlarms(): PumpEnactResult {
if (!isInitialized()) {
val result = PumpEnactResult(injector).success(false)
result.comment = "pump not initialized"
return result
}
val connectionOK = medtrumService?.clearAlarms() ?: false
return PumpEnactResult(injector).success(connectionOK)
}
override fun deactivate(): PumpEnactResult {
val connectionOK = medtrumService?.deactivatePatch() ?: false
return PumpEnactResult(injector).success(connectionOK)
}
override fun updateTime(): PumpEnactResult {
if (!isInitialized()) {
val result = PumpEnactResult(injector).success(false)
result.comment = "pump not initialized"
return result
}
val connectionOK = medtrumService?.updateTimeIfNeeded() ?: false
return PumpEnactResult(injector).success(connectionOK)
}
}

View file

@ -0,0 +1,539 @@
package info.nightscout.pump.medtrum
import android.util.Base64
import info.nightscout.interfaces.profile.Profile
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.interfaces.pump.TemporaryBasalStorage
import info.nightscout.interfaces.pump.defs.PumpType
import info.nightscout.pump.medtrum.code.ConnectionState
import info.nightscout.pump.medtrum.comm.enums.AlarmSetting
import info.nightscout.pump.medtrum.comm.enums.AlarmState
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.rx.events.EventOverviewBolusProgress
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import info.nightscout.shared.utils.DateUtil
import info.nightscout.shared.utils.T
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.round
@Singleton
class MedtrumPump @Inject constructor(
private val aapsLogger: AAPSLogger,
private val rh: ResourceHelper,
private val sp: SP,
private val dateUtil: DateUtil,
private val pumpSync: PumpSync,
private val temporaryBasalStorage: TemporaryBasalStorage
) {
companion object {
const val FAKE_TBR_LENGTH = 4800L
}
// Connection state flow
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionStateFlow: StateFlow<ConnectionState> = _connectionState
var connectionState: ConnectionState
get() = _connectionState.value
set(value) {
_connectionState.value = value
}
// Pump state flow
private val _pumpState = MutableStateFlow(MedtrumPumpState.NONE)
val pumpStateFlow: StateFlow<MedtrumPumpState> = _pumpState
var pumpState: MedtrumPumpState
get() = _pumpState.value
set(value) {
_pumpState.value = value
sp.putInt(R.string.key_pump_state, value.state.toInt())
}
// Active alarms
private var _activeAlarms: EnumSet<AlarmState> = EnumSet.noneOf(AlarmState::class.java)
var activeAlarms: EnumSet<AlarmState>
get() = _activeAlarms
set(value) {
_activeAlarms = value
}
// Prime progress as state flow
private val _primeProgress = MutableStateFlow(0)
val primeProgressFlow: StateFlow<Int> = _primeProgress
var primeProgress: Int
get() = _primeProgress.value
set(value) {
_primeProgress.value = value
}
private var _lastBasalType: MutableStateFlow<BasalType> = MutableStateFlow(BasalType.NONE)
val lastBasalTypeFlow: StateFlow<BasalType> = _lastBasalType
val lastBasalType: BasalType
get() = _lastBasalType.value
private val _lastBasalRate = MutableStateFlow(0.0)
val lastBasalRateFlow: StateFlow<Double> = _lastBasalRate
val lastBasalRate: Double
get() = _lastBasalRate.value
private val _reservoir = MutableStateFlow(0.0)
val reservoirFlow: StateFlow<Double> = _reservoir
var reservoir: Double
get() = _reservoir.value
set(value) {
_reservoir.value = value
}
var batteryVoltage_A = 0.0 // Not used in UI
private val _batteryVoltage_B = MutableStateFlow(0.0)
val batteryVoltage_BFlow: StateFlow<Double> = _batteryVoltage_B
var batteryVoltage_B: Double
get() = _batteryVoltage_B.value
set(value) {
_batteryVoltage_B.value = value
}
/** Stuff stored in SP */
private var _patchSessionToken = 0L
var patchSessionToken: Long
get() = _patchSessionToken
set(value) {
_patchSessionToken = value
sp.putLong(R.string.key_session_token, value)
}
private var _patchId = 0L
var patchId: Long
get() = _patchId
set(value) {
_patchId = value
sp.putLong(R.string.key_patch_id, value)
}
private var _currentSequenceNumber = 0
var currentSequenceNumber: Int
get() = _currentSequenceNumber
set(value) {
_currentSequenceNumber = value
sp.putInt(R.string.key_current_sequence_number, value)
}
private var _syncedSequenceNumber = 0
var syncedSequenceNumber: Int
get() = _syncedSequenceNumber
set(value) {
_syncedSequenceNumber = value
sp.putInt(R.string.key_synced_sequence_number, value)
}
private var _actualBasalProfile = byteArrayOf(0)
var actualBasalProfile: ByteArray
get() = _actualBasalProfile
set(value) {
_actualBasalProfile = value
val encodedString = Base64.encodeToString(value, Base64.DEFAULT)
sp.putString(R.string.key_actual_basal_profile, encodedString ?: "")
}
private var _lastBolusTime = 0L // Time in ms!
var lastBolusTime: Long
get() = _lastBolusTime
set(value) {
_lastBolusTime = value
sp.putLong(R.string.key_last_bolus_time, value)
}
private var _lastBolusAmount = 0.0
var lastBolusAmount: Double
get() = _lastBolusAmount
set(value) {
_lastBolusAmount = value
sp.putDouble(R.string.key_last_bolus_amount, value)
}
private var _lastConnection = 0L // Time in ms!
var lastConnection: Long
get() = _lastConnection
set(value) {
_lastConnection = value
sp.putLong(R.string.key_last_connection, value)
}
private var _deviceType: Int = 80 // As reported by pump
var deviceType: Int
get() = _deviceType
set(value) {
_deviceType = value
sp.putInt(R.string.key_device_type, value)
}
private var _swVersion: String = "" // As reported by pump
var swVersion: String
get() = _swVersion
set(value) {
_swVersion = value
sp.putString(R.string.key_sw_version, value)
}
private var _patchStartTime = 0L // Time in ms!
var patchStartTime: Long
get() = _patchStartTime
set(value) {
_patchStartTime = value
sp.putLong(R.string.key_patch_start_time, value)
}
private var _pumpTimeZoneOffset = 0 // As reported by pump
var pumpTimeZoneOffset: Int
get() = _pumpTimeZoneOffset
set(value) {
_pumpTimeZoneOffset = value
sp.putInt(R.string.key_pump_time_zone_offset, value)
}
private var _pumpSN = 0L
val pumpSN: Long
get() = _pumpSN
val pumpSNFromSP: Long
get() =
try {
sp.getString(R.string.key_sn_input, "0").toLong(radix = 16)
} catch (e: NumberFormatException) {
aapsLogger.debug(LTag.PUMP, "pumpSNFromSP: Invalid input!")
0L
}
var needCheckTimeUpdate = false
var lastTimeReceivedFromPump = 0L // Time in ms!
var suspendTime = 0L // Time in ms!
var patchAge = 0L // Time in seconds?! // As reported by pump, not used (yet)
// bolus status
var bolusingTreatment: EventOverviewBolusProgress.Treatment? = null // actually delivered treatment
var bolusAmountToBeDelivered = 0.0 // amount to be delivered
var bolusProgressLastTimeStamp: Long = 0 // timestamp of last bolus progress message
var bolusStopped = false // bolus stopped by user
var bolusDone = false // Bolus completed or stopped on pump
// Last basal status update (from pump)
private var _lastBasalSequence = 0
val lastBasalSequence: Int
get() = _lastBasalSequence
private var _lastBasalPatchId = 0L
val lastBasalPatchId: Long
get() = _lastBasalPatchId
private var _lastBasalStartTime = 0L
val lastBasalStartTime: Long
get() = _lastBasalStartTime
val baseBasalRate: Double
get() = getHourlyBasalFromMedtrumProfileArray(actualBasalProfile, dateUtil.now())
// TBR status
val tempBasalInProgress: Boolean
get() = lastBasalType == BasalType.ABSOLUTE_TEMP || lastBasalType == BasalType.RELATIVE_TEMP
val tempBasalAbsoluteRate: Double
get() = if (tempBasalInProgress) lastBasalRate else 0.0
// Last stop status update
var lastStopSequence = 0
var lastStopPatchId = 0L
// User settings (desired values, to be set on pump)
var desiredPatchExpiration = false
var desiredAlarmSetting = AlarmSetting.LIGHT_VIBRATE_AND_BEEP
var desiredHourlyMaxInsulin: Int = 40
var desiredDailyMaxInsulin: Int = 180
init {
// Load stuff from SP
_patchSessionToken = sp.getLong(R.string.key_session_token, 0L)
_lastConnection = sp.getLong(R.string.key_last_connection, 0L)
_lastBolusTime = sp.getLong(R.string.key_last_bolus_time, 0L)
_lastBolusAmount = sp.getDouble(R.string.key_last_bolus_amount, 0.0)
_currentSequenceNumber = sp.getInt(R.string.key_current_sequence_number, 0)
_patchId = sp.getLong(R.string.key_patch_id, 0L)
_syncedSequenceNumber = sp.getInt(R.string.key_synced_sequence_number, 0)
_pumpState.value = MedtrumPumpState.fromByte(sp.getInt(R.string.key_pump_state, MedtrumPumpState.NONE.state.toInt()).toByte())
_deviceType = sp.getInt(R.string.key_device_type, 0)
_swVersion = sp.getString(R.string.key_sw_version, "")
_patchStartTime = sp.getLong(R.string.key_patch_start_time, 0L)
_pumpTimeZoneOffset = sp.getInt(R.string.key_pump_time_zone_offset, 0)
loadActiveAlarms()
val encodedString = sp.getString(R.string.key_actual_basal_profile, "0")
try {
_actualBasalProfile = Base64.decode(encodedString, Base64.DEFAULT)
} catch (e: Exception) {
aapsLogger.error(LTag.PUMP, "Error decoding basal profile from SP: $encodedString")
}
}
fun pumpType(): PumpType =
when (deviceType) {
80, 88 -> PumpType.MEDTRUM_NANO
else -> PumpType.MEDTRUM_UNTESTED
}
fun loadUserSettingsFromSP() {
desiredPatchExpiration = sp.getBoolean(info.nightscout.pump.medtrum.R.string.key_patch_expiration, false)
val alarmSettingCode = sp.getString(info.nightscout.pump.medtrum.R.string.key_alarm_setting, AlarmSetting.LIGHT_VIBRATE_AND_BEEP.code.toString()).toByte()
desiredAlarmSetting = AlarmSetting.values().firstOrNull { it.code == alarmSettingCode } ?: AlarmSetting.LIGHT_VIBRATE_AND_BEEP
desiredHourlyMaxInsulin = sp.getInt(info.nightscout.pump.medtrum.R.string.key_hourly_max_insulin, 40)
desiredDailyMaxInsulin = sp.getInt(info.nightscout.pump.medtrum.R.string.key_daily_max_insulin, 180)
_pumpSN = pumpSNFromSP
}
fun buildMedtrumProfileArray(nsProfile: Profile): ByteArray? {
val list = nsProfile.getBasalValues()
var basals = byteArrayOf()
for (item in list) {
val rate = round(item.value / 0.05).toInt()
val time = item.timeAsSeconds / 60
if (rate > 0xFFF || time > 0xFFF) {
aapsLogger.error(LTag.PUMP, "buildMedtrumProfileArray: rate or time too large: $rate, $time")
return null
}
basals += ((rate shl 12) + time).toByteArray(3)
aapsLogger.debug(LTag.PUMP, "buildMedtrumProfileArray: value: ${item.value} time: ${item.timeAsSeconds}, converted: $rate, $time")
}
return (list.size).toByteArray(1) + basals
}
fun getHourlyBasalFromMedtrumProfileArray(basalProfile: ByteArray, timestamp: Long): Double {
val basalCount = basalProfile[0].toInt()
var basal = 0.0
if (basalProfile.size < 4 || (basalProfile.size - 1) % 3 != 0 || basalCount > 24) {
aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: No valid basal profile set")
return basal
}
val date = GregorianCalendar()
date.timeInMillis = timestamp
val hourOfDayMinutes = date.get(GregorianCalendar.HOUR_OF_DAY) * 60 + date.get(GregorianCalendar.MINUTE)
for (index in 0 until basalCount) {
val currentIndex = 1 + (index * 3)
val nextIndex = currentIndex + 3
val rateAndTime = basalProfile.copyOfRange(currentIndex, nextIndex).toInt()
val rate = (rateAndTime shr 12) * 0.05
val startMinutes = rateAndTime and 0xFFF
val endMinutes = if (nextIndex < basalProfile.size) {
val nextRateAndTime = basalProfile.copyOfRange(nextIndex, nextIndex + 3).toInt()
nextRateAndTime and 0xFFF
} else {
24 * 60
}
if (hourOfDayMinutes in startMinutes until endMinutes) {
basal = rate
aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: basal: $basal")
break
}
// aapsLogger.debug(LTag.PUMP, "getHourlyBasalFromMedtrumProfileArray: rate: $rate, startMinutes: $startMinutes, endMinutes: $endMinutes")
}
return basal
}
fun handleBolusStatusUpdate(bolusType: Int, bolusCompleted: Boolean, amountDelivered: Double) {
aapsLogger.debug(LTag.PUMP, "handleBolusStatusUpdate: bolusType: $bolusType bolusCompleted: $bolusCompleted amountDelivered: $amountDelivered")
bolusProgressLastTimeStamp = dateUtil.now()
bolusingTreatment?.insulin = amountDelivered
bolusDone = bolusCompleted
}
fun handleBasalStatusUpdate(basalType: BasalType, basalValue: Double, basalSequence: Int, basalPatchId: Long, basalStartTime: Long) {
handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime, dateUtil.now())
}
fun handleBasalStatusUpdate(basalType: BasalType, basalRate: Double, basalSequence: Int, basalPatchId: Long, basalStartTime: Long, receivedTime: Long) {
aapsLogger.debug(
LTag.PUMP,
"handleBasalStatusUpdate: basalType: $basalType basalValue: $basalRate basalSequence: $basalSequence basalPatchId: $basalPatchId basalStartTime: $basalStartTime " + "receivedTime: $receivedTime"
)
@Suppress("UNNECESSARY_SAFE_CALL") // Safe call to allow mocks to return null
val expectedTemporaryBasal = pumpSync.expectedPumpState()?.temporaryBasal
if (basalType.isTempBasal() && expectedTemporaryBasal?.pumpId != basalStartTime) {
// Note: temporaryBasalInfo will be removed from temporaryBasalStorage after this call
val temporaryBasalInfo = temporaryBasalStorage.findTemporaryBasal(basalStartTime, basalRate)
// If duration is unknown, no way to get it now, set patch lifetime as duration
val duration = temporaryBasalInfo?.duration ?: T.mins(FAKE_TBR_LENGTH).msecs()
val adjustedBasalRate = if (basalType == BasalType.ABSOLUTE_TEMP) {
basalRate
} else {
(basalRate / baseBasalRate) * 100 // calculate the percentage of the original basal rate
}
val newRecord = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = basalStartTime,
rate = adjustedBasalRate,
duration = duration,
isAbsolute = (basalType == BasalType.ABSOLUTE_TEMP),
type = temporaryBasalInfo?.type,
pumpId = basalStartTime,
pumpType = pumpType(),
pumpSerial = pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " + "Rate: $basalRate Duration: ${duration} temporaryBasalInfo: $temporaryBasalInfo, expectedTemporaryBasal: $expectedTemporaryBasal"
)
} else if (basalType.isSuspendedByPump() && expectedTemporaryBasal?.pumpId != basalStartTime) {
val newRecord = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = basalStartTime,
rate = 0.0,
duration = T.mins(FAKE_TBR_LENGTH).msecs(),
isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = basalStartTime,
pumpType = pumpType(),
pumpSerial = pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) expectedTemporaryBasal: $expectedTemporaryBasal"
)
} else if (basalType == BasalType.NONE && expectedTemporaryBasal?.rate != basalRate && expectedTemporaryBasal?.duration != T.mins(FAKE_TBR_LENGTH).msecs()) {
// Pump suspended, set fake TBR
setFakeTBR()
} else if (basalType == BasalType.STANDARD) {
if (expectedTemporaryBasal != null) {
// Pump resumed, sync end
val success = pumpSync.syncStopTemporaryBasalWithPumpId(
timestamp = basalStartTime + 250, // Time of normal basal start = time of tbr end
endPumpId = basalStartTime + 250, // +250ms Make sure there is time between start and stop of TBR
pumpType = pumpType(),
pumpSerial = pumpSN.toString(radix = 16)
)
aapsLogger.debug(LTag.PUMPCOMM, "handleBasalStatusUpdate: EVENT TEMP_END ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) success: $success")
}
}
// Update medtrum pump state
_lastBasalType.value = basalType
_lastBasalRate.value = basalRate
_lastBasalSequence = basalSequence
if (basalSequence > currentSequenceNumber) {
currentSequenceNumber = basalSequence
}
_lastBasalPatchId = basalPatchId
if (basalPatchId != patchId) {
aapsLogger.error(LTag.PUMP, "handleBasalStatusUpdate: WTF? PatchId in status update does not match current patchId!")
}
_lastBasalStartTime = basalStartTime
}
fun handleStopStatusUpdate(stopSequence: Int, stopPatchId: Long) {
aapsLogger.debug(LTag.PUMP, "handleStopStatusUpdate: stopSequence: $stopSequence stopPatchId: $stopPatchId")
lastStopSequence = stopSequence
if (stopSequence > currentSequenceNumber) {
currentSequenceNumber = stopSequence
}
lastStopPatchId = stopPatchId
if (stopPatchId != patchId) {
aapsLogger.error(LTag.PUMP, "handleStopStatusUpdate: WTF? PatchId in status update does not match current patchId!")
}
}
fun setFakeTBRIfNeeded() {
val expectedTemporaryBasal = pumpSync.expectedPumpState().temporaryBasal
if (expectedTemporaryBasal?.duration != T.mins(FAKE_TBR_LENGTH).msecs()) {
setFakeTBR()
}
}
private fun setFakeTBR() {
val newRecord = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = dateUtil.now(),
rate = 0.0,
duration = T.mins(FAKE_TBR_LENGTH).msecs(),
isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = dateUtil.now(),
pumpType = pumpType(),
pumpSerial = pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_START (FAKE)"
)
}
fun temporaryBasalToString(): String {
if (!tempBasalInProgress) return ""
return tempBasalAbsoluteRate.toString() + "U/h"
}
fun addAlarm(alarm: AlarmState) {
activeAlarms.add(alarm)
saveActiveAlarms()
}
fun removeAlarm(alarm: AlarmState) {
activeAlarms.remove(alarm)
saveActiveAlarms()
}
fun clearAlarmState() {
activeAlarms.clear()
saveActiveAlarms()
}
fun alarmStateToString(alarmState: AlarmState): String {
val stringId = when (alarmState) {
AlarmState.NONE -> R.string.alarm_none
AlarmState.PUMP_LOW_BATTERY -> R.string.alarm_pump_low_battery
AlarmState.PUMP_LOW_RESERVOIR -> R.string.alarm_pump_low_reservoir
AlarmState.PUMP_EXPIRES_SOON -> R.string.alarm_pump_expires_soon
AlarmState.LOWBG_SUSPENDED -> R.string.alarm_lowbg_suspended
AlarmState.LOWBG_SUSPENDED2 -> R.string.alarm_lowbg_suspended2
AlarmState.AUTO_SUSPENDED -> R.string.alarm_auto_suspended
AlarmState.HMAX_SUSPENDED -> R.string.alarm_hmax_suspended
AlarmState.DMAX_SUSPENDED -> R.string.alarm_dmax_suspended
AlarmState.SUSPENDED -> R.string.alarm_suspended
AlarmState.PAUSED -> R.string.alarm_paused
AlarmState.OCCLUSION -> R.string.alarm_occlusion
AlarmState.EXPIRED -> R.string.alarm_expired
AlarmState.RESERVOIR_EMPTY -> R.string.alarm_reservoir_empty
AlarmState.PATCH_FAULT -> R.string.alarm_patch_fault
AlarmState.PATCH_FAULT2 -> R.string.alarm_patch_fault2
AlarmState.BASE_FAULT -> R.string.alarm_base_fault
AlarmState.BATTERY_OUT -> R.string.alarm_battery_out
AlarmState.NO_CALIBRATION -> R.string.alarm_no_calibration
}
return rh.gs(stringId)
}
private fun saveActiveAlarms() {
val alarmsStr = activeAlarms.joinToString(separator = ",") { it.name }
sp.putString(R.string.key_active_alarms, alarmsStr)
}
private fun loadActiveAlarms() {
val alarmsStr = sp.getString(R.string.key_active_alarms, "")
if (alarmsStr.isNullOrEmpty()) {
activeAlarms = EnumSet.noneOf(AlarmState::class.java)
} else {
activeAlarms = alarmsStr.split(",")
.mapNotNull { AlarmState.values().find { alarm -> alarm.name == it } }
.let { EnumSet.copyOf(it) }
}
}
}

View file

@ -0,0 +1,26 @@
package info.nightscout.pump.medtrum.bindingadapters
import android.view.View
import java.util.concurrent.atomic.AtomicBoolean
class OnSafeClickListener(
private val clickListener: View.OnClickListener,
private val intervalMs: Long = MIN_CLICK_INTERVAL
) : View.OnClickListener {
private var canClick = AtomicBoolean(true)
override fun onClick(v: View?) {
if (canClick.getAndSet(false)) {
v?.run {
postDelayed({
canClick.set(true)
}, intervalMs)
clickListener.onClick(v)
}
}
}
companion object {
// Set duplicate click prevention time
private const val MIN_CLICK_INTERVAL: Long = 1000
}
}

View file

@ -0,0 +1,29 @@
package info.nightscout.pump.medtrum.bindingadapters
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.databinding.BindingAdapter
import info.nightscout.pump.medtrum.extension.setVisibleOrGone
@BindingAdapter("android:visibility")
fun setVisibility(view: View, visible: Boolean) {
view.setVisibleOrGone(visible)
}
@BindingAdapter("visibleOrGone")
fun setVisibleOrGone(view: View, visibleOrGone: Boolean) {
view.setVisibleOrGone(visibleOrGone)
}
@BindingAdapter("onSafeClick")
fun View.setOnSafeClickListener(clickListener: View.OnClickListener?) {
clickListener?.also {
setOnClickListener(OnSafeClickListener(it))
} ?: setOnClickListener(null)
}
@BindingAdapter("textColor")
fun setTextColor(view: TextView, @ColorRes colorResId: Int) {
view.setTextColor(view.context.getColor(colorResId))
}

View file

@ -0,0 +1,8 @@
package info.nightscout.pump.medtrum.code
enum class ConnectionState {
CONNECTED,
DISCONNECTED,
CONNECTING,
DISCONNECTING;
}

View file

@ -0,0 +1,7 @@
package info.nightscout.pump.medtrum.code
enum class EventType {
CHANGE_PATCH_CLICKED,
PROFILE_NOT_SET,
;
}

View file

@ -0,0 +1,21 @@
package info.nightscout.pump.medtrum.code
enum class PatchStep {
START_DEACTIVATION,
DEACTIVATE,
FORCE_DEACTIVATION,
DEACTIVATION_COMPLETE,
PREPARE_PATCH,
PREPARE_PATCH_CONNECT,
PRIME,
PRIMING,
PRIME_COMPLETE,
ATTACH_PATCH,
ACTIVATE,
ACTIVATE_COMPLETE,
RETRY_ACTIVATION,
RETRY_ACTIVATION_CONNECT,
ERROR,
CANCEL,
COMPLETE;
}

View file

@ -0,0 +1,36 @@
package info.nightscout.pump.medtrum.comm
import kotlin.experimental.and
import info.nightscout.pump.medtrum.extension.toLong
class ManufacturerData(private val manufacturerDataBytes: ByteArray) {
private var deviceID: Long = 0
private var deviceType = 0
private var version = 0
init {
setData(manufacturerDataBytes)
}
fun setData(inputData: ByteArray) {
var index = 0
val deviceIDBytes: ByteArray = inputData.copyOfRange(index, index + 4)
deviceID = deviceIDBytes.toLong()
index += 4
deviceType = (inputData[index] and 0xff.toByte()).toInt()
index += 1
version = (inputData[index] and 0xff.toByte()).toInt()
}
fun getDeviceSN(): Long{
return deviceID
}
fun getDeviceType(): Int {
return deviceType
}
fun getVersion(): Int {
return version
}
}

View file

@ -0,0 +1,19 @@
package info.nightscout.pump.medtrum.comm
class ReadDataPacket(data: ByteArray) {
private var totalData = data.copyOfRange(0, data.size - 1) // Strip crc
private var dataSize: Byte = data[0]
fun addData(newData: ByteArray) {
totalData += newData.copyOfRange(4, newData.size - 1) // Strip header and crc
}
fun allDataReceived(): Boolean {
return (totalData.size >= dataSize)
}
fun getData(): ByteArray {
return totalData
}
}

View file

@ -0,0 +1,65 @@
package info.nightscout.pump.medtrum.comm
class WriteCommandPackets(data: ByteArray, sequenceNumber: Int) {
private val CRC_8_TABLE: IntArray = intArrayOf(0, 155, 173, 54, 193, 90, 108, 247, 25, 130, 180, 47, 216, 67, 117, 238, 50, 169, 159, 4, 243, 104, 94, 197, 43, 176, 134, 29, 234, 113, 71, 220, 100, 255, 201, 82, 165, 62, 8, 147, 125, 230, 208, 75, 188, 39, 17, 138, 86, 205, 251, 96, 151, 12, 58, 161, 79, 212, 226, 121, 142, 21, 35, 184, 200, 83, 101, 254, 9, 146, 164, 63, 209, 74, 124, 231, 16, 139, 189, 38, 250, 97, 87, 204, 59, 160, 150, 13, 227, 120, 78, 213, 34, 185, 143, 20, 172, 55, 1, 154, 109, 246, 192, 91, 181, 46, 24, 131, 116, 239, 217, 66, 158, 5, 51, 168, 95, 196, 242, 105, 135, 28, 42, 177, 70, 221, 235, 112, 11, 144, 166, 61, 202, 81, 103, 252, 18, 137, 191, 36, 211, 72, 126, 229, 57, 162, 148, 15, 248, 99, 85, 206, 32, 187, 141, 22, 225, 122, 76, 215, 111, 244, 194, 89, 174, 53, 3, 152, 118, 237, 219, 64, 183, 44, 26, 129, 93, 198, 240, 107, 156, 7, 49, 170, 68, 223, 233, 114, 133, 30, 40, 179, 195, 88, 110, 245, 2, 153, 175, 52, 218, 65, 119, 236, 27, 128, 182, 45, 241, 106, 92, 199, 48, 171, 157, 6, 232, 115, 69, 222, 41, 178, 132, 31, 167, 60, 10, 145, 102, 253, 203, 80, 190, 37, 19, 136, 127, 228, 210, 73, 149, 14, 56, 163, 84, 207, 249, 98, 140, 23, 33, 186, 77, 214, 224, 123)
private val packages = mutableListOf<ByteArray>()
private var index = 0
init {
// PackageIndex: 0 initially, if there are multiple packets, for the first packet it is set to 0 (not included in CRC calculation but sent in actual header)
var pkgIndex = 0
var header = byteArrayOf(
(data.size + 4).toByte(),
data[0],
sequenceNumber.toByte(),
pkgIndex.toByte()
)
var tmp: ByteArray = header + data.copyOfRange(1, data.size)
var totalCommand: ByteArray = tmp + calcCrc8(tmp, tmp.size).toByte()
if ((totalCommand.size - header.size) <= 15) {
packages.add(totalCommand + 0.toByte())
} else {
pkgIndex = 1
var remainingCommand = totalCommand.copyOfRange(4, totalCommand.size)
while (remainingCommand.size > 15) {
header[3] = pkgIndex.toByte()
tmp = header + remainingCommand.copyOfRange(0, 15)
packages.add(tmp + calcCrc8(tmp, tmp.size).toByte())
remainingCommand = remainingCommand.copyOfRange(15, remainingCommand.size)
pkgIndex = (pkgIndex + 1) % 256
}
// Add last package
header[3] = pkgIndex.toByte()
tmp = header + remainingCommand
packages.add(tmp + calcCrc8(tmp, tmp.size).toByte())
}
}
fun getNextPacket(): ByteArray? {
var ret: ByteArray? = null
if (index < packages.size) {
ret = packages[index]
index++
}
return ret
}
fun allPacketsConsumed(): Boolean {
return !(index < packages.size)
}
private fun calcCrc8(value: ByteArray, size: Int): Int {
var crc8 = 0
for (i in 0 until size) {
crc8 = CRC_8_TABLE[(value[i].toInt() and 255) xor (crc8 and 255)].toInt() and 255
}
return crc8
}
}

View file

@ -0,0 +1,12 @@
package info.nightscout.pump.medtrum.comm.enums
enum class AlarmSetting(val code: Byte) {
LIGHT_VIBRATE_AND_BEEP(0),
LIGHT_AND_VIBRATE(1),
LIGHT_AND_BEEP(2),
LIGHT_ONLY(3),
VIBRATE_AND_BEEP(4),
VIBRATE_ONLY(5),
BEEP_ONLY(6),
NONE(7)
}

View file

@ -0,0 +1,23 @@
package info.nightscout.pump.medtrum.comm.enums
enum class AlarmState {
NONE,
PUMP_LOW_BATTERY, // Mapped from error flag 1
PUMP_LOW_RESERVOIR, // Mapped from error flag 2
PUMP_EXPIRES_SOON, // Mapped from error flag 3
LOWBG_SUSPENDED, // Mapped from pump status 64
LOWBG_SUSPENDED2, // Mapped from pump status 65
AUTO_SUSPENDED, // Mapped from pump status 66
HMAX_SUSPENDED, // Mapped from pump status 67
DMAX_SUSPENDED, // Mapped from pump status 68
SUSPENDED, // Mapped from pump status 69
PAUSED, // Mapped from pump status 70
OCCLUSION, // Mapped from pump status 96
EXPIRED, // Mapped from pump status 97
RESERVOIR_EMPTY, // Mapped from pump status 98
PATCH_FAULT, // Mapped from pump status 99
PATCH_FAULT2, // Mapped from pump status 100
BASE_FAULT, // Mapped from pump status 101
BATTERY_OUT, // Mapped from pump status 102
NO_CALIBRATION // Mapped from pump status 103
}

View file

@ -0,0 +1,54 @@
package info.nightscout.pump.medtrum.comm.enums
enum class BasalType {
NONE,
STANDARD,
EXERCISE,
HOLIDAY,
PROGRAM_A,
PROGRAM_B,
ABSOLUTE_TEMP,
RELATIVE_TEMP,
PROGRAM_C,
PROGRAM_D,
SICK,
AUTO,
NEW,
SUSPEND_LOW_GLUCOSE,
SUSPEND_PREDICT_LOW_GLUCOSE,
SUSPEND_AUTO,
SUSPEND_MORE_THAN_MAX_PER_HOUR,
SUSPEND_MORE_THAN_MAX_PER_DAY,
SUSPEND_MANUAL,
SUSPEND_KEY_LOST,
STOP_OCCLUSION,
STOP_EXPIRED,
STOP_EMPTY,
STOP_PATCH_FAULT,
STOP_PATCH_FAULT2,
STOP_BASE_FAULT,
STOP_DISCARD,
STOP_BATTERY_EXHAUSTED,
STOP,
PAUSE_INTERRUPT,
PRIME,
AUTO_MODE_START,
AUTO_MODE_EXIT,
AUTO_MODE_TARGET_100,
AUTO_MODE_TARGET_110,
AUTO_MODE_TARGET_120,
AUTO_MODE_BREAKFAST,
AUTO_MODE_LUNCH,
AUTO_MODE_DINNER,
AUTO_MODE_SNACK,
AUTO_MODE_EXERCISE_START,
AUTO_MODE_EXERCISE_EXIT;
fun isTempBasal(): Boolean {
return this == ABSOLUTE_TEMP || this == RELATIVE_TEMP
}
fun isSuspendedByPump(): Boolean {
return this in SUSPEND_LOW_GLUCOSE..STOP
}
}

View file

@ -0,0 +1,8 @@
package info.nightscout.pump.medtrum.comm.enums
enum class BolusType {
NONE,
NORMAL,
EXTENDED,
COMBI;
}

View file

@ -0,0 +1,26 @@
package info.nightscout.pump.medtrum.comm.enums
enum class CommandType(val code: Byte) {
SYNCHRONIZE(3),
SUBSCRIBE(4),
AUTH_REQ(5),
GET_DEVICE_TYPE(6),
SET_TIME(10),
GET_TIME(11),
SET_TIME_ZONE(12),
PRIME(16),
ACTIVATE(18),
SET_BOLUS(19),
CANCEL_BOLUS(20),
SET_BASAL_PROFILE(21),
SET_TEMP_BASAL(24),
CANCEL_TEMP_BASAL(25),
RESUME_PUMP(29),
POLL_PATCH(30),
STOP_PATCH(31),
READ_BOLUS_STATE(34),
SET_PATCH(35),
SET_BOLUS_MOTOR(36),
GET_RECORD(99),
CLEAR_ALARM(115)
}

View file

@ -0,0 +1,34 @@
package info.nightscout.pump.medtrum.comm.enums
enum class MedtrumPumpState(val state: Byte) {
NONE(0),
IDLE(1),
FILLED(2),
PRIMING(3),
PRIMED(4),
EJECTING(5),
EJECTED(6),
ACTIVE(32),
ACTIVE_ALT(33),
LOWBG_SUSPENDED(64),
LOWBG_SUSPENDED2(65),
AUTO_SUSPENDED(66),
HMAX_SUSPENDED(67),
DMAX_SUSPENDED(68),
SUSPENDED(69),
PAUSED(70),
OCCLUSION(96),
EXPIRED(97),
RESERVOIR_EMPTY(98),
PATCH_FAULT(99),
PATCH_FAULT2(100),
BASE_FAULT(101),
BATTERY_OUT(102),
NO_CALIBRATION(103),
STOPPED(128.toByte());
companion object {
fun fromByte(state: Byte) = values().find { it.state == state }
?: throw IllegalAccessException("")
}
}

View file

@ -0,0 +1,122 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.pump.DetailedBolusInfo
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.ACTIVATE
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toByte
import info.nightscout.interfaces.stats.TddCalculator
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import javax.inject.Inject
import kotlin.math.round
class ActivatePacket(injector: HasAndroidInjector, private val basalProfile: ByteArray) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
@Inject lateinit var tddCalculator: TddCalculator
@Inject lateinit var pumpSync: PumpSync
companion object {
private const val RESP_PATCH_ID_START = 6
private const val RESP_PATCH_ID_END = RESP_PATCH_ID_START + 4
private const val RESP_TIME_START = 10
private const val RESP_TIME_END = RESP_TIME_START + 4
private const val RESP_BASAL_TYPE_START = 14
private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1
private const val RESP_BASAL_VALUE_START = 15
private const val RESP_BASAL_VALUE_END = RESP_BASAL_VALUE_START + 2
private const val RESP_BASAL_SEQUENCE_START = 17
private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2
private const val RESP_BASAL_PATCH_ID_START = 19
private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2
private const val RESP_BASAL_START_TIME_START = 21
private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4
}
init {
opCode = ACTIVATE.code
expectedMinRespLength = RESP_BASAL_START_TIME_END
}
override fun getRequest(): ByteArray {
/**
* byte 0: opCode
* byte 1: autoSuspendEnable // Value for auto mode, not used for AAPS
* byte 2: autoSuspendTime // Value for auto mode, not used for AAPS
* byte 3: expirationTimer // Expiration timer, 0 = no expiration 1 = 12 hour reminder and expiration after 3 days
* byte 4: alarmSetting // See AlarmSetting
* byte 5: lowSuspend // Value for auto mode, not used for AAPS
* byte 6: predictiveLowSuspend // Value for auto mode, not used for AAPS
* byte 7: predictiveLowSuspendRange // Value for auto mode, not used for AAPS
* byte 8-9: hourlyMaxInsulin // Max hourly dose of insulin, divided by 0.05
* byte 10-11: daylyMaxSet // Max daily dose of insulin, divided by 0.05
* byte 12-13: tddToday // Current TDD (of present day), divided by 0.05
* byte 14: 1 // Always 1
* bytes 15 - end // Basal profile > see MedtrumPump
*/
val autoSuspendEnable: Byte = 0
val autoSuspendTime: Byte = 12 // Not sure why, but pump needs this in order to activate
val patchExpiration: Byte = medtrumPump.desiredPatchExpiration.toByte()
val alarmSetting: Byte = medtrumPump.desiredAlarmSetting.code
val lowSuspend: Byte = 0
val predictiveLowSuspend: Byte = 0
val predictiveLowSuspendRange: Byte = 30 // Not sure why, but pump needs this in order to activate
val hourlyMaxInsulin: Int = round(medtrumPump.desiredHourlyMaxInsulin / 0.05).toInt()
val dailyMaxInsulin: Int = round(medtrumPump.desiredDailyMaxInsulin / 0.05).toInt()
val currentTDD: Double = tddCalculator.calculateToday()?.totalAmount?.div(0.05) ?: 0.0
return byteArrayOf(opCode) + autoSuspendEnable + autoSuspendTime + patchExpiration + alarmSetting + lowSuspend + predictiveLowSuspend + predictiveLowSuspendRange + hourlyMaxInsulin.toByteArray(
2
) + dailyMaxInsulin.toByteArray(2) + currentTDD.toInt().toByteArray(2) + 1.toByte() + basalProfile
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val medtrumTimeUtil = MedtrumTimeUtil()
val patchId = data.copyOfRange(RESP_PATCH_ID_START, RESP_PATCH_ID_END).toLong()
val time = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_TIME_START, RESP_TIME_END).toLong())
val basalType = enumValues<BasalType>()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()]
val basalValue = data.copyOfRange(RESP_BASAL_VALUE_START, RESP_BASAL_VALUE_END).toInt() * 0.05
val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt()
val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong()
val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong())
medtrumPump.patchId = patchId
medtrumPump.lastTimeReceivedFromPump = time
medtrumPump.currentSequenceNumber = basalSequence // We are activated, set the new seq nr
medtrumPump.syncedSequenceNumber = basalSequence // We are activated, reset the synced seq nr ()
// Sync canula change
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(),
type = DetailedBolusInfo.EventType.CANNULA_CHANGE,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
pumpSync.insertTherapyEventIfNewWithTimestamp(
timestamp = System.currentTimeMillis(),
type = DetailedBolusInfo.EventType.INSULIN_CHANGE,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
// Update the actual basal profile
medtrumPump.actualBasalProfile = basalProfile
medtrumPump.handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime, time)
}
return success
}
}

View file

@ -0,0 +1,57 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.AUTH_REQ
import info.nightscout.pump.medtrum.encryption.Crypt
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class AuthorizePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_DEVICE_TYPE_START = 7
private const val RESP_DEVICE_TYPE_END = RESP_DEVICE_TYPE_START + 1
private const val RESP_VERSION_X_START = 8
private const val RESP_VERSION_X_END = RESP_VERSION_X_START + 1
private const val RESP_VERSION_Y_START = 9
private const val RESP_VERSION_Y_END = RESP_VERSION_Y_START + 1
private const val RESP_VERSION_Z_START = 10
private const val RESP_VERSION_Z_END = RESP_VERSION_Z_START + 1
}
init {
opCode = AUTH_REQ.code
expectedMinRespLength = RESP_VERSION_Z_END
}
override fun getRequest(): ByteArray {
val role = 2 // Fixed to 2 for pump
val key = Crypt().keyGen(medtrumPump.pumpSN)
return byteArrayOf(opCode) + byteArrayOf(role.toByte()) + medtrumPump.patchSessionToken.toByteArray(4) + key.toByteArray(4)
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val deviceType = data.copyOfRange(RESP_DEVICE_TYPE_START, RESP_DEVICE_TYPE_END).toInt()
val swVersion = "" + data.copyOfRange(RESP_VERSION_X_START, RESP_VERSION_X_END).toInt() + "." + data.copyOfRange(RESP_VERSION_Y_START, RESP_VERSION_Y_END).toInt() + "." + data.copyOfRange(
RESP_VERSION_Z_START, RESP_VERSION_Z_END
).toInt()
if (medtrumPump.deviceType != deviceType) {
medtrumPump.deviceType = deviceType
}
if (medtrumPump.swVersion != swVersion) {
medtrumPump.swVersion = swVersion
}
aapsLogger.debug(LTag.PUMPCOMM, "GetDeviceTypeState: deviceType: ${deviceType}, swVersion: ${swVersion}")
}
return success
}
}

View file

@ -0,0 +1,20 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.CANCEL_BOLUS
class CancelBolusPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = CANCEL_BOLUS.code
}
override fun getRequest(): ByteArray {
// Bolus types:
// 1 = normal
// 2 = Extended
// 3 = Combi
val bolusType: Byte = 1 // Only support for normal bolus for now
return byteArrayOf(opCode) + bolusType
}
}

View file

@ -0,0 +1,51 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.CANCEL_TEMP_BASAL
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.utils.DateUtil
import javax.inject.Inject
class CancelTempBasalPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
@Inject lateinit var dateUtil: DateUtil
companion object {
private const val RESP_BASAL_TYPE_START = 6
private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1
private const val RESP_BASAL_RATE_START = RESP_BASAL_TYPE_END
private const val RESP_BASAL_RATE_END = RESP_BASAL_RATE_START + 2
private const val RESP_BASAL_SEQUENCE_START = RESP_BASAL_RATE_END
private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2
private const val RESP_BASAL_PATCH_ID_START = RESP_BASAL_SEQUENCE_END
private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2
private const val RESP_BASAL_START_TIME_START = RESP_BASAL_PATCH_ID_END
private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4
}
init {
opCode = CANCEL_TEMP_BASAL.code
expectedMinRespLength = RESP_BASAL_START_TIME_END
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val basalType = enumValues<BasalType>()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()]
val basalRate = data.copyOfRange(RESP_BASAL_RATE_START, RESP_BASAL_RATE_END).toInt() * 0.05
val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt()
val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong()
val basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong())
medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime)
}
return success
}
}

View file

@ -0,0 +1,16 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.CLEAR_ALARM
import info.nightscout.pump.medtrum.extension.toByteArray
class ClearPumpAlarmPacket(injector: HasAndroidInjector, val clearType: Int) : MedtrumPacket(injector) {
init {
opCode = CLEAR_ALARM.code
}
override fun getRequest(): ByteArray {
return byteArrayOf(opCode) + clearType.toByteArray(2)
}
}

View file

@ -0,0 +1,35 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_DEVICE_TYPE
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
class GetDeviceTypePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
var deviceType: Int = 0
var deviceSN: Long = 0
companion object {
private const val RESP_DEVICE_TYPE_START = 6
private const val RESP_DEVICE_TYPE_END = RESP_DEVICE_TYPE_START + 1
private const val RESP_DEVICE_SN_START = 7
private const val RESP_DEVICE_SN_END = RESP_DEVICE_SN_START + 4
}
init {
opCode = GET_DEVICE_TYPE.code
expectedMinRespLength = RESP_DEVICE_SN_END
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
deviceType = data.copyOfRange(RESP_DEVICE_TYPE_START, RESP_DEVICE_TYPE_END).toInt()
deviceSN = data.copyOfRange(RESP_DEVICE_SN_START, RESP_DEVICE_SN_END).toLong()
}
return success
}
}

View file

@ -0,0 +1,353 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.interfaces.pump.DetailedBolusInfoStorage
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.interfaces.pump.TemporaryBasalStorage
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_RECORD
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.comm.enums.BolusType
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toFloat
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.utils.DateUtil
import info.nightscout.shared.utils.T
import javax.inject.Inject
class GetRecordPacket(injector: HasAndroidInjector, private val recordIndex: Int) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
@Inject lateinit var pumpSync: PumpSync
@Inject lateinit var temporaryBasalStorage: TemporaryBasalStorage
@Inject lateinit var detailedBolusInfoStorage: DetailedBolusInfoStorage
@Inject lateinit var dateUtil: DateUtil
companion object {
private const val RESP_RECORD_HEADER_START = 6
private const val RESP_RECORD_HEADER_END = RESP_RECORD_HEADER_START + 1
private const val RESP_RECORD_UNKNOWN_START = RESP_RECORD_HEADER_END
private const val RESP_RECORD_UNKNOWN_END = RESP_RECORD_UNKNOWN_START + 1
private const val RESP_RECORD_TYPE_START = RESP_RECORD_UNKNOWN_END
private const val RESP_RECORD_TYPE_END = RESP_RECORD_TYPE_START + 1
private const val RESP_RECORD_UNKNOWN1_START = RESP_RECORD_TYPE_END
private const val RESP_RECORD_UNKNOWN1_END = RESP_RECORD_UNKNOWN1_START + 1
private const val RESP_RECORD_SERIAL_START = RESP_RECORD_UNKNOWN1_END
private const val RESP_RECORD_SERIAL_END = RESP_RECORD_SERIAL_START + 4
private const val RESP_RECORD_PATCHID_START = RESP_RECORD_SERIAL_END
private const val RESP_RECORD_PATCHID_END = RESP_RECORD_PATCHID_START + 2
private const val RESP_RECORD_SEQUENCE_START = RESP_RECORD_PATCHID_END
private const val RESP_RECORD_SEQUENCE_END = RESP_RECORD_SEQUENCE_START + 2
private const val RESP_RECORD_DATA_START = RESP_RECORD_SEQUENCE_END
private const val VALID_HEADER = 170
private const val BOLUS_RECORD = 1
private const val BOLUS_RECORD_ALT = 65
private const val BASAL_RECORD = 2
private const val BASAL_RECORD_ALT = 66
private const val ALARM_RECORD = 3
private const val AUTO_RECORD = 4
private const val TIME_SYNC_RECORD = 5
private const val AUTO1_RECORD = 6
private const val AUTO2_RECORD = 7
private const val AUTO3_RECORD = 8
private const val TDD_RECORD = 9
}
init {
opCode = GET_RECORD.code
expectedMinRespLength = RESP_RECORD_DATA_START
}
override fun getRequest(): ByteArray {
return byteArrayOf(opCode) + recordIndex.toByteArray(2) + medtrumPump.patchId.toByteArray(2)
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val recordHeader = data.copyOfRange(RESP_RECORD_HEADER_START, RESP_RECORD_HEADER_END).toInt()
val recordUnknown = data.copyOfRange(RESP_RECORD_UNKNOWN_START, RESP_RECORD_UNKNOWN_END).toInt()
val recordType = data.copyOfRange(RESP_RECORD_TYPE_START, RESP_RECORD_TYPE_END).toInt()
val recordSerial = data.copyOfRange(RESP_RECORD_SERIAL_START, RESP_RECORD_SERIAL_END).toLong()
val recordPatchId = data.copyOfRange(RESP_RECORD_PATCHID_START, RESP_RECORD_PATCHID_END).toInt()
val recordSequence = data.copyOfRange(RESP_RECORD_SEQUENCE_START, RESP_RECORD_SEQUENCE_END).toInt()
aapsLogger.debug(
LTag.PUMPCOMM,
"GetRecordPacket HandleResponse: Record header: $recordHeader, unknown: $recordUnknown, type: $recordType, serial: $recordSerial, patchId: $recordPatchId, " + "sequence: $recordSequence"
)
medtrumPump.syncedSequenceNumber = recordSequence // Assume sync upwards
if (recordHeader == VALID_HEADER) {
when (recordType) {
BOLUS_RECORD, BOLUS_RECORD_ALT -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD")
val typeAndWizard = data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 1).toInt()
val bolusCause = data.copyOfRange(RESP_RECORD_DATA_START + 1, RESP_RECORD_DATA_START + 2).toInt()
val unknown = data.copyOfRange(RESP_RECORD_DATA_START + 2, RESP_RECORD_DATA_START + 4).toInt()
val bolusStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 8).toLong())
val bolusNormalAmount = data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 10).toInt() * 0.05
val bolusNormalDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 10, RESP_RECORD_DATA_START + 12).toInt() * 0.05
val bolusExtendedAmount = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 14).toInt() * 0.05
val bolusExtendedDuration = data.copyOfRange(RESP_RECORD_DATA_START + 14, RESP_RECORD_DATA_START + 16).toLong() * 1000
val bolusExtendedDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 16, RESP_RECORD_DATA_START + 18).toInt() * 0.05
val bolusCarb = data.copyOfRange(RESP_RECORD_DATA_START + 18, RESP_RECORD_DATA_START + 20).toInt()
val bolusGlucose = data.copyOfRange(RESP_RECORD_DATA_START + 20, RESP_RECORD_DATA_START + 22).toInt()
val bolusIOB = data.copyOfRange(RESP_RECORD_DATA_START + 22, RESP_RECORD_DATA_START + 24).toInt()
val unkown1 = data.copyOfRange(RESP_RECORD_DATA_START + 24, RESP_RECORD_DATA_START + 26).toInt()
val unkown2 = data.copyOfRange(RESP_RECORD_DATA_START + 26, RESP_RECORD_DATA_START + 28).toInt()
val bolusType = enumValues<BolusType>()[typeAndWizard and 0x0F]
val bolusWizard = (typeAndWizard and 0xF0) != 0
aapsLogger.debug(
LTag.PUMPCOMM,
"GetRecordPacket HandleResponse: BOLUS_RECORD: typeAndWizard: $typeAndWizard, bolusCause: $bolusCause, unknown: $unknown, bolusStartTime: $bolusStartTime, " + "bolusNormalAmount: $bolusNormalAmount, bolusNormalDelivered: $bolusNormalDelivered, bolusExtendedAmount: $bolusExtendedAmount, bolusExtendedDuration: $bolusExtendedDuration, " + "bolusExtendedDelivered: $bolusExtendedDelivered, bolusCarb: $bolusCarb, bolusGlucose: $bolusGlucose, bolusIOB: $bolusIOB, unkown1: $unkown1, unkown2: $unkown2, " + "bolusType: $bolusType, bolusWizard: $bolusWizard"
)
if (bolusType == BolusType.NORMAL) {
val detailedBolusInfo = detailedBolusInfoStorage.findDetailedBolusInfo(bolusStartTime, bolusNormalDelivered)
var newRecord = false
if (detailedBolusInfo != null) {
val syncOk = pumpSync.syncBolusWithTempId(
timestamp = bolusStartTime,
amount = bolusNormalDelivered,
temporaryId = detailedBolusInfo.timestamp,
type = detailedBolusInfo.bolusType,
pumpId = bolusStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
if (syncOk == false) {
aapsLogger.warn(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD: Failed to sync bolus with tempId: ${detailedBolusInfo.timestamp}")
// detailedInfo can be from another similar record. Reinsert
detailedBolusInfoStorage.add(detailedBolusInfo)
}
} else {
newRecord = pumpSync.syncBolusWithPumpId(
timestamp = bolusStartTime,
amount = bolusNormalDelivered,
type = null,
pumpId = bolusStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
}
aapsLogger.debug(
LTag.PUMPCOMM,
"from record: ${if (newRecord) "**NEW** " else ""}EVENT BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U "
)
if (bolusStartTime > medtrumPump.lastBolusTime) {
medtrumPump.lastBolusTime = bolusStartTime
medtrumPump.lastBolusAmount = bolusNormalDelivered
}
} else if (bolusType == BolusType.EXTENDED) {
val newRecord = pumpSync.syncExtendedBolusWithPumpId(
timestamp = bolusStartTime,
amount = bolusExtendedDelivered,
duration = bolusExtendedDuration,
isEmulatingTB = false,
pumpId = bolusStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"from record: ${if (newRecord) "**NEW** " else ""}EVENT EXTENDED BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U "
)
} else if (bolusType == BolusType.COMBI) {
// Note, this should never happen, as we don't use combo bolus
val detailedBolusInfo = detailedBolusInfoStorage.findDetailedBolusInfo(bolusStartTime, bolusNormalDelivered)
val newRecord = pumpSync.syncBolusWithPumpId(
timestamp = bolusStartTime,
amount = bolusNormalDelivered,
type = detailedBolusInfo?.bolusType,
pumpId = bolusStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
pumpSync.syncExtendedBolusWithPumpId(
timestamp = bolusStartTime,
amount = bolusExtendedDelivered,
duration = bolusExtendedDuration,
isEmulatingTB = false,
pumpId = bolusStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.error(
LTag.PUMPCOMM,
"from record: ${if (newRecord) "**NEW** " else ""}EVENT COMBI BOLUS ${dateUtil.dateAndTimeString(bolusStartTime)} ($bolusStartTime) Bolus: ${bolusNormalDelivered}U Extended: ${bolusExtendedDelivered} THIS SHOULD NOT HAPPEN!!!"
)
if (!newRecord && detailedBolusInfo != null) {
// detailedInfo can be from another similar record. Reinsert
detailedBolusInfoStorage.add(detailedBolusInfo)
}
if (bolusStartTime > medtrumPump.lastBolusTime) {
medtrumPump.lastBolusTime = bolusStartTime
medtrumPump.lastBolusAmount = bolusNormalDelivered
}
} else {
aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BOLUS_RECORD: Unknown bolus type: $bolusType")
}
}
BASAL_RECORD, BASAL_RECORD_ALT -> {
val medtrumTimeUtil = MedtrumTimeUtil()
val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 4).toLong())
val basalEndTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 8).toLong())
val basalType = enumValues<BasalType>()[data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 9).toInt()]
val basalEndReason = data.copyOfRange(RESP_RECORD_DATA_START + 9, RESP_RECORD_DATA_START + 10).toInt()
val basalRate = data.copyOfRange(RESP_RECORD_DATA_START + 10, RESP_RECORD_DATA_START + 12).toInt() * 0.05
val basalDelivered = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 14).toInt() * 0.05
val basalPercent = data.copyOfRange(RESP_RECORD_DATA_START + 14, RESP_RECORD_DATA_START + 16).toInt()
aapsLogger.debug(
LTag.PUMPCOMM,
"GetRecordPacket HandleResponse: BASAL_RECORD: Start: $basalStartTime, End: $basalEndTime, Type: $basalType, EndReason: $basalEndReason, Rate: $basalRate, Delivered: $basalDelivered, Percent: $basalPercent"
)
when (basalType) {
BasalType.STANDARD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Standard basal")
// If we are here it means the basal has ended
}
BasalType.ABSOLUTE_TEMP, BasalType.RELATIVE_TEMP -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Absolute temp basal")
var duration = (basalEndTime - basalStartTime)
// Work around for pumpSync not accepting 0 duration.
// sometimes we get 0 duration for very short basal because the pump only reports time in seconds
if (duration < 250) duration = 250 // 250ms to make sure AAPS accepts it
val newRecord = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = basalStartTime,
rate = if (basalType == BasalType.ABSOLUTE_TEMP) basalRate else basalPercent.toDouble(),
duration = duration,
isAbsolute = (basalType == BasalType.ABSOLUTE_TEMP),
type = PumpSync.TemporaryBasalType.NORMAL,
pumpId = basalStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT TEMP_SYNC: ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " +
"Rate: $basalRate Duration: ${duration}"
)
}
in BasalType.SUSPEND_LOW_GLUCOSE..BasalType.STOP -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Suspend basal")
val duration = (basalEndTime - basalStartTime)
val newRecord = pumpSync.syncTemporaryBasalWithPumpId(
timestamp = basalEndTime,
rate = 0.0,
duration = duration,
isAbsolute = true,
type = PumpSync.TemporaryBasalType.PUMP_SUSPEND,
pumpId = basalStartTime,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT SUSPEND: ($basalType) ${dateUtil.dateAndTimeString(basalStartTime)} ($basalStartTime) " +
"Rate: $basalRate Duration: ${duration}"
)
}
else -> {
aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: BASAL_RECORD: Unknown basal type: $basalType")
}
}
}
ALARM_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: ALARM_RECORD")
}
AUTO_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO_RECORD")
}
TIME_SYNC_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: TIME_SYNC_RECORD")
}
AUTO1_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO1_RECORD")
}
AUTO2_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO2_RECORD")
}
AUTO3_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: AUTO3_RECORD")
}
TDD_RECORD -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: TDD_RECORD")
val timestamp = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_RECORD_DATA_START, RESP_RECORD_DATA_START + 4).toLong())
val timeZoneOffset = data.copyOfRange(RESP_RECORD_DATA_START + 4, RESP_RECORD_DATA_START + 6).toInt()
val tddMins = data.copyOfRange(RESP_RECORD_DATA_START + 6, RESP_RECORD_DATA_START + 8).toInt()
val glucoseRecordTime = data.copyOfRange(RESP_RECORD_DATA_START + 8, RESP_RECORD_DATA_START + 12).toLong()
val tdd = data.copyOfRange(RESP_RECORD_DATA_START + 12, RESP_RECORD_DATA_START + 16).toFloat()
val basalTdd = data.copyOfRange(RESP_RECORD_DATA_START + 16, RESP_RECORD_DATA_START + 20).toFloat()
val glucose = data.copyOfRange(RESP_RECORD_DATA_START + 20, RESP_RECORD_DATA_START + 24).toFloat()
val unknown = data.copyOfRange(RESP_RECORD_DATA_START + 24, RESP_RECORD_DATA_START + 28).toFloat()
val meanSomething = data.copyOfRange(RESP_RECORD_DATA_START + 28, RESP_RECORD_DATA_START + 32).toFloat()
val usedTdd = data.copyOfRange(RESP_RECORD_DATA_START + 32, RESP_RECORD_DATA_START + 36).toFloat()
val usedIBasal = data.copyOfRange(RESP_RECORD_DATA_START + 36, RESP_RECORD_DATA_START + 40).toFloat()
val usedSgBasal = data.copyOfRange(RESP_RECORD_DATA_START + 40, RESP_RECORD_DATA_START + 44).toFloat()
val usedUMax = data.copyOfRange(RESP_RECORD_DATA_START + 44, RESP_RECORD_DATA_START + 48).toFloat()
val newTdd = data.copyOfRange(RESP_RECORD_DATA_START + 48, RESP_RECORD_DATA_START + 52).toFloat()
val newIBasal = data.copyOfRange(RESP_RECORD_DATA_START + 52, RESP_RECORD_DATA_START + 56).toFloat()
val newSgBasal = data.copyOfRange(RESP_RECORD_DATA_START + 56, RESP_RECORD_DATA_START + 60).toFloat()
val newUMax = data.copyOfRange(RESP_RECORD_DATA_START + 60, RESP_RECORD_DATA_START + 64).toFloat()
aapsLogger.debug(
LTag.PUMPCOMM, "TDD_RECORD: timestamp: $timestamp, timeZoneOffset: $timeZoneOffset, tddMins: $tddMins, glucoseRecordTime: $glucoseRecordTime, tdd: " +
"$tdd, basalTdd: $basalTdd, glucose: $glucose, unknown: $unknown, meanSomething: $meanSomething, usedTdd: $usedTdd, usedIBasal: $usedIBasal, usedSgBasal: " +
"$usedSgBasal, usedUMax: $usedUMax, newTdd: $newTdd, newIBasal: $newIBasal, newSgBasal: $newSgBasal, newUMax: $newUMax"
)
val newRecord = pumpSync.createOrUpdateTotalDailyDose(
timestamp = timestamp,
bolusAmount = (tdd - basalTdd).toDouble(),
basalAmount = basalTdd.toDouble(),
totalAmount = tdd.toDouble(),
pumpId = timestamp,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"handleBasalStatusUpdate from record: ${if (newRecord) "**NEW** " else ""}EVENT TDD: ${dateUtil.dateAndTimeString(timestamp)} ($timestamp) " +
"TDD: $tdd, BasalTDD: $basalTdd, BolusTDD: ${tdd - basalTdd}"
)
}
else -> {
aapsLogger.debug(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: Unknown record type: $recordType")
}
}
} else {
aapsLogger.error(LTag.PUMPCOMM, "GetRecordPacket HandleResponse: Invalid record header")
}
}
return success
}
}

View file

@ -0,0 +1,34 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.GET_TIME
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import javax.inject.Inject
class GetTimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_TIME_START = 6
private const val RESP_TIME_END = RESP_TIME_START + 4
}
init {
opCode = GET_TIME.code
expectedMinRespLength = RESP_TIME_END
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val time = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_TIME_START, RESP_TIME_END).toLong())
medtrumPump.lastTimeReceivedFromPump = time
}
return success
}
}

View file

@ -0,0 +1,79 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
open class MedtrumPacket(protected var injector: HasAndroidInjector) {
@Inject lateinit var aapsLogger: AAPSLogger
var opCode: Byte = 0
var failed = false
var expectedMinRespLength = RESP_RESULT_END
companion object {
const val RESP_OPCODE_START = 1
const val RESP_OPCODE_END = RESP_OPCODE_START + 1
const val RESP_RESULT_START = 4
const val RESP_RESULT_END = RESP_RESULT_START + 2
private const val RESP_WAITING = 16384
}
init {
// @Suppress("LeakingThis")
injector.androidInjector().inject(this)
}
open fun getRequest(): ByteArray {
return byteArrayOf(opCode)
}
/** handles a response from the Medtrum pump, returns true if command was successfull, returns false if command failed or waiting for response */
open fun handleResponse(data: ByteArray): Boolean {
// Check for broken packets
if (RESP_RESULT_END > data.size) {
failed = true
aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Unexpected response length, expected: $expectedMinRespLength got: ${data.size}")
return false
}
val incomingOpCode: Byte = data.copyOfRange(RESP_OPCODE_START, RESP_OPCODE_END).first()
val responseCode = data.copyOfRange(RESP_RESULT_START, RESP_RESULT_END).toInt()
return when {
incomingOpCode != opCode -> {
failed = true
aapsLogger.error(LTag.PUMPCOMM, "handleResponse: Unexpected command, expected: $opCode got: $incomingOpCode")
false
}
responseCode == 0 -> {
// Check if length is what is expected from this type of packet
if (expectedMinRespLength > data.size) {
failed = true
aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Unexpected response length, expected: $expectedMinRespLength got: ${data.size}")
return false
}
aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Happy command: $opCode response: $responseCode")
true
}
responseCode == RESP_WAITING -> {
aapsLogger.debug(LTag.PUMPCOMM, "handleResponse: Waiting command: $opCode response: $responseCode")
// Waiting do nothing
false
}
else -> {
failed = true
aapsLogger.warn(LTag.PUMPCOMM, "handleResponse: Error in command: $opCode response: $responseCode")
false
}
}
}
}

View file

@ -0,0 +1,236 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.AlarmState
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class NotificationPacket(val injector: HasAndroidInjector) {
/**
* This is a bit of a special packet, as it is not a command packet
* but a notification packet. It is sent by the pump to the phone
* when the pump has a notification to send.
*
* Notifications are sent regualary, regardless of the pump state.
*
* There can be multiple messages in one packet, this is noted by the fieldMask.
*
* Byte 1: State (Handle a state change directly? before analyzing further?)
* Byte 2-3: FieldMask (BitMask which tells the fields present in the message)
* Byte 4-end : status data
*
* When multiple fields are in the message, the data is concatenated.
* This kind of message can also come as a response of SynchronizePacket,
* and can be handled here by handleMaskedMessage() as well.
*/
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val NOTIF_STATE_START = 0
private const val NOTIF_STATE_END = NOTIF_STATE_START + 1
private const val MASK_SUSPEND = 0x01
private const val MASK_NORMAL_BOLUS = 0x02
private const val MASK_EXTENDED_BOLUS = 0x04
private const val MASK_BASAL = 0x08
private const val MASK_SETUP = 0x10
private const val MASK_RESERVOIR = 0x20
private const val MASK_START_TIME = 0x40
private const val MASK_BATTERY = 0x80
private const val MASK_STORAGE = 0x100
private const val MASK_ALARM = 0x200
private const val MASK_AGE = 0x400
private const val MASK_UNKNOWN_1 = 0x800
private const val MASK_UNUSED_CGM = 0x1000
private const val MASK_UNUSED_COMMAND_CONFIRM = 0x2000
private const val MASK_UNUSED_AUTO_STATUS = 0x4000
private const val MASK_UNUSED_LEGACY = 0x8000
}
init {
injector.androidInjector().inject(this)
}
fun handleNotification(notification: ByteArray) {
val state = MedtrumPumpState.fromByte(notification[0])
aapsLogger.debug(LTag.PUMPCOMM, "Notification state: $state, current state: ${medtrumPump.pumpState}")
if (state != medtrumPump.pumpState) {
aapsLogger.debug(LTag.PUMPCOMM, "State changed from ${medtrumPump.pumpState} to $state")
medtrumPump.pumpState = state
}
if (notification.size > NOTIF_STATE_END) {
handleMaskedMessage(notification.copyOfRange(NOTIF_STATE_END, notification.size))
}
}
/**
* Handle a message with a field mask, can be used by other packets as well
*/
fun handleMaskedMessage(data: ByteArray) {
val fieldMask = data.copyOfRange(0, 2).toInt()
var offset = 2
aapsLogger.debug(LTag.PUMPCOMM, "Message field mask: $fieldMask")
if (fieldMask and MASK_SUSPEND != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Suspend notification received")
medtrumPump.suspendTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset, offset + 4).toLong())
aapsLogger.debug(LTag.PUMPCOMM, "Suspend time: ${medtrumPump.suspendTime}")
offset += 4
}
if (fieldMask and MASK_NORMAL_BOLUS != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Normal bolus notification received")
var bolusData = data.copyOfRange(offset, offset + 1).toInt()
var bolusType = bolusData and 0x7F
val bolusCompleted: Boolean = ((bolusData shr 7) and 0x01) != 0
var bolusDelivered = data.copyOfRange(offset + 1, offset + 3).toInt() * 0.05
aapsLogger.debug(LTag.PUMPCOMM, "Bolus type: $bolusType, bolusData: $bolusData bolus completed: $bolusCompleted, bolus delivered: $bolusDelivered")
medtrumPump.handleBolusStatusUpdate(bolusType, bolusCompleted, bolusDelivered)
offset += 3
}
if (fieldMask and MASK_EXTENDED_BOLUS != 0) {
aapsLogger.error(LTag.PUMPCOMM, "Extended bolus notification received, extended bolus not supported!")
// TODO Handle error and stop pump if this happens?
offset += 3
}
if (fieldMask and MASK_BASAL != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Basal notification received")
val basalType = enumValues<BasalType>()[data.copyOfRange(offset, offset + 1).toInt()]
var basalSequence = data.copyOfRange(offset + 1, offset + 3).toInt()
var basalPatchId = data.copyOfRange(offset + 3, offset + 5).toLong()
var basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset + 5, offset + 9).toLong())
var basalRateAndDelivery = data.copyOfRange(offset + 9, offset + 12).toInt()
var basalRate = (basalRateAndDelivery and 0xFFF) * 0.05
var basalDelivery = (basalRateAndDelivery shr 12) * 0.05
aapsLogger.debug(
LTag.PUMPCOMM,
"Basal type: $basalType, basal sequence: $basalSequence, basal patch id: $basalPatchId, basal time: $basalStartTime, basal rate: $basalRate, basal delivery: $basalDelivery"
)
// Don't spam with basal updates here, only if the running basal rate has changed, or a new basal is set
if (medtrumPump.lastBasalRate != basalRate || medtrumPump.lastBasalStartTime != basalStartTime) {
medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime)
}
offset += 12
}
if (fieldMask and MASK_SETUP != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Setup notification received")
medtrumPump.primeProgress = data.copyOfRange(offset, offset + 1).toInt()
aapsLogger.debug(LTag.PUMPCOMM, "Prime progress: ${medtrumPump.primeProgress}")
offset += 1
}
if (fieldMask and MASK_RESERVOIR != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Reservoir notification received")
medtrumPump.reservoir = data.copyOfRange(offset, offset + 2).toInt() * 0.05
aapsLogger.debug(LTag.PUMPCOMM, "Reservoir: ${medtrumPump.reservoir}")
offset += 2
}
if (fieldMask and MASK_START_TIME != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Start time notification received")
val patchStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(offset, offset + 4).toLong())
if (medtrumPump.patchStartTime != patchStartTime) {
aapsLogger.debug(LTag.PUMPCOMM, "Patch start time changed from ${medtrumPump.patchStartTime} to $patchStartTime")
medtrumPump.patchStartTime = patchStartTime
}
aapsLogger.debug(LTag.PUMPCOMM, "Patch start time: ${patchStartTime}")
offset += 4
}
if (fieldMask and MASK_BATTERY != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Battery notification received")
var parameter = data.copyOfRange(offset, offset + 3).toInt()
// Precision for voltage A is a guess, voltage B is the important one, threshold: < 2.64
medtrumPump.batteryVoltage_A = (parameter and 0xFFF) / 512.0
medtrumPump.batteryVoltage_B = (parameter shr 12) / 512.0
aapsLogger.debug(LTag.PUMPCOMM, "Battery voltage A: ${medtrumPump.batteryVoltage_A}, battery voltage B: ${medtrumPump.batteryVoltage_B}")
offset += 3
}
if (fieldMask and MASK_STORAGE != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Storage notification received")
val sequence = data.copyOfRange(offset, offset + 2).toInt()
if (sequence > medtrumPump.currentSequenceNumber) {
medtrumPump.currentSequenceNumber = sequence
}
val patchId = data.copyOfRange(offset + 2, offset + 4).toLong()
if (patchId != medtrumPump.patchId) {
aapsLogger.warn(LTag.PUMPCOMM, "handleMaskedMessage: We got wrong patch id!")
}
aapsLogger.debug(LTag.PUMPCOMM, "Last known sequence number: ${medtrumPump.currentSequenceNumber}, patch id: ${patchId}")
offset += 4
}
if (fieldMask and MASK_ALARM != 0) {
val alarmFlags = data.copyOfRange(offset, offset + 2).toInt()
val alarmParameter = data.copyOfRange(offset + 2, offset + 4).toInt()
aapsLogger.debug(LTag.PUMPCOMM, "Alarm notification received, Alarm flags: $alarmFlags, alarm parameter: $alarmParameter")
// If no alarm, clear activeAlarm list
if (alarmFlags == 0 && medtrumPump.activeAlarms.isNotEmpty()) {
medtrumPump.clearAlarmState()
} else if (alarmFlags != 0) {
// Check each alarm bit
for (i in 0..3) { // Only the first 3 flags are interesting for us, the rest we will get from the pump state
val alarmState = AlarmState.values()[i]
if ((alarmFlags shr i) and 1 != 0) {
// If the alarm bit is set, add the corresponding alarm to activeAlarms
medtrumPump.addAlarm(alarmState)
} else if (medtrumPump.activeAlarms.contains(alarmState)) {
// If the alarm bit is not set, and the corresponding alarm is in activeAlarms, remove it
medtrumPump.removeAlarm(alarmState)
}
}
}
offset += 4
}
if (fieldMask and MASK_AGE != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Age notification received")
medtrumPump.patchAge = data.copyOfRange(offset, offset + 4).toLong()
aapsLogger.debug(LTag.PUMPCOMM, "Patch age: ${medtrumPump.patchAge}")
offset += 4
}
if (fieldMask and MASK_UNKNOWN_1 != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Unknown 1 notification received, not handled!")
}
if (fieldMask and MASK_UNUSED_CGM != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Unused CGM notification received, not handled!")
}
if (fieldMask and MASK_UNUSED_COMMAND_CONFIRM != 0) {
// This one is a warning, as this happens we need to know about it, and maybe implement
aapsLogger.warn(LTag.PUMPCOMM, "Unused command confirm notification received, not handled!")
}
if (fieldMask and MASK_UNUSED_AUTO_STATUS != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Unused auto status notification received, not handled!")
}
if (fieldMask and MASK_UNUSED_LEGACY != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "Unused legacy notification received, not handled!")
}
}
}

View file

@ -0,0 +1,11 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.POLL_PATCH
class PollPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = POLL_PATCH.code
}
}

View file

@ -0,0 +1,11 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.PRIME
class PrimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = PRIME.code
}
}

View file

@ -0,0 +1,31 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.READ_BOLUS_STATE
class ReadBolusStatePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
// UNUSED
// Bolus sync is currently done by getting the records and syncing then with AAPS pumpSync there
var bolusData: ByteArray = byteArrayOf()
companion object {
private const val RESP_BOLUS_DATA_START = 6
}
init {
opCode = READ_BOLUS_STATE.code
expectedMinRespLength = RESP_BOLUS_DATA_START + 1
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
// UNUSED
bolusData = data.copyOfRange(RESP_BOLUS_DATA_START, data.size)
}
return success
}
}

View file

@ -0,0 +1,16 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.RESUME_PUMP
import info.nightscout.pump.medtrum.extension.toByteArray
class ResumePumpPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = RESUME_PUMP.code
}
override fun getRequest(): ByteArray {
return byteArrayOf(opCode)
}
}

View file

@ -0,0 +1,57 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BASAL_PROFILE
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import javax.inject.Inject
class SetBasalProfilePacket(injector: HasAndroidInjector, private val basalProfile: ByteArray) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_BASAL_TYPE_START = 6
private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1
private const val RESP_BASAL_VALUE_START = 7
private const val RESP_BASAL_VALUE_END = RESP_BASAL_VALUE_START + 2
private const val RESP_BASAL_SEQUENCE_START = 9
private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2
private const val RESP_BASAL_PATCH_ID_START = 11
private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2
private const val RESP_BASAL_START_TIME_START = 13
private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4
}
init {
opCode = SET_BASAL_PROFILE.code
expectedMinRespLength = RESP_BASAL_START_TIME_END
}
override fun getRequest(): ByteArray {
val basalType: Byte = 1 // Fixed to normal basal
return byteArrayOf(opCode) + basalType + basalProfile
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val medtrumTimeUtil = MedtrumTimeUtil()
val basalType = enumValues<BasalType>()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()]
val basalValue = data.copyOfRange(RESP_BASAL_VALUE_START, RESP_BASAL_VALUE_END).toInt() * 0.05
val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt()
val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong()
val basalStartTime = medtrumTimeUtil.convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong())
// Update the actual basal profile
medtrumPump.actualBasalProfile = basalProfile
medtrumPump.handleBasalStatusUpdate(basalType, basalValue, basalSequence, basalPatchId, basalStartTime)
}
return success
}
}

View file

@ -0,0 +1,17 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BOLUS_MOTOR
class SetBolusMotorPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
// UNUSED in our driver
init {
opCode = SET_BOLUS_MOTOR.code
}
override fun getRequest(): ByteArray {
return byteArrayOf(opCode) + 0.toByte()
}
}

View file

@ -0,0 +1,23 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_BOLUS
import info.nightscout.pump.medtrum.extension.toByteArray
import kotlin.math.round
class SetBolusPacket(injector: HasAndroidInjector, private val insulin: Double) : MedtrumPacket(injector) {
init {
opCode = SET_BOLUS.code
}
override fun getRequest(): ByteArray {
// Bolus types:
// 1 = normal
// 2 = Extended
// 3 = Combi
val bolusType: Byte = 1 // Only support for normal bolus for now
val bolusAmount: Int = round(insulin / 0.05).toInt()
return byteArrayOf(opCode) + bolusType + bolusAmount.toByteArray(2) + 0.toByte()
}
}

View file

@ -0,0 +1,46 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.AlarmSetting
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_PATCH
import info.nightscout.pump.medtrum.extension.toByte
import info.nightscout.pump.medtrum.extension.toByteArray
import javax.inject.Inject
import kotlin.math.round
class SetPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
init {
opCode = SET_PATCH.code
}
override fun getRequest(): ByteArray {
/**
* byte 0: opCode
* byte 1: alarmSetting // See AlarmSetting
* byte 2-3: hourlyMaxInsulin // Max hourly dose of insulin, divided by 0.05
* byte 4-5: dailyMaxSet // Max daily dose of insulin, divided by 0.05
* byte 6: expirationTimer // Expiration timer, 0 = no expiration 1 = 12 hour reminder and expiration
* byte 7: autoSuspendEnable // Value for auto mode, not used for AAPS
* byte 8: autoSuspendTime // Value for auto mode, not used for AAPS
* byte 9: lowSuspend // Value for auto mode, not used for AAPS
* byte 10: predictiveLowSuspend // Value for auto mode, not used for AAPS
* byte 11: predictiveLowSuspendRange // Value for auto mode, not used for AAPS
*/
val alarmSetting: AlarmSetting = medtrumPump.desiredAlarmSetting
val hourlyMaxInsulin: Int = round(medtrumPump.desiredHourlyMaxInsulin / 0.05).toInt()
val dailyMaxInsulin: Int = round(medtrumPump.desiredDailyMaxInsulin / 0.05).toInt()
val patchExpiration: Byte = medtrumPump.desiredPatchExpiration.toByte()
val autoSuspendEnable: Byte = 0
val autoSuspendTime: Byte = 12 // Not sure why, but pump needs this
val lowSuspend: Byte = 0
val predictiveLowSuspend: Byte = 0
val predictiveLowSuspendRange: Byte = 30 // Not sure why, but pump needs this
return byteArrayOf(opCode) + alarmSetting.code + hourlyMaxInsulin.toByteArray(2) + dailyMaxInsulin.toByteArray(2) + patchExpiration + autoSuspendEnable + autoSuspendTime + lowSuspend + predictiveLowSuspend + predictiveLowSuspendRange
}
}

View file

@ -0,0 +1,67 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TEMP_BASAL
import info.nightscout.pump.medtrum.comm.enums.BasalType
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.extension.toLong
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
import kotlin.math.round
class SetTempBasalPacket(injector: HasAndroidInjector, private val absoluteRate: Double, private val durationInMinutes: Int) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_BASAL_TYPE_START = 6
private const val RESP_BASAL_TYPE_END = RESP_BASAL_TYPE_START + 1
private const val RESP_BASAL_RATE_START = RESP_BASAL_TYPE_END
private const val RESP_BASAL_RATE_END = RESP_BASAL_RATE_START + 2
private const val RESP_BASAL_SEQUENCE_START = RESP_BASAL_RATE_END
private const val RESP_BASAL_SEQUENCE_END = RESP_BASAL_SEQUENCE_START + 2
private const val RESP_BASAL_PATCH_ID_START = RESP_BASAL_SEQUENCE_END
private const val RESP_BASAL_PATCH_ID_END = RESP_BASAL_PATCH_ID_START + 2
private const val RESP_BASAL_START_TIME_START = RESP_BASAL_PATCH_ID_END
private const val RESP_BASAL_START_TIME_END = RESP_BASAL_START_TIME_START + 4
}
init {
opCode = SET_TEMP_BASAL.code
expectedMinRespLength = RESP_BASAL_START_TIME_END
}
override fun getRequest(): ByteArray {
/**
* byte 0: opCode
* byte 1: tempBasalType
* byte 2-3: tempBasalRate
* byte 4-5: tempBasalDuration
*/
val tempBasalType: Byte = 6 // Fixed to temp basal value for now
val tempBasalRate: Int = round(absoluteRate / 0.05).toInt()
val tempBasalDuration: Int = durationInMinutes
return byteArrayOf(opCode) + tempBasalType + tempBasalRate.toByteArray(2) + tempBasalDuration.toByteArray(2)
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val basalType = enumValues<BasalType>()[data.copyOfRange(RESP_BASAL_TYPE_START, RESP_BASAL_TYPE_END).toInt()]
val basalRate = data.copyOfRange(RESP_BASAL_RATE_START, RESP_BASAL_RATE_END).toInt() * 0.05
val basalSequence = data.copyOfRange(RESP_BASAL_SEQUENCE_START, RESP_BASAL_SEQUENCE_END).toInt()
val basalPatchId = data.copyOfRange(RESP_BASAL_PATCH_ID_START, RESP_BASAL_PATCH_ID_END).toLong()
val rawTime = data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong()
val basalStartTime = MedtrumTimeUtil().convertPumpTimeToSystemTimeMillis(data.copyOfRange(RESP_BASAL_START_TIME_START, RESP_BASAL_START_TIME_END).toLong())
aapsLogger.debug(LTag.PUMPCOMM, "Basal status update: type=$basalType, rate=$basalRate, sequence=$basalSequence, patchId=$basalPatchId, startTime=$basalStartTime, rawTime=$rawTime")
medtrumPump.handleBasalStatusUpdate(basalType, basalRate, basalSequence, basalPatchId, basalStartTime)
}
return success
}
}

View file

@ -0,0 +1,18 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TIME
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
class SetTimePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = SET_TIME.code
}
override fun getRequest(): ByteArray {
val time = MedtrumTimeUtil().getCurrentTimePumpSeconds()
return byteArrayOf(opCode) + 2.toByte() + time.toByteArray(4)
}
}

View file

@ -0,0 +1,36 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.SET_TIME_ZONE
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.shared.utils.DateUtil
import javax.inject.Inject
class SetTimeZonePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var medtrumPump: MedtrumPump
var offsetMins: Int = 0
init {
opCode = SET_TIME_ZONE.code
}
override fun getRequest(): ByteArray {
val time = MedtrumTimeUtil().getCurrentTimePumpSeconds()
offsetMins = dateUtil.getTimeZoneOffsetMinutes(dateUtil.now())
if (offsetMins < 0) offsetMins += 65536
return byteArrayOf(opCode) + offsetMins.toByteArray(2) + time.toByteArray(4)
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
medtrumPump.pumpTimeZoneOffset = offsetMins
}
return success
}
}

View file

@ -0,0 +1,37 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.comm.enums.CommandType.STOP_PATCH
import info.nightscout.pump.medtrum.extension.toLong
import javax.inject.Inject
class StopPatchPacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_STOP_SEQUENCE_START = 6
private const val RESP_STOP_SEQUENCE_END = RESP_STOP_SEQUENCE_START + 2
private const val RESP_STOP_PATCH_ID_START = RESP_STOP_SEQUENCE_END
private const val RESP_STOP_PATCH_ID_END = RESP_STOP_PATCH_ID_START + 2
}
init {
opCode = STOP_PATCH.code
expectedMinRespLength = RESP_STOP_PATCH_ID_END
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
val stopSequence = data.copyOfRange(RESP_STOP_SEQUENCE_START, RESP_STOP_SEQUENCE_END).toInt()
val stopPatchId = data.copyOfRange(RESP_STOP_PATCH_ID_START, RESP_STOP_PATCH_ID_END).toLong()
medtrumPump.handleStopStatusUpdate(stopSequence, stopPatchId)
}
return success
}
}

View file

@ -0,0 +1,16 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.comm.enums.CommandType.SUBSCRIBE
import info.nightscout.pump.medtrum.extension.toByteArray
class SubscribePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
init {
opCode = SUBSCRIBE.code
}
override fun getRequest(): ByteArray {
return byteArrayOf(opCode) + 4095.toByteArray(2)
}
}

View file

@ -0,0 +1,74 @@
package info.nightscout.pump.medtrum.comm.packets
import dagger.android.HasAndroidInjector
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.comm.enums.CommandType.SYNCHRONIZE
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class SynchronizePacket(injector: HasAndroidInjector) : MedtrumPacket(injector) {
@Inject lateinit var medtrumPump: MedtrumPump
companion object {
private const val RESP_STATE_START = 6
private const val RESP_STATE_END = RESP_STATE_START + 1
private const val RESP_FIELDS_START = 7
private const val RESP_FIELDS_END = RESP_FIELDS_START + 2
private const val RESP_SYNC_DATA_START = 9
private const val MASK_SUSPEND = 0x01
private const val MASK_NORMAL_BOLUS = 0x02
private const val MASK_EXTENDED_BOLUS = 0x04
}
init {
opCode = SYNCHRONIZE.code
expectedMinRespLength = RESP_SYNC_DATA_START + 1
}
override fun handleResponse(data: ByteArray): Boolean {
val success = super.handleResponse(data)
if (success) {
var state = MedtrumPumpState.fromByte(data[RESP_STATE_START])
aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: state: $state")
if (state != medtrumPump.pumpState) {
aapsLogger.debug(LTag.PUMPCOMM, "State changed from ${medtrumPump.pumpState} to $state")
medtrumPump.pumpState = state
}
var fieldMask = data.copyOfRange(RESP_FIELDS_START, RESP_FIELDS_END).toInt()
var syncData = data.copyOfRange(RESP_SYNC_DATA_START, data.size)
var offset = 0
if (fieldMask != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: fieldMask: $fieldMask")
}
// Remove bolus fields from fieldMask if fields are present (we sync bolus trough other commands)
if (fieldMask and MASK_SUSPEND != 0) {
offset += 4 // If field is present, skip 4 bytes
}
if (fieldMask and MASK_NORMAL_BOLUS != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: Normal bolus present removing from fieldMask")
fieldMask = fieldMask and MASK_NORMAL_BOLUS.inv()
syncData = syncData.copyOfRange(0, offset) + syncData.copyOfRange(offset + 3, syncData.size)
}
if (fieldMask and MASK_EXTENDED_BOLUS != 0) {
aapsLogger.debug(LTag.PUMPCOMM, "SynchronizePacket: Extended bolus present removing from fieldMask")
fieldMask = fieldMask and MASK_EXTENDED_BOLUS.inv()
syncData = syncData.copyOfRange(0, offset) + syncData.copyOfRange(offset + 3, syncData.size)
}
// Let the notification packet handle the rest of the sync data
NotificationPacket(injector).handleMaskedMessage(fieldMask.toByteArray(2) + syncData)
}
return success
}
}

View file

@ -0,0 +1,57 @@
package info.nightscout.pump.medtrum.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import info.nightscout.pump.medtrum.comm.packets.ActivatePacket
import info.nightscout.pump.medtrum.comm.packets.AuthorizePacket
import info.nightscout.pump.medtrum.comm.packets.CancelBolusPacket
import info.nightscout.pump.medtrum.comm.packets.CancelTempBasalPacket
import info.nightscout.pump.medtrum.comm.packets.ClearPumpAlarmPacket
import info.nightscout.pump.medtrum.comm.packets.GetDeviceTypePacket
import info.nightscout.pump.medtrum.comm.packets.GetRecordPacket
import info.nightscout.pump.medtrum.comm.packets.GetTimePacket
import info.nightscout.pump.medtrum.comm.packets.MedtrumPacket
import info.nightscout.pump.medtrum.comm.packets.NotificationPacket
import info.nightscout.pump.medtrum.comm.packets.PollPatchPacket
import info.nightscout.pump.medtrum.comm.packets.PrimePacket
import info.nightscout.pump.medtrum.comm.packets.ReadBolusStatePacket
import info.nightscout.pump.medtrum.comm.packets.ResumePumpPacket
import info.nightscout.pump.medtrum.comm.packets.SetBasalProfilePacket
import info.nightscout.pump.medtrum.comm.packets.SetBolusMotorPacket
import info.nightscout.pump.medtrum.comm.packets.SetBolusPacket
import info.nightscout.pump.medtrum.comm.packets.SetPatchPacket
import info.nightscout.pump.medtrum.comm.packets.SetTempBasalPacket
import info.nightscout.pump.medtrum.comm.packets.SetTimePacket
import info.nightscout.pump.medtrum.comm.packets.SetTimeZonePacket
import info.nightscout.pump.medtrum.comm.packets.StopPatchPacket
import info.nightscout.pump.medtrum.comm.packets.SubscribePacket
import info.nightscout.pump.medtrum.comm.packets.SynchronizePacket
@Module
abstract class MedtrumCommModule {
@ContributesAndroidInjector abstract fun contributesActivatePacket(): ActivatePacket
@ContributesAndroidInjector abstract fun contributesAuthorizePacket(): AuthorizePacket
@ContributesAndroidInjector abstract fun contributesCancelBolusPacket(): CancelBolusPacket
@ContributesAndroidInjector abstract fun contributesCancelTempBasalPacket(): CancelTempBasalPacket
@ContributesAndroidInjector abstract fun contributesClearPumpAlarmPacket(): ClearPumpAlarmPacket
@ContributesAndroidInjector abstract fun contributesGetDeviceTypePacket(): GetDeviceTypePacket
@ContributesAndroidInjector abstract fun contributesGetRecordPacket(): GetRecordPacket
@ContributesAndroidInjector abstract fun contributesGetTimePacket(): GetTimePacket
@ContributesAndroidInjector abstract fun contributesMedtrumPacket(): MedtrumPacket
@ContributesAndroidInjector abstract fun contributesNotificationPacket(): NotificationPacket
@ContributesAndroidInjector abstract fun contributesPollPatchPacket(): PollPatchPacket
@ContributesAndroidInjector abstract fun contributesPrimePacket(): PrimePacket
@ContributesAndroidInjector abstract fun contributesReadBolusStatePacket(): ReadBolusStatePacket
@ContributesAndroidInjector abstract fun contributesResumePumpPacket(): ResumePumpPacket
@ContributesAndroidInjector abstract fun contributesSetBasalProfilePacket(): SetBasalProfilePacket
@ContributesAndroidInjector abstract fun contributesSetBolusMotorPacket(): SetBolusMotorPacket
@ContributesAndroidInjector abstract fun contributesSetBolusPacket(): SetBolusPacket
@ContributesAndroidInjector abstract fun contributesSetPatchPacket(): SetPatchPacket
@ContributesAndroidInjector abstract fun contributesSetTempBasalPacket(): SetTempBasalPacket
@ContributesAndroidInjector abstract fun contributesSetTimePacket(): SetTimePacket
@ContributesAndroidInjector abstract fun contributesSetTimeZonePacket(): SetTimeZonePacket
@ContributesAndroidInjector abstract fun contributesStopPatchPacket(): StopPatchPacket
@ContributesAndroidInjector abstract fun contributesSubscribePacket(): SubscribePacket
@ContributesAndroidInjector abstract fun contributesSynchronizePacket(): SynchronizePacket
}

View file

@ -0,0 +1,12 @@
package info.nightscout.pump.medtrum.di
import javax.inject.Qualifier
import javax.inject.Scope
@Qualifier
annotation class MedtrumPluginQualifier
@MustBeDocumented
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope

View file

@ -0,0 +1,121 @@
package info.nightscout.pump.medtrum.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import dagger.multibindings.IntoMap
import info.nightscout.pump.medtrum.services.MedtrumService
import info.nightscout.pump.medtrum.ui.MedtrumActivateCompleteFragment
import info.nightscout.pump.medtrum.ui.MedtrumActivateFragment
import info.nightscout.pump.medtrum.ui.MedtrumAttachPatchFragment
import info.nightscout.pump.medtrum.ui.MedtrumDeactivatePatchFragment
import info.nightscout.pump.medtrum.ui.MedtrumDeactivationCompleteFragment
import info.nightscout.pump.medtrum.ui.MedtrumPreparePatchConnectFragment
import info.nightscout.pump.medtrum.ui.MedtrumPreparePatchFragment
import info.nightscout.pump.medtrum.ui.MedtrumPrimeCompleteFragment
import info.nightscout.pump.medtrum.ui.MedtrumPrimeFragment
import info.nightscout.pump.medtrum.ui.MedtrumPrimingFragment
import info.nightscout.pump.medtrum.ui.MedtrumStartDeactivationFragment
import info.nightscout.pump.medtrum.ui.MedtrumActivity
import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment
import info.nightscout.pump.medtrum.ui.MedtrumRetryActivationConnectFragment
import info.nightscout.pump.medtrum.ui.MedtrumRetryActivationFragment
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumOverviewViewModel
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelFactory
import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelKey
import javax.inject.Provider
@Module(includes = [MedtrumCommModule::class])
@Suppress("unused")
abstract class MedtrumModule {
companion object {
@Provides
@MedtrumPluginQualifier
fun providesViewModelFactory(@MedtrumPluginQualifier viewModels: MutableMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(viewModels)
}
}
// VIEW MODELS
@Binds
@IntoMap
@MedtrumPluginQualifier
@ViewModelKey(MedtrumOverviewViewModel::class)
internal abstract fun bindsMedtrumOverviewViewmodel(viewModel: MedtrumOverviewViewModel): ViewModel
@Binds
@IntoMap
@MedtrumPluginQualifier
@ViewModelKey(MedtrumViewModel::class)
internal abstract fun bindsMedtrumViewModel(viewModel: MedtrumViewModel): ViewModel
// FRAGMENTS
@ContributesAndroidInjector
abstract fun contributesMedtrumOverviewFragment(): MedtrumOverviewFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesStartDeactivationFragment(): MedtrumStartDeactivationFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesDeactivatePatchFragment(): MedtrumDeactivatePatchFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesDeactivationCompleteFragment(): MedtrumDeactivationCompleteFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesPreparePatchFragment(): MedtrumPreparePatchFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesPreparePatchConnectFragment(): MedtrumPreparePatchConnectFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesRetryActivationFragment(): MedtrumRetryActivationFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesRetryActivationConnectFragment(): MedtrumRetryActivationConnectFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesPrimeFragment(): MedtrumPrimeFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesPrimeCompleteFragment(): MedtrumPrimeCompleteFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesPrimingFragment(): MedtrumPrimingFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesAttachPatchFragment(): MedtrumAttachPatchFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesActivateFragment(): MedtrumActivateFragment
@FragmentScope
@ContributesAndroidInjector
internal abstract fun contributesActivateCompleteFragment(): MedtrumActivateCompleteFragment
// ACTIVITIES
@ContributesAndroidInjector
abstract fun contributesMedtrumActivity(): MedtrumActivity
// SERVICE
@ContributesAndroidInjector
abstract fun contributesMedtrumService(): MedtrumService
}

View file

@ -0,0 +1,74 @@
package info.nightscout.pump.medtrum.encryption
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toLong
class Crypt {
private val RIJNDEAL_S_BOX: IntArray = intArrayOf(99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22)
private val RIJNDEAL_INVERSE_S_BOX: IntArray = intArrayOf(82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37, 114, 248, 246, 100, 134, 104, 152, 22, 212, 164, 92, 204, 93, 101, 182, 146, 108, 112, 72, 80, 253, 237, 185, 218, 94, 21, 70, 87, 167, 141, 157, 132, 144, 216, 171, 0, 140, 188, 211, 10, 247, 228, 88, 5, 184, 179, 69, 6, 208, 44, 30, 143, 202, 63, 15, 2, 193, 175, 189, 3, 1, 19, 138, 107, 58, 145, 17, 65, 79, 103, 220, 234, 151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116, 34, 231, 173, 53, 133, 226, 249, 55, 232, 28, 117, 223, 110, 71, 241, 26, 113, 29, 41, 197, 137, 111, 183, 98, 14, 170, 24, 190, 27, 252, 86, 62, 75, 198, 210, 121, 32, 154, 219, 192, 254, 120, 205, 90, 244, 31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125)
val MED_CIPHER: Long = 1344751489
fun keyGen(input: Long): Long {
val key = randomGen(randomGen(MED_CIPHER xor input))
return simpleCrypt(key)
}
fun generateRandomToken(): Long {
val randomBytes = ByteArray(4)
val random = java.security.SecureRandom()
random.nextBytes(randomBytes)
return randomBytes.toLong()
}
private fun simpleCrypt(inputData: Long): Long {
var temp = inputData xor MED_CIPHER
for (i in 0 until 32) {
temp = changeByTable(rotatoLeft(temp, 32, 1), RIJNDEAL_S_BOX).toLong()
}
return temp
}
fun simpleDecrypt(inputData: Long): Long {
var temp = inputData
for (i in 0 until 32) {
temp = rotatoRight(changeByTable(temp, RIJNDEAL_INVERSE_S_BOX), 32, 1).toLong()
}
return temp xor MED_CIPHER
}
private fun randomGen(input: Long): Long {
val A = 16807
val Q = 127773
val R = 2836
val tmp1 = input / Q
var ret = (input - (tmp1 * Q)) * A - (tmp1 * R)
if (ret < 0) {
ret += 2147483647L
}
return ret
}
private fun changeByTable(inputData: Long, tableData: IntArray): Long {
val value = inputData.toByteArray(4)
val results = ByteArray(4)
for (i in value.indices) {
var byte = value[i].toInt()
if (byte < 0) {
byte += 256
}
results[i] = tableData[byte].toByte()
}
return results.toLong()
}
private fun rotatoLeft(x: Long, s: Int, n: Int): Long {
return (x shl n) or (x ushr (s - n))
}
private fun rotatoRight(x: Long, s: Int, n: Int): Int {
return (x ushr n or (x shl (s - n))).toInt()
}
}

View file

@ -0,0 +1,19 @@
package info.nightscout.pump.medtrum.extension
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
fun AppCompatActivity.replaceFragmentInActivity(fragment: Fragment, frameId: Int, addToBackStack: Boolean = false) {
supportFragmentManager.transact {
replace(frameId, fragment)
if (addToBackStack) addToBackStack(null)
}
}
private inline fun FragmentManager.transact(action: FragmentTransaction.() -> Unit) {
beginTransaction().apply {
action()
}.commit()
}

View file

@ -0,0 +1,8 @@
package info.nightscout.pump.medtrum.extension
fun Boolean.toByte(): Byte {
return if (this == true)
0x1
else
0x0
}

View file

@ -0,0 +1,41 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun ByteArray.toLong(): Long {
require(this.size <= 8) {
"Array size must be <= 8 for 'toLong' conversion operation"
}
var result = 0L
for (i in this.indices) {
val byte = this[i]
val shifted = (byte.toInt() and 0xFF).toLong() shl 8 * i
result = result or shifted
}
return result
}
fun ByteArray.toInt(): Int {
require(this.size <= 4) {
"Array size must be <= 4 for 'toInt' conversion operation"
}
var result = 0
for (i in this.indices) {
val byte = this[i]
val shifted = (byte.toInt() and 0xFF).toInt() shl 8 * i
result = result or shifted
}
return result
}
fun ByteArray.toFloat(): Float {
require(this.size == 4) {
"Array size must be == 4 for 'toFloat' conversion operation"
}
var asInt = 0
for (i in this.indices) {
val byte = this[i]
val shifted = (byte.toInt() and 0xFF) shl 8 * i
asInt = asInt or shifted
}
return Float.fromBits(asInt)
}

View file

@ -0,0 +1,10 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun Int.toByteArray(byteLength: Int): ByteArray {
val bytes = ByteArray(byteLength)
for (i in 0 until byteLength) {
bytes[i] = (this shr (i * 8) and 0xFF).toByte()
}
return bytes
}

View file

@ -0,0 +1,10 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun Long.toByteArray(byteLength: Int): ByteArray {
val bytes = ByteArray(byteLength)
for (i in 0 until byteLength) {
bytes[i] = (this shr (i * 8) and 0xFF).toByte()
}
return bytes
}

View file

@ -0,0 +1,34 @@
package info.nightscout.pump.medtrum.extension
import android.view.View
fun View?.visible() = this?.run { visibility = View.VISIBLE }
fun View?.visible(vararg views: View?) {
visible()
for (view in views)
view.visible()
}
fun View?.invisible() = this?.run { visibility = View.INVISIBLE }
fun View?.invisible(vararg views: View?) {
invisible()
for (view in views)
view.invisible()
}
fun View?.gone() = this?.run { visibility = View.GONE }
fun View?.gone(vararg views: View?) {
gone()
for (view in views)
view.gone()
}
fun View?.setVisibleOrGone(visibleOrGone: Boolean, vararg views: View?) {
for (view in views)
if (visibleOrGone) view.visible() else view.gone()
}
fun View?.setVisibleOrGone(visibleOrGone: Boolean) = setVisibleOrGone(visibleOrGone, this)

View file

@ -0,0 +1,475 @@
package info.nightscout.pump.medtrum.services
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.bluetooth.le.ScanFilter
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
import androidx.core.app.ActivityCompat
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.comm.WriteCommandPackets
import info.nightscout.pump.medtrum.comm.ManufacturerData
import info.nightscout.pump.medtrum.comm.ReadDataPacket
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import java.util.UUID
import java.util.Arrays
import javax.inject.Inject
import javax.inject.Singleton
interface BLECommCallback {
fun onBLEConnected()
fun onBLEDisconnected()
fun onNotification(notification: ByteArray)
fun onIndication(indication: ByteArray)
fun onSendMessageError(reason: String)
}
@Singleton
class BLEComm @Inject internal constructor(
private val aapsLogger: AAPSLogger,
private val context: Context
) {
companion object {
private const val WRITE_DELAY_MILLIS: Long = 10
private const val SERVICE_UUID = "669A9001-0008-968F-E311-6050405558B3"
private const val READ_UUID = "669a9120-0008-968f-e311-6050405558b3"
private const val WRITE_UUID = "669a9101-0008-968f-e311-6050405558b3"
private const val CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb"
private const val NEEDS_ENABLE_NOTIFICATION = 0x10
private const val NEEDS_ENABLE_INDICATION = 0x20
private const val NEEDS_ENABLE = 0x30
private const val MANUFACTURER_ID = 18305
}
private val handler =
Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper)
private val mBluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
private var mBluetoothGatt: BluetoothGatt? = null
private var isConnected = false // Only to track internal ble state
private var isConnecting = false // Only to track internal ble state
private var uartWrite: BluetoothGattCharacteristic? = null
private var uartRead: BluetoothGattCharacteristic? = null
// Read and write buffers
private var mWritePackets: WriteCommandPackets? = null
private var mWriteSequenceNumber: Int = 0
private var mReadPacket: ReadDataPacket? = null
private val readLock = Any()
private var mDeviceSN: Long = 0
private var mCallback: BLECommCallback? = null
private var mDevice: BluetoothDevice? = null
fun setCallback(callback: BLECommCallback?) {
this.mCallback = callback
}
/** Connect flow: 1. Start scanning for our device (SN entered in settings) */
@SuppressLint("MissingPermission")
@Synchronized
fun startScan(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
) {
ToastUtils.errorToast(context, context.getString(info.nightscout.core.ui.R.string.need_connect_permission))
aapsLogger.error(LTag.PUMPBTCOMM, "missing permissions")
return false
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Start scan!!")
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filters = mutableListOf<ScanFilter>()
// Find our Medtrum Device!
filters.add(
ScanFilter.Builder().setDeviceName("MT").build()
)
mBluetoothAdapter?.bluetoothLeScanner?.startScan(filters, settings, mScanCallback)
return true
}
@SuppressLint("MissingPermission")
@Synchronized
fun stopScan() {
mBluetoothAdapter?.bluetoothLeScanner?.stopScan(mScanCallback)
}
@Synchronized
fun connect(from: String, deviceSN: Long): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
) {
ToastUtils.errorToast(context, context.getString(info.nightscout.core.ui.R.string.need_connect_permission))
aapsLogger.error(LTag.PUMPBTCOMM, "missing permission: $from")
return false
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Initializing BLEComm.")
if (mBluetoothAdapter == null) {
aapsLogger.error("Unable to obtain a BluetoothAdapter.")
return false
}
isConnected = false
isConnecting = true
mWritePackets = null
mReadPacket = null
if (mDevice != null && mDeviceSN == deviceSN) {
// Skip scanning and directly connect to gatt
aapsLogger.debug(LTag.PUMPBTCOMM, "Skipping scan and directly connecting to gatt")
connectGatt(mDevice!!)
} else {
// Scan for device
aapsLogger.debug(LTag.PUMPBTCOMM, "Scanning for device")
mDeviceSN = deviceSN
startScan()
}
return true
}
/** Connect flow: 2. When device is found this is called by onScanResult() */
@SuppressLint("MissingPermission")
@Synchronized
private fun connectGatt(device: BluetoothDevice) {
// Reset sequence counter
mWriteSequenceNumber = 0
if (mBluetoothGatt == null) {
mBluetoothGatt = device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE)
} else {
// Already connected?, this should not happen force disconnect
aapsLogger.error(LTag.PUMPBTCOMM, "connectGatt, mBluetoothGatt is not null")
disconnect("connectGatt, mBluetoothGatt is not null")
}
}
@SuppressLint("MissingPermission")
@Synchronized
fun disconnect(from: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
) {
aapsLogger.error(LTag.PUMPBTCOMM, "missing permission: $from")
return
}
aapsLogger.debug(LTag.PUMPBTCOMM, "disconnect from: $from")
if (isConnecting) {
isConnecting = false
stopScan()
SystemClock.sleep(100)
}
if (isConnected) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Connected, disconnecting")
mBluetoothGatt?.disconnect()
} else {
aapsLogger.debug(LTag.PUMPBTCOMM, "Not connected, closing gatt")
close()
isConnected = false
mCallback?.onBLEDisconnected()
}
}
@SuppressLint("MissingPermission")
@Synchronized fun close() {
aapsLogger.debug(LTag.PUMPBTCOMM, "BluetoothAdapter close")
mBluetoothGatt?.close()
SystemClock.sleep(100)
mBluetoothGatt = null
}
/** Scan callback */
private val mScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnScanResult!" + result)
super.onScanResult(callbackType, result)
val manufacturerData =
result.scanRecord?.getManufacturerSpecificData(MANUFACTURER_ID)
?.let { ManufacturerData(it) }
aapsLogger.debug(LTag.PUMPBTCOMM, "Found deviceSN: " + manufacturerData?.getDeviceSN())
if (manufacturerData?.getDeviceSN() == mDeviceSN) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Found our device! deviceSN: " + manufacturerData.getDeviceSN())
stopScan()
mDevice = result.device
connectGatt(result.device)
}
}
override fun onScanFailed(errorCode: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Scan FAILED!")
}
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
private val mGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
onConnectionStateChangeSynchronized(gatt, status, newState) // call it synchronized
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onServicesDiscovered")
if (status == BluetoothGatt.GATT_SUCCESS) {
findCharacteristic()
}
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicRead data: " + characteristic.value.contentToString() + " UUID: " + characteristic.getUuid().toString() + " status: " + status)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged data: " + characteristic.value.contentToString() + " UUID: " + characteristic.getUuid().toString())
val value = characteristic.getValue()
if (characteristic.getUuid() == UUID.fromString(READ_UUID)) {
mCallback?.onNotification(value)
} else if (characteristic.getUuid() == UUID.fromString(WRITE_UUID)) {
synchronized(readLock) {
if (mReadPacket == null) {
mReadPacket = ReadDataPacket(value)
} else {
mReadPacket?.addData(value)
}
if (mReadPacket?.allDataReceived() == true) {
mReadPacket?.getData()?.let { mCallback?.onIndication(it) }
mReadPacket = null
}
}
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite status = " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
// Check if we need to finish our command!
mWritePackets?.let {
synchronized(it) {
val value: ByteArray? = mWritePackets?.getNextPacket()
if (value != null) {
writeCharacteristic(uartWriteBTGattChar, value)
}
}
}
} else {
mCallback?.onSendMessageError("onCharacteristicWrite failure")
}
}
override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status)
aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
readDescriptor(descriptor)
}
}
/** Connect flow: 5. Notifications enabled read descriptor to verify and start auth process*/
override fun onDescriptorRead(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorRead(gatt, descriptor, status)
aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorRead status: " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
checkDescriptor(descriptor)
}
}
}
@SuppressLint("MissingPermission")
@Synchronized
private fun readDescriptor(descriptor: BluetoothGattDescriptor?) {
aapsLogger.debug(LTag.PUMPBTCOMM, "readDescriptor")
if (mBluetoothAdapter == null || mBluetoothGatt == null || descriptor == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
mBluetoothGatt?.readDescriptor(descriptor)
}
@Suppress("DEPRECATION")
private fun checkDescriptor(descriptor: BluetoothGattDescriptor) {
aapsLogger.debug(LTag.PUMPBTCOMM, "checkDescriptor")
val service = getGattService()
if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
if (descriptor.value.toInt() > 0) {
var notificationEnabled = true
val characteristics = service.characteristics
for (j in 0 until characteristics.size) {
val configDescriptor =
characteristics[j].getDescriptor(UUID.fromString(CONFIG_UUID))
if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) {
notificationEnabled = false
}
}
if (notificationEnabled) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!")
/** Connect flow: 6. Connected */
mCallback?.onBLEConnected()
}
}
}
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
@Synchronized
private fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic?, enabled: Boolean) {
aapsLogger.debug(LTag.PUMPBTCOMM, "setCharacteristicNotification")
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
mBluetoothGatt?.setCharacteristicNotification(characteristic, enabled)
characteristic?.getDescriptor(UUID.fromString(CONFIG_UUID))?.let {
if (characteristic.properties and NEEDS_ENABLE_NOTIFICATION > 0) {
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
mBluetoothGatt?.writeDescriptor(it)
} else if (characteristic.properties and NEEDS_ENABLE_INDICATION > 0) {
it.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
mBluetoothGatt?.writeDescriptor(it)
} else {
}
}
}
/** Connect flow: 3. When we are connected discover services*/
@SuppressLint("MissingPermission")
@Synchronized
private fun onConnectionStateChangeSynchronized(gatt: BluetoothGatt, status: Int, newState: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onConnectionStateChange newState: " + newState + " status: " + status)
if (newState == BluetoothProfile.STATE_CONNECTED) {
isConnected = true
isConnecting = false
mBluetoothGatt?.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
if (isConnecting) {
// When we are disconnected during connecting, we reset the device address to force a new scan
aapsLogger.warn(LTag.PUMPBTCOMM, "Disconnected while connecting! Reset device address")
mDevice = null
// Wait a bit before retrying
SystemClock.sleep(2000)
}
close()
isConnected = false
isConnecting = false
mCallback?.onBLEDisconnected()
aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected
}
}
@Synchronized
fun sendMessage(message: ByteArray) {
aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message))
if (mWritePackets?.allPacketsConsumed() == false) {
aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage not all packets consumed!! unable to sent message!")
return
}
mWritePackets = WriteCommandPackets(message, mWriteSequenceNumber)
mWriteSequenceNumber = (mWriteSequenceNumber + 1) % 256
val value: ByteArray? = mWritePackets?.getNextPacket()
if (value != null) {
writeCharacteristic(uartWriteBTGattChar, value)
} else {
aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage error in writePacket!")
mCallback?.onSendMessageError("error in writePacket!")
}
}
private fun getGattService(): BluetoothGattService? {
aapsLogger.debug(LTag.PUMPBTCOMM, "getGattService")
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return null
}
return mBluetoothGatt?.getService(UUID.fromString(SERVICE_UUID))
}
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
@Synchronized
private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) {
handler.postDelayed({
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
} else {
characteristic.value = data
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
aapsLogger.debug(LTag.PUMPBTCOMM, "writeCharacteristic: ${Arrays.toString(data)}")
val success = mBluetoothGatt?.writeCharacteristic(characteristic)
if (success != true) {
mCallback?.onSendMessageError("Failed to write characteristic")
}
}
}, WRITE_DELAY_MILLIS)
}
private val uartWriteBTGattChar: BluetoothGattCharacteristic
get() = uartWrite
?: BluetoothGattCharacteristic(UUID.fromString(WRITE_UUID), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, 0).also { uartWrite = it }
/** Connect flow: 4. When services are discovered find characteristics and set notifications*/
private fun findCharacteristic() {
val gattService = getGattService() ?: return
var uuid: String
val gattCharacteristics = gattService.characteristics
for (i in 0..gattCharacteristics.size - 1) {
val gattCharacteristic = gattCharacteristics.get(i)
// Check whether read or write properties is set, the pump needs us to enable notifications on all characteristics that have these properties
if (gattCharacteristic.properties and NEEDS_ENABLE > 0) {
handler.postDelayed({
uuid = gattCharacteristic.uuid.toString()
setCharacteristicNotification(gattCharacteristic, true)
if (READ_UUID == uuid) {
uartRead = gattCharacteristic
}
if (WRITE_UUID == uuid) {
uartWrite = gattCharacteristic
}
}, (i * 600).toLong())
}
}
}
}

View file

@ -0,0 +1,980 @@
package info.nightscout.pump.medtrum.services
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.os.SystemClock
import dagger.android.DaggerService
import dagger.android.HasAndroidInjector
import info.nightscout.core.utils.fabric.FabricPrivacy
import info.nightscout.interfaces.constraints.Constraints
import info.nightscout.interfaces.notifications.Notification
import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.profile.Profile
import info.nightscout.interfaces.profile.ProfileFunction
import info.nightscout.interfaces.pump.DetailedBolusInfo
import info.nightscout.interfaces.pump.DetailedBolusInfoStorage
import info.nightscout.interfaces.pump.BolusProgressData
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.interfaces.pump.defs.PumpType
import info.nightscout.interfaces.queue.Callback
import info.nightscout.interfaces.queue.CommandQueue
import info.nightscout.interfaces.ui.UiInteraction
import info.nightscout.pump.medtrum.MedtrumPlugin
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.ConnectionState
import info.nightscout.pump.medtrum.comm.enums.AlarmState
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.comm.packets.*
import info.nightscout.pump.medtrum.util.MedtrumTimeUtil
import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventAppExit
import info.nightscout.rx.events.EventDismissNotification
import info.nightscout.rx.events.EventOverviewBolusProgress
import info.nightscout.rx.events.EventPreferenceChange
import info.nightscout.rx.events.EventPumpStatusChanged
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import info.nightscout.shared.utils.DateUtil
import info.nightscout.shared.utils.T
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.round
class MedtrumService : DaggerService(), BLECommCallback {
@Inject lateinit var injector: HasAndroidInjector
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var aapsSchedulers: AapsSchedulers
@Inject lateinit var rxBus: RxBus
@Inject lateinit var sp: SP
@Inject lateinit var rh: ResourceHelper
@Inject lateinit var profileFunction: ProfileFunction
@Inject lateinit var commandQueue: CommandQueue
@Inject lateinit var context: Context
@Inject lateinit var medtrumPlugin: MedtrumPlugin
@Inject lateinit var medtrumPump: MedtrumPump
@Inject lateinit var activePlugin: ActivePlugin
@Inject lateinit var constraintChecker: Constraints
@Inject lateinit var uiInteraction: UiInteraction
@Inject lateinit var bleComm: BLEComm
@Inject lateinit var fabricPrivacy: FabricPrivacy
@Inject lateinit var pumpSync: PumpSync
@Inject lateinit var detailedBolusInfoStorage: DetailedBolusInfoStorage
@Inject lateinit var dateUtil: DateUtil
companion object {
private const val COMMAND_DEFAULT_TIMEOUT_SEC: Long = 60
private const val COMMAND_SYNC_TIMEOUT_SEC: Long = 120
private const val COMMAND_CONNECTING_TIMEOUT_SEC: Long = 30
private const val ALARM_HOURLY_MAX_CLEAR_CODE = 4
private const val ALARM_DAILY_MAX_CLEAR_CODE = 5
}
val timeUtil = MedtrumTimeUtil()
private val disposable = CompositeDisposable()
private val mBinder: IBinder = LocalBinder()
private var currentState: State = IdleState()
private var mPacket: MedtrumPacket? = null
private val scope = CoroutineScope(Dispatchers.Default)
val isConnected: Boolean
get() = medtrumPump.connectionState == ConnectionState.CONNECTED
val isConnecting: Boolean
get() = medtrumPump.connectionState == ConnectionState.CONNECTING
override fun onCreate() {
super.onCreate()
bleComm.setCallback(this)
disposable += rxBus
.toObservable(EventAppExit::class.java)
.observeOn(aapsSchedulers.io)
.subscribe({ stopSelf() }, fabricPrivacy::logException)
disposable += rxBus
.toObservable(EventPreferenceChange::class.java)
.observeOn(aapsSchedulers.io)
.subscribe({ event ->
if (event.isChanged(rh.gs(R.string.key_sn_input))) {
aapsLogger.debug(LTag.PUMPCOMM, "Serial number changed, reporting new pump!")
pumpSync.connectNewPump()
medtrumPump.loadUserSettingsFromSP()
medtrumPump.setFakeTBRIfNeeded()
}
if (event.isChanged(rh.gs(R.string.key_alarm_setting))
|| event.isChanged(rh.gs(R.string.key_patch_expiration))
|| event.isChanged(rh.gs(R.string.key_hourly_max_insulin))
|| event.isChanged(rh.gs(R.string.key_daily_max_insulin))
) {
medtrumPump.loadUserSettingsFromSP()
commandQueue.setUserOptions(object : Callback() {
override fun run() {
if (medtrumPlugin.isInitialized() && this.result.success == false) {
uiInteraction.addNotification(
Notification.PUMP_SETTINGS_FAILED,
rh.gs(R.string.pump_setting_failed),
Notification.NORMAL,
)
}
}
})
}
}, fabricPrivacy::logException)
scope.launch {
medtrumPump.pumpStateFlow.collect { state ->
handlePumpStateUpdate(state)
}
}
scope.launch {
medtrumPump.connectionStateFlow.collect { state ->
if (medtrumPlugin.isInitialized()) {
when (state) {
ConnectionState.CONNECTED -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED))
ConnectionState.DISCONNECTED -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED))
ConnectionState.CONNECTING -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTING))
ConnectionState.DISCONNECTING -> rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTING))
}
}
}
}
medtrumPump.loadUserSettingsFromSP()
}
override fun onDestroy() {
super.onDestroy()
disposable.clear()
scope.cancel()
}
fun connect(from: String): Boolean {
aapsLogger.debug(LTag.PUMP, "connect: called from: $from")
if (currentState is IdleState) {
medtrumPump.connectionState = ConnectionState.CONNECTING
return bleComm.connect(from, medtrumPump.pumpSN)
} else {
aapsLogger.error(LTag.PUMPCOMM, "Connect attempt when in non Idle state from: $from")
return false
}
}
fun startPrime(): Boolean {
return sendPacketAndGetResponse(PrimePacket(injector))
}
fun startActivate(): Boolean {
val profile = profileFunction.getProfile()?.let { medtrumPump.buildMedtrumProfileArray(it) }
val packet = profile?.let { ActivatePacket(injector, it) }
return packet?.let { sendPacketAndGetResponse(it) } == true
}
fun deactivatePatch(): Boolean {
var result = true
if (medtrumPump.tempBasalInProgress) {
result = sendPacketAndGetResponse(CancelTempBasalPacket(injector))
}
// Make sure we have all events of this patch if possible
loadEvents()
if (result) result = sendPacketAndGetResponse(StopPatchPacket(injector))
return result
}
fun stopConnecting() {
bleComm.disconnect("stopConnecting")
}
fun disconnect(from: String) {
medtrumPump.connectionState = ConnectionState.DISCONNECTING
bleComm.disconnect(from)
}
fun readPumpStatus() {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingpumpstatus)))
updateTimeIfNeeded(false)
loadEvents()
}
fun timeUpdateNotification(updateSuccess: Boolean) {
if (updateSuccess) {
aapsLogger.debug(LTag.PUMPCOMM, "Pump time updated")
uiInteraction.addNotification(
Notification.INSIGHT_DATE_TIME_UPDATED, // :---)
rh.gs(info.nightscout.core.ui.R.string.pump_time_updated),
Notification.INFO,
)
} else {
aapsLogger.error(LTag.PUMPCOMM, "Failed to update pump time")
uiInteraction.addNotification(
Notification.PUMP_TIMEZONE_UPDATE_FAILED,
rh.gs(R.string.pump_time_update_failed),
Notification.URGENT,
)
}
}
fun updateTimeIfNeeded(needLoadHistory: Boolean = true): Boolean {
// Note we only check timeZone here, time is updated each connection attempt if needed, because the pump requires it to be checked
// But we dont check timeZone each time, therefore we do it here (if needed)
var result = true
if (medtrumPump.pumpTimeZoneOffset != dateUtil.getTimeZoneOffsetMinutes(dateUtil.now())) {
result = sendPacketAndGetResponse(SetTimePacket(injector))
if (result) result = sendPacketAndGetResponse(SetTimeZonePacket(injector))
timeUpdateNotification(result)
}
// Do this here, because TBR can be cancelled due to time change by connect flow
if (needLoadHistory) {
if (result) result = loadEvents()
}
if (result) medtrumPump.needCheckTimeUpdate = false
return result
}
fun loadEvents(): Boolean {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingpumpstatus)))
// Sync records
val result = syncRecords()
if (result) {
aapsLogger.debug(LTag.PUMPCOMM, "Events loaded")
medtrumPump.lastConnection = System.currentTimeMillis()
} else {
aapsLogger.error(LTag.PUMPCOMM, "Failed to load events")
}
return result
}
fun clearAlarms(): Boolean {
var result = true
if (medtrumPump.pumpState in listOf(
MedtrumPumpState.PAUSED,
MedtrumPumpState.HMAX_SUSPENDED,
MedtrumPumpState.DMAX_SUSPENDED
)
) {
when (medtrumPump.pumpState) {
MedtrumPumpState.HMAX_SUSPENDED -> {
result = sendPacketAndGetResponse(ClearPumpAlarmPacket(injector, ALARM_HOURLY_MAX_CLEAR_CODE))
}
MedtrumPumpState.DMAX_SUSPENDED -> {
result = sendPacketAndGetResponse(ClearPumpAlarmPacket(injector, ALARM_DAILY_MAX_CLEAR_CODE))
}
else -> {
// Nothing to reset
}
}
// Resume suspended pump
if (result) result = sendPacketAndGetResponse(ResumePumpPacket(injector))
}
return result
}
fun setUserSettings(): Boolean {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.settingpumpsettings)))
return sendPacketAndGetResponse(SetPatchPacket(injector))
}
fun setBolus(detailedBolusInfo: DetailedBolusInfo, t: EventOverviewBolusProgress.Treatment): Boolean {
if (!isConnected) return false
if (BolusProgressData.stopPressed) return false
val insulin = detailedBolusInfo.insulin
val bolusStart = System.currentTimeMillis()
medtrumPump.bolusDone = false
medtrumPump.bolusingTreatment = t
medtrumPump.bolusAmountToBeDelivered = insulin
medtrumPump.bolusStopped = false
medtrumPump.bolusProgressLastTimeStamp = bolusStart
if (insulin > 0 && !medtrumPump.bolusStopped) {
val result = sendPacketAndGetResponse(SetBolusPacket(injector, insulin))
if (result == false) {
aapsLogger.error(LTag.PUMPCOMM, "Failed to set bolus")
commandQueue.loadEvents(null) // make sure if anything is delivered (which is highly unlikely at this point) we get it
t.insulin = 0.0
return false
}
} else {
aapsLogger.debug(LTag.PUMPCOMM, "Bolus not set, insulin: $insulin, bolusStopped: ${medtrumPump.bolusStopped}")
t.insulin = 0.0
return false
}
detailedBolusInfo.timestamp = bolusStart // Make sure the timestamp is set to the start of the bolus
detailedBolusInfoStorage.add(detailedBolusInfo) // will be picked up on reading history
// Sync the initial bolus
val newRecord = pumpSync.addBolusWithTempId(
timestamp = detailedBolusInfo.timestamp,
amount = detailedBolusInfo.insulin,
temporaryId = detailedBolusInfo.timestamp,
type = detailedBolusInfo.bolusType,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
if (newRecord) {
aapsLogger.debug(
LTag.PUMPCOMM,
"set bolus: **NEW** EVENT BOLUS (tempId) ${dateUtil.dateAndTimeString(detailedBolusInfo.timestamp)} (${detailedBolusInfo.timestamp}) Bolus: ${detailedBolusInfo.insulin}U "
)
} else {
aapsLogger.error(LTag.PUMPCOMM, "Bolus with tempId ${detailedBolusInfo.timestamp} already exists")
}
val bolusingEvent = EventOverviewBolusProgress
var communicationLost = false
while (!medtrumPump.bolusStopped && !medtrumPump.bolusDone && !communicationLost) {
SystemClock.sleep(100)
if (System.currentTimeMillis() - medtrumPump.bolusProgressLastTimeStamp > T.secs(20).msecs()) {
communicationLost = true
aapsLogger.warn(LTag.PUMPCOMM, "Communication stopped")
disconnect("Communication stopped")
} else {
bolusingEvent.t = medtrumPump.bolusingTreatment
bolusingEvent.status = rh.gs(info.nightscout.pump.common.R.string.bolus_delivered_so_far, medtrumPump.bolusingTreatment?.insulin, medtrumPump.bolusAmountToBeDelivered)
bolusingEvent.percent = round((medtrumPump.bolusingTreatment?.insulin?.div(medtrumPump.bolusAmountToBeDelivered) ?: 0.0) * 100).toInt() - 1
rxBus.send(bolusingEvent)
}
}
bolusingEvent.percent = 99
val bolusDurationInMSec = (insulin * 60 * 1000)
val expectedEnd = bolusStart + bolusDurationInMSec + 2000
while (System.currentTimeMillis() < expectedEnd && !medtrumPump.bolusDone) {
SystemClock.sleep(1000)
}
bolusingEvent.t = medtrumPump.bolusingTreatment
medtrumPump.bolusingTreatment = null
if (medtrumPump.bolusStopped && t.insulin == 0.0) {
// In this case we don't get a bolus end event, so need to remove all the stuff added previously
val syncOk = pumpSync.syncBolusWithTempId(
timestamp = bolusStart,
amount = 0.0,
temporaryId = bolusStart,
type = detailedBolusInfo.bolusType,
pumpId = bolusStart,
pumpType = medtrumPump.pumpType(),
pumpSerial = medtrumPump.pumpSN.toString(radix = 16)
)
aapsLogger.debug(
LTag.PUMPCOMM,
"set bolus: **SYNC** EVENT BOLUS (tempId) ${dateUtil.dateAndTimeString(detailedBolusInfo.timestamp)} (${bolusStart}) Bolus: ${0.0}U SyncOK: $syncOk"
)
// remove detailed bolus info
detailedBolusInfoStorage.findDetailedBolusInfo(bolusStart, detailedBolusInfo.insulin)
}
// Do not call update status directly, reconnection may be needed
commandQueue.loadEvents(object : Callback() {
override fun run() {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingbolusstatus)))
bolusingEvent.percent = 100
}
})
return true
}
fun stopBolus() {
aapsLogger.debug(LTag.PUMPCOMM, "bolusStop >>>>> @ " + if (medtrumPump.bolusingTreatment == null) "" else medtrumPump.bolusingTreatment?.insulin)
if (isConnected) {
var success = sendPacketAndGetResponse(CancelBolusPacket(injector))
var timeout = System.currentTimeMillis() + T.secs(30).msecs()
while (!success && System.currentTimeMillis() < timeout) {
success = sendPacketAndGetResponse(CancelBolusPacket(injector))
SystemClock.sleep(200)
}
aapsLogger.debug(LTag.PUMPCOMM, "bolusStop success: $success")
medtrumPump.bolusStopped = true
} else {
medtrumPump.bolusStopped = true
}
}
fun setTempBasal(absoluteRate: Double, durationInMinutes: Int): Boolean {
var result = true
if (medtrumPump.tempBasalInProgress) {
result = sendPacketAndGetResponse(CancelTempBasalPacket(injector))
}
if (result) result = sendPacketAndGetResponse(SetTempBasalPacket(injector, absoluteRate, durationInMinutes))
// Get history records, this will update the prevoius basals
// Do not call update status directly, reconnection may be needed
commandQueue.loadEvents(object : Callback() {
override fun run() {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingtempbasalstatus)))
}
})
return result
}
fun cancelTempBasal(): Boolean {
var result = sendPacketAndGetResponse(CancelTempBasalPacket(injector))
// Get history records, this will update the prevoius basals
// Do not call update status directly, reconnection may be needed
commandQueue.loadEvents(object : Callback() {
override fun run() {
rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingtempbasalstatus)))
}
})
return result
}
fun updateBasalsInPump(profile: Profile): Boolean {
var result = true
// Update basal affects the TBR records (the pump will cancel the TBR, set our basal profile, and resume the TBR in a new record)
// Cancel any TBR in progress
if (medtrumPump.tempBasalInProgress) {
result = sendPacketAndGetResponse(CancelTempBasalPacket(injector))
}
val packet = medtrumPump.buildMedtrumProfileArray(profile)?.let { SetBasalProfilePacket(injector, it) }
if (result) result = packet?.let { sendPacketAndGetResponse(it) } == true
// Get history records, this will update the pump state and add changes in TBR to AAPS history
commandQueue.loadEvents(null)
return result
}
/** This gets the history records from the pump */
private fun syncRecords(): Boolean {
aapsLogger.debug(LTag.PUMP, "syncRecords: called!, syncedSequenceNumber: ${medtrumPump.syncedSequenceNumber}, currentSequenceNumber: ${medtrumPump.currentSequenceNumber}")
var result = true
// Note: medtrum app fetches all records when they sync?
if (medtrumPump.syncedSequenceNumber < medtrumPump.currentSequenceNumber) {
for (sequence in (medtrumPump.syncedSequenceNumber + 1)..medtrumPump.currentSequenceNumber) {
result = sendPacketAndGetResponse(GetRecordPacket(injector, sequence), COMMAND_SYNC_TIMEOUT_SEC)
SystemClock.sleep(100)
if (result == false) break
}
}
return result
}
private fun handlePumpStateUpdate(state: MedtrumPumpState) {
// Map the pump state to an alarm state and add it to the active alarms
val alarmState = when (state) {
MedtrumPumpState.NONE -> AlarmState.NONE
MedtrumPumpState.LOWBG_SUSPENDED -> AlarmState.LOWBG_SUSPENDED
MedtrumPumpState.LOWBG_SUSPENDED2 -> AlarmState.LOWBG_SUSPENDED2
MedtrumPumpState.AUTO_SUSPENDED -> AlarmState.AUTO_SUSPENDED
MedtrumPumpState.HMAX_SUSPENDED -> AlarmState.HMAX_SUSPENDED
MedtrumPumpState.DMAX_SUSPENDED -> AlarmState.DMAX_SUSPENDED
MedtrumPumpState.SUSPENDED -> AlarmState.SUSPENDED
MedtrumPumpState.PAUSED -> AlarmState.PAUSED
MedtrumPumpState.OCCLUSION -> AlarmState.OCCLUSION
MedtrumPumpState.EXPIRED -> AlarmState.EXPIRED
MedtrumPumpState.RESERVOIR_EMPTY -> AlarmState.RESERVOIR_EMPTY
MedtrumPumpState.PATCH_FAULT -> AlarmState.PATCH_FAULT
MedtrumPumpState.PATCH_FAULT2 -> AlarmState.PATCH_FAULT2
MedtrumPumpState.BASE_FAULT -> AlarmState.BASE_FAULT
MedtrumPumpState.BATTERY_OUT -> AlarmState.BATTERY_OUT
MedtrumPumpState.NO_CALIBRATION -> AlarmState.NO_CALIBRATION
else -> null
}
if (alarmState != null && alarmState != AlarmState.NONE) {
medtrumPump.addAlarm(alarmState)
pumpSync.insertAnnouncement(
medtrumPump.alarmStateToString(alarmState),
null,
medtrumPump.pumpType(),
medtrumPump.pumpSN.toString(radix = 16)
)
}
// Map the pump state to a notification
when (state) {
MedtrumPumpState.NONE,
MedtrumPumpState.STOPPED -> {
rxBus.send(EventDismissNotification(Notification.PUMP_ERROR))
rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED))
uiInteraction.addNotification(
Notification.PATCH_NOT_ACTIVE,
rh.gs(R.string.patch_not_active),
Notification.URGENT,
)
medtrumPump.setFakeTBRIfNeeded()
medtrumPump.clearAlarmState()
}
MedtrumPumpState.IDLE,
MedtrumPumpState.FILLED,
MedtrumPumpState.PRIMING,
MedtrumPumpState.PRIMED,
MedtrumPumpState.EJECTING,
MedtrumPumpState.EJECTED -> {
rxBus.send(EventDismissNotification(Notification.PUMP_ERROR))
rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED))
medtrumPump.setFakeTBRIfNeeded()
medtrumPump.clearAlarmState()
}
MedtrumPumpState.ACTIVE,
MedtrumPumpState.ACTIVE_ALT -> {
rxBus.send(EventDismissNotification(Notification.PATCH_NOT_ACTIVE))
rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED))
medtrumPump.clearAlarmState()
}
MedtrumPumpState.LOWBG_SUSPENDED,
MedtrumPumpState.LOWBG_SUSPENDED2,
MedtrumPumpState.AUTO_SUSPENDED,
MedtrumPumpState.SUSPENDED,
MedtrumPumpState.PAUSED -> {
uiInteraction.addNotification(
Notification.PUMP_SUSPENDED,
rh.gs(R.string.pump_is_suspended),
Notification.NORMAL,
)
// Pump will report proper TBR for this
}
MedtrumPumpState.HMAX_SUSPENDED -> {
uiInteraction.addNotification(
Notification.PUMP_SUSPENDED,
rh.gs(R.string.pump_is_suspended_hour_max),
Notification.NORMAL,
)
// Pump will report proper TBR for this
}
MedtrumPumpState.DMAX_SUSPENDED -> {
uiInteraction.addNotification(
Notification.PUMP_SUSPENDED,
rh.gs(R.string.pump_is_suspended_day_max),
Notification.NORMAL,
)
// Pump will report proper TBR for this
}
MedtrumPumpState.OCCLUSION,
MedtrumPumpState.EXPIRED,
MedtrumPumpState.RESERVOIR_EMPTY,
MedtrumPumpState.PATCH_FAULT,
MedtrumPumpState.PATCH_FAULT2,
MedtrumPumpState.BASE_FAULT,
MedtrumPumpState.BATTERY_OUT,
MedtrumPumpState.NO_CALIBRATION -> {
rxBus.send(EventDismissNotification(Notification.PATCH_NOT_ACTIVE))
rxBus.send(EventDismissNotification(Notification.PUMP_SUSPENDED))
// Pump suspended due to error, show error!
uiInteraction.addNotificationWithSound(
Notification.PUMP_ERROR,
rh.gs(R.string.pump_error, alarmState?.let { medtrumPump.alarmStateToString(it) }),
Notification.URGENT,
info.nightscout.core.ui.R.raw.alarm
)
medtrumPump.setFakeTBRIfNeeded()
}
}
}
/** BLECommCallbacks */
override fun onBLEConnected() {
aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onBLEConnected")
currentState.onConnected()
}
override fun onBLEDisconnected() {
aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onBLEDisconnected")
currentState.onDisconnected()
}
override fun onNotification(notification: ByteArray) {
aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onNotification" + notification.contentToString())
NotificationPacket(injector).handleNotification(notification)
}
override fun onIndication(indication: ByteArray) {
aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onIndication" + indication.contentToString())
currentState.onIndication(indication)
}
override fun onSendMessageError(reason: String) {
aapsLogger.debug(LTag.PUMPCOMM, "<<<<< error during send message $reason")
currentState.onSendMessageError(reason)
}
/** Service stuff */
inner class LocalBinder : Binder() {
val serviceInstance: MedtrumService
get() = this@MedtrumService
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
/**
* States are used to keep track of the communication and to guide the flow
*/
private fun toState(nextState: State) {
currentState = nextState
currentState.onEnter()
}
private fun sendPacketAndGetResponse(packet: MedtrumPacket, timeout: Long = COMMAND_DEFAULT_TIMEOUT_SEC): Boolean {
var result = false
if (currentState is ReadyState) {
toState(CommandState())
mPacket = packet
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
result = currentState.waitForResponse(timeout)
} else {
aapsLogger.error(LTag.PUMPCOMM, "Send packet attempt when in non Ready state")
}
return result
}
// State class
private abstract inner class State {
protected var responseHandled = false
protected var responseSuccess = false
protected var sendRetryCounter = 0
open fun onEnter() {}
open fun onIndication(data: ByteArray) {
aapsLogger.warn(LTag.PUMPCOMM, "onIndication: " + this.toString() + "Should not be called here!")
}
open fun onConnected() {
aapsLogger.debug(LTag.PUMPCOMM, "onConnected")
}
fun onDisconnected() {
aapsLogger.debug(LTag.PUMPCOMM, "onDisconnected")
medtrumPump.connectionState = ConnectionState.DISCONNECTED
responseHandled = true
responseSuccess = false
toState(IdleState())
}
fun waitForResponse(timeout: Long): Boolean {
val startTime = System.currentTimeMillis()
val timeoutMillis = T.secs(timeout).msecs()
while (!responseHandled) {
if (System.currentTimeMillis() - startTime > timeoutMillis) {
// If we haven't received a response in the specified time, assume the command failed
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service State timeout")
// Disconnect to cancel any outstanding commands and go back to ready state
disconnect("Timeout")
toState(IdleState())
return false
}
SystemClock.sleep(25)
}
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service State responseHandled: $responseHandled responseSuccess: $responseSuccess")
return responseSuccess
}
fun onSendMessageError(reason: String) {
aapsLogger.warn(LTag.PUMPCOMM, "onSendMessageError: " + this.toString() + "reason: $reason")
// Retry 3 times
if (sendRetryCounter < 3) {
sendRetryCounter++
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
} else {
responseHandled = true
responseSuccess = false
disconnect("onSendMessageError")
toState(IdleState())
}
}
}
private inner class IdleState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached IdleState")
}
override fun onConnected() {
super.onConnected()
toState(AuthState())
}
}
// State for connect flow
private inner class AuthState : State() {
val retryCounter = 0
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached AuthState")
mPacket = AuthorizePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
// Check if we have a supported pump
if (medtrumPump.pumpType() == PumpType.MEDTRUM_UNTESTED) {
// Throw error
aapsLogger.error(LTag.PUMPCOMM, "Unsupported pump type")
uiInteraction.addNotificationWithSound(
Notification.PUMP_ERROR,
rh.gs(R.string.pump_unsupported, medtrumPump.deviceType),
Notification.URGENT,
info.nightscout.core.ui.R.raw.alarm
)
disconnect("Unsupported pump")
toState(IdleState())
} else {
toState(GetDeviceTypeState())
}
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class GetDeviceTypeState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetDeviceTypeState")
mPacket = GetDeviceTypePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
// Place holder, not really used (yet)
val deviceType = (mPacket as GetDeviceTypePacket).deviceType
val deviceSN = (mPacket as GetDeviceTypePacket).deviceSN
aapsLogger.debug(LTag.PUMPCOMM, "GetDeviceTypeState: deviceType: $deviceType deviceSN: $deviceSN")
toState(GetTimeState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class GetTimeState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetTimeState")
mPacket = GetTimePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
val currTime = dateUtil.now()
aapsLogger.debug(LTag.PUMPCOMM, "GetTimeState.onIndication systemTime: $currTime, pumpTime: ${medtrumPump.lastTimeReceivedFromPump}")
if (abs(medtrumPump.lastTimeReceivedFromPump - currTime) <= T.secs(10).msecs()) { // Allow 10 sec deviation
toState(SynchronizeState())
} else {
aapsLogger.warn(LTag.PUMPCOMM, "GetTimeState.onIndication time difference too big, setting time")
toState(SetTimeState())
}
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class SetTimeState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeState")
mPacket = SetTimePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
toState(SetTimeZoneState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class SetTimeZoneState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeZoneState")
mPacket = SetTimeZonePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
medtrumPump.needCheckTimeUpdate = false
timeUpdateNotification(true)
toState(SynchronizeState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class SynchronizeState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SynchronizeState")
mPacket = SynchronizePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
toState(SubscribeState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// State for connect flow
private inner class SubscribeState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SubscribeState")
mPacket = SubscribePacket(injector)
mPacket?.getRequest()?.let { bleComm.sendMessage(it) }
scope.launch {
waitForResponse(COMMAND_CONNECTING_TIMEOUT_SEC)
}
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
toState(ReadyState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
disconnect("Failure")
toState(IdleState())
}
}
}
// This state is reached when the patch is ready to receive commands (Activation, Bolus, temp basal and whatever)
private inner class ReadyState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached ReadyState!")
// Now we are fully connected and authenticated and we can start sending commands. Let AAPS know
if (isConnected == false) {
medtrumPump.connectionState = ConnectionState.CONNECTED
}
}
}
// This state is when a command is send and we wait for a response for that command
private inner class CommandState : State() {
override fun onEnter() {
aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached CommandState")
}
override fun onIndication(data: ByteArray) {
if (mPacket?.handleResponse(data) == true) {
// Succes!
responseHandled = true
responseSuccess = true
toState(ReadyState())
} else if (mPacket?.failed == true) {
// Failure
responseHandled = true
responseSuccess = false
toState(ReadyState())
}
}
}
}

View file

@ -0,0 +1,47 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumActivateCompleteBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumActivateCompleteFragment : MedtrumBaseFragment<FragmentMedtrumActivateCompleteBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumActivateCompleteFragment = MedtrumActivateCompleteFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_activate_complete
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL,
MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state
MedtrumViewModel.SetupStep.ACTIVATED -> btnPositive.visibility = View.VISIBLE
else -> {
ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString()))
aapsLogger.error(LTag.PUMP, "Unexpected state: $it")
}
}
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumActivateBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumActivateFragment : MedtrumBaseFragment<FragmentMedtrumActivateBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumActivateFragment = MedtrumActivateFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_activate
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL,
MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state
MedtrumViewModel.SetupStep.ACTIVATED -> moveStep(PatchStep.ACTIVATE_COMPLETE)
MedtrumViewModel.SetupStep.ERROR -> {
moveStep(PatchStep.ERROR)
updateSetupStep(MedtrumViewModel.SetupStep.PRIMED) // Reset setup step
binding.textActivatingPump.text = rh.gs(R.string.activating_error)
binding.btnPositive.visibility = View.VISIBLE
}
else -> {
ToastUtils.errorToast(requireContext(), "Unexpected state: $it")
aapsLogger.error(LTag.PUMP, "Unexpected state: $it")
}
}
}
startActivate()
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

View file

@ -0,0 +1,111 @@
package info.nightscout.pump.medtrum.ui
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.MotionEvent
import android.view.WindowManager
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.utils.extensions.safeGetSerializableExtra
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.pump.medtrum.databinding.ActivityMedtrumBinding
import info.nightscout.pump.medtrum.extension.replaceFragmentInActivity
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
class MedtrumActivity : MedtrumBaseActivity<ActivityMedtrumBinding>() {
override fun getLayoutId(): Int = R.layout.activity_medtrum
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return super.dispatchTouchEvent(event)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.apply {
viewModel = ViewModelProvider(this@MedtrumActivity, viewModelFactory).get(MedtrumViewModel::class.java)
viewModel?.apply {
processIntent(intent)
patchStep.observe(this@MedtrumActivity) {
when (it) {
PatchStep.PREPARE_PATCH -> setupViewFragment(MedtrumPreparePatchFragment.newInstance())
PatchStep.PREPARE_PATCH_CONNECT -> setupViewFragment(MedtrumPreparePatchConnectFragment.newInstance())
PatchStep.PRIME -> setupViewFragment(MedtrumPrimeFragment.newInstance())
PatchStep.PRIMING -> setupViewFragment(MedtrumPrimingFragment.newInstance())
PatchStep.PRIME_COMPLETE -> setupViewFragment(MedtrumPrimeCompleteFragment.newInstance())
PatchStep.ATTACH_PATCH -> setupViewFragment(MedtrumAttachPatchFragment.newInstance())
PatchStep.ACTIVATE -> setupViewFragment(MedtrumActivateFragment.newInstance())
PatchStep.ACTIVATE_COMPLETE -> setupViewFragment(MedtrumActivateCompleteFragment.newInstance())
PatchStep.CANCEL,
PatchStep.COMPLETE -> this@MedtrumActivity.finish()
PatchStep.ERROR -> Unit // Do nothing, let activity handle this
PatchStep.RETRY_ACTIVATION -> setupViewFragment(MedtrumRetryActivationFragment.newInstance())
PatchStep.RETRY_ACTIVATION_CONNECT -> setupViewFragment(MedtrumRetryActivationConnectFragment.newInstance())
PatchStep.START_DEACTIVATION -> setupViewFragment(MedtrumStartDeactivationFragment.newInstance())
PatchStep.DEACTIVATE -> setupViewFragment(MedtrumDeactivatePatchFragment.newInstance())
PatchStep.FORCE_DEACTIVATION -> {
medtrumPump.pumpState = MedtrumPumpState.STOPPED
moveStep(PatchStep.DEACTIVATION_COMPLETE)
}
PatchStep.DEACTIVATION_COMPLETE -> setupViewFragment(MedtrumDeactivationCompleteFragment.newInstance())
null -> Unit
}
}
}
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
processIntent(intent)
}
private fun processIntent(intent: Intent?) {
binding.viewModel?.apply {
intent?.run {
val step = intent.safeGetSerializableExtra(EXTRA_START_PATCH_STEP, PatchStep::class.java)
if (step != null) {
initializePatchStep(step)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
}
override fun onBackPressed() {
binding.viewModel?.apply {
// Do nothing
}
}
companion object {
const val EXTRA_START_PATCH_STEP = "EXTRA_START_PATCH_FRAGMENT_UI"
const val EXTRA_START_FROM_MENU = "EXTRA_START_FROM_MENU"
@JvmStatic fun createIntentFromMenu(context: Context, patchStep: PatchStep): Intent {
return Intent(context, MedtrumActivity::class.java).apply {
putExtra(EXTRA_START_PATCH_STEP, patchStep)
putExtra(EXTRA_START_FROM_MENU, true)
}
}
}
private fun setupViewFragment(baseFragment: MedtrumBaseFragment<*>) {
replaceFragmentInActivity(baseFragment, R.id.framelayout_fragment, false)
}
}

View file

@ -0,0 +1,56 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumAttachPatchBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumAttachPatchFragment : MedtrumBaseFragment<FragmentMedtrumAttachPatchBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumAttachPatchFragment = MedtrumAttachPatchFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_attach_patch
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
aapsLogger.debug(LTag.PUMP, "MedtrumAttachPatchFragment onViewCreated")
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL,
MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state
else -> {
ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString()))
aapsLogger.error(LTag.PUMP, "Unexpected state: $it")
}
}
}
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

View file

@ -0,0 +1,57 @@
package info.nightscout.pump.medtrum.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import dagger.android.support.DaggerAppCompatActivity
import info.nightscout.core.ui.R
import info.nightscout.pump.medtrum.di.MedtrumPluginQualifier
import info.nightscout.rx.AapsSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import javax.inject.Inject
abstract class MedtrumBaseActivity<B : ViewDataBinding> : DaggerAppCompatActivity(), MedtrumBaseNavigator {
@Inject
@MedtrumPluginQualifier
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var aapsSchedulers: AapsSchedulers
protected lateinit var binding: B
private val compositeDisposable = CompositeDisposable()
protected lateinit var getResult: ActivityResultLauncher<Intent>
@LayoutRes
abstract fun getLayoutId(): Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme_NoActionBar)
binding = DataBindingUtil.setContentView(this, getLayoutId())
binding.lifecycleOwner = this
}
override fun back() {
if(supportFragmentManager.backStackEntryCount == 0) {
finish()
} else {
supportFragmentManager.popBackStack()
}
}
override fun finish(finishAffinity: Boolean) {
if(finishAffinity) {
finishAffinity()
} else {
finish()
}
}
}

View file

@ -0,0 +1,66 @@
package info.nightscout.pump.medtrum.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import dagger.android.support.DaggerFragment
import info.nightscout.pump.medtrum.di.MedtrumPluginQualifier
import io.reactivex.rxjava3.disposables.CompositeDisposable
import javax.inject.Inject
abstract class MedtrumBaseFragment<B : ViewDataBinding> : DaggerFragment(), MedtrumBaseNavigator {
@Inject
@MedtrumPluginQualifier
lateinit var viewModelFactory: ViewModelProvider.Factory
protected var baseActivity: MedtrumBaseActivity<*>? = null
protected lateinit var binding: B
private val compositeDisposable = CompositeDisposable()
@LayoutRes
abstract fun getLayoutId(): Int
@CallSuper
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is MedtrumBaseActivity<*>) {
baseActivity = context
}
}
@CallSuper
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
@CallSuper
override fun onDestroy() {
super.onDestroy()
compositeDisposable.dispose()
}
@CallSuper
override fun onDetach() {
super.onDetach()
baseActivity = null
}
override fun back() {
baseActivity?.back()
}
override fun finish(finishAffinity: Boolean) {
baseActivity?.finish(finishAffinity)
}
}

View file

@ -0,0 +1,7 @@
package info.nightscout.pump.medtrum.ui
interface MedtrumBaseNavigator {
fun back()
fun finish(finishAffinity: Boolean = false)
}

View file

@ -0,0 +1,62 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumDeactivatePatchBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumDeactivatePatchFragment : MedtrumBaseFragment<FragmentMedtrumDeactivatePatchBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumDeactivatePatchFragment = MedtrumDeactivatePatchFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_deactivate_patch
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
aapsLogger.debug(LTag.PUMP, "MedtrumDeactivatePatchFragment onViewCreated")
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.STOPPED -> {
moveStep(PatchStep.DEACTIVATION_COMPLETE)
}
MedtrumViewModel.SetupStep.ERROR -> {
moveStep(PatchStep.ERROR)
updateSetupStep(MedtrumViewModel.SetupStep.START_DEACTIVATION) // Reset setup step
binding.textDeactivatingPump.text = rh.gs(R.string.deactivating_error)
binding.btnNegative.visibility = View.VISIBLE
binding.btnPositive.visibility = View.VISIBLE
}
else -> Unit // Nothing to do here
}
}
deactivatePatch()
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

View file

@ -0,0 +1,34 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumDeactivationCompleteBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class MedtrumDeactivationCompleteFragment : MedtrumBaseFragment<FragmentMedtrumDeactivationCompleteBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
companion object {
fun newInstance(): MedtrumDeactivationCompleteFragment = MedtrumDeactivationCompleteFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_deactivation_complete
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
aapsLogger.debug(LTag.PUMP, "MedtrumStartDeactivationFragment onViewCreated")
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
// Nothing to do here (yet)
}
}
}
}

View file

@ -0,0 +1,83 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.interfaces.protection.ProtectionCheck
import info.nightscout.pump.medtrum.MedtrumPump
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumOverviewBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumOverviewViewModel
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.EventType
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.comm.enums.MedtrumPumpState
import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.logging.AAPSLogger
import io.reactivex.rxjava3.disposables.CompositeDisposable
import javax.inject.Inject
class MedtrumOverviewFragment : MedtrumBaseFragment<FragmentMedtrumOverviewBinding>() {
@Inject lateinit var aapsSchedulers: AapsSchedulers
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var medtrumPump: MedtrumPump
@Inject lateinit var protectionCheck: ProtectionCheck
private var disposable: CompositeDisposable = CompositeDisposable()
override fun getLayoutId(): Int = R.layout.fragment_medtrum_overview
override fun onDestroy() {
super.onDestroy()
disposable.clear()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewmodel = ViewModelProvider(this@MedtrumOverviewFragment, viewModelFactory).get(MedtrumOverviewViewModel::class.java)
viewmodel?.apply {
eventHandler.observe(viewLifecycleOwner) { evt ->
when (evt.peekContent()) {
EventType.CHANGE_PATCH_CLICKED -> requireContext().apply {
protectionCheck.queryProtection(
requireActivity(),
ProtectionCheck.Protection.PREFERENCES,
{
val nextStep = when {
medtrumPump.pumpState > MedtrumPumpState.EJECTED && medtrumPump.pumpState < MedtrumPumpState.STOPPED -> {
PatchStep.START_DEACTIVATION
}
medtrumPump.pumpState in listOf(MedtrumPumpState.STOPPED, MedtrumPumpState.NONE) -> {
PatchStep.PREPARE_PATCH
}
else -> {
PatchStep.RETRY_ACTIVATION
}
}
startActivity(MedtrumActivity.createIntentFromMenu(this, nextStep))
}
)
}
EventType.PROFILE_NOT_SET -> ToastUtils.infoToast(requireContext(), R.string.no_profile_selected)
else -> Unit
}
}
}
}
}
override fun onPause() {
super.onPause()
}
override fun onResume() {
super.onResume()
}
}

View file

@ -0,0 +1,59 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPreparePatchConnectBinding
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumPreparePatchConnectFragment : MedtrumBaseFragment<FragmentMedtrumPreparePatchConnectBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumPreparePatchConnectFragment = MedtrumPreparePatchConnectFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_prepare_patch_connect
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
aapsLogger.debug(LTag.PUMP, "MedtrumPreparePatchFragment onViewCreated")
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL -> btnPositive.visibility = View.GONE
MedtrumViewModel.SetupStep.FILLED -> btnPositive.visibility = View.VISIBLE
MedtrumViewModel.SetupStep.ERROR -> {
ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString()))
moveStep(PatchStep.CANCEL)
}
else -> Unit
}
}
preparePatchConnect()
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

View file

@ -0,0 +1,35 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPreparePatchBinding
import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import javax.inject.Inject
class MedtrumPreparePatchFragment : MedtrumBaseFragment<FragmentMedtrumPreparePatchBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
companion object {
fun newInstance(): MedtrumPreparePatchFragment = MedtrumPreparePatchFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_prepare_patch
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
aapsLogger.debug(LTag.PUMP, "MedtrumPreparePatchFragment onViewCreated")
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
preparePatch()
}
}
}
}

View file

@ -0,0 +1,56 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPrimeCompleteBinding
import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumPrimeCompleteFragment : MedtrumBaseFragment<FragmentMedtrumPrimeCompleteBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumPrimeCompleteFragment = MedtrumPrimeCompleteFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_prime_complete
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL,
MedtrumViewModel.SetupStep.PRIMED -> Unit // Nothing to do here, previous state
else -> {
ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString()))
aapsLogger.error(LTag.PUMP, "Unexpected state: $it")
}
}
}
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

View file

@ -0,0 +1,56 @@
package info.nightscout.pump.medtrum.ui
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.pump.medtrum.R
import info.nightscout.pump.medtrum.code.PatchStep
import info.nightscout.pump.medtrum.databinding.FragmentMedtrumPrimeBinding
import info.nightscout.pump.medtrum.ui.MedtrumBaseFragment
import info.nightscout.pump.medtrum.ui.viewmodel.MedtrumViewModel
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import javax.inject.Inject
class MedtrumPrimeFragment : MedtrumBaseFragment<FragmentMedtrumPrimeBinding>() {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rh: ResourceHelper
companion object {
fun newInstance(): MedtrumPrimeFragment = MedtrumPrimeFragment()
}
override fun getLayoutId(): Int = R.layout.fragment_medtrum_prime
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MedtrumViewModel::class.java]
viewModel?.apply {
setupStep.observe(viewLifecycleOwner) {
when (it) {
MedtrumViewModel.SetupStep.INITIAL,
MedtrumViewModel.SetupStep.FILLED -> Unit // Nothing to do here, previous state
else -> {
ToastUtils.errorToast(requireContext(), rh.gs(R.string.unexpected_state, it.toString()))
aapsLogger.error(LTag.PUMP, "Unexpected state: $it")
}
}
}
}
btnNegative.setOnClickListener {
OKDialog.showConfirmation(requireActivity(), rh.gs(R.string.cancel_sure)) {
viewModel?.apply {
moveStep(PatchStep.CANCEL)
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more