From 18be54f48997f503a7b68c59efbc3341f6b971b0 Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Sat, 18 Feb 2023 19:46:35 +0100 Subject: [PATCH 01/97] Empty plugin --- app/build.gradle | 1 + .../activities/MyPreferenceFragment.kt | 3 + .../nightscout/androidaps/di/AppComponent.kt | 2 + .../androidaps/di/PluginsListModule.kt | 7 + .../interfaces/pump/defs/ManufacturerType.kt | 1 + pump/medtrum/.gitignore | 1 + pump/medtrum/build.gradle | 27 ++ pump/medtrum/consumer-rules.pro | 0 pump/medtrum/proguard-rules.pro | 21 ++ pump/medtrum/src/main/AndroidManifest.xml | 3 + .../pump/medtrum/MedtrumPumpPlugin.kt | 219 +++++++++++++ .../pump/medtrum/di/MedtrumPumpModule.kt | 14 + .../events/EventMedtrumPumpUpdateGui.kt | 5 + .../pump/medtrum/ui/MedtrumPumpFragment.kt | 103 ++++++ .../main/res/layout/medtrum_pump_fragment.xml | 306 ++++++++++++++++++ pump/medtrum/src/main/res/values/strings.xml | 17 + .../src/main/res/xml/pref_medtrum_pump.xml | 17 + .../info/nightscout/androidaps/TestBase.kt | 37 +++ settings.gradle | 1 + 19 files changed, 785 insertions(+) create mode 100644 pump/medtrum/.gitignore create mode 100644 pump/medtrum/build.gradle create mode 100644 pump/medtrum/consumer-rules.pro create mode 100644 pump/medtrum/proguard-rules.pro create mode 100644 pump/medtrum/src/main/AndroidManifest.xml create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt create mode 100644 pump/medtrum/src/main/res/layout/medtrum_pump_fragment.xml create mode 100644 pump/medtrum/src/main/res/values/strings.xml create mode 100644 pump/medtrum/src/main/res/xml/pref_medtrum_pump.xml create mode 100644 pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt diff --git a/app/build.gradle b/app/build.gradle index 19d1aab3a1..16c2b9c158 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,6 +214,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') diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index 50a4030a9b..0fcd7aa767 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -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.MedtrumPumpPlugin 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 medtrumPumpPlugin: MedtrumPumpPlugin @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(medtrumPumpPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey) addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, rootKey) diff --git a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt index def98a723c..4afc89af2d 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt @@ -33,6 +33,7 @@ import info.nightscout.pump.dana.di.DanaModule import info.nightscout.pump.danars.di.DanaRSModule import info.nightscout.pump.diaconn.di.DiaconnG8Module import info.nightscout.pump.virtual.di.VirtualPumpModule +import info.nightscout.pump.medtrum.di.MedtrumPumpModule import info.nightscout.rx.di.RxModule import info.nightscout.shared.di.SharedModule import info.nightscout.shared.impl.di.SharedImplModule @@ -87,6 +88,7 @@ import javax.inject.Singleton OmnipodErosModule::class, PumpCommonModule::class, RileyLinkModule::class, + MedtrumPumpModule::class, VirtualPumpModule::class ] ) diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt index b30a77bf89..9dd3ea48b1 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt @@ -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.MedtrumPumpPlugin 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 bindMedtrumPumpPlugin(plugin: MedtrumPumpPlugin): PluginBase + @Binds @AllConfigs @IntoMap diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt index ddecad988c..8ffcfeb6c6 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/ManufacturerType.kt @@ -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"), diff --git a/pump/medtrum/.gitignore b/pump/medtrum/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/pump/medtrum/.gitignore @@ -0,0 +1 @@ +/build diff --git a/pump/medtrum/build.gradle b/pump/medtrum/build.gradle new file mode 100644 index 0000000000..39d231800f --- /dev/null +++ b/pump/medtrum/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' + id 'kotlin-allopen' + id 'com.hiya.jacoco-android' +} + +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' +} + +dependencies { + implementation project(':app-wear-shared:shared') + implementation project(':database:entities') + implementation project(':core:interfaces') + implementation project(':core:main') + implementation project(':core:ui') + implementation project(':core:utils') +} \ No newline at end of file diff --git a/pump/medtrum/consumer-rules.pro b/pump/medtrum/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pump/medtrum/proguard-rules.pro b/pump/medtrum/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/pump/medtrum/proguard-rules.pro @@ -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 diff --git a/pump/medtrum/src/main/AndroidManifest.xml b/pump/medtrum/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0a0938ae37 --- /dev/null +++ b/pump/medtrum/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt new file mode 100644 index 0000000000..1d2e4c0742 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt @@ -0,0 +1,219 @@ +package info.nightscout.pump.medtrum + +import dagger.android.HasAndroidInjector +import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.plugin.PluginDescription +import info.nightscout.interfaces.plugin.PluginType +import info.nightscout.interfaces.profile.Profile +import info.nightscout.interfaces.profile.ProfileFunction +import info.nightscout.interfaces.pump.DetailedBolusInfo +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.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.CommandQueue +import info.nightscout.interfaces.queue.CustomCommand +import info.nightscout.interfaces.ui.UiInteraction +import info.nightscout.interfaces.utils.TimeChangeType +import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventAppInitialized +import info.nightscout.rx.events.EventOverviewBolusProgress +import info.nightscout.rx.events.EventPreferenceChange +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.functions.Consumer +import io.reactivex.rxjava3.subjects.BehaviorSubject +import org.json.JSONException +import org.json.JSONObject +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class MedtrumPumpPlugin @Inject constructor( + injector: HasAndroidInjector, + aapsLogger: AAPSLogger, + rh: ResourceHelper, + commandQueue: CommandQueue, + private val aapsSchedulers: AapsSchedulers, + private val rxBus: RxBus, + private val fabricPrivacy: FabricPrivacy, + private val dateUtil: DateUtil, + private val pumpSync: PumpSync, + private val uiInteraction: UiInteraction, + private val profileFunction: ProfileFunction +) : PumpPluginBase( + PluginDescription() + .mainType(PluginType.PUMP) // TODO Prefs etc + .fragmentClass(MedtrumPumpFragment::class.java.name) + .pluginIcon(info.nightscout.core.ui.R.drawable.ic_eopatch2_128) // TODO + .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 { + + + override fun onStart() { + super.onStart() + } + + override fun onStop() { + super.onStop() + aapsLogger.debug(LTag.PUMP, "MedtrumPumpPlugin onStop()") + } + + override fun isInitialized(): Boolean { + return false + } + + override fun isSuspended(): Boolean { + return false + } + + override fun isBusy(): Boolean { + return false + } + + override fun isConnected(): Boolean { + return false + } + + override fun isConnecting(): Boolean { + return false + } + + override fun isHandshakeInProgress(): Boolean { + return false + } + + override fun finishHandshaking() { + } + + override fun connect(reason: String) { + aapsLogger.debug(LTag.PUMP, "Medtrum connect - reason:$reason") + } + + override fun disconnect(reason: String) { + aapsLogger.debug(LTag.PUMP, "Medtrum disconnect - reason:$reason") + } + + override fun stopConnecting() { + } + + override fun getPumpStatus(reason: String) { + } + + override fun setNewBasalProfile(profile: Profile): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + override fun isThisProfileSet(profile: Profile): Boolean { + return false // TODO + } + + override fun lastDataTime(): Long { + return 0 // TODO + } + + override val baseBasalRate: Double + get() = 0.0 // TODO + + override val reservoirLevel: Double + get() = 0.0 // TODO + + override val batteryLevel: Int + get() = 0 // TODO + + override fun deliverTreatment(detailedBolusInfo: DetailedBolusInfo): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + override fun stopBolusDelivering() { + // TODO + } + + override fun setTempBasalAbsolute(absoluteRate: Double, durationInMinutes: Int, profile: Profile, enforceNew: Boolean, tbrType: PumpSync.TemporaryBasalType): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + 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) // TODO + } + + override fun setExtendedBolus(insulin: Double, durationInMinutes: Int): PumpEnactResult { + aapsLogger.info(LTag.PUMP, "setExtendedBolus - insulin: $insulin, durationInMinutes: $durationInMinutes") + return PumpEnactResult(injector) // TODO + } + + override fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + override fun cancelExtendedBolus(): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + override fun getJSONStatus(profile: Profile, profileName: String, version: String): JSONObject { + return JSONObject() // TODO + } + + override fun manufacturer(): ManufacturerType { + return ManufacturerType.Medtrum + } + + override fun model(): PumpType { + return PumpType.GENERIC_AAPS // TODO + } + + override fun serialNumber(): String { + return "0" // TODO + } + + override val pumpDescription: PumpDescription + get() = PumpDescription(PumpType.GENERIC_AAPS) // TODO + + override fun shortStatus(veryShort: Boolean): String { + return ""// TODO + } + + override val isFakingTempsByExtendedBoluses: Boolean = false //TODO + + override fun loadTDDs(): PumpEnactResult { + return PumpEnactResult(injector) // TODO + } + + override fun canHandleDST(): Boolean { + return false + } + + override fun getCustomActions(): List? { + return null + } + + override fun executeCustomAction(customActionType: CustomActionType) { + } + + override fun executeCustomCommand(customCommand: CustomCommand): PumpEnactResult? { + return null + } + + override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { + } + + private fun readTBR(): PumpSync.PumpState.TemporaryBasal? { + return pumpSync.expectedPumpState().temporaryBasal // TODO + } +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt new file mode 100644 index 0000000000..fb2f5a0296 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt @@ -0,0 +1,14 @@ +package info.nightscout.pump.medtrum.di + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment + +@Module +@Suppress("unused") +abstract class MedtrumPumpModule { + + @ContributesAndroidInjector abstract fun contributesMedtrumPumpFragment(): MedtrumPumpFragment + +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt new file mode 100644 index 0000000000..e589f2131e --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt @@ -0,0 +1,5 @@ +package info.nightscout.pump.medtrum.events + +import info.nightscout.rx.events.EventUpdateGui + +class EventMedtrumPumpUpdateGui : EventUpdateGui() \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt new file mode 100644 index 0000000000..8d3297c56c --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt @@ -0,0 +1,103 @@ +package info.nightscout.pump.medtrum.ui + +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.android.support.DaggerFragment +import info.nightscout.core.extensions.toStringFull +import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.iob.IobCobCalculator +import info.nightscout.interfaces.profile.ProfileFunction +import info.nightscout.pump.medtrum.databinding.MedtrumPumpFragmentBinding +import info.nightscout.pump.medtrum.events.EventMedtrumPumpUpdateGui +import info.nightscout.pump.medtrum.MedtrumPumpPlugin +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventExtendedBolusChange +import info.nightscout.rx.events.EventTempBasalChange +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 javax.inject.Inject + +class MedtrumPumpFragment : DaggerFragment() { + + @Inject lateinit var rxBus: RxBus + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var medtrumPumpPlugin: MedtrumPumpPlugin + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var iobCobCalculator: IobCobCalculator + @Inject lateinit var aapsSchedulers: AapsSchedulers + + private val disposable = CompositeDisposable() + + private lateinit var refreshLoop: Runnable + private var handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) + + private var _binding: MedtrumPumpFragmentBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + MedtrumPumpFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root + + @Synchronized + override fun onResume() { + super.onResume() + disposable += rxBus + .toObservable(EventMedtrumPumpUpdateGui::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ updateGui() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventTempBasalChange::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ updateGui() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventExtendedBolusChange::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ updateGui() }, fabricPrivacy::logException) + refreshLoop = Runnable { + activity?.runOnUiThread { updateGui() } + handler.postDelayed(refreshLoop, T.mins(1).msecs()) + } + handler.postDelayed(refreshLoop, T.mins(1).msecs()) + updateGui() + } + + @Synchronized + override fun onPause() { + super.onPause() + disposable.clear() + handler.removeCallbacksAndMessages(null) + } + + @Synchronized + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + @Synchronized + private fun updateGui() { + if (_binding == null) return + val profile = profileFunction.getProfile() ?: return + binding.baseBasalRate.text = rh.gs(info.nightscout.core.ui.R.string.pump_base_basal_rate, medtrumPumpPlugin.baseBasalRate) + binding.tempbasal.text = iobCobCalculator.getTempBasal(dateUtil.now())?.toStringFull(profile, dateUtil) + ?: "" + binding.extendedbolus.text = iobCobCalculator.getExtendedBolus(dateUtil.now())?.toStringFull(dateUtil) + ?: "" + binding.battery.text = rh.gs(info.nightscout.core.ui.R.string.format_percent, 0) // TODO + binding.reservoir.text = rh.gs(info.nightscout.interfaces.R.string.format_insulin_units, 0.0) // TODO + + binding.serialNumber.text = medtrumPumpPlugin.serialNumber() + } +} diff --git a/pump/medtrum/src/main/res/layout/medtrum_pump_fragment.xml b/pump/medtrum/src/main/res/layout/medtrum_pump_fragment.xml new file mode 100644 index 0000000000..222bed82df --- /dev/null +++ b/pump/medtrum/src/main/res/layout/medtrum_pump_fragment.xml @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pump/medtrum/src/main/res/values/strings.xml b/pump/medtrum/src/main/res/values/strings.xml new file mode 100644 index 0000000000..b361dec76e --- /dev/null +++ b/pump/medtrum/src/main/res/values/strings.xml @@ -0,0 +1,17 @@ + + + + snInput + medtrumpump_settings + + + Medtrum + MEDTRUM + Medtrum Nano + MEDTRUM + Medtrum pump settings + + SN + Serial number pump base + + diff --git a/pump/medtrum/src/main/res/xml/pref_medtrum_pump.xml b/pump/medtrum/src/main/res/xml/pref_medtrum_pump.xml new file mode 100644 index 0000000000..9b40fc9864 --- /dev/null +++ b/pump/medtrum/src/main/res/xml/pref_medtrum_pump.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt b/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt new file mode 100644 index 0000000000..4fd2aef548 --- /dev/null +++ b/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt @@ -0,0 +1,37 @@ +package info.nightscout.androidaps + +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.TestAapsSchedulers +import info.nightscout.rx.logging.AAPSLoggerTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.Locale + +@ExtendWith(MockitoExtension::class) +@MockitoSettings(strictness = Strictness.LENIENT) +open class TestBase { + + val aapsLogger = AAPSLoggerTest() + val aapsSchedulers: AapsSchedulers = TestAapsSchedulers() + + @BeforeEach + fun setupLocale() { + Locale.setDefault(Locale.ENGLISH) + System.setProperty("disableFirebase", "true") + } + + // Workaround for Kotlin nullability. + // https://medium.com/@elye.project/befriending-kotlin-and-mockito-1c2e7b0ef791 + // https://stackoverflow.com/questions/30305217/is-it-possible-to-use-mockito-in-kotlin + fun anyObject(): T { + Mockito.any() + return uninitialized() + } + + @Suppress("Unchecked_Cast") + fun uninitialized(): T = null as T +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 137cc8c2bd..24a3acf745 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,6 +33,7 @@ include ':pump:diaconn' include ':pump:eopatch' include ':pump:eopatch-core' include ':insight' +include ':pump:medtrum' include ':pump:medtronic' include ':pump:omnipod-common' include ':pump:omnipod-eros' From 37a6fa92208661540d87c782bb1ddfb328ac7115 Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Sat, 18 Feb 2023 21:10:50 +0100 Subject: [PATCH 02/97] Added Medtrum pump type etc --- .../interfaces/pump/defs/PumpCapability.kt | 1 + .../interfaces/pump/defs/PumpType.kt | 21 +++++++++++++++++++ .../nightscout/core/pump/PumpTypeExtension.kt | 2 ++ .../entities/embedments/InterfaceIDs.kt | 1 + .../pump/medtrum/MedtrumPumpPlugin.kt | 6 ++++-- 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt index ed9620a818..f425ffdff7 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpCapability.kt @@ -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)), // Technically the pump supports ExtendedBolus, but not implemented (yet) BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed, BasalRate_Duration15and30minAllowed(arrayOf(BasalRate_Duration15minAllowed, BasalRate_Duration30minAllowed)), diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt index 30163c9ddf..4199af95a8 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/pump/defs/PumpType.kt @@ -391,6 +391,26 @@ 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, + source = Source.Medtrum ); val description: String @@ -458,6 +478,7 @@ enum class PumpType { OmnipodEros, OmnipodDash, EOPatch2, + Medtrum, MDI, VirtualPump, Unknown diff --git a/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt b/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt index 7024036f9e..8a0b120cc1 100644 --- a/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt +++ b/core/main/src/main/java/info/nightscout/core/pump/PumpTypeExtension.kt @@ -59,6 +59,7 @@ 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.CACHE -> PumpType.CACHE } @@ -117,5 +118,6 @@ 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.CACHE -> InterfaceIDs.PumpType.CACHE } diff --git a/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt b/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt index 5f78da4e8c..a10a7740cd 100644 --- a/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt +++ b/database/entities/src/main/java/info/nightscout/database/entities/embedments/InterfaceIDs.kt @@ -43,6 +43,7 @@ data class InterfaceIDs( MDI, DIACONN_G8, EOPATCH2, + MEDTRUM, USER, CACHE; diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt index 1d2e4c0742..404668c68f 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt @@ -64,6 +64,8 @@ class MedtrumPumpPlugin @Inject constructor( .description(R.string.medtrum_pump_description), injector, aapsLogger, rh, commandQueue ), Pump { + private var mPumpType: PumpType = PumpType.MEDTRUM_NANO + private val mPumpDescription = PumpDescription(mPumpType) override fun onStart() { super.onStart() @@ -175,7 +177,7 @@ class MedtrumPumpPlugin @Inject constructor( } override fun model(): PumpType { - return PumpType.GENERIC_AAPS // TODO + return mPumpType } override fun serialNumber(): String { @@ -183,7 +185,7 @@ class MedtrumPumpPlugin @Inject constructor( } override val pumpDescription: PumpDescription - get() = PumpDescription(PumpType.GENERIC_AAPS) // TODO + get() = mPumpDescription override fun shortStatus(veryShort: Boolean): String { return ""// TODO From b635ad26d85585b89ee728207602d9dd46c9bfee Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Sun, 19 Feb 2023 20:09:52 +0100 Subject: [PATCH 03/97] Initial Medtrum BLEComm --- pump/medtrum/build.gradle | 2 +- .../pump/medtrum/MedtrumPumpPlugin.kt | 2 +- .../pump/medtrum/comm/ManufacturerData.kt | 36 ++ .../pump/medtrum/comm/WriteCommandPackets.kt | 79 ++++ .../pump/medtrum/di/MedtrumPumpModule.kt | 2 +- .../pump/medtrum/encryption/Crypt.kt | 75 ++++ .../events/EventMedtrumPumpUpdateGui.kt | 2 +- .../medtrum/extension/ByteArrayExtension.kt | 28 ++ .../pump/medtrum/extension/IntExtension.kt | 10 + .../pump/medtrum/extension/LongExtension.kt | 10 + .../pump/medtrum/services/BLEComm.kt | 399 ++++++++++++++++++ .../info/nightscout/androidaps/TestBase.kt | 2 +- .../medtrum/comm/WriteCommandPacketsTest.kt | 90 ++++ .../pump/medtrum/encryption/CryptTest.kt | 27 ++ 14 files changed, 759 insertions(+), 5 deletions(-) create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt create mode 100644 pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt create mode 100644 pump/medtrum/src/test/java/info/nightscout/pump/medtrum/encryption/CryptTest.kt diff --git a/pump/medtrum/build.gradle b/pump/medtrum/build.gradle index 39d231800f..9163baf95a 100644 --- a/pump/medtrum/build.gradle +++ b/pump/medtrum/build.gradle @@ -24,4 +24,4 @@ dependencies { implementation project(':core:main') implementation project(':core:ui') implementation project(':core:utils') -} \ No newline at end of file +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt index 404668c68f..8cd141e446 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt @@ -218,4 +218,4 @@ class MedtrumPumpPlugin @Inject constructor( private fun readTBR(): PumpSync.PumpState.TemporaryBasal? { return pumpSync.expectedPumpState().temporaryBasal // TODO } -} \ No newline at end of file +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt new file mode 100644 index 0000000000..d567feb1ba --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt @@ -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 = manufacturerDataBytes.copyOfRange(index, index + 4) + deviceID = deviceIDBytes.toLong() + index += 4 + deviceType = (manufacturerDataBytes[index] and 0xff.toByte()).toInt() + index += 1 + version = (manufacturerDataBytes[index] and 0xff.toByte()).toInt() + } + + fun getDeviceID(): Long{ + return deviceID + } + + fun getDeviceType(): Int { + return deviceType + } + + fun getVersion(): Int { + return version + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt new file mode 100644 index 0000000000..517072f8d6 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt @@ -0,0 +1,79 @@ +package info.nightscout.pump.medtrum.comm + +import info.nightscout.pump.medtrum.encryption.Crypt + + +class WriteCommandPackets(private val command: ByteArray) { + + val crypt = Crypt() + + private val packages = mutableListOf() + private var index = 0 + private var writeCommandIndex = 0 + private var allPacketsConsumed = false + + + init { + setData(command) + } + + fun setData(inputData: ByteArray) { + resetPackets() + // PackageIndex: 0 initially, if there are multiple packet, for the first packet it is set to 0 (not included in crc calc but sent in actual header) + var pkgIndex = 0 + var header = byteArrayOf( + (inputData.size + 4).toByte(), + inputData[0], + writeCommandIndex.toByte(), + pkgIndex.toByte() + ) + + var tmp: ByteArray = header + inputData.copyOfRange(1, inputData.size) + var totalCommand: ByteArray = tmp + crypt.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 + crypt.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 + crypt.calcCrc8(tmp, tmp.size).toByte()) + } + writeCommandIndex = (writeCommandIndex % 255) + 1 + } + + + fun allPacketsConsumed(): Boolean { + return allPacketsConsumed + } + + fun getNextPacket(): ByteArray? { + var ret: ByteArray? = null + if (index < packages.size) { + ret = packages[index] + index++ + } + if (index >= packages.size) { + allPacketsConsumed = true + } + return ret + } + + private fun resetPackets() { + packages.clear() + index = 0 + allPacketsConsumed = false + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt index fb2f5a0296..9589839625 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt @@ -11,4 +11,4 @@ abstract class MedtrumPumpModule { @ContributesAndroidInjector abstract fun contributesMedtrumPumpFragment(): MedtrumPumpFragment -} \ No newline at end of file +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt new file mode 100644 index 0000000000..093c3a8c12 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt @@ -0,0 +1,75 @@ +package info.nightscout.pump.medtrum.encryption + +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toLong + +class Crypt { + 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) + 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 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) + + val MED_CIPHER: Long = 1344751489 + + fun keyGen(input: Long): Long { + val key = randomGen(randomGen(MED_CIPHER xor input)) + return simpleCrypt(key) + } + + 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 + } + + 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 + } + + 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 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() + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt index e589f2131e..e93896d063 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/events/EventMedtrumPumpUpdateGui.kt @@ -2,4 +2,4 @@ package info.nightscout.pump.medtrum.events import info.nightscout.rx.events.EventUpdateGui -class EventMedtrumPumpUpdateGui : EventUpdateGui() \ No newline at end of file +class EventMedtrumPumpUpdateGui : EventUpdateGui() diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt new file mode 100644 index 0000000000..0aa4a100b1 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/ByteArrayExtension.kt @@ -0,0 +1,28 @@ +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 +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt new file mode 100644 index 0000000000..8589496e18 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/IntExtension.kt @@ -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 +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt new file mode 100644 index 0000000000..a294670ec1 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/extension/LongExtension.kt @@ -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 +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt new file mode 100644 index 0000000000..700bc49266 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -0,0 +1,399 @@ +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.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.SystemClock +import androidx.core.app.ActivityCompat +import dagger.android.HasAndroidInjector +import info.nightscout.core.ui.toast.ToastUtils +import info.nightscout.core.utils.notify +import info.nightscout.core.utils.waitMillis +import info.nightscout.interfaces.notifications.Notification +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.ui.UiInteraction +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.extension.toInt +import info.nightscout.pump.medtrum.encryption.Crypt +import info.nightscout.pump.medtrum.comm.WriteCommandPackets +import info.nightscout.pump.medtrum.comm.ManufacturerData +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventDismissNotification +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 java.util.UUID +import java.util.Arrays +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BLEComm @Inject internal constructor( + private val injector: HasAndroidInjector, + private val aapsLogger: AAPSLogger, + private val rh: ResourceHelper, + private val context: Context, + private val rxBus: RxBus, + private val sp: SP, + private val pumpSync: PumpSync, + private val dateUtil: DateUtil, + private val uiInteraction: UiInteraction +) { + + companion object { + + private const val WRITE_DELAY_MILLIS: Long = 50 + 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-6050405558b" + private const val CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" + + private const val MANUFACTURER_ID = 18305 + private const val COMMAND_AUTH_REQ: Byte = 5 + } + + private val mBluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter + private var mBluetoothGatt: BluetoothGatt? = null + private val mCrypt = Crypt() + + var isConnected = false + var isConnecting = false + private var uartWrite: BluetoothGattCharacteristic? = null + + private var deviceID: Long = 0 + + /** 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() + + if (deviceID == 0.toLong()) deviceID = rh.gs(info.nightscout.pump.medtrum.R.string.key_snInput).toLong(radix = 16) + + isConnected = false + // TODO: Maybe replace this by (or add) a isScanning parameter? + isConnecting = true + + // Find our Medtrum Device! + filters.add( + ScanFilter.Builder().setDeviceName("MT").build() + ) + // TODO Check if we need to add MAC for reconnects? Not sure if otherwise we can find the device + mBluetoothAdapter?.bluetoothLeScanner?.startScan(filters, settings, mScanCallback) + return true + } + + @SuppressLint("MissingPermission") + @Synchronized + fun stopScan() { + mBluetoothAdapter?.bluetoothLeScanner?.stopScan(mScanCallback) + } + + /** Connect flow: 2. When device is found this is called by onScanResult() */ + @SuppressLint("MissingPermission") + @Synchronized + fun connect(device: BluetoothDevice) { + mBluetoothGatt = + device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) + } + + @SuppressLint("MissingPermission") + @Synchronized + fun disconnect() { + mBluetoothGatt?.disconnect() + mBluetoothGatt = null + } + + @Synchronized + fun stopConnecting() { + isConnecting = false + } + + @SuppressLint("MissingPermission") + @Synchronized fun close() { + aapsLogger.debug(LTag.PUMPBTCOMM, "BluetoothAdapter close") + mBluetoothGatt?.close() + 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 DeviceID: " + manufacturerData?.getDeviceID()) + + if (manufacturerData?.getDeviceID() == deviceID) { + stopScan() + connect(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 status = " + status) + if (status == BluetoothGatt.GATT_SUCCESS) { + readDataParsing(characteristic.value) + } + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged") + readDataParsing(characteristic.value) + } + + override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite status = " + status) + // TODO (note also queue, note that in danars there is no response, so check where to handle multiple packets) + // aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite: " + DanaRS_Packet.toHexString(characteristic.value)) + // Thread { + // synchronized(mSendQueue) { + // // after message sent, check if there is the rest of the message waiting and send it + // if (mSendQueue.size > 0) { + // val bytes = mSendQueue[0] + // mSendQueue.removeAt(0) + // writeCharacteristicNoResponse(uartWriteBTGattChar, bytes) + // } + // } + // }.start() + } + + 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) + } + + 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(CHARACTERISTIC_CONFIG_UUID)) + if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) { + notificationEnabled = false + } + } + if (notificationEnabled) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!") + authorize() + } + } + } + + @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(CHARACTERISTIC_CONFIG_UUID))?.let { + if (characteristic.properties and 0x10 > 0) { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + mBluetoothGatt?.writeDescriptor(it) + } else if (characteristic.properties and 0x20 > 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) { + gatt.discoverServices() + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + close() + isConnected = false + isConnecting = false + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected + disconnect() + startScan() + } + } + + private fun readDataParsing(receivedData: ByteArray) { + aapsLogger.debug(LTag.PUMPBTCOMM, "<<>> " + Arrays.toString(receivedData)) + // TODO + /** Connect flow: 6. Authorized */ // TODO place this at the correct place + } + + private fun authorize() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Start auth!") + val role = 2 // Fixed to 2 for pump + val key = mCrypt.keyGen(deviceID) + val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) + sendMessage(commandData) + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + private fun sendMessage(message: ByteArray) { + // TODO: Handle packages which consist of multiple, Create a queue of packages + aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) + val writePacket = WriteCommandPackets(message) + val value: ByteArray? = writePacket.getNextPacket() + + // TODO: queue + writeCharacteristic(uartWriteBTGattChar, value) + } + + 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)) + } + + private fun getGattCharacteristic(uuid: UUID): BluetoothGattCharacteristic? { + aapsLogger.debug(LTag.PUMPBTCOMM, "getGattCharacteristic $uuid") + val service = getGattService() + if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return null + } + return service.getCharacteristic(uuid) + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) { + Thread(Runnable { + SystemClock.sleep(WRITE_DELAY_MILLIS) + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return@Runnable + } + characteristic.value = data + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + aapsLogger.debug("writeCharacteristic:" + Arrays.toString(data)) + mBluetoothGatt?.writeCharacteristic(characteristic) + }).start() + SystemClock.sleep(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 + val gattCharacteristics = gattService.characteristics + for (gattCharacteristic in gattCharacteristics) { + setCharacteristicNotification(gattCharacteristic, true) + } + } + } +} \ No newline at end of file diff --git a/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt b/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt index 4fd2aef548..96af831f2b 100644 --- a/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt +++ b/pump/medtrum/src/test/java/info/nightscout/androidaps/TestBase.kt @@ -34,4 +34,4 @@ open class TestBase { @Suppress("Unchecked_Cast") fun uninitialized(): T = null as T -} \ No newline at end of file +} diff --git a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt new file mode 100644 index 0000000000..10d05c2ad6 --- /dev/null +++ b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt @@ -0,0 +1,90 @@ +package info.nightscout.pump.medtrum.comm + +import org.junit.jupiter.api.Test +import org.junit.Assert.* + +class WriteCommandPacketsTest { + + @Test + fun Given14LongCommandExpectOnePacket() { + val input = byteArrayOf(5, 2, 0, 0, 0, 0, -21, 57, -122, -56) + val expect = byteArrayOf(14, 5, 0, 0, 2, 0, 0, 0, 0, -21, 57, -122, -56, -93, 0) + val cmdPackets = WriteCommandPackets(input) + val output = cmdPackets.getNextPacket() + + assertEquals(expect.contentToString(), output.contentToString()) + } + + @Test + fun Given41LongCommandExpectThreePackets() { + val input = byteArrayOf(18, 0, 12, 0, 3, 0, 1, 30, 32, 3, 16, 14, 0, 0, 1, 7, 0, -96, 2, -16, 96, 2, 104, 33, 2, -32, -31, 1, -64, 3, 2, -20, 36, 2, 100, -123, 2) + val expect1 = byteArrayOf(41, 18, 0, 1, 0, 12, 0, 3, 0, 1, 30, 32, 3, 16, 14, 0, 0, 1, 7, -121) + val expect2 = byteArrayOf(41, 18, 0, 2, 0, -96, 2, -16, 96, 2, 104, 33, 2, -32, -31, 1, -64, 3, 2, -3) + val expect3 = byteArrayOf(41, 18, 0, 3, -20, 36, 2, 100, -123, 2, -125, -89) + + val cmdPackets = WriteCommandPackets(input) + val output1 = cmdPackets.getNextPacket() + val output2 = cmdPackets.getNextPacket() + val output3 = cmdPackets.getNextPacket() + val output4 = cmdPackets.getNextPacket() + + + assertEquals(expect1.contentToString(), output1.contentToString()) + assertEquals(expect2.contentToString(), output2.contentToString()) + assertEquals(expect3.contentToString(), output3.contentToString()) + assertNull(output4) + assertEquals(true, cmdPackets.allPacketsConsumed()) + + } + + @Test + fun Given2CommandsExpectWriteIndexInHeaderIncrease() { + val input1 = byteArrayOf(66) + val input2 = byteArrayOf(99) + + val expect1 = byteArrayOf(5, 66, 0, 0, -25, 0) + val expect2 = byteArrayOf(5, 99, 1, 0, 64, 0) + + val cmdPackets = WriteCommandPackets(input1) + + val output1 = cmdPackets.getNextPacket() + + cmdPackets.setData(input2) + + val output2 = cmdPackets.getNextPacket() + + assertEquals(expect1.contentToString(), output1.contentToString()) + assertEquals(expect2.contentToString(), output2.contentToString()) + } + + @Test + fun GivenWriteIndexOverflowExpectWriteIndex1() { + val input1 = byteArrayOf(55) + val input2 = byteArrayOf(66) + val input3 = byteArrayOf(99) + + val expect1 = byteArrayOf(5, 55, -2, 0, -19, 0) + val expect2 = byteArrayOf(5, 66, -1, 0, 86, 0) + val expect3 = byteArrayOf(5, 99, 1, 0, 64, 0) + + val cmdPackets = WriteCommandPackets(byteArrayOf(0.toByte())) + + // All this stuff to set the private field ^^ + val writeCommandIndex = WriteCommandPackets::class.java.getDeclaredField("writeCommandIndex") + writeCommandIndex.isAccessible = true + writeCommandIndex.setInt(cmdPackets, 254) + + cmdPackets.setData(input1) + val output1 = cmdPackets.getNextPacket() + + cmdPackets.setData(input2) + val output2 = cmdPackets.getNextPacket() + + cmdPackets.setData(input3) + val output3 = cmdPackets.getNextPacket() + + assertEquals(expect1.contentToString(), output1.contentToString()) + assertEquals(expect2.contentToString(), output2.contentToString()) + assertEquals(expect3.contentToString(), output3.contentToString()) + } +} diff --git a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/encryption/CryptTest.kt b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/encryption/CryptTest.kt new file mode 100644 index 0000000000..213bcdddbe --- /dev/null +++ b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/encryption/CryptTest.kt @@ -0,0 +1,27 @@ +package info.nightscout.pump.medtrum.encryption + +import org.junit.jupiter.api.Test +import org.junit.Assert.* + +class CryptTest { + + @Test + fun GivenSNExpectKey() { + val crypt = Crypt() + + val input: Long = 2859923929 + val expect: Long = 3364239851 + val output: Long = crypt.keyGen(input) + assertEquals(expect, output) + } + + @Test + fun GivenSNExpectReal() { + val crypt = Crypt() + + val input: Long = 2859923929 + val expect: Long = 126009121 + val output: Long = crypt.simpleDecrypt(input) + assertEquals(expect, output) + } +} From baa4376f688fc523162c316b1a025a70bd17df69 Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Tue, 21 Feb 2023 10:59:31 +0100 Subject: [PATCH 04/97] Added MedtrumService --- .../activities/MyPreferenceFragment.kt | 6 +- .../nightscout/androidaps/di/AppComponent.kt | 4 +- .../androidaps/di/PluginsListModule.kt | 4 +- pump/medtrum/src/main/AndroidManifest.xml | 15 +- ...{MedtrumPumpPlugin.kt => MedtrumPlugin.kt} | 82 +++- .../pump/medtrum/comm/ManufacturerData.kt | 8 +- ...mpModule.kt => MedtrumActivitiesModule.kt} | 3 +- .../pump/medtrum/di/MedtrumModule.kt | 9 + .../pump/medtrum/di/MedtrumServicesModule.kt | 11 + .../pump/medtrum/services/BLEComm.kt | 352 ++++++++++-------- .../pump/medtrum/services/MedtrumService.kt | 201 ++++++++++ .../pump/medtrum/ui/MedtrumPumpFragment.kt | 8 +- 12 files changed, 503 insertions(+), 200 deletions(-) rename pump/medtrum/src/main/java/info/nightscout/pump/medtrum/{MedtrumPumpPlugin.kt => MedtrumPlugin.kt} (67%) rename pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/{MedtrumPumpModule.kt => MedtrumActivitiesModule.kt} (84%) create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index 0fcd7aa767..fc568738ea 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -54,7 +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.MedtrumPumpPlugin +import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.pump.virtual.VirtualPumpPlugin import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventPreferenceChange @@ -123,7 +123,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var wearPlugin: WearPlugin @Inject lateinit var maintenancePlugin: MaintenancePlugin @Inject lateinit var eopatchPumpPlugin: EopatchPumpPlugin - @Inject lateinit var medtrumPumpPlugin: MedtrumPumpPlugin + @Inject lateinit var MedtrumPlugin: MedtrumPlugin @Inject lateinit var passwordCheck: PasswordCheck @Inject lateinit var nsSettingStatus: NSSettingsStatus @@ -214,7 +214,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang addPreferencesFromResourceIfEnabled(medtronicPumpPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(diaconnG8Plugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(eopatchPumpPlugin, rootKey, config.PUMPDRIVERS) - addPreferencesFromResourceIfEnabled(medtrumPumpPlugin, rootKey, config.PUMPDRIVERS) + addPreferencesFromResourceIfEnabled(MedtrumPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey) addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, rootKey) diff --git a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt index 4afc89af2d..f96fd72374 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt @@ -31,9 +31,9 @@ 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.pump.medtrum.di.MedtrumPumpModule import info.nightscout.rx.di.RxModule import info.nightscout.shared.di.SharedModule import info.nightscout.shared.impl.di.SharedImplModule @@ -88,7 +88,7 @@ import javax.inject.Singleton OmnipodErosModule::class, PumpCommonModule::class, RileyLinkModule::class, - MedtrumPumpModule::class, + MedtrumModule::class, VirtualPumpModule::class ] ) diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt index 9dd3ea48b1..4753c24877 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsListModule.kt @@ -46,7 +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.MedtrumPumpPlugin +import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.pump.diaconn.DiaconnG8Plugin import info.nightscout.pump.virtual.VirtualPumpPlugin import info.nightscout.sensitivity.SensitivityAAPSPlugin @@ -214,7 +214,7 @@ abstract class PluginsListModule { @PumpDriver @IntoMap @IntKey(160) - abstract fun bindMedtrumPumpPlugin(plugin: MedtrumPumpPlugin): PluginBase + abstract fun bindMedtrumPlugin(plugin: MedtrumPlugin): PluginBase @Binds @AllConfigs diff --git a/pump/medtrum/src/main/AndroidManifest.xml b/pump/medtrum/src/main/AndroidManifest.xml index 0a0938ae37..f589b49319 100644 --- a/pump/medtrum/src/main/AndroidManifest.xml +++ b/pump/medtrum/src/main/AndroidManifest.xml @@ -1,3 +1,16 @@ + - + + + + + + + + + + \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt similarity index 67% rename from pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt rename to pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt index 8cd141e446..60304f456d 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPumpPlugin.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt @@ -1,6 +1,12 @@ 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 dagger.android.HasAndroidInjector +import info.nightscout.core.ui.toast.ToastUtils import info.nightscout.core.utils.fabric.FabricPrivacy import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginType @@ -21,33 +27,38 @@ import info.nightscout.interfaces.queue.CustomCommand import info.nightscout.interfaces.ui.UiInteraction import info.nightscout.interfaces.utils.TimeChangeType import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment +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.EventAppInitialized import info.nightscout.rx.events.EventOverviewBolusProgress import info.nightscout.rx.events.EventPreferenceChange 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.functions.Consumer +import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.subjects.BehaviorSubject import org.json.JSONException import org.json.JSONObject import javax.inject.Inject import javax.inject.Singleton - @Singleton -class MedtrumPumpPlugin @Inject constructor( +class MedtrumPlugin @Inject constructor( injector: HasAndroidInjector, aapsLogger: AAPSLogger, rh: ResourceHelper, commandQueue: CommandQueue, + private val sp: SP, private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, + private val context: Context, private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, private val pumpSync: PumpSync, @@ -55,7 +66,7 @@ class MedtrumPumpPlugin @Inject constructor( private val profileFunction: ProfileFunction ) : PumpPluginBase( PluginDescription() - .mainType(PluginType.PUMP) // TODO Prefs etc + .mainType(PluginType.PUMP) .fragmentClass(MedtrumPumpFragment::class.java.name) .pluginIcon(info.nightscout.core.ui.R.drawable.ic_eopatch2_128) // TODO .pluginName(R.string.medtrum) @@ -64,16 +75,51 @@ class MedtrumPumpPlugin @Inject constructor( .description(R.string.medtrum_pump_description), injector, aapsLogger, rh, commandQueue ), Pump { + private val disposable = CompositeDisposable() + private var medtrumService: MedtrumService? = null private var mPumpType: PumpType = PumpType.MEDTRUM_NANO private val mPumpDescription = PumpDescription(mPumpType) + private var mDeviceSN: Long = 0 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) + changePump() } override fun onStop() { + aapsLogger.debug(LTag.PUMP, "MedtrumPlugin onStop()") + context.unbindService(mConnection) + disposable.clear() super.onStop() - aapsLogger.debug(LTag.PUMP, "MedtrumPumpPlugin 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 changePump() { // TODO: Call this on inputfield change? + try { + mDeviceSN = sp.getString(info.nightscout.pump.medtrum.R.string.key_snInput, " ").toLong(radix = 16) + commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.device_changed), null) + } catch (e: NumberFormatException) { + aapsLogger.debug(LTag.PUMP, "changePump: invalid input!") + } } override fun isInitialized(): Boolean { @@ -88,33 +134,35 @@ class MedtrumPumpPlugin @Inject constructor( return false } - override fun isConnected(): Boolean { - return false - } - - override fun isConnecting(): Boolean { - return false - } - - override fun isHandshakeInProgress(): Boolean { - return false - } + override fun isConnected(): Boolean = medtrumService?.isConnected ?: false + override fun isConnecting(): Boolean = medtrumService?.isConnecting ?: false + override fun isHandshakeInProgress(): Boolean = false override fun finishHandshaking() { } override fun connect(reason: String) { aapsLogger.debug(LTag.PUMP, "Medtrum connect - reason:$reason") + aapsLogger.debug(LTag.PUMP, "Medtrum connect - service::$medtrumService") + aapsLogger.debug(LTag.PUMP, "Medtrum connect - mDeviceSN:$mDeviceSN") + if (medtrumService != null && mDeviceSN != 0.toLong()) { + aapsLogger.debug(LTag.PUMP, "Medtrum connect - Attempt connection!") + val success = medtrumService?.connect(reason, mDeviceSN) ?: false + if (!success) ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_supported_or_not_paired) + } } override fun disconnect(reason: String) { - aapsLogger.debug(LTag.PUMP, "Medtrum disconnect - reason:$reason") + aapsLogger.debug(LTag.PUMP, "RS disconnect from: $reason") + medtrumService?.disconnect(reason) } override fun stopConnecting() { + medtrumService?.stopConnecting() } override fun getPumpStatus(reason: String) { + // TODO } override fun setNewBasalProfile(profile: Profile): PumpEnactResult { @@ -177,7 +225,7 @@ class MedtrumPumpPlugin @Inject constructor( } override fun model(): PumpType { - return mPumpType + return mPumpType } override fun serialNumber(): String { diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt index d567feb1ba..098d31d984 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ManufacturerData.kt @@ -14,15 +14,15 @@ class ManufacturerData(private val manufacturerDataBytes: ByteArray) { fun setData(inputData: ByteArray) { var index = 0 - val deviceIDBytes: ByteArray = manufacturerDataBytes.copyOfRange(index, index + 4) + val deviceIDBytes: ByteArray = inputData.copyOfRange(index, index + 4) deviceID = deviceIDBytes.toLong() index += 4 - deviceType = (manufacturerDataBytes[index] and 0xff.toByte()).toInt() + deviceType = (inputData[index] and 0xff.toByte()).toInt() index += 1 - version = (manufacturerDataBytes[index] and 0xff.toByte()).toInt() + version = (inputData[index] and 0xff.toByte()).toInt() } - fun getDeviceID(): Long{ + fun getDeviceSN(): Long{ return deviceID } diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt similarity index 84% rename from pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt rename to pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt index 9589839625..8bac156f22 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumPumpModule.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt @@ -1,13 +1,12 @@ package info.nightscout.pump.medtrum.di -import dagger.Binds import dagger.Module import dagger.android.ContributesAndroidInjector import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment @Module @Suppress("unused") -abstract class MedtrumPumpModule { +abstract class MedtrumActivitiesModule { @ContributesAndroidInjector abstract fun contributesMedtrumPumpFragment(): MedtrumPumpFragment diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt new file mode 100644 index 0000000000..612053b6ec --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt @@ -0,0 +1,9 @@ +package info.nightscout.pump.medtrum.di + +import dagger.Module + +@Module(includes = [ + MedtrumActivitiesModule::class, + MedtrumServicesModule::class +]) +open class MedtrumModule \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt new file mode 100644 index 0000000000..0c587bacc2 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt @@ -0,0 +1,11 @@ +package info.nightscout.pump.medtrum.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import info.nightscout.pump.medtrum.services.MedtrumService + +@Module +@Suppress("unused") +abstract class MedtrumServicesModule { + @ContributesAndroidInjector abstract fun contributesDanaRSService(): MedtrumService +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt index 700bc49266..30fe7c2536 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -79,7 +79,7 @@ class BLEComm @Inject internal constructor( var isConnecting = false private var uartWrite: BluetoothGattCharacteristic? = null - private var deviceID: Long = 0 + private var mDeviceSN: Long = 0 /** Connect flow: 1. Start scanning for our device (SN entered in settings) */ @SuppressLint("MissingPermission") @@ -98,7 +98,6 @@ class BLEComm @Inject internal constructor( .build() val filters = mutableListOf() - if (deviceID == 0.toLong()) deviceID = rh.gs(info.nightscout.pump.medtrum.R.string.key_snInput).toLong(radix = 16) isConnected = false // TODO: Maybe replace this by (or add) a isScanning parameter? @@ -119,17 +118,43 @@ class BLEComm @Inject internal constructor( mBluetoothAdapter?.bluetoothLeScanner?.stopScan(mScanCallback) } + 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 + } + mDeviceSN = deviceSN + isConnecting = true + startScan() + return true + } + /** Connect flow: 2. When device is found this is called by onScanResult() */ @SuppressLint("MissingPermission") @Synchronized - fun connect(device: BluetoothDevice) { + fun connectGatt(device: BluetoothDevice) { mBluetoothGatt = device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) } @SuppressLint("MissingPermission") @Synchronized - fun disconnect() { + 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") mBluetoothGatt?.disconnect() mBluetoothGatt = null } @@ -156,11 +181,12 @@ class BLEComm @Inject internal constructor( result.scanRecord?.getManufacturerSpecificData(MANUFACTURER_ID) ?.let { ManufacturerData(it) } - aapsLogger.debug(LTag.PUMPBTCOMM, "Found DeviceID: " + manufacturerData?.getDeviceID()) + aapsLogger.debug(LTag.PUMPBTCOMM, "Found deviceSN: " + manufacturerData?.getDeviceSN()) - if (manufacturerData?.getDeviceID() == deviceID) { + if (manufacturerData?.getDeviceSN() == mDeviceSN) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Found our device! deviceSN: " + manufacturerData.getDeviceSN()) stopScan() - connect(result.device) + connectGatt(result.device) } } @@ -230,170 +256,166 @@ class BLEComm @Inject internal constructor( 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) + @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) + } - 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(CHARACTERISTIC_CONFIG_UUID)) - if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) { - notificationEnabled = false - } - } - if (notificationEnabled) { - aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!") - authorize() + @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(CHARACTERISTIC_CONFIG_UUID)) + if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) { + notificationEnabled = false } } - } - - @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(CHARACTERISTIC_CONFIG_UUID))?.let { - if (characteristic.properties and 0x10 > 0) { - it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - mBluetoothGatt?.writeDescriptor(it) - } else if (characteristic.properties and 0x20 > 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) { - gatt.discoverServices() - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - close() - isConnected = false - isConnecting = false - rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) - aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected - disconnect() - startScan() - } - } - - private fun readDataParsing(receivedData: ByteArray) { - aapsLogger.debug(LTag.PUMPBTCOMM, "<<>> " + Arrays.toString(receivedData)) - // TODO - /** Connect flow: 6. Authorized */ // TODO place this at the correct place - } - - private fun authorize() { - aapsLogger.debug(LTag.PUMPBTCOMM, "Start auth!") - val role = 2 // Fixed to 2 for pump - val key = mCrypt.keyGen(deviceID) - val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) - sendMessage(commandData) - } - - @Suppress("DEPRECATION") - @SuppressLint("MissingPermission") - @Synchronized - private fun sendMessage(message: ByteArray) { - // TODO: Handle packages which consist of multiple, Create a queue of packages - aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) - val writePacket = WriteCommandPackets(message) - val value: ByteArray? = writePacket.getNextPacket() - - // TODO: queue - writeCharacteristic(uartWriteBTGattChar, value) - } - - 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)) - } - - private fun getGattCharacteristic(uuid: UUID): BluetoothGattCharacteristic? { - aapsLogger.debug(LTag.PUMPBTCOMM, "getGattCharacteristic $uuid") - val service = getGattService() - if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) { - aapsLogger.error("BluetoothAdapter not initialized_ERROR") - isConnecting = false - isConnected = false - return null - } - return service.getCharacteristic(uuid) - } - - @Suppress("DEPRECATION") - @SuppressLint("MissingPermission") - @Synchronized - private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) { - Thread(Runnable { - SystemClock.sleep(WRITE_DELAY_MILLIS) - if (mBluetoothAdapter == null || mBluetoothGatt == null) { - aapsLogger.error("BluetoothAdapter not initialized_ERROR") - isConnecting = false - isConnected = false - return@Runnable - } - characteristic.value = data - characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - aapsLogger.debug("writeCharacteristic:" + Arrays.toString(data)) - mBluetoothGatt?.writeCharacteristic(characteristic) - }).start() - SystemClock.sleep(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 - val gattCharacteristics = gattService.characteristics - for (gattCharacteristic in gattCharacteristics) { - setCharacteristicNotification(gattCharacteristic, true) + if (notificationEnabled) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!") + authorize() } } } + + @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(CHARACTERISTIC_CONFIG_UUID))?.let { + if (characteristic.properties and 0x10 > 0) { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + mBluetoothGatt?.writeDescriptor(it) + } else if (characteristic.properties and 0x20 > 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) { + gatt.discoverServices() + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + close() + isConnected = false + isConnecting = false + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected + } + } + + private fun readDataParsing(receivedData: ByteArray) { + aapsLogger.debug(LTag.PUMPBTCOMM, "<<>> " + Arrays.toString(receivedData)) + // TODO + /** Connect flow: 6. Authorized */ // TODO place this at the correct place + } + + private fun authorize() { + aapsLogger.debug(LTag.PUMPBTCOMM, "Start auth!") + val role = 2 // Fixed to 2 for pump + val key = mCrypt.keyGen(mDeviceSN) + val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) + sendMessage(commandData) + } + + fun sendMessage(message: ByteArray) { + // TODO: Handle packages which consist of multiple, Create a queue of packages + aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) + val writePacket = WriteCommandPackets(message) + val value: ByteArray? = writePacket.getNextPacket() + + // TODO: queue + writeCharacteristic(uartWriteBTGattChar, value) + } + + 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)) + } + + private fun getGattCharacteristic(uuid: UUID): BluetoothGattCharacteristic? { + aapsLogger.debug(LTag.PUMPBTCOMM, "getGattCharacteristic $uuid") + val service = getGattService() + if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return null + } + return service.getCharacteristic(uuid) + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + @Synchronized + private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) { + Thread(Runnable { + SystemClock.sleep(WRITE_DELAY_MILLIS) + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + aapsLogger.error("BluetoothAdapter not initialized_ERROR") + isConnecting = false + isConnected = false + return@Runnable + } + characteristic.value = data + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + aapsLogger.debug("writeCharacteristic:" + Arrays.toString(data)) + mBluetoothGatt?.writeCharacteristic(characteristic) + }).start() + SystemClock.sleep(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 + val gattCharacteristics = gattService.characteristics + for (gattCharacteristic in gattCharacteristics) { + setCharacteristicNotification(gattCharacteristic, true) + } + } } \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt new file mode 100644 index 0000000000..6b45d59f38 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -0,0 +1,201 @@ +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.Constants +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.BolusProgressData +import info.nightscout.interfaces.pump.PumpEnactResult +import info.nightscout.interfaces.pump.PumpSync +import info.nightscout.interfaces.queue.Callback +import info.nightscout.interfaces.queue.Command +import info.nightscout.interfaces.queue.CommandQueue +import info.nightscout.interfaces.ui.UiInteraction +import info.nightscout.pump.medtrum.MedtrumPlugin +import info.nightscout.rx.AapsSchedulers +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventAppExit +import info.nightscout.rx.events.EventInitializationChanged +import info.nightscout.rx.events.EventOverviewBolusProgress +import info.nightscout.rx.events.EventProfileSwitchChanged +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 org.joda.time.DateTime +import org.joda.time.DateTimeZone +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.min + +class MedtrumService : DaggerService() { + + @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 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 dateUtil: DateUtil + + private val disposable = CompositeDisposable() + private val mBinder: IBinder = LocalBinder() + + override fun onCreate() { + super.onCreate() + disposable += rxBus + .toObservable(EventAppExit::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ stopSelf() }, fabricPrivacy::logException) + } + + override fun onDestroy() { + disposable.clear() + super.onDestroy() + } + + val isConnected: Boolean + get() = bleComm.isConnected + + val isConnecting: Boolean + get() = bleComm.isConnecting + + fun connect(from: String, deviceSN: Long): Boolean { + // TODO Check we might want to replace this with start scan? + return bleComm.connect(from, deviceSN) + } + + fun stopConnecting() { + bleComm.stopConnecting() + } + + fun disconnect(from: String) { + bleComm.disconnect(from) + } + + fun sendMessage(message: ByteArray) { // TODO Check what we use here? + // TODO + bleComm.sendMessage(message) + } + + fun readPumpStatus() { + // TODO + } + + fun loadEvents(): PumpEnactResult { + if (!medtrumPlugin.isInitialized()) { + val result = PumpEnactResult(injector).success(false) + result.comment = "pump not initialized" + return result + } + // TODO need this? Check + val result = PumpEnactResult(injector) + return result + } + + fun setUserSettings(): PumpEnactResult { + // TODO need this? Check + val result = PumpEnactResult(injector) + return result + } + + fun bolus(insulin: Double, carbs: Int, carbTime: Long, t: EventOverviewBolusProgress.Treatment): Boolean { + if (!isConnected) return false + // TODO + return false + } + + fun bolusStop() { + // TODO + } + + fun tempBasal(percent: Int, durationInHours: Int): Boolean { + // TODO + return false + } + + fun highTempBasal(percent: Int): Boolean { + // TODO + return false + } + + fun tempBasalShortDuration(percent: Int, durationInMinutes: Int): Boolean { + if (durationInMinutes != 15 && durationInMinutes != 30) { + aapsLogger.error(LTag.PUMPCOMM, "Wrong duration param") + return false + } + // TODO + return false + } + + fun tempBasalStop(): Boolean { + if (!isConnected) return false + // TODO + return false + } + + fun extendedBolus(insulin: Double, durationInHalfHours: Int): Boolean { + if (!isConnected) return false + // TODO + return false + } + + fun extendedBolusStop(): Boolean { + if (!isConnected) return false + // TODO + return false + } + + fun updateBasalsInPump(profile: Profile): Boolean { + if (!isConnected) return false + // TODO + return false + } + + fun loadHistory(type: Byte): PumpEnactResult { + val result = PumpEnactResult(injector) + if (!isConnected) return result + // TODO + return result + } + + 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 + } +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt index 8d3297c56c..ac94004d8b 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt @@ -13,7 +13,7 @@ import info.nightscout.interfaces.iob.IobCobCalculator import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.pump.medtrum.databinding.MedtrumPumpFragmentBinding import info.nightscout.pump.medtrum.events.EventMedtrumPumpUpdateGui -import info.nightscout.pump.medtrum.MedtrumPumpPlugin +import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventExtendedBolusChange @@ -31,7 +31,7 @@ class MedtrumPumpFragment : DaggerFragment() { @Inject lateinit var rh: ResourceHelper @Inject lateinit var dateUtil: DateUtil @Inject lateinit var fabricPrivacy: FabricPrivacy - @Inject lateinit var medtrumPumpPlugin: MedtrumPumpPlugin + @Inject lateinit var MedtrumPlugin: MedtrumPlugin @Inject lateinit var profileFunction: ProfileFunction @Inject lateinit var iobCobCalculator: IobCobCalculator @Inject lateinit var aapsSchedulers: AapsSchedulers @@ -90,7 +90,7 @@ class MedtrumPumpFragment : DaggerFragment() { private fun updateGui() { if (_binding == null) return val profile = profileFunction.getProfile() ?: return - binding.baseBasalRate.text = rh.gs(info.nightscout.core.ui.R.string.pump_base_basal_rate, medtrumPumpPlugin.baseBasalRate) + binding.baseBasalRate.text = rh.gs(info.nightscout.core.ui.R.string.pump_base_basal_rate, MedtrumPlugin.baseBasalRate) binding.tempbasal.text = iobCobCalculator.getTempBasal(dateUtil.now())?.toStringFull(profile, dateUtil) ?: "" binding.extendedbolus.text = iobCobCalculator.getExtendedBolus(dateUtil.now())?.toStringFull(dateUtil) @@ -98,6 +98,6 @@ class MedtrumPumpFragment : DaggerFragment() { binding.battery.text = rh.gs(info.nightscout.core.ui.R.string.format_percent, 0) // TODO binding.reservoir.text = rh.gs(info.nightscout.interfaces.R.string.format_insulin_units, 0.0) // TODO - binding.serialNumber.text = medtrumPumpPlugin.serialNumber() + binding.serialNumber.text = MedtrumPlugin.serialNumber() } } From f31f6c1649b2123f5b79b7a416be5acf2c6dd8d7 Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Thu, 23 Feb 2023 15:41:12 +0100 Subject: [PATCH 05/97] Proper handling of setCharacteristicNotification --- app/build.gradle | 2 +- .../nightscout/pump/medtrum/MedtrumPlugin.kt | 21 ++- .../pump/medtrum/comm/WriteCommandPackets.kt | 43 +++--- .../pump/medtrum/encryption/Crypt.kt | 13 +- .../pump/medtrum/services/BLEComm.kt | 128 +++++++++++------- .../pump/medtrum/services/MedtrumService.kt | 8 +- .../medtrum/comm/WriteCommandPacketsTest.kt | 20 +-- 7 files changed, 132 insertions(+), 103 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 16c2b9c158..b3efbe17e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -111,7 +111,7 @@ android { defaultConfig { multiDexEnabled true versionCode 1500 - version "3.2.0-dev-i" + version "3.2.0-dev-i-medtrum" buildConfigField "String", "VERSION", '"' + version + '"' buildConfigField "String", "BUILDVERSION", '"' + generateGitBuild() + '-' + generateDate() + '"' buildConfigField "String", "REMOTE", '"' + generateGitRemote() + '"' diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt index 60304f456d..f799d15e51 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt @@ -90,6 +90,15 @@ class MedtrumPlugin @Inject constructor( .toObservable(EventAppExit::class.java) .observeOn(aapsSchedulers.io) .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ event -> + if (event.isChanged(rh.gs(info.nightscout.pump.medtrum.R.string.key_snInput))) { + pumpSync.connectNewPump() + changePump() + } + }, fabricPrivacy::logException) changePump() } @@ -113,13 +122,15 @@ class MedtrumPlugin @Inject constructor( } } - fun changePump() { // TODO: Call this on inputfield change? + fun changePump() { + aapsLogger.debug(LTag.PUMP, "changePump: called!") try { mDeviceSN = sp.getString(info.nightscout.pump.medtrum.R.string.key_snInput, " ").toLong(radix = 16) - commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.device_changed), null) } catch (e: NumberFormatException) { - aapsLogger.debug(LTag.PUMP, "changePump: invalid input!") + aapsLogger.debug(LTag.PUMP, "changePump: Invalid input!") } + // TODO: add medtrumPump.reset() + commandQueue.readStatus(rh.gs(info.nightscout.core.ui.R.string.device_changed), null) } override fun isInitialized(): Boolean { @@ -148,7 +159,7 @@ class MedtrumPlugin @Inject constructor( if (medtrumService != null && mDeviceSN != 0.toLong()) { aapsLogger.debug(LTag.PUMP, "Medtrum connect - Attempt connection!") val success = medtrumService?.connect(reason, mDeviceSN) ?: false - if (!success) ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_supported_or_not_paired) + if (!success) ToastUtils.errorToast(context, info.nightscout.core.ui.R.string.ble_not_supported_or_not_paired) } } @@ -162,7 +173,7 @@ class MedtrumPlugin @Inject constructor( } override fun getPumpStatus(reason: String) { - // TODO + medtrumService?.readPumpStatus() } override fun setNewBasalProfile(profile: Profile): PumpEnactResult { diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt index 517072f8d6..7a80cab3c3 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommandPackets.kt @@ -3,19 +3,13 @@ package info.nightscout.pump.medtrum.comm import info.nightscout.pump.medtrum.encryption.Crypt -class WriteCommandPackets(private val command: ByteArray) { +class WriteCommandPackets() { - val crypt = Crypt() + 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() private var index = 0 - private var writeCommandIndex = 0 - private var allPacketsConsumed = false - - - init { - setData(command) - } + private var sequenceNumber = 0 fun setData(inputData: ByteArray) { resetPackets() @@ -24,12 +18,12 @@ class WriteCommandPackets(private val command: ByteArray) { var header = byteArrayOf( (inputData.size + 4).toByte(), inputData[0], - writeCommandIndex.toByte(), + sequenceNumber.toByte(), pkgIndex.toByte() ) var tmp: ByteArray = header + inputData.copyOfRange(1, inputData.size) - var totalCommand: ByteArray = tmp + crypt.calcCrc8(tmp, tmp.size).toByte() + var totalCommand: ByteArray = tmp + calcCrc8(tmp, tmp.size).toByte() if ((totalCommand.size - header.size) <= 15) { packages.add(totalCommand + 0.toByte()) @@ -40,7 +34,7 @@ class WriteCommandPackets(private val command: ByteArray) { while (remainingCommand.size > 15) { header[3] = pkgIndex.toByte() tmp = header + remainingCommand.copyOfRange(0, 15) - packages.add(tmp + crypt.calcCrc8(tmp, tmp.size).toByte()) + packages.add(tmp + calcCrc8(tmp, tmp.size).toByte()) remainingCommand = remainingCommand.copyOfRange(15, remainingCommand.size) pkgIndex = (pkgIndex + 1) % 256 @@ -49,14 +43,9 @@ class WriteCommandPackets(private val command: ByteArray) { // Add last package header[3] = pkgIndex.toByte() tmp = header + remainingCommand - packages.add(tmp + crypt.calcCrc8(tmp, tmp.size).toByte()) + packages.add(tmp + calcCrc8(tmp, tmp.size).toByte()) } - writeCommandIndex = (writeCommandIndex % 255) + 1 - } - - - fun allPacketsConsumed(): Boolean { - return allPacketsConsumed + sequenceNumber = (sequenceNumber % 255) + 1 } fun getNextPacket(): ByteArray? { @@ -65,15 +54,23 @@ class WriteCommandPackets(private val command: ByteArray) { ret = packages[index] index++ } - if (index >= packages.size) { - allPacketsConsumed = true - } return ret } + fun allPacketsConsumed(): Boolean { + return !(index < packages.size) + } + private fun resetPackets() { packages.clear() index = 0 - allPacketsConsumed = false + } + + 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 } } diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt index 093c3a8c12..4cd3e182bc 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/encryption/Crypt.kt @@ -4,9 +4,8 @@ import info.nightscout.pump.medtrum.extension.toByteArray import info.nightscout.pump.medtrum.extension.toLong class Crypt { - 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) - 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 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 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 @@ -27,14 +26,6 @@ class Crypt { return ret } - 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 - } - private fun simpleCrypt(inputData: Long): Long { var temp = inputData xor MED_CIPHER for (i in 0 until 32) { diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt index 30fe7c2536..316466dbce 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -19,6 +19,8 @@ import android.content.Context import android.content.Intent 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 dagger.android.HasAndroidInjector @@ -64,13 +66,19 @@ class BLEComm @Inject internal constructor( private const val WRITE_DELAY_MILLIS: Long = 50 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-6050405558b" - private const val CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" + 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 const val COMMAND_AUTH_REQ: Byte = 5 } + 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 val mCrypt = Crypt() @@ -78,9 +86,12 @@ class BLEComm @Inject internal constructor( var isConnected = false var isConnecting = false private var uartWrite: BluetoothGattCharacteristic? = null + private var uartRead: BluetoothGattCharacteristic? = null private var mDeviceSN: Long = 0 + private var mWritePackets = WriteCommandPackets() + /** Connect flow: 1. Start scanning for our device (SN entered in settings) */ @SuppressLint("MissingPermission") @Synchronized @@ -210,30 +221,29 @@ class BLEComm @Inject internal constructor( override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicRead status = " + status) - if (status == BluetoothGatt.GATT_SUCCESS) { - readDataParsing(characteristic.value) - } } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged") + // TODO: Make split between Notif and Indication + // Notif contains pump status and alarms readDataParsing(characteristic.value) } override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite status = " + status) - // TODO (note also queue, note that in danars there is no response, so check where to handle multiple packets) - // aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite: " + DanaRS_Packet.toHexString(characteristic.value)) - // Thread { - // synchronized(mSendQueue) { - // // after message sent, check if there is the rest of the message waiting and send it - // if (mSendQueue.size > 0) { - // val bytes = mSendQueue[0] - // mSendQueue.removeAt(0) - // writeCharacteristicNoResponse(uartWriteBTGattChar, bytes) - // } - // } - // }.start() + + if (status == BluetoothGatt.GATT_SUCCESS) { + // Check if we need to finish our command! + synchronized(mWritePackets) { + val value: ByteArray? = mWritePackets.getNextPacket() + if (value != null) { + writeCharacteristic(uartWriteBTGattChar, value) + } + } + } else { + // TODO: What to do here? + } } override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) { @@ -286,7 +296,7 @@ class BLEComm @Inject internal constructor( val characteristics = service.characteristics for (j in 0 until characteristics.size) { val configDescriptor = - characteristics[j].getDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG_UUID)) + characteristics[j].getDescriptor(UUID.fromString(CONFIG_UUID)) if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) { notificationEnabled = false } @@ -310,11 +320,11 @@ class BLEComm @Inject internal constructor( return } mBluetoothGatt?.setCharacteristicNotification(characteristic, enabled) - characteristic?.getDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG_UUID))?.let { - if (characteristic.properties and 0x10 > 0) { + 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 0x20 > 0) { + } else if (characteristic.properties and NEEDS_ENABLE_INDICATION > 0) { it.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE mBluetoothGatt?.writeDescriptor(it) } else { @@ -329,7 +339,9 @@ class BLEComm @Inject internal constructor( private fun onConnectionStateChangeSynchronized(gatt: BluetoothGatt, status: Int, newState: Int) { aapsLogger.debug(LTag.PUMPBTCOMM, "onConnectionStateChange newState: " + newState + " status: " + status) if (newState == BluetoothProfile.STATE_CONNECTED) { - gatt.discoverServices() + handler.postDelayed({ + mBluetoothGatt?.discoverServices() + }, WRITE_DELAY_MILLIS) } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { close() isConnected = false @@ -340,9 +352,12 @@ class BLEComm @Inject internal constructor( } private fun readDataParsing(receivedData: ByteArray) { - aapsLogger.debug(LTag.PUMPBTCOMM, "<<>> " + Arrays.toString(receivedData)) - // TODO - /** Connect flow: 6. Authorized */ // TODO place this at the correct place + aapsLogger.debug(LTag.PUMPBTCOMM, "<<<<< readDataParsing " + Arrays.toString(receivedData)) + // TODO Implement + // TODO place this at the correct place + /** Connect flow: 6. Authorized */ + isConnected = true + isConnecting = false } private fun authorize() { @@ -354,13 +369,20 @@ class BLEComm @Inject internal constructor( } fun sendMessage(message: ByteArray) { - // TODO: Handle packages which consist of multiple, Create a queue of packages aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) - val writePacket = WriteCommandPackets(message) - val value: ByteArray? = writePacket.getNextPacket() - - // TODO: queue - writeCharacteristic(uartWriteBTGattChar, value) + if (!mWritePackets.allPacketsConsumed()) { + aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage not all packets consumed!! unable to sent message!") + return + } + synchronized(mWritePackets) { + mWritePackets.setData(message) + val value: ByteArray? = mWritePackets.getNextPacket() + if (value != null) { + writeCharacteristic(uartWriteBTGattChar, value) + } else { + aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage error in writePacket!") + } + } } private fun getGattService(): BluetoothGattService? { @@ -390,20 +412,18 @@ class BLEComm @Inject internal constructor( @SuppressLint("MissingPermission") @Synchronized private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) { - Thread(Runnable { - SystemClock.sleep(WRITE_DELAY_MILLIS) - if (mBluetoothAdapter == null || mBluetoothGatt == null) { - aapsLogger.error("BluetoothAdapter not initialized_ERROR") - isConnecting = false - isConnected = false - return@Runnable - } - characteristic.value = data - characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - aapsLogger.debug("writeCharacteristic:" + Arrays.toString(data)) - mBluetoothGatt?.writeCharacteristic(characteristic) - }).start() - SystemClock.sleep(WRITE_DELAY_MILLIS) + 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)) + mBluetoothGatt?.writeCharacteristic(characteristic) + } + }, WRITE_DELAY_MILLIS) } private val uartWriteBTGattChar: BluetoothGattCharacteristic @@ -413,9 +433,23 @@ class BLEComm @Inject internal constructor( /** 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 (gattCharacteristic in gattCharacteristics) { - setCharacteristicNotification(gattCharacteristic, true) + 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()) + } } } } \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt index 6b45d59f38..d4b8e2b674 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -179,13 +179,7 @@ class MedtrumService : DaggerService() { return false } - fun loadHistory(type: Byte): PumpEnactResult { - val result = PumpEnactResult(injector) - if (!isConnected) return result - // TODO - return result - } - + /** Service stuff */ inner class LocalBinder : Binder() { val serviceInstance: MedtrumService get() = this@MedtrumService diff --git a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt index 10d05c2ad6..a414540b97 100644 --- a/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt +++ b/pump/medtrum/src/test/java/info/nightscout/pump/medtrum/comm/WriteCommandPacketsTest.kt @@ -9,7 +9,8 @@ class WriteCommandPacketsTest { fun Given14LongCommandExpectOnePacket() { val input = byteArrayOf(5, 2, 0, 0, 0, 0, -21, 57, -122, -56) val expect = byteArrayOf(14, 5, 0, 0, 2, 0, 0, 0, 0, -21, 57, -122, -56, -93, 0) - val cmdPackets = WriteCommandPackets(input) + val cmdPackets = WriteCommandPackets() + cmdPackets.setData(input) val output = cmdPackets.getNextPacket() assertEquals(expect.contentToString(), output.contentToString()) @@ -22,7 +23,8 @@ class WriteCommandPacketsTest { val expect2 = byteArrayOf(41, 18, 0, 2, 0, -96, 2, -16, 96, 2, 104, 33, 2, -32, -31, 1, -64, 3, 2, -3) val expect3 = byteArrayOf(41, 18, 0, 3, -20, 36, 2, 100, -123, 2, -125, -89) - val cmdPackets = WriteCommandPackets(input) + val cmdPackets = WriteCommandPackets() + cmdPackets.setData(input) val output1 = cmdPackets.getNextPacket() val output2 = cmdPackets.getNextPacket() val output3 = cmdPackets.getNextPacket() @@ -34,7 +36,6 @@ class WriteCommandPacketsTest { assertEquals(expect3.contentToString(), output3.contentToString()) assertNull(output4) assertEquals(true, cmdPackets.allPacketsConsumed()) - } @Test @@ -45,7 +46,8 @@ class WriteCommandPacketsTest { val expect1 = byteArrayOf(5, 66, 0, 0, -25, 0) val expect2 = byteArrayOf(5, 99, 1, 0, 64, 0) - val cmdPackets = WriteCommandPackets(input1) + val cmdPackets = WriteCommandPackets() + cmdPackets.setData(input1) val output1 = cmdPackets.getNextPacket() @@ -58,7 +60,7 @@ class WriteCommandPacketsTest { } @Test - fun GivenWriteIndexOverflowExpectWriteIndex1() { + fun GivenSequenceNumberOverflowExpectSequenceNumber1() { val input1 = byteArrayOf(55) val input2 = byteArrayOf(66) val input3 = byteArrayOf(99) @@ -67,12 +69,12 @@ class WriteCommandPacketsTest { val expect2 = byteArrayOf(5, 66, -1, 0, 86, 0) val expect3 = byteArrayOf(5, 99, 1, 0, 64, 0) - val cmdPackets = WriteCommandPackets(byteArrayOf(0.toByte())) + val cmdPackets = WriteCommandPackets() // All this stuff to set the private field ^^ - val writeCommandIndex = WriteCommandPackets::class.java.getDeclaredField("writeCommandIndex") - writeCommandIndex.isAccessible = true - writeCommandIndex.setInt(cmdPackets, 254) + val sequenceNumber = WriteCommandPackets::class.java.getDeclaredField("sequenceNumber") + sequenceNumber.isAccessible = true + sequenceNumber.setInt(cmdPackets, 254) cmdPackets.setData(input1) val output1 = cmdPackets.getNextPacket() From ed100a4eb05d0fef7c9c0e3a39b1a9fb4d5efbb2 Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Thu, 23 Feb 2023 21:05:50 +0100 Subject: [PATCH 06/97] BLEComm: Added callbacks to enable handling of notifications in calling class --- .../pump/medtrum/comm/ReadDataPacket.kt | 19 ++++++ .../pump/medtrum/services/BLEComm.kt | 68 +++++++++++-------- .../pump/medtrum/services/MedtrumService.kt | 50 ++++++++++++-- 3 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt new file mode 100644 index 0000000000..e091cb8f73 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/ReadDataPacket.kt @@ -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 + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt index 316466dbce..dbff1ccc9e 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -32,9 +32,9 @@ import info.nightscout.interfaces.pump.PumpSync import info.nightscout.interfaces.ui.UiInteraction import info.nightscout.pump.medtrum.extension.toByteArray import info.nightscout.pump.medtrum.extension.toInt -import info.nightscout.pump.medtrum.encryption.Crypt 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.bus.RxBus import info.nightscout.rx.events.EventDismissNotification import info.nightscout.rx.events.EventPumpStatusChanged @@ -48,6 +48,14 @@ import java.util.Arrays import javax.inject.Inject import javax.inject.Singleton +interface BLECommCallback { + + open fun onBLEConnected() {} + open fun onNotification(notification: ByteArray) {} + open fun onIndication(indication: ByteArray) {} + open fun onSendMessageError(reason: String) +} + @Singleton class BLEComm @Inject internal constructor( private val injector: HasAndroidInjector, @@ -74,23 +82,28 @@ class BLEComm @Inject internal constructor( private const val NEEDS_ENABLE = 0x30 private const val MANUFACTURER_ID = 18305 - private const val COMMAND_AUTH_REQ: Byte = 5 } 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 val mCrypt = Crypt() var isConnected = false var isConnecting = false private var uartWrite: BluetoothGattCharacteristic? = null private var uartRead: BluetoothGattCharacteristic? = null - private var mDeviceSN: Long = 0 - + // Read and write buffers private var mWritePackets = WriteCommandPackets() + private var mReadPacket: ReadDataPacket? = null + + private var mDeviceSN: Long = 0 + private var mCallback: BLECommCallback? = null + + fun setCallback(callback: BLECommCallback?) { + this.mCallback = callback + } /** Connect flow: 1. Start scanning for our device (SN entered in settings) */ @SuppressLint("MissingPermission") @@ -225,14 +238,26 @@ class BLEComm @Inject internal constructor( override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged") - // TODO: Make split between Notif and Indication - // Notif contains pump status and alarms - readDataParsing(characteristic.value) + + val value = characteristic.getValue() + if (characteristic.getUuid() == UUID.fromString(READ_UUID)) { + mCallback?.onNotification(value) + } else if (characteristic.getUuid() == UUID.fromString(WRITE_UUID)) { + if (mReadPacket == null) { + mReadPacket = ReadDataPacket(value) + } else { + mReadPacket?.addData(value) + } + if (mReadPacket?.allDataReceived() == true) { + mReadPacket = null + mReadPacket?.getData()?.let { mCallback?.onIndication(it) } + } + } } 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! synchronized(mWritePackets) { @@ -242,7 +267,7 @@ class BLEComm @Inject internal constructor( } } } else { - // TODO: What to do here? + mCallback?.onSendMessageError("onCharacteristicWrite failure") } } @@ -303,7 +328,10 @@ class BLEComm @Inject internal constructor( } if (notificationEnabled) { aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!") - authorize() + /** Connect flow: 6. Connected */ + mCallback?.onBLEConnected() + isConnected = true + isConnecting = false } } } @@ -351,23 +379,6 @@ class BLEComm @Inject internal constructor( } } - private fun readDataParsing(receivedData: ByteArray) { - aapsLogger.debug(LTag.PUMPBTCOMM, "<<<<< readDataParsing " + Arrays.toString(receivedData)) - // TODO Implement - // TODO place this at the correct place - /** Connect flow: 6. Authorized */ - isConnected = true - isConnecting = false - } - - private fun authorize() { - aapsLogger.debug(LTag.PUMPBTCOMM, "Start auth!") - val role = 2 // Fixed to 2 for pump - val key = mCrypt.keyGen(mDeviceSN) - val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) - sendMessage(commandData) - } - fun sendMessage(message: ByteArray) { aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message)) if (!mWritePackets.allPacketsConsumed()) { @@ -381,6 +392,7 @@ class BLEComm @Inject internal constructor( writeCharacteristic(uartWriteBTGattChar, value) } else { aapsLogger.error(LTag.PUMPBTCOMM, "sendMessage error in writePacket!") + mCallback?.onSendMessageError("error in writePacket!") } } } diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt index d4b8e2b674..2b6120d52e 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -23,6 +23,8 @@ import info.nightscout.interfaces.queue.Command import info.nightscout.interfaces.queue.CommandQueue import info.nightscout.interfaces.ui.UiInteraction import info.nightscout.pump.medtrum.MedtrumPlugin +import info.nightscout.pump.medtrum.encryption.Crypt +import info.nightscout.pump.medtrum.extension.toByteArray import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventAppExit @@ -40,12 +42,13 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import org.joda.time.DateTime import org.joda.time.DateTimeZone +import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.abs import kotlin.math.min -class MedtrumService : DaggerService() { +class MedtrumService : DaggerService(), BLECommCallback { @Inject lateinit var injector: HasAndroidInjector @Inject lateinit var aapsLogger: AAPSLogger @@ -65,11 +68,19 @@ class MedtrumService : DaggerService() { @Inject lateinit var pumpSync: PumpSync @Inject lateinit var dateUtil: DateUtil + companion object { + private const val COMMAND_AUTH_REQ: Byte = 5 + } + private val disposable = CompositeDisposable() private val mBinder: IBinder = LocalBinder() + private val mCrypt = Crypt() + + private var mDeviceSN: Long = 0 override fun onCreate() { super.onCreate() + bleComm.setCallback(this) disposable += rxBus .toObservable(EventAppExit::class.java) .observeOn(aapsSchedulers.io) @@ -89,22 +100,20 @@ class MedtrumService : DaggerService() { fun connect(from: String, deviceSN: Long): Boolean { // TODO Check we might want to replace this with start scan? + mDeviceSN = deviceSN return bleComm.connect(from, deviceSN) } fun stopConnecting() { + // TODO proper way for this might need send commands etc bleComm.stopConnecting() } fun disconnect(from: String) { + // TODO proper way for this might need send commands etc bleComm.disconnect(from) } - fun sendMessage(message: ByteArray) { // TODO Check what we use here? - // TODO - bleComm.sendMessage(message) - } - fun readPumpStatus() { // TODO } @@ -179,6 +188,35 @@ class MedtrumService : DaggerService() { return false } + private fun authorize() { + aapsLogger.debug(LTag.PUMPCOMM, "Start auth!") + val role = 2 // Fixed to 2 for pump + val key = mCrypt.keyGen(mDeviceSN) + val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) + bleComm.sendMessage(commandData) + } + + /** BLECommCallbacks */ + override fun onBLEConnected() { + // TODO Replace by FSM Entry? + authorize() + } + + override fun onNotification(notification: ByteArray) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onNotification" + notification.contentToString()) + // TODO + } + + override fun onIndication(indication: ByteArray) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onIndication" + indication.contentToString()) + // TODO + } + + override fun onSendMessageError(reason: String) { + aapsLogger.debug(LTag.PUMPCOMM, "<<<<< error during send message " + reason) + // TODO + } + /** Service stuff */ inner class LocalBinder : Binder() { val serviceInstance: MedtrumService From 27159d6e47e8b7f969ead3ab7e5eac094c82e5fa Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Fri, 24 Feb 2023 20:01:07 +0100 Subject: [PATCH 07/97] Review comments --- .../androidaps/activities/MyPreferenceFragment.kt | 4 ++-- .../pump/medtrum/services/MedtrumService.kt | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index fc568738ea..7467304b22 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -123,7 +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 medtrumPlugin: MedtrumPlugin @Inject lateinit var passwordCheck: PasswordCheck @Inject lateinit var nsSettingStatus: NSSettingsStatus @@ -214,7 +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) + addPreferencesFromResourceIfEnabled(medtrumPlugin, rootKey, config.PUMPDRIVERS) addPreferencesFromResource(R.xml.pref_pump, rootKey, config.PUMPDRIVERS) addPreferencesFromResourceIfEnabled(virtualPumpPlugin, rootKey) addPreferencesFromResourceIfEnabled(insulinOrefFreePeakPlugin, rootKey) diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt index 2b6120d52e..a48ec614d0 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -170,18 +170,6 @@ class MedtrumService : DaggerService(), BLECommCallback { return false } - fun extendedBolus(insulin: Double, durationInHalfHours: Int): Boolean { - if (!isConnected) return false - // TODO - return false - } - - fun extendedBolusStop(): Boolean { - if (!isConnected) return false - // TODO - return false - } - fun updateBasalsInPump(profile: Profile): Boolean { if (!isConnected) return false // TODO From 485826682ef66f701f3996c7e74ef6be197e18ad Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Sun, 26 Feb 2023 19:41:41 +0100 Subject: [PATCH 08/97] Implemented state to initialize pump connection --- .../pump/medtrum/comm/WriteCommand.kt | 61 ++++ .../pump/medtrum/services/BLEComm.kt | 13 +- .../pump/medtrum/services/MedtrumService.kt | 264 +++++++++++++++--- .../pump/medtrum/util/MedtrumTimeUtil.kt | 20 ++ 4 files changed, 307 insertions(+), 51 deletions(-) create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommand.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommand.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommand.kt new file mode 100644 index 0000000000..ceb8902040 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/comm/WriteCommand.kt @@ -0,0 +1,61 @@ +package info.nightscout.pump.medtrum.comm + +import dagger.android.HasAndroidInjector +import info.nightscout.pump.medtrum.encryption.Crypt +import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.util.MedtrumTimeUtil +import info.nightscout.shared.utils.DateUtil +import javax.inject.Inject +import javax.inject.Singleton + +// TODO object would be better? Or split this class up in an entirely different way +@Singleton +class WriteCommand @Inject internal constructor( + private val dateUtil: DateUtil +) { + + val COMMAND_SYNCHRONIZE: Byte = 3 + val COMMAND_SUBSCRIBE: Byte = 4 + val COMMAND_AUTH_REQ: Byte = 5 + val COMMAND_GET_DEVICE_TYPE: Byte = 6 + val COMMAND_SET_TIME: Byte = 10 + val COMMAND_GET_TIME: Byte = 11 + val COMMAND_SET_TIME_ZONE: Byte = 12 + + private val mCrypt = Crypt() + private val timeUtil = MedtrumTimeUtil() + + fun authorize(deviceSerial: Long): ByteArray { + val role = 2 // Fixed to 2 for pump + val key = mCrypt.keyGen(deviceSerial) + return byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) + } + + fun getDeviceType(): ByteArray { + return byteArrayOf(COMMAND_GET_DEVICE_TYPE) + } + + fun getTime(): ByteArray { + return byteArrayOf(COMMAND_GET_TIME) + } + + fun setTime(): ByteArray { + val time = timeUtil.getCurrentTimePumpSeconds() + return byteArrayOf(COMMAND_SET_TIME) + 2.toByte() + time.toByteArray(4) + } + + fun setTimeZone(): ByteArray { + val time = timeUtil.getCurrentTimePumpSeconds() + var offsetMins = dateUtil.getTimeZoneOffsetMinutes(dateUtil.now()) + if (offsetMins < 0) offsetMins += 65536 + return byteArrayOf(COMMAND_SET_TIME_ZONE) + offsetMins.toByteArray(2) + time.toByteArray(4) + } + + fun synchronize(): ByteArray { + return byteArrayOf(COMMAND_SYNCHRONIZE) + } + + fun subscribe(): ByteArray { + return byteArrayOf(COMMAND_SUBSCRIBE) + 4095.toByteArray(2) + } +} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt index dbff1ccc9e..b6ad902279 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/BLEComm.kt @@ -49,11 +49,10 @@ import javax.inject.Inject import javax.inject.Singleton interface BLECommCallback { - - open fun onBLEConnected() {} - open fun onNotification(notification: ByteArray) {} - open fun onIndication(indication: ByteArray) {} - open fun onSendMessageError(reason: String) + fun onBLEConnected() + fun onNotification(notification: ByteArray) + fun onIndication(indication: ByteArray) + fun onSendMessageError(reason: String) } @Singleton @@ -237,7 +236,7 @@ class BLEComm @Inject internal constructor( } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged") + aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged data: " + characteristic.value + "UUID: " + characteristic.getUuid().toString()) val value = characteristic.getValue() if (characteristic.getUuid() == UUID.fromString(READ_UUID)) { @@ -249,8 +248,8 @@ class BLEComm @Inject internal constructor( mReadPacket?.addData(value) } if (mReadPacket?.allDataReceived() == true) { - mReadPacket = null mReadPacket?.getData()?.let { mCallback?.onIndication(it) } + mReadPacket = null } } } diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt index a48ec614d0..aa9fd7e552 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/services/MedtrumService.kt @@ -23,8 +23,10 @@ import info.nightscout.interfaces.queue.Command import info.nightscout.interfaces.queue.CommandQueue import info.nightscout.interfaces.ui.UiInteraction import info.nightscout.pump.medtrum.MedtrumPlugin -import info.nightscout.pump.medtrum.encryption.Crypt -import info.nightscout.pump.medtrum.extension.toByteArray +import info.nightscout.pump.medtrum.comm.WriteCommand +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.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventAppExit @@ -38,6 +40,7 @@ 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.core.ObservableEmitter import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import org.joda.time.DateTime @@ -67,16 +70,18 @@ class MedtrumService : DaggerService(), BLECommCallback { @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var pumpSync: PumpSync @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var writeCommand: WriteCommand - companion object { - private const val COMMAND_AUTH_REQ: Byte = 5 - } + val timeUtil = MedtrumTimeUtil() private val disposable = CompositeDisposable() private val mBinder: IBinder = LocalBinder() - private val mCrypt = Crypt() private var mDeviceSN: Long = 0 + private var currentState: State = IdleState() + + // TODO: Stuff like this in a settings class? + private var mLastDeviceTime: Long = 0 override fun onCreate() { super.onCreate() @@ -106,12 +111,12 @@ class MedtrumService : DaggerService(), BLECommCallback { fun stopConnecting() { // TODO proper way for this might need send commands etc - bleComm.stopConnecting() + // bleComm.stopConnecting() } fun disconnect(from: String) { // TODO proper way for this might need send commands etc - bleComm.disconnect(from) + // bleComm.disconnect(from) } fun readPumpStatus() { @@ -145,49 +150,15 @@ class MedtrumService : DaggerService(), BLECommCallback { // TODO } - fun tempBasal(percent: Int, durationInHours: Int): Boolean { - // TODO - return false - } - - fun highTempBasal(percent: Int): Boolean { - // TODO - return false - } - - fun tempBasalShortDuration(percent: Int, durationInMinutes: Int): Boolean { - if (durationInMinutes != 15 && durationInMinutes != 30) { - aapsLogger.error(LTag.PUMPCOMM, "Wrong duration param") - return false - } - // TODO - return false - } - - fun tempBasalStop(): Boolean { - if (!isConnected) return false - // TODO - return false - } - fun updateBasalsInPump(profile: Profile): Boolean { if (!isConnected) return false // TODO return false } - private fun authorize() { - aapsLogger.debug(LTag.PUMPCOMM, "Start auth!") - val role = 2 // Fixed to 2 for pump - val key = mCrypt.keyGen(mDeviceSN) - val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4) - bleComm.sendMessage(commandData) - } - /** BLECommCallbacks */ override fun onBLEConnected() { - // TODO Replace by FSM Entry? - authorize() + currentState.onConnected() } override fun onNotification(notification: ByteArray) { @@ -197,7 +168,7 @@ class MedtrumService : DaggerService(), BLECommCallback { override fun onIndication(indication: ByteArray) { aapsLogger.debug(LTag.PUMPCOMM, "<<<<< onIndication" + indication.contentToString()) - // TODO + currentState.onIndication(indication) } override fun onSendMessageError(reason: String) { @@ -207,6 +178,7 @@ class MedtrumService : DaggerService(), BLECommCallback { /** Service stuff */ inner class LocalBinder : Binder() { + val serviceInstance: MedtrumService get() = this@MedtrumService } @@ -218,4 +190,208 @@ class MedtrumService : DaggerService(), BLECommCallback { 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() + } + + // State class, Can we move this to different file? + private abstract inner class State { + + open fun onEnter() {} + open fun onIndication(data: ByteArray) { + aapsLogger.debug(LTag.PUMPCOMM, "onIndicationr: " + this.toString() + "Should not be called here!") + } + + open fun onConnected() {} + } + + private inner class IdleState : State() { + + override fun onConnected() { + toState(AuthState()) + } + } + + private inner class AuthState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached AuthState") + bleComm.sendMessage(writeCommand.authorize(mDeviceSN)) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + // TODO Get pump version info (do we care?) + if (responseCode == 0 && commandCode == writeCommand.COMMAND_AUTH_REQ) { + // Succes! + toState(GetDeviceTypeState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class GetDeviceTypeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetDeviceTypeState") + bleComm.sendMessage(writeCommand.getDeviceType()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + // TODO Get device type (do we care?) + if (responseCode == 0 && commandCode == writeCommand.COMMAND_GET_DEVICE_TYPE) { + // Succes! + toState(GetTimeState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class GetTimeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached GetTimeState") + bleComm.sendMessage(writeCommand.getTime()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + val time = data.copyOfRange(6, 10).toLong() + if (responseCode == 0 && commandCode == writeCommand.COMMAND_GET_TIME) { + // Succes! + mLastDeviceTime = time + val currTimeSec = dateUtil.nowWithoutMilliseconds() / 1000 + if (abs(timeUtil.convertPumpTimeToSystemTimeSeconds(time) - currTimeSec) <= 5) { // Allow 5 sec deviation + toState(SynchronizeState()) + } else { + aapsLogger.debug( + LTag.PUMPCOMM, + "GetTimeState.onIndication need to set time. systemTime: " + currTimeSec + " PumpTime: " + time + " Pump Time to system time: " + timeUtil.convertPumpTimeToSystemTimeSeconds( + time + ) + ) + toState(SetTimeState()) + } + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class SetTimeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeState") + bleComm.sendMessage(writeCommand.setTime()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + if (responseCode == 0 && commandCode == writeCommand.COMMAND_SET_TIME) { + // Succes! + toState(SetTimeZoneState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class SetTimeZoneState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SetTimeZoneState") + bleComm.sendMessage(writeCommand.setTimeZone()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + if (responseCode == 0 && commandCode == writeCommand.COMMAND_SET_TIME_ZONE) { + // Succes! + toState(SynchronizeState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class SynchronizeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SynchronizeState") + bleComm.sendMessage(writeCommand.synchronize()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + if (responseCode == 0 && commandCode == writeCommand.COMMAND_SYNCHRONIZE) { + // Succes! + // TODO: Handle pump state parameters + toState(SubscribeState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class SubscribeState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached SubscribeState") + bleComm.sendMessage(writeCommand.subscribe()) + } + + override fun onIndication(data: ByteArray) { + // TODO Create class for this? Maybe combine with authorize write command, something like danaRS packets? + Unit Tests + val commandCode: Byte = data.copyOfRange(1, 2).toInt().toByte() + val responseCode = data.copyOfRange(4, 6).toInt() + if (responseCode == 0 && commandCode == writeCommand.COMMAND_SUBSCRIBE) { + // Succes! + toState(ReadyState()) + } else { + // Failure + bleComm.disconnect("Failure") + toState(IdleState()) + } + } + } + + private inner class ReadyState : State() { + + override fun onEnter() { + aapsLogger.debug(LTag.PUMPCOMM, "Medtrum Service reached ReadyState!") + } + // Just a placeholder, this state is reached when the patch is ready to receive commands (Bolus, temp basal and whatever) + } } \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt new file mode 100644 index 0000000000..256bb850a0 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/util/MedtrumTimeUtil.kt @@ -0,0 +1,20 @@ +package info.nightscout.pump.medtrum.util + +import java.time.Duration +import java.time.Instant + +class MedtrumTimeUtil { + + fun getCurrentTimePumpSeconds() : Long { + val startInstant = Instant.parse("2020-01-01T00:00:00Z") + val currentInstant = Instant.now() + return Duration.between(startInstant, currentInstant).seconds + } + + fun convertPumpTimeToSystemTimeSeconds(pumpTime: Long) : Long { + val startInstant = Instant.parse("2020-01-01T00:00:00Z") + val pumpInstant = startInstant.plusSeconds(pumpTime) + val epochInstant = Instant.EPOCH + return Duration.between(epochInstant, pumpInstant).seconds + } +} \ No newline at end of file From 43515f7b17e7b3b45e7b5a14e0ec53aef4fb06db Mon Sep 17 00:00:00 2001 From: jbr7rr <> Date: Tue, 28 Feb 2023 08:29:45 +0100 Subject: [PATCH 09/97] Buttons in overview --- pump/medtrum/build.gradle | 5 +- .../nightscout/pump/medtrum/MedtrumPlugin.kt | 4 +- .../medtrum/di/MedtrumActivitiesModule.kt | 13 - .../pump/medtrum/di/MedtrumInjectHelpers.kt | 12 + .../pump/medtrum/di/MedtrumModule.kt | 47 ++- .../pump/medtrum/di/MedtrumServicesModule.kt | 11 - .../pump/medtrum/ui/BaseNavigator.kt | 7 + ...Fragment.kt => MedtrumOverviewFragment.kt} | 10 +- .../medtrum/ui/viewmodel/BaseViewModel.kt | 31 ++ .../medtrum/ui/viewmodel/OverviewViewModel.kt | 31 ++ .../medtrum/ui/viewmodel/ViewModelFactory.kt | 40 +++ .../res/layout/medtrum_overview_fragment.xml | 304 +++++++++++++++++ .../main/res/layout/medtrum_pump_fragment.xml | 306 ------------------ pump/medtrum/src/main/res/values/strings.xml | 2 + 14 files changed, 479 insertions(+), 344 deletions(-) delete mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt delete mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/BaseNavigator.kt rename pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/{MedtrumPumpFragment.kt => MedtrumOverviewFragment.kt} (90%) create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/OverviewViewModel.kt create mode 100644 pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt create mode 100644 pump/medtrum/src/main/res/layout/medtrum_overview_fragment.xml delete mode 100644 pump/medtrum/src/main/res/layout/medtrum_pump_fragment.xml diff --git a/pump/medtrum/build.gradle b/pump/medtrum/build.gradle index 9163baf95a..c24e386377 100644 --- a/pump/medtrum/build.gradle +++ b/pump/medtrum/build.gradle @@ -13,13 +13,16 @@ 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') diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt index f799d15e51..959fac5b59 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/MedtrumPlugin.kt @@ -26,7 +26,7 @@ import info.nightscout.interfaces.queue.CommandQueue import info.nightscout.interfaces.queue.CustomCommand import info.nightscout.interfaces.ui.UiInteraction import info.nightscout.interfaces.utils.TimeChangeType -import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment +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 @@ -67,7 +67,7 @@ class MedtrumPlugin @Inject constructor( ) : PumpPluginBase( PluginDescription() .mainType(PluginType.PUMP) - .fragmentClass(MedtrumPumpFragment::class.java.name) + .fragmentClass(MedtrumOverviewFragment::class.java.name) .pluginIcon(info.nightscout.core.ui.R.drawable.ic_eopatch2_128) // TODO .pluginName(R.string.medtrum) .shortName(R.string.medtrum_pump_shortname) diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt deleted file mode 100644 index 8bac156f22..0000000000 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumActivitiesModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package info.nightscout.pump.medtrum.di - -import dagger.Module -import dagger.android.ContributesAndroidInjector -import info.nightscout.pump.medtrum.ui.MedtrumPumpFragment - -@Module -@Suppress("unused") -abstract class MedtrumActivitiesModule { - - @ContributesAndroidInjector abstract fun contributesMedtrumPumpFragment(): MedtrumPumpFragment - -} diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt new file mode 100644 index 0000000000..96118f4670 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumInjectHelpers.kt @@ -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 diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt index 612053b6ec..9fcaf5d9d8 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumModule.kt @@ -1,9 +1,46 @@ 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.androidaps.plugins.pump.eopatch.ui.viewmodel.OverviewViewModel +import info.nightscout.pump.medtrum.services.MedtrumService +import info.nightscout.pump.medtrum.ui.MedtrumOverviewFragment +import info.nightscout.pump.medtrum.ui.viewmodel.OverviewViewModel +// import info.nightscout.pump.medtrum.ui.viewmodel.ViewModel +import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelFactory +import info.nightscout.pump.medtrum.ui.viewmodel.ViewModelKey +import javax.inject.Provider -@Module(includes = [ - MedtrumActivitiesModule::class, - MedtrumServicesModule::class -]) -open class MedtrumModule \ No newline at end of file + +@Module +@Suppress("unused") +abstract class MedtrumModule { + companion object { + + @Provides + @MedtrumPluginQualifier + fun providesViewModelFactory(@MedtrumPluginQualifier viewModels: MutableMap, @JvmSuppressWildcards Provider>): ViewModelProvider.Factory { + return ViewModelFactory(viewModels) + } + } + + // VIEW MODELS + @Binds + @IntoMap + @MedtrumPluginQualifier + @ViewModelKey(OverviewViewModel::class) + internal abstract fun bindsOverviewViewmodel(viewModel: OverviewViewModel): ViewModel + + // FRAGMENTS + @ContributesAndroidInjector + abstract fun contributesMedtrumOverviewFragment(): MedtrumOverviewFragment + + // SERVICE + @ContributesAndroidInjector + abstract fun contributesDanaRSService(): MedtrumService +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt deleted file mode 100644 index 0c587bacc2..0000000000 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/di/MedtrumServicesModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package info.nightscout.pump.medtrum.di - -import dagger.Module -import dagger.android.ContributesAndroidInjector -import info.nightscout.pump.medtrum.services.MedtrumService - -@Module -@Suppress("unused") -abstract class MedtrumServicesModule { - @ContributesAndroidInjector abstract fun contributesDanaRSService(): MedtrumService -} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/BaseNavigator.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/BaseNavigator.kt new file mode 100644 index 0000000000..c1bebb0a93 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/BaseNavigator.kt @@ -0,0 +1,7 @@ +package info.nightscout.pump.medtrum.ui + +interface BaseNavigator { + fun back() + + fun finish(finishAffinity: Boolean = false) +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt similarity index 90% rename from pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt rename to pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt index ac94004d8b..76f36dded1 100644 --- a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumPumpFragment.kt +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/MedtrumOverviewFragment.kt @@ -11,7 +11,7 @@ import info.nightscout.core.extensions.toStringFull import info.nightscout.core.utils.fabric.FabricPrivacy import info.nightscout.interfaces.iob.IobCobCalculator import info.nightscout.interfaces.profile.ProfileFunction -import info.nightscout.pump.medtrum.databinding.MedtrumPumpFragmentBinding +import info.nightscout.pump.medtrum.databinding.MedtrumOverviewFragmentBinding import info.nightscout.pump.medtrum.events.EventMedtrumPumpUpdateGui import info.nightscout.pump.medtrum.MedtrumPlugin import info.nightscout.rx.AapsSchedulers @@ -25,7 +25,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject -class MedtrumPumpFragment : DaggerFragment() { +class MedtrumOverviewFragment : DaggerFragment() { @Inject lateinit var rxBus: RxBus @Inject lateinit var rh: ResourceHelper @@ -41,14 +41,14 @@ class MedtrumPumpFragment : DaggerFragment() { private lateinit var refreshLoop: Runnable private var handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) - private var _binding: MedtrumPumpFragmentBinding? = null + private var _binding: MedtrumOverviewFragmentBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - MedtrumPumpFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root + MedtrumOverviewFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root @Synchronized override fun onResume() { @@ -93,8 +93,6 @@ class MedtrumPumpFragment : DaggerFragment() { binding.baseBasalRate.text = rh.gs(info.nightscout.core.ui.R.string.pump_base_basal_rate, MedtrumPlugin.baseBasalRate) binding.tempbasal.text = iobCobCalculator.getTempBasal(dateUtil.now())?.toStringFull(profile, dateUtil) ?: "" - binding.extendedbolus.text = iobCobCalculator.getExtendedBolus(dateUtil.now())?.toStringFull(dateUtil) - ?: "" binding.battery.text = rh.gs(info.nightscout.core.ui.R.string.format_percent, 0) // TODO binding.reservoir.text = rh.gs(info.nightscout.interfaces.R.string.format_insulin_units, 0.0) // TODO diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000000..2ff064649f --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/BaseViewModel.kt @@ -0,0 +1,31 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import androidx.lifecycle.ViewModel +import info.nightscout.pump.medtrum.ui.BaseNavigator +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import java.lang.ref.WeakReference + +abstract class BaseViewModel : ViewModel() { + + private var _navigator: WeakReference? = null + var navigator: N? + set(value) { + _navigator = WeakReference(value) + } + get() = _navigator?.get() + + private val compositeDisposable = CompositeDisposable() + + override fun onCleared() { + compositeDisposable.clear() + super.onCleared() + } + + fun back() = navigator?.back() + + fun finish() = navigator?.finish() + + fun Disposable.addTo() = apply { compositeDisposable.add(this) } + +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/OverviewViewModel.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/OverviewViewModel.kt new file mode 100644 index 0000000000..c2288e5e23 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/OverviewViewModel.kt @@ -0,0 +1,31 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import info.nightscout.pump.medtrum.ui.BaseNavigator +import info.nightscout.pump.medtrum.ui.viewmodel.BaseViewModel +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.logging.LTag +import javax.inject.Inject + +class OverviewViewModel @Inject constructor( + private val aapsLogger: AAPSLogger +) : BaseViewModel() { + + val isPatchActivated : Boolean + get() = false // TODO + val isPatchConnected: Boolean + get() = false // TODO + + init { + // TODO + } + + fun onClickActivation(){ + aapsLogger.debug(LTag.PUMP, "Start Patch clicked!") + // TODO + } + + fun onClickDeactivation(){ + aapsLogger.debug(LTag.PUMP, "Stop Patch clicked!") + // TODO + } +} \ No newline at end of file diff --git a/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000000..6acf7d4404 --- /dev/null +++ b/pump/medtrum/src/main/java/info/nightscout/pump/medtrum/ui/viewmodel/ViewModelFactory.kt @@ -0,0 +1,40 @@ +package info.nightscout.pump.medtrum.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.MapKey +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.reflect.KClass + +@MustBeDocumented +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +internal annotation class ViewModelKey(val value: KClass) + +@Singleton +class ViewModelFactory @Inject constructor(private val creators: Map, @JvmSuppressWildcards Provider>) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + var creator: Provider? = creators[modelClass] + if (creator == null) { + for ((key, value) in creators) { + if (modelClass.isAssignableFrom(key)) { + creator = value + break + } + } + } + if (creator == null) { + throw IllegalArgumentException("unknown model class $modelClass") + } + try { + @Suppress("UNCHECKED_CAST") + return creator.get() as T + } catch (e: Exception) { + throw IllegalStateException(e) + } + + } +} \ No newline at end of file diff --git a/pump/medtrum/src/main/res/layout/medtrum_overview_fragment.xml b/pump/medtrum/src/main/res/layout/medtrum_overview_fragment.xml new file mode 100644 index 0000000000..f104a351de --- /dev/null +++ b/pump/medtrum/src/main/res/layout/medtrum_overview_fragment.xml @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +