diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 3850ff6da69..d3c601380f7 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -28,6 +28,7 @@ import app.aaps.plugins.smoothing.AvgSmoothingPlugin import app.aaps.plugins.smoothing.ExponentialSmoothingPlugin import app.aaps.plugins.smoothing.NoSmoothingPlugin import app.aaps.plugins.source.DexcomPlugin +import app.aaps.plugins.source.EversensePlugin import app.aaps.plugins.source.GlimpPlugin import app.aaps.plugins.source.GlunovoPlugin import app.aaps.plugins.source.IntelligoPlugin @@ -280,6 +281,7 @@ abstract class PluginsListModule { @IntoMap @IntKey(440) abstract fun bindDexcomPlugin(plugin: DexcomPlugin): PluginBase + @Binds abstract fun bindEversensePlugin(plugin: EversensePlugin): PluginBase @Binds @AllConfigs diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt b/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt index ffb7ca41d07..9026c57d2dd 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt @@ -31,6 +31,8 @@ enum class SourceSensor(val text: String) { SIBIONIC("SI App"), SINO("Sino App"), EVERSENSE("Eversense"), + EVERSENSE_E3("Eversense E3"), + EVERSENSE_365("Eversense 365"), AIDEX("GlucoRx Aidex"), SYAI_TAG("Syai Tag"), RANDOM("Random"), diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt b/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt index 0880a4bafcd..1a79742a288 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt @@ -14,4 +14,6 @@ private val ADVANCED_FILTERING_SENSORS = setOf( SourceSensor.LIBRE_3, SourceSensor.SYAI_TAG, SourceSensor.RANDOM, + SourceSensor.EVERSENSE_365, + SourceSensor.EVERSENSE_E3, ) diff --git a/core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt b/core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt index a12bc51baa1..c1a62d7cb1f 100644 --- a/core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt +++ b/core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt @@ -35,7 +35,13 @@ class SourceSensorExtensionsTest { } @Test - fun `eversense does not support advanced filtering`() { + fun `eversense 365 and e3 support advanced filtering`() { + assertThat(SourceSensor.EVERSENSE_365.advancedFilteringSupported()).isTrue() + assertThat(SourceSensor.EVERSENSE_E3.advancedFilteringSupported()).isTrue() + } + + @Test + fun `eversense legacy does not support advanced filtering`() { assertThat(SourceSensor.EVERSENSE.advancedFilteringSupported()).isFalse() } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt index 0625ae993a0..98f9469aa37 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt @@ -93,6 +93,10 @@ enum class NotificationId( // CGM BG_READINGS_MISSED(27, URGENT, CGM), + EVERSENSE_ALARM(95, URGENT, CGM, allowMultiple = true), + EVERSENSE_FIRMWARE(96, INFO, CGM), + EVERSENSE_RELEASE(97, INFO, CGM), + EVERSENSE_PLACEMENT(98, URGENT, CGM), // Loop / APS EASY_MODE_ENABLED(2, URGENT, LOOP), diff --git a/database/impl/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt b/database/impl/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt index aff2ba7f999..5dabc75bd86 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/entities/GlucoseValue.kt @@ -93,6 +93,8 @@ data class GlucoseValue( MM_600_SERIES, MM_SIMPLERA, EVERSENSE, + EVERSENSE_E3, + EVERSENSE_365, AIDEX, RANDOM, UNKNOWN, diff --git a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourceSensorExtension.kt b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourceSensorExtension.kt index d352ab051a1..fd6d0a0f8f0 100644 --- a/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourceSensorExtension.kt +++ b/database/persistence/src/main/kotlin/app/aaps/database/persistence/converters/SourceSensorExtension.kt @@ -32,6 +32,8 @@ fun GlucoseValue.SourceSensor.fromDb(): SourceSensor = GlucoseValue.SourceSensor.MM_600_SERIES -> SourceSensor.MM_600_SERIES GlucoseValue.SourceSensor.MM_SIMPLERA -> SourceSensor.MM_SIMPLERA GlucoseValue.SourceSensor.EVERSENSE -> SourceSensor.EVERSENSE + GlucoseValue.SourceSensor.EVERSENSE_E3 -> SourceSensor.EVERSENSE_E3 + GlucoseValue.SourceSensor.EVERSENSE_365 -> SourceSensor.EVERSENSE_365 GlucoseValue.SourceSensor.AIDEX -> SourceSensor.AIDEX GlucoseValue.SourceSensor.RANDOM -> SourceSensor.RANDOM GlucoseValue.SourceSensor.UNKNOWN -> SourceSensor.UNKNOWN @@ -76,6 +78,8 @@ fun SourceSensor.toDb(): GlucoseValue.SourceSensor = SourceSensor.MM_600_SERIES -> GlucoseValue.SourceSensor.MM_600_SERIES SourceSensor.MM_SIMPLERA -> GlucoseValue.SourceSensor.MM_SIMPLERA SourceSensor.EVERSENSE -> GlucoseValue.SourceSensor.EVERSENSE + SourceSensor.EVERSENSE_E3 -> GlucoseValue.SourceSensor.EVERSENSE_E3 + SourceSensor.EVERSENSE_365 -> GlucoseValue.SourceSensor.EVERSENSE_365 SourceSensor.AIDEX -> GlucoseValue.SourceSensor.AIDEX SourceSensor.RANDOM -> GlucoseValue.SourceSensor.RANDOM SourceSensor.UNKNOWN -> GlucoseValue.SourceSensor.UNKNOWN diff --git a/plugins/eversense/.gitignore b/plugins/eversense/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/plugins/eversense/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/plugins/eversense/build.gradle.kts b/plugins/eversense/build.gradle.kts new file mode 100644 index 00000000000..2db4c061d47 --- /dev/null +++ b/plugins/eversense/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.ksp) + + id("kotlinx-serialization") + id("android-module-dependencies") + id("test-module-dependencies") +} + +android { + namespace = "com.nightscout.eversense" +} + +dependencies { + api(libs.androidx.core) + api(platform(libs.kotlinx.serialization.bom)) + api(libs.kotlinx.serialization.json) + + api(libs.org.slf4j.api) + api(libs.com.github.tony19.logback.android) + + implementation("org.bouncycastle:bcpkix-jdk18on:1.81") + implementation("org.bouncycastle:bcprov-jdk18on:1.81") + + testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") +} + + diff --git a/plugins/eversense/consumer-rules.pro b/plugins/eversense/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/eversense/proguard-rules.pro b/plugins/eversense/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/plugins/eversense/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 \ No newline at end of file diff --git a/plugins/eversense/src/main/AndroidManifest.xml b/plugins/eversense/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..9189b499de4 --- /dev/null +++ b/plugins/eversense/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt new file mode 100644 index 00000000000..58909fb4748 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt @@ -0,0 +1,316 @@ +package com.nightscout.eversense + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.content.SharedPreferences +import android.os.ParcelUuid +import androidx.core.content.edit +import com.nightscout.eversense.callbacks.EversenseScanCallback +import com.nightscout.eversense.callbacks.EversenseWatcher +import com.nightscout.eversense.models.EversenseState +import com.nightscout.eversense.models.EversenseTransmitterSettings +import com.nightscout.eversense.packets.EversenseE3Communicator +import com.nightscout.eversense.packets.e3.GetSignalStrengthRawPacket +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.util.EversenseScanner +import com.nightscout.eversense.util.StorageKeys +import kotlinx.serialization.json.Json + +class EversenseCGMPlugin { + + // FIX 1: Use ApplicationContext to avoid leaking Activity context. + private var context: Context? = null + + private var bluetoothManager: BluetoothManager? = null + private var preferences: SharedPreferences? = null + private var gattCallback: EversenseGattCallback? = null + + // FIX 2: Lock object for synchronized access to connection state. + private val connectionLock = Any() + + private var scanner: EversenseScanner? = null + var watchers: List = listOf() + + fun setContext(context: Context, loggingEnabled: Boolean) { + // FIX 1: Always store applicationContext. + this.context = context.applicationContext + EversenseLogger.instance.enableLogging(loggingEnabled) + + val preference = context.applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE) + bluetoothManager = context.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + preferences = preference + gattCallback = EversenseGattCallback(this, preference) + } + + fun setSmoothing(value: Boolean): Boolean { + val state = getCurrentState() ?: run { + EversenseLogger.error(TAG, "Cannot set smoothing: current state is null. Has setContext been called?") + return false + } + state.useSmoothing = value + preferences?.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + return true + } + + fun addWatcher(watcher: EversenseWatcher) { + this.watchers += watcher + } + + fun removeWatcher(watcher: EversenseWatcher) { + this.watchers -= watcher + } + + fun isConnected(): Boolean = gattCallback?.isConnected() ?: false + fun is365(): Boolean = gattCallback?.is365() ?: false + + fun getCurrentState(): EversenseState? { + val preferences = preferences ?: run { + EversenseLogger.error(TAG, "No preferences available. Make sure setContext has been called") + return null + } + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + return JSON.decodeFromString(stateJson) + } + + @SuppressLint("MissingPermission") + fun startScan(callback: EversenseScanCallback) { + val bluetoothScanner = bluetoothManager?.adapter?.bluetoothLeScanner ?: run { + EversenseLogger.error(TAG, "No bluetooth manager available. Make sure setContext has been called") + return + } + scanner = EversenseScanner(callback) + // Scan without service UUID filter — Eversense transmitters may not always advertise + // the service UUID before pairing. Show all BLE devices so user can identify their transmitter. + val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() + bluetoothScanner.startScan(null, settings, scanner) + EversenseLogger.info(TAG, "BLE scan started") + } + + // FIX 4: Added public stopScan() so callers can cancel scanning independently. + @SuppressLint("MissingPermission") + fun stopScan() { + val bluetoothScanner = bluetoothManager?.adapter?.bluetoothLeScanner ?: run { + EversenseLogger.error(TAG, "No bluetooth scanner available when trying to stop scan") + return + } + scanner?.let { + bluetoothScanner.stopScan(it) + scanner = null + EversenseLogger.info(TAG, "Scan stopped") + } ?: EversenseLogger.info(TAG, "stopScan called but no active scan found") + } + + // FIX 5: connect() now explicitly differentiates between supplied device vs stored device. + // FIX 6: Synchronized block prevents race condition on connection state check. + @SuppressLint("MissingPermission") + fun connect(device: BluetoothDevice? = null): Boolean { + val bluetoothManager = this.bluetoothManager ?: run { + EversenseLogger.error(TAG, "No bluetooth manager available. Make sure setContext has been called") + return false + } + val gattCallback = this.gattCallback ?: run { + EversenseLogger.error(TAG, "No gattCallback available. Make sure setContext has been called") + return false + } + + stopScan() + + synchronized(connectionLock) { + if (gattCallback.isConnected()) { + EversenseLogger.info(TAG, "Already connected, skipping reconnect") + return true + } + + // Clean up stale GATT connections before connecting + gattCallback.cleanUp() + + return if (device != null) { + EversenseLogger.info(TAG, "Connecting to supplied device: ${device.name}") + // Save address so we can auto-reconnect after app restart or phone reboot + preferences?.edit()?.putString(StorageKeys.REMOTE_DEVICE_KEY, device.address)?.apply() + EversenseLogger.info(TAG, "Saved device address for auto-reconnect: ${device.address}") + device.connectGatt(context, true, gattCallback, android.bluetooth.BluetoothDevice.TRANSPORT_LE) + true + } else { + val address = preferences?.getString(StorageKeys.REMOTE_DEVICE_KEY, null) ?: run { + EversenseLogger.error(TAG, "No device supplied and no stored device address found.") + return false + } + val remoteDevice = bluetoothManager.adapter.getRemoteDevice(address) ?: run { + EversenseLogger.error(TAG, "Could not retrieve remote device for address $address") + return false + } + EversenseLogger.info(TAG, "Reconnecting to stored device: $address") + remoteDevice.connectGatt(context, true, gattCallback, android.bluetooth.BluetoothDevice.TRANSPORT_LE) + true + } + } + } + + // FIX 7: Added disconnect() which calls both disconnect() and close() on the GATT client. + fun clearStoredDevice() { + preferences?.edit()?.remove(StorageKeys.REMOTE_DEVICE_KEY)?.apply() + EversenseLogger.info(TAG, "Cleared stored device address") + } + + fun disconnect() { + val gattCallback = this.gattCallback ?: run { + EversenseLogger.info(TAG, "disconnect() called but no gattCallback exists") + return + } + if (!gattCallback.isConnected()) { + EversenseLogger.info(TAG, "disconnect() called but not currently connected") + return + } + gattCallback.disconnect() + EversenseLogger.info(TAG, "Disconnected from transmitter") + } + + // writeSettings delegates to EversenseE3Communicator. Transmitter type (E3 vs 365) is + // determined at GATT connection time via EversenseSecurityType, not stored in EversenseState. + fun writeSettings(settings: EversenseTransmitterSettings): Boolean { + val preferences = preferences ?: run { + EversenseLogger.error(TAG, "No preferences available. Make sure setContext has been called") + return false + } + val gattCallback = this.gattCallback ?: run { + EversenseLogger.error(TAG, "No gattCallback available. Make sure transmitter is connected before writing settings") + return false + } + if (!gattCallback.isConnected()) { + EversenseLogger.error(TAG, "Transmitter is not connected") + return false + } + return EversenseE3Communicator.writeSettings(gattCallback, preferences, settings) + } + + + // Send a blood glucose calibration value to the transmitter. + // Requires CalibrationReadiness.READY state and an active connection. + // Returns true if the packet was sent successfully, false otherwise. + fun sendCalibration(glucoseMgDl: Int, timestampMs: Long = System.currentTimeMillis()): Boolean { + val gattCallback = this.gattCallback ?: run { + EversenseLogger.error(TAG, "No gattCallback available. Make sure transmitter is connected before calibrating") + return false + } + if (!gattCallback.isConnected()) { + EversenseLogger.error(TAG, "Transmitter is not connected") + return false + } + val state = getCurrentState() ?: run { + EversenseLogger.error(TAG, "Cannot calibrate: state is null") + return false + } + if (state.calibrationReadiness.name != "READY") { + EversenseLogger.error(TAG, "Transmitter is not ready for calibration: ${state.calibrationReadiness}") + return false + } + return try { + if (gattCallback.is365()) { + val packet = com.nightscout.eversense.packets.e365.SetBloodGlucosePointPacket365(glucoseMgDl, timestampMs) + gattCallback.writePacket(packet) + EversenseLogger.info(TAG, "365 calibration sent: $glucoseMgDl mg/dL") + } else { + EversenseE3Communicator.sendCalibration(gattCallback, glucoseMgDl) + } + true + } catch (e: Exception) { + EversenseLogger.error(TAG, "Failed to send calibration: $e") + false + } + } + + + // Triggers both a full sync and a glucose read on the connected transmitter. + // Should be called from a background thread (ioScope). + fun triggerFullSync(force: Boolean = false) { + val gattCallback = this.gattCallback ?: run { + EversenseLogger.error(TAG, "Cannot sync — no gattCallback available") + return + } + val preferences = preferences ?: run { + EversenseLogger.error(TAG, "Cannot sync — no preferences available") + return + } + if (!gattCallback.isConnected()) { + EversenseLogger.error(TAG, "Cannot sync — not connected") + return + } + EversenseLogger.info(TAG, "Triggering full sync on user request") + EversenseE3Communicator.fullSync(gattCallback, preferences, watchers.toList(), force) + EversenseE3Communicator.readGlucose(gattCallback, preferences, watchers.toList()) + // Update placement signal after sync + gattCallback.readRssi() + } + + // Called by EversenseGattCallback when RSSI is read + fun onRssiRead(rssi: Int) { + val preferences = preferences ?: return + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + state.placementSignalRssi = rssi + state.sensorSignalStrength = rssiToStrength(rssi) + preferences.edit()?.putString(StorageKeys.STATE, JSON.encodeToString(state))?.apply() + EversenseLogger.debug(TAG, "RSSI updated: $rssi dBm") + watchers.forEach { it.onStateChanged(state) } + } + + + // Read transmitter-to-sensor signal strength. + // Tries the Eversense 365 ReadSignalStrength packet first. + // Falls back to BLE RSSI for E3 transmitters which don't support the packet. + fun readSignalStrength() { + val gattCallback = this.gattCallback ?: run { EversenseLogger.error(TAG, "Cannot read signal strength — no gattCallback"); return } + val preferences = this.preferences ?: run { EversenseLogger.error(TAG, "Cannot read signal strength — no preferences"); return } + if (!gattCallback.isConnected()) { EversenseLogger.warning(TAG, "Cannot read signal strength — not connected"); return } + try { + val signalStrength = if (gattCallback.is365()) { + val response = gattCallback.writePacket(com.nightscout.eversense.packets.e365.GetSignalStrengthPacket()) + response.signalStrength + } else { + val response = gattCallback.writePacket(GetSignalStrengthRawPacket()) + EversenseLogger.info(TAG, "E3 signal raw: $($response.rawValue) -> $($response.signalStrength)%") + response.signalStrength + } + val stateJson = preferences.getString(com.nightscout.eversense.util.StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + state.sensorSignalStrength = signalStrength + preferences.edit()?.putString(com.nightscout.eversense.util.StorageKeys.STATE, JSON.encodeToString(state))?.apply() + EversenseLogger.info(TAG, "Signal strength: $signalStrength%") + watchers.forEach { it.onStateChanged(state) } + } catch (e: Exception) { + EversenseLogger.warning(TAG, "readSignalStrength failed: $e") + gattCallback.readRssi() + } + } + + private fun rssiToStrength(rssi: Int): Int = when { + rssi == 0 -> 0 + rssi >= -65 -> 100 + rssi >= -75 -> 80 + rssi >= -85 -> 60 + rssi >= -95 -> 40 + else -> 20 + } + + fun readRssi() { + gattCallback?.readRssi() + } + + companion object { + private const val TAG = "EversenseCGMManager" + + // ignoreUnknownKeys: tolerates firmware version differences between E3 and 365 transmitters. + private val JSON = Json { ignoreUnknownKeys = true } + + val instance: EversenseCGMPlugin by lazy { + EversenseCGMPlugin() + } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt new file mode 100644 index 00000000000..f17bf4b0ae7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt @@ -0,0 +1,583 @@ +package com.nightscout.eversense + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothProfile +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper +import androidx.core.content.edit +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.exceptions.EversenseWriteException +import com.nightscout.eversense.packets.Eversense365Communicator +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversenseE3Communicator +import com.nightscout.eversense.packets.e365.AuthIdentityPacket +import com.nightscout.eversense.packets.e365.AuthStartPacket +import com.nightscout.eversense.packets.e365.AuthWhoAmIPacket +import com.nightscout.eversense.packets.e365.Eversense365Packets +import com.nightscout.eversense.packets.e365.KeepAlivePacket +import com.nightscout.eversense.packets.e3.EversenseE3Packets +import com.nightscout.eversense.packets.e3.SaveBondingInformationPacket +import com.nightscout.eversense.util.EversenseCrypto365Util +import com.nightscout.eversense.util.EversenseHttp365Util +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.util.StorageKeys +import java.util.UUID +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import kotlin.jvm.Throws + +class EversenseGattCallback( + private val plugin: EversenseCGMPlugin, + private val preferences: SharedPreferences +) : BluetoothGattCallback() { + + companion object { + private const val TAG = "EversenseGattCallback" + + const val serviceUUID = "c3230001-9308-47ae-ac12-3d030892a211" + + private const val requestUUID = "6eb0f021-a7ba-7e7d-66c9-6d813f01d273" + private const val requestSecureV2UUID = "c3230002-9308-47ae-ac12-3d030892a211" + + private const val responseUUID = "6eb0f024-bd60-7aaa-25a7-0029573f4f23" + private const val responseSecureV2UUID = "c3230003-9308-47ae-ac12-3d030892a211" + private const val magicDescriptorUUID = "00002902-0000-1000-8000-00805f9b34fb" + + private const val WRITE_TIMEOUT_MS = 5000L + private const val CALIBRATION_TIMEOUT_MS = 15000L + } + + // FIX 1: Dedicated BLE executor for callbacks; separate network executor for HTTP calls + // so that network operations in authV2flow() cannot block BLE processing. + private val bleExecutor = Executors.newSingleThreadExecutor() + private val networkExecutor = Executors.newSingleThreadExecutor() + + private val handler = Handler(Looper.getMainLooper()) + private var bluetoothGatt: BluetoothGatt? = null + private var eversenseBluetoothService: BluetoothGattService? = null + private var requestCharacteristic: BluetoothGattCharacteristic? = null + private var responseCharacteristic: BluetoothGattCharacteristic? = null + + private var payloadSize: Int = 20 + private var security: EversenseSecurityType = EversenseSecurityType.None + private var cryptoUtil = EversenseCrypto365Util(preferences) + + // FIX 2: Use AtomicReference for currentPacket to avoid the race condition where a stale + // BLE notification could be processed against the wrong packet between assignment and write. + var currentPacket: AtomicReference = AtomicReference(null) + + // FIX 3: Track connection state with a dedicated flag rather than relying on bluetoothGatt + // being non-null, which is not a reliable indicator of actual connection state. + @Volatile + private var connected: Boolean = false + + // Tracks consecutive status-19 failures to detect transmitter placement issues + @Volatile + private var failedConnectionAttempts: Int = 0 + private val PLACEMENT_WARNING_THRESHOLD = 3 + + // Tracks consecutive general reconnect attempts (reset on successful connection). + // Used to compute exponential backoff so AAPS retries quickly after boot (when the + // official Eversense app temporarily holds the BLE connection) and backs off for + // sustained failures to avoid draining the battery. + @Volatile + private var reconnectAttempts: Int = 0 + + fun isConnected(): Boolean = connected + fun is365(): Boolean = security == EversenseSecurityType.SecureV2 + + // FIX 4: Added disconnect() which calls both disconnect() and close() on the GATT client. + // Calling only disconnect() without close() leaks the underlying GATT client resource. + @SuppressLint("MissingPermission") + fun disconnect() { + bluetoothGatt?.disconnect() + bluetoothGatt?.close() + bluetoothGatt = null + connected = false + EversenseLogger.info(TAG, "GATT disconnected and closed") + } + @SuppressLint("MissingPermission") + fun cleanUp() { + bluetoothGatt?.disconnect() + bluetoothGatt?.close() + bluetoothGatt = null + connected = false + EversenseLogger.info(TAG, "GATT cleaned up before reconnect") + } + @SuppressLint("MissingPermission") + fun readRssi() { + bluetoothGatt?.readRemoteRssi() ?: EversenseLogger.warning(TAG, "Cannot read RSSI — not connected") + } + + @SuppressLint("MissingPermission") + override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { + if (status == BluetoothGatt.GATT_SUCCESS) { + EversenseLogger.debug(TAG, "RSSI: $rssi dBm") + plugin.onRssiRead(rssi) + } else { + EversenseLogger.warning(TAG, "Failed to read RSSI - status: $status") + } + } + + @SuppressLint("MissingPermission") + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + EversenseLogger.info(TAG, "Connection state changed - status: $status, newState: $newState, device: ${gatt.device.name}") + + if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { + bluetoothGatt = gatt + // FIX 3: Set connected flag on confirmed STATE_CONNECTED. + connected = true + // Reset backoff counters on successful connection + reconnectAttempts = 0 + failedConnectionAttempts = 0 + + preferences.edit(commit = true) { + putString(StorageKeys.REMOTE_DEVICE_KEY, gatt.device.address) + } + + // FIX 5: Both connect and disconnect watcher notifications are now dispatched via + // handler.post() so they always arrive on the main thread, preventing UI thread crashes. + handler.post { + plugin.watchers.forEach { it.onConnectionChanged(true) } + } + + if (!gatt.requestMtu(512)) { + EversenseLogger.warning(TAG, "requestMtu returned false — skipping to discoverServices with default payload size") + payloadSize = 20 + gatt.discoverServices() + } + return + } + + if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) { + EversenseLogger.warning(TAG, "Disconnected or failed - status: $status, newState: $newState") + gatt.close() + bluetoothGatt = null + connected = false + + handler.post { + plugin.watchers.forEach { it.onConnectionChanged(false) } + } + + if (status == 19) { + failedConnectionAttempts++ + EversenseLogger.warning(TAG, "Connection terminated by transmitter (status 19) — attempt $failedConnectionAttempts") + if (failedConnectionAttempts >= PLACEMENT_WARNING_THRESHOLD) { + handler.post { plugin.watchers.forEach { it.onTransmitterNotPlaced() } } + } + } else { + failedConnectionAttempts = 0 + } + + val storedAddress = preferences.getString(StorageKeys.REMOTE_DEVICE_KEY, null) + if (storedAddress != null) { + // Exponential backoff so AAPS reclaims the transmitter quickly after boot + // (when the official Eversense app temporarily holds the BLE connection) + // and avoids battery drain during sustained unavailability. + // + // Status 19 = transmitter actively rejected us (placement issue, not competition) — + // use a fixed 30 s interval so we don't spam it. + // Status GATT_SUCCESS = clean disconnect (we or the transmitter closed cleanly) — + // reconnect quickly in 5 s. + // All other status codes (e.g. 133 = GATT_ERROR, device busy) = backoff: + // attempt 0 → 5 s, attempt 1 → 10 s, attempt 2 → 20 s, attempt 3 → 40 s, + // attempt 4+ → 60 s cap. + val delayMs: Long = when { + status == 19 -> 30_000L + status == BluetoothGatt.GATT_SUCCESS -> 5_000L + else -> { + val attempt = reconnectAttempts++ + minOf(5_000L * (1L shl minOf(attempt, 4)), 60_000L) + } + } + EversenseLogger.info(TAG, "Scheduling auto-reconnect in ${delayMs / 1000}s (status: $status, attempt: $reconnectAttempts)") + handler.postDelayed({ + EversenseLogger.info(TAG, "Attempting auto-reconnect (attempt $reconnectAttempts)...") + plugin.connect(null) + }, delayMs) + } else { + EversenseLogger.warning(TAG, "No stored device address — skipping auto-reconnect") + } + } + } + + @SuppressLint("MissingPermission") + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + if (status != 0) { + EversenseLogger.error(TAG, "Failed to set payload size - status: $status") + return + } + + payloadSize = mtu - 3 + EversenseLogger.debug(TAG, "New payload size: $payloadSize") + + val success = gatt?.discoverServices() + EversenseLogger.info(TAG, "Trigger discover services - success: $success") + } + + @SuppressLint("MissingPermission") + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + EversenseLogger.info(TAG, "Discovered services - status: $status") + + if (gatt == null) { + EversenseLogger.error(TAG, "Gatt is null") + return + } + + // FIX 6: Use firstOrNull instead of first. The original code used .first {} which throws + // NoSuchElementException if the service is missing. The null check below it was dead code + // that could never be reached. firstOrNull correctly returns null on no match. + val service = gatt.services.firstOrNull { it.uuid.toString() == serviceUUID } + if (service == null) { + EversenseLogger.error(TAG, "Required service not found -> disconnecting") + gatt.disconnect() + return + } + + eversenseBluetoothService = service + if (service.characteristics.isEmpty()) { + EversenseLogger.error(TAG, "Service has no characteristics -> disconnecting") + gatt.disconnect() + return + } + + var requestChar = service.characteristics.find { it.uuid.toString() == requestUUID } + var responseChar = service.characteristics.find { it.uuid.toString() == responseUUID } + if (requestChar != null && responseChar != null) { + EversenseLogger.info(TAG, "Connected to Eversense E3!") + security = EversenseSecurityType.None + requestCharacteristic = requestChar + responseCharacteristic = responseChar + + gatt.setCharacteristicNotification(requestChar, true) + gatt.setCharacteristicNotification(responseChar, true) + enableNotify(gatt, responseChar) + return + } + + requestChar = service.characteristics.find { it.uuid.toString() == requestSecureV2UUID } + responseChar = service.characteristics.find { it.uuid.toString() == responseSecureV2UUID } + if (requestChar == null || responseChar == null) { + EversenseLogger.error(TAG, "No Eversense request/response characteristic found -> disconnecting") + gatt.disconnect() + return + } + + EversenseLogger.info(TAG, "Connected to Eversense 365!") + security = EversenseSecurityType.SecureV2 + requestCharacteristic = requestChar + responseCharacteristic = responseChar + + gatt.setCharacteristicNotification(requestChar, true) + gatt.setCharacteristicNotification(responseChar, true) + enableNotify(gatt, responseChar) + } + + override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { + EversenseLogger.debug(TAG, "onDescriptorWrite (${descriptor.uuid}) for characteristic (${descriptor.characteristic.uuid}) - status $status") + + if (status == BluetoothGatt.GATT_SUCCESS && descriptor.uuid.toString() == magicDescriptorUUID) { + if (descriptor.characteristic.uuid.toString() == responseUUID) { + bleExecutor.submit { authE3flow() } + } else if (descriptor.characteristic.uuid.toString() == responseSecureV2UUID) { + bleExecutor.submit { authV2flow() } + } + } + } + + // FIX 7: Override both the deprecated and current API 33+ signature of onCharacteristicChanged. + // On Android 13+ (API 33+) the old single-argument override is never called by the system — + // only the new three-argument version is. Without this override, glucose data would be silently + // dropped on API 33+ devices. Both delegates to a shared handler to avoid code duplication. + @SuppressLint("MissingPermission") + @OptIn(ExperimentalStdlibApi::class) + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray + ) { + handleCharacteristicChanged(gatt, value) + } + + @Deprecated("Deprecated in API 33 — overridden for compatibility with Android < 13") + @SuppressLint("MissingPermission") + @OptIn(ExperimentalStdlibApi::class) + @Suppress("DEPRECATION") + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + handleCharacteristicChanged(gatt, characteristic.value) + } + + @SuppressLint("MissingPermission") + @OptIn(ExperimentalStdlibApi::class) + private fun handleCharacteristicChanged(gatt: BluetoothGatt, rawData: ByteArray) { + EversenseLogger.debug(TAG, "Received data: ${rawData.toHexString()}") + + var data = rawData + if (security == EversenseSecurityType.SecureV2) { + data = data.drop(3).toByteArray() + + if (data[0] != Eversense365Packets.AuthenticateResponseId) { + data = cryptoUtil.decrypt(data) + EversenseLogger.debug(TAG, "Decrypted data -> ${data.toHexString()}") + if (data.isEmpty()) { + EversenseLogger.error(TAG, "Failed to decrypt data — re-running security handshake on next connection") + cryptoUtil.disallowUseShortcut() + gatt.disconnect() + return + } + } + } + + if (EversenseE3Packets.isPushPacket(data[0])) { + EversenseLogger.debug(TAG, "Keep Alive packet received (E3)!") + bleExecutor.submit { + EversenseE3Communicator.readGlucose(this, preferences, plugin.watchers) + EversenseE3Communicator.fullSync(this, preferences, plugin.watchers) + } + return + } + + if (Eversense365Packets.isKeepAlivePacket(data[0], data[1])) { + EversenseLogger.debug(TAG, "Keep Alive packet received (365)!") + + val packet = KeepAlivePacket() + packet.appendData(data.toUByteArray()) + val response = packet.parseResponse() ?: return + + val fourHalfMinAgo = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(270) + bleExecutor.submit { + if (response.glucoseDatetime > fourHalfMinAgo) { + Eversense365Communicator.readGlucose(this, preferences, plugin.watchers) + Eversense365Communicator.fullSync(this, preferences, plugin.watchers) + } + } + return + } else if (data.size >= 4 && data[0] == Eversense365Packets.NotificationResponseId && data[1] == 0x03.toByte()) { + // Push alarm notification + val alarmCode = data[3].toInt() and 0xFF + val alarm = com.nightscout.eversense.models.ActiveAlarm( + code = com.nightscout.eversense.enums.EversenseAlarm.from(alarmCode), + codeRaw = alarmCode, + flag = 0, + priority = 0 + ) + EversenseLogger.info(TAG, "Push alarm received: ${alarm.code.title}") + handler.post { + plugin.watchers.forEach { it.onAlarmReceived(alarm) } + } + return + } else if (Eversense365Packets.isNotificationPacket(data[0])) { + EversenseLogger.warning(TAG, "Unknown notification packet received") + return + } + + val packet = currentPacket.get() ?: run { + EversenseLogger.warning(TAG, "currentPacket is null -> skipping packet") + return + } + + synchronized(packet) { + val packetAnnotation = packet.getAnnotation() ?: run { + EversenseLogger.warning(TAG, "annotation is null -> skipping packet") + return + } + + if (EversenseE3Packets.isErrorPacket(data[0])) { + EversenseLogger.error(TAG, "Received error response - data: ${data.toHexString()}") + packet.isErrorResponse = true + packet.notifyAll() + return + } + + if (security == EversenseSecurityType.None) { + if (!packet.skipResponseIdValidation && packetAnnotation.responseId != data[0]) { + EversenseLogger.warning(TAG, "Incorrect responseId - expected: ${packetAnnotation.responseId}, got: ${data[0]}") + return + } + packet.appendData(data.toUByteArray()) + packet.notifyAll() + } else { + if (packetAnnotation.responseId != data[0]) { + EversenseLogger.warning(TAG, "Incorrect responseId - expected: ${packetAnnotation.responseId}, got: ${data[0]}") + return + } + if (packetAnnotation.typeId != data[1]) { + EversenseLogger.warning(TAG, "Incorrect responseType - expected: ${packetAnnotation.typeId}, got: ${data[1]}") + return + } + packet.appendData(data.toUByteArray()) + packet.notifyAll() + } + } + } + + @Suppress("UNCHECKED_CAST") + @SuppressLint("MissingPermission") + @OptIn(ExperimentalStdlibApi::class) + @Throws(EversenseWriteException::class) + fun writePacket(packet: EversenseBasePacket, timeoutMs: Long = WRITE_TIMEOUT_MS): T { + val gatt = bluetoothGatt ?: throw EversenseWriteException("Gatt is null — not connected") + + val requestCharacteristic = requestCharacteristic + ?: throw EversenseWriteException("requestCharacteristic is null") + + val requestData = packet.buildRequest(cryptoUtil, payloadSize) + ?: throw EversenseWriteException("Failed to build request data") + + // FIX 2: Use AtomicReference.set() for thread-safe assignment of currentPacket. + currentPacket.set(packet) + + EversenseLogger.debug(TAG, "Writing data: ${requestData.toHexString()}") + requestCharacteristic.setValue(requestData) + gatt.writeCharacteristic(requestCharacteristic) + + synchronized(packet) { + try { + // FIX 8: Explicitly detect timeout by comparing elapsed time after wait() returns. + // Previously, a timeout would fall through to parseResponse() silently, likely + // producing a confusing cast exception rather than a clear timeout error. + val start = System.currentTimeMillis() + packet.wait(timeoutMs) + val elapsed = System.currentTimeMillis() - start + if (elapsed >= timeoutMs) { + currentPacket.set(null) + throw EversenseWriteException("Timed out waiting for response after ${timeoutMs}ms — packet: ${packet.getAnnotation()?.responseId}") + } else if (packet.isErrorResponse) { + currentPacket.set(null) + throw EversenseWriteException("Transmitter returned error response — packet: ${packet.getAnnotation()?.responseId}") + } + } catch (e: EversenseWriteException) { + throw e + } catch (e: Exception) { + EversenseLogger.error(TAG, "Exception during packet wait: $e") + e.printStackTrace() + } + } + + return try { + val response = packet.parseResponse() + currentPacket.set(null) + response as? T + ?: throw EversenseWriteException("Unable to cast response — packet: ${packet.getAnnotation()?.responseId}") + } catch (e: EversenseWriteException) { + throw e + } catch (e: Exception) { + throw EversenseWriteException("Failed to parse response: $e") + } + } + + private fun authE3flow() { + EversenseLogger.info(TAG, "Starting auth flow E3...") + try { + writePacket(SaveBondingInformationPacket()) + } catch (exception: Exception) { + EversenseLogger.error(TAG, "Auth flow E3 failed: $exception") + return + } + + EversenseLogger.info(TAG, "E3 auth complete — ready for full sync") + EversenseE3Communicator.fullSync(this, preferences, plugin.watchers) + EversenseLogger.info(TAG, "E3 transmitter ready — notifying watchers") + handler.post { plugin.watchers.forEach { it.onTransmitterReady() } } + } + + @SuppressLint("MissingPermission") + private fun authV2flow() { + // FIX 9: Network calls (login, getFleetSecretV2) are dispatched to a separate networkExecutor + // so they do not block the bleExecutor, which must remain available for BLE callbacks. + try { + if (!cryptoUtil.generateKeyPairIfNotExists()) { + bluetoothGatt?.disconnect() + return + } + + if (!cryptoUtil.canUseShortcut()) { + val clientId = cryptoUtil.getClientId() + val whoAmI = writePacket(AuthWhoAmIPacket(clientId)) + + // Dispatch HTTP work to the network executor and block bleExecutor until complete. + val authSession = networkExecutor.submit { + EversenseHttp365Util.login(preferences) + }.get() ?: run { + bluetoothGatt?.disconnect() + return + } + + authSession as? EversenseHttp365Util.LoginResponseModel ?: run { + bluetoothGatt?.disconnect() + return + } + + // Cache access token so it can be used for cloud uploads without re-login + val expiryMs = System.currentTimeMillis() + (authSession.expires_in * 1000L) + preferences.edit().putString(StorageKeys.ACCESS_TOKEN, authSession.access_token) + .putLong(StorageKeys.ACCESS_TOKEN_EXPIRY, expiryMs).apply() + + val fleet = networkExecutor.submit { + EversenseHttp365Util.getFleetSecretV2( + accessToken = authSession.access_token, + serialNumber = whoAmI.serialNumber, + nonce = whoAmI.nonce, + flags = whoAmI.flags, + publicKey = cryptoUtil.getClientPublicKey() + ) + }.get() ?: run { + bluetoothGatt?.disconnect() + return + } + + val fleetResponse = fleet as? EversenseHttp365Util.FleetSecretV2ResponseModel ?: run { + bluetoothGatt?.disconnect() + return + } + + @OptIn(ExperimentalStdlibApi::class) + writePacket( + AuthIdentityPacket(fleetResponse.Result.Certificate?.hexToByteArray() ?: byteArrayOf()) + ) + + cryptoUtil.allowUseShortcut() + } + + val signature = cryptoUtil.generateEphem() ?: run { + bluetoothGatt?.disconnect() + return + } + + val session = writePacket(AuthStartPacket(cryptoUtil.getStartSecret(signature))) + cryptoUtil.generateSessionKey(session.sessionPublicKey) + + EversenseLogger.info(TAG, "365 auth complete — ready for full sync") + Eversense365Communicator.fullSync(this, preferences, plugin.watchers) + EversenseLogger.info(TAG, "365 transmitter ready — notifying watchers") + handler.post { plugin.watchers.forEach { it.onTransmitterReady() } } + + } catch (exception: Exception) { + EversenseLogger.error(TAG, "[365] authV2 failed: $exception") + exception.printStackTrace() + bluetoothGatt?.disconnect() + } + } + + // FIX 10: enableNotify uses the API 33+ writeDescriptor(descriptor, value) overload when + // available, falling back to the deprecated setValue approach on older API levels. + @SuppressLint("MissingPermission") + @Suppress("DEPRECATION") + private fun enableNotify(gatt: BluetoothGatt, responseCharacteristic: BluetoothGattCharacteristic) { + val descriptor = responseCharacteristic.getDescriptor(UUID.fromString(magicDescriptorUUID)) ?: return + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseScanCallback.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseScanCallback.kt new file mode 100644 index 00000000000..201c80d865e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseScanCallback.kt @@ -0,0 +1,7 @@ +package com.nightscout.eversense.callbacks + +import com.nightscout.eversense.models.EversenseScanResult + +interface EversenseScanCallback { + fun onResult(var0: EversenseScanResult) +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseWatcher.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseWatcher.kt new file mode 100644 index 00000000000..8fe8a961583 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseWatcher.kt @@ -0,0 +1,15 @@ +package com.nightscout.eversense.callbacks + +import com.nightscout.eversense.enums.EversenseType +import com.nightscout.eversense.models.ActiveAlarm +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseState + +interface EversenseWatcher { + fun onCGMRead(type: EversenseType, readings: List) + fun onStateChanged(state: EversenseState) + fun onConnectionChanged(connected: Boolean) + fun onAlarmReceived(alarm: ActiveAlarm) {} + fun onTransmitterNotPlaced() {} + fun onTransmitterReady() {} +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/BatteryLevel.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/BatteryLevel.kt new file mode 100644 index 00000000000..12116e77795 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/BatteryLevel.kt @@ -0,0 +1,38 @@ +package com.nightscout.eversense.enums + +enum class BatteryLevel(val code: Int) { + PERCENTAGE_0(0), + PERCENTAGE_5(1), + PERCENTAGE_10(2), + PERCENTAGE_25(3), + PERCENTAGE_35(4), + PERCENTAGE_45(5), + PERCENTAGE_55(6), + PERCENTAGE_65(7), + PERCENTAGE_75(8), + PERCENTAGE_85(9), + PERCENTAGE_95(10), + PERCENTAGE_100(11), + UNKNOWN(255); + + fun toPercentage(): Int = when (this) { + PERCENTAGE_0 -> 0 + PERCENTAGE_5 -> 5 + PERCENTAGE_10 -> 10 + PERCENTAGE_25 -> 25 + PERCENTAGE_35 -> 35 + PERCENTAGE_45 -> 45 + PERCENTAGE_55 -> 55 + PERCENTAGE_65 -> 65 + PERCENTAGE_75 -> 75 + PERCENTAGE_85 -> 85 + PERCENTAGE_95 -> 95 + PERCENTAGE_100 -> 100 + UNKNOWN -> -1 + } + + companion object { + fun from(code: Int): BatteryLevel = + values().firstOrNull { it.code == code } ?: UNKNOWN + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationFlag.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationFlag.kt new file mode 100644 index 00000000000..4a406c2f791 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationFlag.kt @@ -0,0 +1,46 @@ +package com.nightscout.eversense.enums + +enum class CalibrationFlag(val code: Int) { + NOT_ENTERED_FOR_CALIBRATION(0), + ACTUALLY_USED_FOR_CALIBRATION(1), + MARKED_SUSPICIOUS(2), + GLUCOSE_TOO_LOW_TO_READ(3), + GLUCOSE_TOO_HIGH_TO_READ(4), + GLUCOSE_RAPID_CHANGE(5), + INVALID_TIME(6), + INSUFFICIENT_DATA(7), + SENSOR_EOL(8), + DROPOUT_PHASE(9), + AUTO_LINK_MODE_ACTIVE(10), + SENSOR_LED_DISCONNECT(11), + OTHER_FAILURE(12), + THIS_ONE_USED_PREVIOUS_ONE_DELETED(13), + THIS_SUSPICIOUS_PREVIOUS_DELETED(14), + INSUFFICIENT_DATA_POST_FS_ENTRY(15), + UNKNOWN_FAILURE(255); + + fun getTitle(): String = when (this) { + ACTUALLY_USED_FOR_CALIBRATION, + NOT_ENTERED_FOR_CALIBRATION -> "Calibration accepted" + MARKED_SUSPICIOUS -> "Suspicious" + GLUCOSE_TOO_LOW_TO_READ -> "Glucose too low" + GLUCOSE_TOO_HIGH_TO_READ -> "Glucose too high" + GLUCOSE_RAPID_CHANGE -> "Glucose changing too fast" + INVALID_TIME -> "Invalid time" + INSUFFICIENT_DATA, + INSUFFICIENT_DATA_POST_FS_ENTRY -> "Insufficient data" + SENSOR_EOL -> "Sensor End of Life" + DROPOUT_PHASE -> "Dropout phase" + AUTO_LINK_MODE_ACTIVE -> "Autolink" + SENSOR_LED_DISCONNECT -> "Sensor disconnected" + OTHER_FAILURE -> "Other failure" + THIS_ONE_USED_PREVIOUS_ONE_DELETED, + THIS_SUSPICIOUS_PREVIOUS_DELETED -> "Previous calibration deleted" + UNKNOWN_FAILURE -> "Unknown failure" + } + + companion object { + fun from(code: Int): CalibrationFlag = + values().firstOrNull { it.code == code } ?: UNKNOWN_FAILURE + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationMode.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationMode.kt new file mode 100644 index 00000000000..7056d0433bd --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationMode.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.enums + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class CalibrationMode(private val value: Int) { + @SerialName("DAILY_SINGLE") + DAILY_SINGLE(0x01), + + @SerialName("DAILY_DUAL") + DAILY_DUAL(0x02), + + @SerialName("WEEKLY_SINGLE") + WEEKLY_SINGLE(0x03), + + @SerialName("DEFAULT") + DEFAULT(0x04); + + companion object { + fun from365(value: Int): CalibrationMode { + return when(value) { + 0 -> DAILY_SINGLE + 1 -> WEEKLY_SINGLE + else -> DEFAULT + } + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationPhase.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationPhase.kt new file mode 100644 index 00000000000..26fa9616bbd --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationPhase.kt @@ -0,0 +1,62 @@ +package com.nightscout.eversense.enums + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class CalibrationPhase(private val value: Int) { + + @SerialName("WARMING_UP") + WARMING_UP(0x01), + + @SerialName("INITIALIZATION") + INITIALIZATION(0x02), + + @SerialName("DAILY_CALIBRATION") + DAILY_CALIBRATION(0x03), + + @SerialName("WEEKLY_CALIBRATION") + WEEKLY_CALIBRATION(0x08), + + @SerialName("SUSPICIOUS") + SUSPICIOUS(0x04), + + @SerialName("UNKNOWN") + UNKNOWN(0x05), + + @SerialName("DEBUG") + DEBUG(0x06), + + @SerialName("DROPOUT") + DROPOUT(0x07); + + companion object { + fun fromE3(value: Int): CalibrationPhase { + return when(value) { + 1 -> WARMING_UP + 2 -> DAILY_CALIBRATION + 3 -> INITIALIZATION + 4 -> SUSPICIOUS + 5 -> UNKNOWN + 6 -> DEBUG + 7 -> DROPOUT + else -> UNKNOWN + } + } + + // phase = raw CalibrationPhase byte (ordinal from official app CAL_PHASE enum) + // calPerDay = raw NumberOfCalPerDay byte: 0 = daily, 1 = weekly + fun from365(phase: Int, calPerDay: Int = 0): CalibrationPhase { + return when(phase) { + 0 -> UNKNOWN + 1 -> WARMING_UP + 2 -> INITIALIZATION + 3 -> if (calPerDay == 1) WEEKLY_CALIBRATION else DAILY_CALIBRATION + 4 -> SUSPICIOUS + 5 -> DROPOUT + 6 -> DEBUG + else -> UNKNOWN + } + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationReadiness.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationReadiness.kt new file mode 100644 index 00000000000..a5a08766b06 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationReadiness.kt @@ -0,0 +1,73 @@ +package com.nightscout.eversense.enums + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class CalibrationReadiness(private val value: Int) { + /** Calibration can be accepted */ + @SerialName("READY") + READY(0x00), + + /** Transmitter does not have enough data to do a calibration */ + @SerialName("NOT_ENOUGH_DATA") + NOT_ENOUGH_DATA(0x01), + + /** Glucose is too high to do a calibration*/ + @SerialName("GLUCOSE_TOO_HIGH") + GLUCOSE_TOO_HIGH(0x02), + + /** A calibration has already be done in the past 2h */ + @SerialName("TOO_SOON") + TOO_SOON(0x03), + + /** Transmitter is in Dropout phase */ + @SerialName("DROPOUT_PHASE") + DROPOUT_PHASE(0x04), + + /** Implant is in EOL */ + @SerialName("SENSOR_EOL") + SENSOR_EOL(0x05), + + /** No implant is linked to transmitter */ + @SerialName("NO_SENSOR_LINKED") + NO_SENSOR_LINKED(0x06), + + /** Transmitter is in unsupported state */ + @SerialName("UNSUPPORTED_MODE") + UNSUPPORTED_MODE(0x07), + + /** Transmitter is currently calibrating already */ + @SerialName("CALIBRATING") + CALIBRATING(0x08), + + /** Transmitter disconnect detected */ + @SerialName("LED_DISCONNECT_DETECTED") + LED_DISCONNECT_DETECTED(0x09), + + /** Transmitter is in EOL */ + @SerialName("TRANSMITTER_EOL") + TRANSMITTER_EOL(0x0A), + + @SerialName("UNKNOWN") + UNKNOWN(0xFF); + + companion object { + fun from(value: Int): CalibrationReadiness { + return when(value) { + 0 -> READY + 1 -> NOT_ENOUGH_DATA + 2 -> GLUCOSE_TOO_HIGH + 3 -> TOO_SOON + 4 -> DROPOUT_PHASE + 5 -> SENSOR_EOL + 6 -> NO_SENSOR_LINKED + 7 -> UNSUPPORTED_MODE + 8 -> CALIBRATING + 9 -> LED_DISCONNECT_DETECTED + 10 -> TRANSMITTER_EOL + else -> UNKNOWN + } + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CommandError.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CommandError.kt new file mode 100644 index 00000000000..e1035b601bd --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CommandError.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.enums + +enum class CommandError(val code: Int) { + NOT_ALLOWED(1), + UNUSED(2), + INVALID_COMMAND_CODE(3), + INVALID_CRC(4), + INVALID_MESSAGE_LENGTH(5), + BUFFER_OVERFLOW(6), + INVALID_COMMAND_ARGUMENT(7), + SENSOR_READ_ERROR(8), + LOW_BATTERY_ERROR(9), + SENSOR_HARDWARE_FAILURE(10), + TRANSMITTER_HARDWARE_FAILURE(11), + SENSOR_UNABLE_TO_BE_LINKED(12), + TRANSMITTER_IS_BUSY(13), + INVALID_RECORD_NUMBER_RANGE(14), + INVALID_RECORD(15), + CORRUPT_RECORD(16), + CRITICAL_FAULT_ERROR(17), + CRC_ERROR_LOGICAL_BLOCK(18), + ACCESS_DENIED(19), + USB_ONLY(20), + NO_DATA_AVAILABLE(21), + GLUCOSE_BLINDED(22); + + companion object { + fun from(code: Int): CommandError? = values().firstOrNull { it.code == code } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseAlarm.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseAlarm.kt new file mode 100644 index 00000000000..7559f4d7733 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseAlarm.kt @@ -0,0 +1,82 @@ +package com.nightscout.eversense.enums + +enum class EversenseAlarm(val code: Int) { + CRITICAL_FAULT(0), SENSOR_RETIRED(1), EMPTY_BATTERY(2), + SENSOR_TEMPERATURE(3), SENSOR_LOW_TEMPERATURE(4), READER_TEMPERATURE(5), + SENSOR_AWOL(6), INVALID_SENSOR(8), CALIBRATION_REQUIRED(11), + SERIOUSLY_LOW(12), SERIOUSLY_HIGH(13), LOW_GLUCOSE(14), HIGH_GLUCOSE(15), + PREDICTIVE_LOW(18), PREDICTIVE_HIGH(19), RATE_FALLING(20), RATE_RISING(21), + CALIBRATION_GRACE_PERIOD(22), CALIBRATION_EXPIRED(23), + SENSOR_RETIRING_SOON_1(24), SENSOR_RETIRING_SOON_3(26), + SENSOR_RETIRING_SOON_4(27), SENSOR_RETIRING_SOON_5(28), + SENSOR_RETIRING_SOON_6(29), SENSOR_RETIRING_SOON_7(53), + VERY_LOW_BATTERY(31), INVALID_CLOCK(33), SENSOR_STABILITY(34), + TRANSMITTER_DISCONNECTED(35), VIBRATION_CURRENT(36), MSP_ALARM(45), + CALIBRATION_FAILED(47), CALIBRATION_SUSPICIOUS(48), CALIBRATION_NOW(49), + TRANSMITTER_EOL_396(50), TRANSMITTER_EOL_366(51), BATTERY_ERROR(52), + TRANSMITTER_EOL_330(55), TRANSMITTER_EOL_395(56), ONE_CAL(57), + CALIBRATION_SUSPICIOUS_2(59), BATTERY_STATUS(60), SENSOR_CONNECTION(62), + EARLY_SENSOR_RETIREMENT(64), GENERAL_GLUCOSE_SUSPENDED(65), + SENSOR_GRACE(66), SENSOR_SYNC_CONFIRMED(67), TX_DOCKED(68), + TX_UNDOCKED(69), TWO_CAL(90), UNKNOWN(255); + + val title: String get() = when (this) { + CRITICAL_FAULT -> "Transmitter Error" + SENSOR_RETIRED, SENSOR_GRACE, SENSOR_RETIRING_SOON_1, SENSOR_RETIRING_SOON_3, + SENSOR_RETIRING_SOON_4, SENSOR_RETIRING_SOON_5, SENSOR_RETIRING_SOON_6, + SENSOR_RETIRING_SOON_7 -> "Sensor Replacement" + EMPTY_BATTERY -> "Battery Empty" + SENSOR_TEMPERATURE -> "High Sensor Temperature" + SENSOR_LOW_TEMPERATURE -> "Low Sensor Temperature" + READER_TEMPERATURE -> "High Transmitter Temperature" + SENSOR_AWOL -> "No Sensor Detected" + INVALID_SENSOR -> "New Sensor Detected" + CALIBRATION_REQUIRED -> "Calibrate Now" + SERIOUSLY_LOW -> "Out of Range Low Glucose" + SERIOUSLY_HIGH -> "Out of Range High Glucose" + LOW_GLUCOSE -> "Low Glucose" + HIGH_GLUCOSE -> "High Glucose" + PREDICTIVE_LOW -> "Predicted Low Glucose" + PREDICTIVE_HIGH -> "Predicted High Glucose" + RATE_FALLING -> "Rate Falling" + RATE_RISING -> "Rate Rising" + CALIBRATION_GRACE_PERIOD -> "Calibration Past Due" + CALIBRATION_EXPIRED -> "Calibration Expired" + VERY_LOW_BATTERY -> "Low Battery" + INVALID_CLOCK -> "Invalid Transmitter Time" + TRANSMITTER_DISCONNECTED -> "Transmitter Disconnected" + VIBRATION_CURRENT -> "Vibration Motor" + MSP_ALARM -> "Sensor Replacement" + CALIBRATION_FAILED -> "Calibrate Again" + CALIBRATION_SUSPICIOUS -> "New Calibration Needed" + CALIBRATION_NOW, CALIBRATION_SUSPICIOUS_2 -> "Calibrate Now" + TRANSMITTER_EOL_330, TRANSMITTER_EOL_366, TRANSMITTER_EOL_395, + TRANSMITTER_EOL_396 -> "Transmitter Replacement" + BATTERY_ERROR -> "Battery Error" + ONE_CAL -> "1 Weekly Calibration Phase" + TWO_CAL -> "2 Daily Calibration Phase" + BATTERY_STATUS -> "Battery Status" + SENSOR_CONNECTION -> "Sensor Connection" + EARLY_SENSOR_RETIREMENT -> "Sensor Retirement Area" + GENERAL_GLUCOSE_SUSPENDED -> "Glucose Suspend" + SENSOR_SYNC_CONFIRMED -> "Sensor Sync Confirmed" + TX_DOCKED -> "Transmitter Inactive" + TX_UNDOCKED -> "Transmitter Active" + SENSOR_STABILITY -> "Sensor Stability" + UNKNOWN -> "Unknown Error" + } + + val isCritical: Boolean get() = this in listOf( + CALIBRATION_REQUIRED, CALIBRATION_EXPIRED, BATTERY_ERROR, + READER_TEMPERATURE, SENSOR_TEMPERATURE, SENSOR_LOW_TEMPERATURE + ) + + val isWarning: Boolean get() = this in listOf( + CALIBRATION_NOW, CALIBRATION_FAILED + ) + + companion object { + fun from(code: Int): EversenseAlarm = + values().firstOrNull { it.code == code } ?: UNKNOWN + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseE3Memory.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseE3Memory.kt new file mode 100644 index 00000000000..304aafe005f --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseE3Memory.kt @@ -0,0 +1,49 @@ +package com.nightscout.eversense.enums + +enum class EversenseE3Memory(private val address: Long) { + BatteryPercentage(0x0000_0406), + CalibrationReadiness(0x0000_040A), + NextCalibrationDate(0x0000_0470), + NextCalibrationTime(0x0000_0472), + IsOneCalibration(0x0000_0496), + SensorInsertionDate(0x0000_0890), + CalibrationPhase(0x0000_089C), + SensorInsertionTime(0x0000_0892), + LastCalibrationDate(0x0000_08A3), + LastCalibrationTime(0x0000_08A5), + VibrateMode(0x0000_0902), + HighGlucoseAlarmEnabled(0x0000_1029), + HighGlucoseAlarmThreshold(0x0000_110C), + LowGlucoseAlarmThreshold(0x0000_110A), + PredictiveAlert(0x0000_1020), + PredictiveLowTime(0x0000_1021), + PredictiveHighTime(0x0000_1022), + PredictiveLowAlert(0x0000_1027), + PredictiveHighAlert(0x0000_1028), + PredictiveLowTarget(0x0000_1102), + PredictiveHighTarget(0x0000_1104), + RateAlert(0x0000_1010), + RateFallingAlert(0x0000_1025), + RateRisingAlert(0x0000_1026), + RateFallingThreshold(0x0000_1011), + RateRisingThreshold(0x0000_1012), + SensorFieldCurrentRaw(0x0000_0874), + TransmitterSoftwareVersion(0x0000_000A), + TransmitterSoftwareVersionExt(0x0000_00A2), + MmaFeatures(0x0000_0137), + AppVersion(0x0000_0B4B), + BleDisconnect(0x0000_08B2), + HighGlucoseAlarmRepeatIntervalDay(0x0000_1033), + LowGlucoseAlarmRepeatIntervalDay(0x0000_1032), + HighGlucoseAlarmRepeatIntervalNight(0x0000_110F), + LowGlucoseAlarmRepeatIntervalNight(0x0000_110E), + CalibrationsMadeInThisPhase(0x0000_08A1); + + fun getRequestData(): ByteArray { + return byteArrayOf( + this.address.toByte(), + (this.address shr 8).toByte(), + (this.address shr 16).toByte(), + ) + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseSecurityType.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseSecurityType.kt new file mode 100644 index 00000000000..8eee37eab41 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseSecurityType.kt @@ -0,0 +1,10 @@ +package com.nightscout.eversense.enums + +enum class EversenseSecurityType { + // Eversense E3 + None, + + // Eversense 365 -> generation 2 + // Eversense 365 -> generation 1 is deprecated and not available anymore + SecureV2 +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseTrendArrow.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseTrendArrow.kt new file mode 100644 index 00000000000..dff7b445be8 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseTrendArrow.kt @@ -0,0 +1,10 @@ +package com.nightscout.eversense.enums + +enum class EversenseTrendArrow(val type: String) { + NONE("NONE"), + SINGLE_UP("SingleUp"), + FORTY_FIVE_UP("FortyFiveUp"), + FLAT("Flat"), + FORTY_FIVE_DOWN("FortyFiveDown"), + SINGLE_DOWN("SingleDown") +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseType.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseType.kt new file mode 100644 index 00000000000..104f452afd1 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseType.kt @@ -0,0 +1,6 @@ +package com.nightscout.eversense.enums + +enum class EversenseType { + EVERSENSE_E3, + EVERSENSE_365 +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/SignalStrength.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/SignalStrength.kt new file mode 100644 index 00000000000..b9d50555c3b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/SignalStrength.kt @@ -0,0 +1,39 @@ +package com.nightscout.eversense.enums + +enum class SignalStrength(val rawThreshold: Int, val threshold: Int) { + NO_SIGNAL(0, 0), + POOR(350, 350), + VERY_LOW(500, 395), + LOW(800, 494), + GOOD(1300, 705), + EXCELLENT(1600, 903); + + val title: String get() = when (this) { + NO_SIGNAL -> "No signal" + POOR -> "Poor" + VERY_LOW -> "Very low" + LOW -> "Low" + GOOD -> "Good" + EXCELLENT -> "Excellent" + } + + companion object { + fun from365(value: Int): SignalStrength = when { + value >= 75 -> EXCELLENT + value >= 48 -> GOOD + value >= 30 -> LOW + value >= 28 -> VERY_LOW + value >= 25 -> POOR + else -> NO_SIGNAL + } + + fun fromRaw(value: Int): SignalStrength = when { + value >= 1600 -> EXCELLENT + value >= 1300 -> GOOD + value >= 800 -> LOW + value >= 500 -> VERY_LOW + value >= 350 -> POOR + else -> NO_SIGNAL + } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/TransmitterAlert.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/TransmitterAlert.kt new file mode 100644 index 00000000000..6af820fdb0d --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/TransmitterAlert.kt @@ -0,0 +1,150 @@ +package com.nightscout.eversense.enums + +enum class TransmitterAlert(val code: Int) { + CRITICAL_FAULT_ALARM(0), + SENSOR_RETIRED_ALARM(1), + EMPTY_BATTERY_ALARM(2), + SENSOR_TEMPERATURE_ALARM(3), + SENSOR_LOW_TEMPERATURE_ALARM(4), + READER_TEMPERATURE_ALARM(5), + SENSOR_AWOL_ALARM(6), + SENSOR_ERROR_ALARM(7), + INVALID_SENSOR_ALARM(8), + HIGH_AMBIENT_LIGHT_ALARM(9), + RESERVED_1(10), + SERIOUSLY_LOW_ALARM(12), + SERIOUSLY_HIGH_ALARM(13), + LOW_GLUCOSE_ALARM(14), + HIGH_GLUCOSE_ALARM(15), + LOW_GLUCOSE_ALERT(16), + HIGH_GLUCOSE_ALERT(17), + PREDICTIVE_LOW_ALARM(18), + PREDICTIVE_HIGH_ALARM(19), + RATE_FALLING_ALARM(20), + RATE_RISING_ALARM(21), + CALIBRATION_GRACE_PERIOD_ALARM(22), + CALIBRATION_EXPIRED_ALARM(23), + SENSOR_RETIRING_SOON_1_ALARM(24), + SENSOR_RETIRING_SOON_2_ALARM(25), + SENSOR_RETIRING_SOON_3_ALARM(26), + SENSOR_RETIRING_SOON_4_ALARM(27), + SENSOR_RETIRING_SOON_5_ALARM(28), + SENSOR_RETIRING_SOON_6_ALARM(29), + SENSOR_PREMATURE_REPLACEMENT_ALARM(30), + VERY_LOW_BATTERY_ALARM(31), + LOW_BATTERY_ALARM(32), + INVALID_CLOCK_ALARM(33), + SENSOR_STABILITY(34), + TRANSMITTER_DISCONNECTED(35), + VIBRATION_CURRENT_ALARM(36), + SENSOR_AGED_OUT_ALARM(37), + SENSOR_ON_HOLD_ALARM(38), + MEP_ALARM(39), + EDR_ALARM_0(40), + EDR_ALARM_1(41), + EDR_ALARM_2(42), + EDR_ALARM_3(43), + EDR_ALARM_4(44), + MSP_ALARM(45), + RESERVED_2(46), + TRANSMITTER_EOL_396(50), + TRANSMITTER_EOL_366(51), + BATTERY_ERROR_ALARM(52), + SENSOR_RETIRING_SOON_7_ALARM(53), + RESERVED_3(54), + TRANSMITTER_EOL_330(55), + TRANSMITTER_EOL_395(56), + ONE_CAL(57), + TWO_CAL(58), + TRANSMITTER_RECONNECTED(60), + APP_RESERVED_1(63), + SYSTEM_TIME(64), + APP_RESERVED_2(65), + INCOMPATIBLE_TX(66), + SENSOR_FILE(67), + SENSOR_RELINK(68), + NEW_PASSWORD_DETECTED(69), + BATTERY_OPTIMIZATION(70), + NO_ALARM_ACTIVE(71), + NUMBER_OF_MESSAGES(72); + + val canBlindGlucose: Boolean get() = this in setOf( + HIGH_GLUCOSE_ALARM, HIGH_GLUCOSE_ALERT, + LOW_GLUCOSE_ALARM, LOW_GLUCOSE_ALERT, + PREDICTIVE_HIGH_ALARM, PREDICTIVE_LOW_ALARM, + RATE_FALLING_ALARM, RATE_RISING_ALARM + ) + + val title: String get() = when (this) { + CRITICAL_FAULT_ALARM -> "Critical Fault" + SENSOR_RETIRED_ALARM -> "Sensor Retired" + EMPTY_BATTERY_ALARM -> "Empty Battery" + SENSOR_TEMPERATURE_ALARM -> "Sensor High Temperature" + SENSOR_LOW_TEMPERATURE_ALARM -> "Sensor Low Temperature" + READER_TEMPERATURE_ALARM -> "Transmitter High Temperature" + SENSOR_AWOL_ALARM -> "No Sensor Detected" + SENSOR_ERROR_ALARM -> "Sensor Hardware Error" + INVALID_SENSOR_ALARM -> "Invalid Sensor" + HIGH_AMBIENT_LIGHT_ALARM -> "High Ambient Light" + RESERVED_1 -> "Reserved 1" + SERIOUSLY_LOW_ALARM -> "Seriously Low Glucose" + SERIOUSLY_HIGH_ALARM -> "Seriously High Glucose" + LOW_GLUCOSE_ALARM -> "Low Glucose" + HIGH_GLUCOSE_ALARM -> "High Glucose" + LOW_GLUCOSE_ALERT -> "Low Glucose Alert" + HIGH_GLUCOSE_ALERT -> "High Glucose Alert" + PREDICTIVE_LOW_ALARM -> "Predicted Low Glucose" + PREDICTIVE_HIGH_ALARM -> "Predicted High Glucose" + RATE_FALLING_ALARM -> "Rate Falling" + RATE_RISING_ALARM -> "Rate Rising" + CALIBRATION_GRACE_PERIOD_ALARM -> "Calibration Grace Period" + CALIBRATION_EXPIRED_ALARM -> "Calibration Expired" + SENSOR_RETIRING_SOON_1_ALARM -> "Sensor Retiring Soon 1" + SENSOR_RETIRING_SOON_2_ALARM -> "Sensor Retiring Soon 2" + SENSOR_RETIRING_SOON_3_ALARM -> "Sensor Retiring Soon 3" + SENSOR_RETIRING_SOON_4_ALARM -> "Sensor Retiring Soon 4" + SENSOR_RETIRING_SOON_5_ALARM -> "Sensor Retiring Soon 5" + SENSOR_RETIRING_SOON_6_ALARM -> "Sensor Retiring Soon 6" + SENSOR_PREMATURE_REPLACEMENT_ALARM -> "Sensor Premature Replacement" + VERY_LOW_BATTERY_ALARM -> "Very Low Battery" + LOW_BATTERY_ALARM -> "Low Battery" + INVALID_CLOCK_ALARM -> "Invalid Clock" + SENSOR_STABILITY -> "Sensor Instability" + TRANSMITTER_DISCONNECTED -> "Transmitter Disconnected" + VIBRATION_CURRENT_ALARM -> "Vibration Motor" + SENSOR_AGED_OUT_ALARM -> "Sensor Aged Out" + SENSOR_ON_HOLD_ALARM -> "Sensor Suspend" + MEP_ALARM -> "MEP Alarm" + EDR_ALARM_0 -> "EDR Alarm 0" + EDR_ALARM_1 -> "EDR Alarm 1" + EDR_ALARM_2 -> "EDR Alarm 2" + EDR_ALARM_3 -> "EDR Alarm 3" + EDR_ALARM_4 -> "EDR Alarm 4" + MSP_ALARM -> "MSP Alarm" + RESERVED_2 -> "Reserved 2" + TRANSMITTER_EOL_396 -> "Transmitter EOL 396" + TRANSMITTER_EOL_366 -> "Transmitter EOL 366" + BATTERY_ERROR_ALARM -> "Battery Error" + SENSOR_RETIRING_SOON_7_ALARM -> "Sensor Retiring Soon 7" + RESERVED_3 -> "Reserved 3" + TRANSMITTER_EOL_330 -> "Transmitter EOL 330" + TRANSMITTER_EOL_395 -> "Transmitter EOL 395" + ONE_CAL -> "1 Daily Calibration Phase" + TWO_CAL -> "2 Daily Calibrations Phase" + TRANSMITTER_RECONNECTED -> "Transmitter Reconnected" + APP_RESERVED_1 -> "App Reserved 1" + SYSTEM_TIME -> "System Time" + APP_RESERVED_2 -> "App Reserved 2" + INCOMPATIBLE_TX -> "Incompatible Transmitter" + SENSOR_FILE -> "Sensor File" + SENSOR_RELINK -> "Sensor Re-link" + NEW_PASSWORD_DETECTED -> "New Password Detected" + BATTERY_OPTIMIZATION -> "App Performance" + NO_ALARM_ACTIVE -> "No Alarm Active" + NUMBER_OF_MESSAGES -> "Number of Messages" + } + + companion object { + fun from(code: Int): TransmitterAlert? = values().firstOrNull { it.code == code } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/exceptions/EversenseWriteException.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/exceptions/EversenseWriteException.kt new file mode 100644 index 00000000000..604b3cbd50b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/exceptions/EversenseWriteException.kt @@ -0,0 +1,4 @@ +package com.nightscout.eversense.exceptions + +class EversenseWriteException(override val message: String) : Exception(message) { +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/ActiveAlarm.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/ActiveAlarm.kt new file mode 100644 index 00000000000..7af0f83e4b0 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/ActiveAlarm.kt @@ -0,0 +1,12 @@ +package com.nightscout.eversense.models + +import com.nightscout.eversense.enums.EversenseAlarm +import kotlinx.serialization.Serializable + +@Serializable +data class ActiveAlarm( + val code: EversenseAlarm, + val codeRaw: Int, + val flag: Int, + val priority: Int +) diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseCGMResult.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseCGMResult.kt new file mode 100644 index 00000000000..4ec97646598 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseCGMResult.kt @@ -0,0 +1,11 @@ +package com.nightscout.eversense.models + +import com.nightscout.eversense.enums.EversenseTrendArrow + +data class EversenseCGMResult( + val glucoseInMgDl: Int, + val datetime: Long, + val trend: EversenseTrendArrow, + val sensorId: String = "", + val rawResponseHex: String = "" +) diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseScanResult.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseScanResult.kt new file mode 100644 index 00000000000..b3a9bcffef7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseScanResult.kt @@ -0,0 +1,5 @@ +package com.nightscout.eversense.models + +import android.bluetooth.BluetoothDevice + +data class EversenseScanResult(val name: String, val rssi: Int, val device: BluetoothDevice) diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseSecureState.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseSecureState.kt new file mode 100644 index 00000000000..197b1e5a565 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseSecureState.kt @@ -0,0 +1,13 @@ +package com.nightscout.eversense.models + +import kotlinx.serialization.Serializable + +@Serializable +class EversenseSecureState { + var canUseShortcut: Boolean = false + var username: String = "" + var password: String = "" + var clientId: String = "" + var privateKey: String = "" + var publicKey: String = "" +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseState.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseState.kt new file mode 100644 index 00000000000..6a4589f47b9 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseState.kt @@ -0,0 +1,50 @@ +package com.nightscout.eversense.models + +import com.nightscout.eversense.enums.CalibrationMode +import com.nightscout.eversense.enums.CalibrationPhase +import com.nightscout.eversense.enums.CalibrationReadiness +import kotlinx.serialization.Serializable + +@Serializable +class EversenseState { + var lastSync: Long = 0 + var useSmoothing: Boolean = false + var insertionDate: Long = 0 + var calibrationPhase: CalibrationPhase = CalibrationPhase.UNKNOWN + var calibrationReadiness: CalibrationReadiness = CalibrationReadiness.UNKNOWN + var calibrationMode: CalibrationMode = CalibrationMode.DEFAULT + var nextCalibrationDate: Long = 0 + var lastCalibrationDate: Long = 0 + var batteryPercentage: Int = 0 + var recentGlucoseDatetime: Long = 0 + var recentGlucoseValue: Int = 0 + var lastGlucoseRaw: Int = 0 + var placementSignalRssi: Int = 0 + var sensorSignalStrength: Int = 0 + var activeAlarms: List = emptyList() + var firmwareVersion: String = "" + var mmaFeatures: Int = 0 + var extFirmwareVersion: String = "" + var transmitterSerialNumber: String = "" + var transmitterName: String = "" + var sensorId: String = "" + var settings = EversenseTransmitterSettings() +} + +@Serializable +class EversenseTransmitterSettings { + var vibrateEnabled: Boolean = true + var glucoseHighAlarmEnabled: Boolean = true + var glucoseHighAlarmThreshold: Int = 250 + var glucoseLowAlarmThreshold: Int = 60 + var rateFallingAlarmEnabled: Boolean = true + var rateFallingAlarmThreshold: Double = 1.5 + var rateRisingAlarmEnabled: Boolean = true + var rateRisingAlarmThreshold: Double = 1.5 + var predictiveHighAlarmEnabled: Boolean = true + var predictiveHighAlarmThreshold: Int = 180 + var predictiveHighAlarmMinutes: Int = 5 + var predictiveLowAlarmEnabled: Boolean = true + var predictiveLowAlarmThreshold: Int = 70 + var predictiveLowAlarmMinutes: Int = 5 +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/GlucoseHistoryItem.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/GlucoseHistoryItem.kt new file mode 100644 index 00000000000..b5b7c705946 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/GlucoseHistoryItem.kt @@ -0,0 +1,9 @@ +package com.nightscout.eversense.models + +import com.nightscout.eversense.enums.EversenseTrendArrow + +data class GlucoseHistoryItem( + val valueInMgDl: Int, + val datetime: Long, + val trend: EversenseTrendArrow +) diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/Eversense365Communicator.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/Eversense365Communicator.kt new file mode 100644 index 00000000000..a4f0fa68742 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/Eversense365Communicator.kt @@ -0,0 +1,228 @@ +package com.nightscout.eversense.packets + +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper +import androidx.core.content.edit +import com.nightscout.eversense.EversenseGattCallback +import com.nightscout.eversense.callbacks.EversenseWatcher +import com.nightscout.eversense.enums.EversenseType +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseState +import com.nightscout.eversense.models.EversenseTransmitterSettings +import com.nightscout.eversense.packets.e365.GetActiveAlarmsPacket +import com.nightscout.eversense.packets.e365.Ping365Packet +import com.nightscout.eversense.packets.e365.SetAppVersion365Packet +import com.nightscout.eversense.packets.e365.SetBleDisconnect365Packet +import com.nightscout.eversense.packets.e365.GetGlucoseLogValuesPacket +import com.nightscout.eversense.packets.e365.GetLogRangePacket365 +import com.nightscout.eversense.packets.e365.SetHighGlucoseAlarm365Packet +import com.nightscout.eversense.packets.e365.SetHighGlucoseAlarmEnabled365Packet +import com.nightscout.eversense.packets.e365.SetLowGlucoseAlarm365Packet +import com.nightscout.eversense.packets.e365.SetPredictionHighEnabled365Packet +import com.nightscout.eversense.packets.e365.SetPredictionHighThreshold365Packet +import com.nightscout.eversense.packets.e365.SetPredictionHighTime365Packet +import com.nightscout.eversense.packets.e365.SetPredictionLowEnabled365Packet +import com.nightscout.eversense.packets.e365.SetPredictionLowThreshold365Packet +import com.nightscout.eversense.packets.e365.SetPredictionLowTime365Packet +import com.nightscout.eversense.packets.e365.SetRateFallingEnabled365Packet +import com.nightscout.eversense.packets.e365.SetRateFallingThreshold365Packet +import com.nightscout.eversense.packets.e365.SetRateRisingEnabled365Packet +import com.nightscout.eversense.packets.e365.SetRateRisingThreshold365Packet +import com.nightscout.eversense.packets.e365.SetRepeatHighGlucose365Packet +import com.nightscout.eversense.packets.e365.SetRepeatLowGlucose365Packet +import com.nightscout.eversense.packets.e365.SetVibrateMode365Packet +import com.nightscout.eversense.packets.e365.LogType +import com.nightscout.eversense.packets.e365.GetCalibrationInfoPacket +import com.nightscout.eversense.packets.e365.GetGlucoseDataPacket +import com.nightscout.eversense.packets.e365.GetPatientSettingsPacket +import com.nightscout.eversense.packets.e365.GetSensorInformationPacket +import com.nightscout.eversense.packets.e365.SetCurrentDateTimePacket +import com.nightscout.eversense.util.EselSmoothing +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.util.StorageKeys +import kotlinx.serialization.json.Json +import kotlin.math.abs + +class Eversense365Communicator { + companion object { + private const val TAG = "EversenseE3Communicator" + private val JSON = Json { ignoreUnknownKeys = true } + private val handler = Handler(Looper.getMainLooper()) + + private var sensorIdLength = 10 + + fun readGlucose(gatt: EversenseGattCallback, preferences: SharedPreferences, watchers: List) { + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + + val glucoseData = gatt.writePacket(GetGlucoseDataPacket(sensorIdLength)) + if (glucoseData.datetime <= state.recentGlucoseDatetime) { + EversenseLogger.warning(TAG, "Glucose data is still recent after reading - currentReading: ${glucoseData.datetime}, lastReading: ${state.recentGlucoseDatetime}") + return + } + + if (glucoseData.glucoseInMgDl > 1000) { + EversenseLogger.error(TAG, "recentGlucose exceeds range - received: ${glucoseData.glucoseInMgDl}") + return + } + + var currentGlucose = glucoseData.glucoseInMgDl + if (state.useSmoothing && state.recentGlucoseValue > 0 && state.lastGlucoseRaw > 0) { + currentGlucose = EselSmoothing.smooth(currentGlucose, state.recentGlucoseValue, state.lastGlucoseRaw) + } + + val result = mutableListOf() + state.recentGlucoseDatetime = glucoseData.datetime + state.recentGlucoseValue = currentGlucose + state.lastGlucoseRaw = glucoseData.glucoseInMgDl + state.sensorSignalStrength = glucoseData.signalStrength + EversenseLogger.info(TAG, "Sensor signal strength from glucose packet: ${glucoseData.signalStrength}") + + state.sensorId = glucoseData.sensorId + + result += EversenseCGMResult( + glucoseInMgDl = currentGlucose, + datetime = glucoseData.datetime, + trend = glucoseData.trend, + sensorId = glucoseData.sensorId, + rawResponseHex = glucoseData.rawResponseHex + ) + + // Read glucose history for backfill + try { + val logRange = gatt.writePacket(GetLogRangePacket365(LogType.GLUCOSE)) + val range = com.nightscout.eversense.util.RangeCalculator.calculateGlucoseRange( + logRange.rangeFrom, logRange.rangeTo, state.recentGlucoseDatetime + ) + val history = gatt.writePacket( + GetGlucoseLogValuesPacket(from = range.from, to = range.to, sensorIdLength = sensorIdLength) + ) + val backfill = history.glucoseHistory + .filter { it.datetime > state.recentGlucoseDatetime } + .map { item -> EversenseCGMResult(glucoseInMgDl = item.valueInMgDl, datetime = item.datetime, trend = item.trend) } + if (backfill.isNotEmpty()) { + result.addAll(0, backfill) + EversenseLogger.info(TAG, "Backfill: added ${backfill.size} historical readings") + } + } catch (e: Exception) { + EversenseLogger.warning(TAG, "Could not read glucose history: $e") + } + + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + + handler.post { + watchers.forEach { it.onCGMRead(EversenseType.EVERSENSE_365, result) } + watchers.forEach { it.onStateChanged(state) } + } + } + + fun fullSync(gatt: EversenseGattCallback, preferences: SharedPreferences, watchers: List) { + try { + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + + var sensorInformation = gatt.writePacket(GetSensorInformationPacket()) + + // Ping transmitter first — matches iOS fullSync order + try { gatt.writePacket(Ping365Packet()) } catch (e: Exception) { EversenseLogger.warning(TAG, "Ping failed: $e") } + + if (abs(System.currentTimeMillis() - sensorInformation.transmitterDatetime) > 10_000) { + EversenseLogger.debug(TAG, "Updating transmitter datetime") + gatt.writePacket(SetCurrentDateTimePacket()) + sensorInformation = gatt.writePacket(GetSensorInformationPacket()) + } + + sensorIdLength = sensorInformation.sensorIdLength + state.insertionDate = sensorInformation.insertionDate + state.batteryPercentage = sensorInformation.batteryLevel + state.firmwareVersion = sensorInformation.version + state.extFirmwareVersion = sensorInformation.extVersion + state.transmitterSerialNumber = sensorInformation.serialNumber + state.transmitterName = sensorInformation.transmitterName + EversenseLogger.info(TAG, "Transmitter serialNumber='${sensorInformation.serialNumber}' transmitterName='${sensorInformation.transmitterName}'") + EversenseLogger.info(TAG, "Firmware version: ${sensorInformation.version} / ${sensorInformation.extVersion}") + + val calibrationInfo = gatt.writePacket(GetCalibrationInfoPacket()) + state.calibrationPhase = calibrationInfo.currentPhase + state.calibrationReadiness = calibrationInfo.calibrationReadiness + state.calibrationMode = calibrationInfo.calibrationMode + state.nextCalibrationDate = calibrationInfo.nextCalibration + state.lastCalibrationDate = calibrationInfo.lastCalibration + + val patientSettings = gatt.writePacket(GetPatientSettingsPacket()) + state.settings.vibrateEnabled = patientSettings.vibrateMode + state.settings.glucoseHighAlarmEnabled = patientSettings.highGlucoseEnabled + state.settings.glucoseHighAlarmThreshold = patientSettings.highGlucoseAlarmInMgDl + state.settings.glucoseLowAlarmThreshold = patientSettings.lowGlucoseAlarmInMgDl + state.settings.rateFallingAlarmEnabled = patientSettings.rateFallingEnabled + state.settings.rateFallingAlarmThreshold = patientSettings.rateFallingThreshold + state.settings.rateRisingAlarmEnabled = patientSettings.rateRisingEnabled + state.settings.rateRisingAlarmThreshold = patientSettings.rateRisingThreshold + state.settings.predictiveHighAlarmEnabled = patientSettings.predictionHighEnabled + state.settings.predictiveHighAlarmMinutes = patientSettings.predictionRisingInterval + state.settings.predictiveHighAlarmThreshold = patientSettings.predictionRisingThreshold + state.settings.predictiveLowAlarmEnabled = patientSettings.predictionLowEnabled + state.settings.predictiveLowAlarmMinutes = patientSettings.predictionFallingInterval + state.settings.predictiveLowAlarmThreshold = patientSettings.predictionFallingThreshold + + // Send app version — iOS sends "8.0.4" in every fullSync + try { gatt.writePacket(SetAppVersion365Packet()) } catch (e: Exception) { EversenseLogger.warning(TAG, "SetAppVersion failed: $e") } + + // Set BLE disconnect timeout to 5 minutes matching iOS default + try { gatt.writePacket(SetBleDisconnect365Packet(300)) } catch (e: Exception) { EversenseLogger.warning(TAG, "SetBleDisconnect failed: $e") } + + // Read active alarms + try { + val activeAlarms = gatt.writePacket(GetActiveAlarmsPacket()) + state.activeAlarms = activeAlarms.alarms + EversenseLogger.info(TAG, "Active alarms: ${activeAlarms.alarms.map { it.code.title }}") + } catch (e: Exception) { + EversenseLogger.warning(TAG, "Could not read active alarms: $e") + } + + state.lastSync = System.currentTimeMillis() + EversenseLogger.info(TAG, "Completed full sync - datetime: ${state.lastSync}") + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + + handler.post { + watchers.forEach { it.onStateChanged(state) } + } + } catch (exception: Exception) { + EversenseLogger.error(TAG, "Failed to do full sync: $exception") + exception.printStackTrace() + } + } + + fun writeSettings(gatt: EversenseGattCallback, preferences: SharedPreferences, settings: EversenseTransmitterSettings): Boolean { + return try { + gatt.writePacket(SetVibrateMode365Packet(settings.vibrateEnabled)) + gatt.writePacket(SetHighGlucoseAlarmEnabled365Packet(settings.glucoseHighAlarmEnabled)) + gatt.writePacket(SetHighGlucoseAlarm365Packet(settings.glucoseHighAlarmThreshold)) + gatt.writePacket(SetLowGlucoseAlarm365Packet(settings.glucoseLowAlarmThreshold)) + gatt.writePacket(SetRateFallingEnabled365Packet(settings.rateFallingAlarmEnabled)) + gatt.writePacket(SetRateFallingThreshold365Packet(settings.rateFallingAlarmThreshold)) + gatt.writePacket(SetRateRisingEnabled365Packet(settings.rateRisingAlarmEnabled)) + gatt.writePacket(SetRateRisingThreshold365Packet(settings.rateRisingAlarmThreshold)) + gatt.writePacket(SetPredictionLowEnabled365Packet(settings.predictiveLowAlarmEnabled)) + gatt.writePacket(SetPredictionLowThreshold365Packet(settings.predictiveLowAlarmThreshold)) + gatt.writePacket(SetPredictionLowTime365Packet(settings.predictiveLowAlarmMinutes)) + gatt.writePacket(SetPredictionHighEnabled365Packet(settings.predictiveHighAlarmEnabled)) + gatt.writePacket(SetPredictionHighThreshold365Packet(settings.predictiveHighAlarmThreshold)) + gatt.writePacket(SetPredictionHighTime365Packet(settings.predictiveHighAlarmMinutes)) + EversenseLogger.info(TAG, "365 settings written successfully") + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(JSON.decodeFromString(preferences.getString(StorageKeys.STATE, null) ?: "{}").also { it.settings = settings })) + } + true + } catch (e: Exception) { + EversenseLogger.error(TAG, "Failed to write 365 settings: $e") + false + } + } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseBasePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseBasePacket.kt new file mode 100644 index 00000000000..404d1d66c90 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseBasePacket.kt @@ -0,0 +1,106 @@ +package com.nightscout.eversense.packets + +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.e3.EversenseE3Packets +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer +import com.nightscout.eversense.packets.e365.Eversense365Packets +import com.nightscout.eversense.util.EversenseCrypto365Util +import kotlin.math.min + +abstract class EversenseBasePacket : Object() { + abstract fun getRequestData(): ByteArray + abstract fun parseResponse(): Response? + + protected var receivedData = UByteArray(0) + @Volatile var isErrorResponse: Boolean = false + open val skipResponseIdValidation: Boolean = false + + fun getAnnotation(): EversensePacket? { + return this.javaClass.annotations.find { it.annotationClass == EversensePacket::class } as? EversensePacket + } + + protected fun getStartIndex(): Int { + val annotation = getAnnotation() ?:run { + EversenseLogger.error("EversenseBasePacket", this.javaClass.name + " does not have the EversensePacket annotation...") + return 0 + } + + return when(annotation.responseId) { + EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + EversenseE3Packets.ReadFourByteSerialFlashRegisterResponseId -> 4 + + else -> 1 + } + } + + fun appendData(data: UByteArray) { + receivedData += data + } + + fun buildRequest(cryptoUtil: EversenseCrypto365Util, payloadSize: Int): ByteArray? { + val annotation = getAnnotation() ?:run { + EversenseLogger.error("EversenseBasePacket", this.javaClass.name + " does not have the EversensePacket annotation...") + return null + } + + when(annotation.securityType) { + EversenseSecurityType.None -> { + var requestData = byteArrayOf(annotation.requestId) + requestData += this.getRequestData() + requestData += EversenseE3Writer.generateChecksumCRC16(requestData) + + return requestData + } + + EversenseSecurityType.SecureV2 -> { + var requestData = byteArrayOf(annotation.requestId, annotation.typeId) + requestData += this.getRequestData() + + if (annotation.requestId != Eversense365Packets.AuthenticateCommandId) { + requestData = cryptoUtil.encrypt(requestData) + } + + return encodeMessage(requestData, payloadSize) + } + } + } + + private fun encodeMessage(data: ByteArray = getRequestData(), chunkSize: Int = 20): ByteArray { + val adjustedChunkSize = chunkSize - 2 + val totalChunks = (data.size + adjustedChunkSize - 1) / adjustedChunkSize + + // Calculate total size needed for the result array + val totalHeaderSize = 3 + 2 * (totalChunks - 1) + val totalSize = totalHeaderSize + data.size + + val result = ByteArray(totalSize) + var currentIndex = 0 + var currentPos = 0 + + for (chunkIndex in 1..totalChunks) { + val header: ByteArray = if (chunkIndex == 1) { + byteArrayOf(1.toByte(), totalChunks.toByte(), 1.toByte()) + } else { + byteArrayOf(chunkIndex.toByte(), totalChunks.toByte()) + } + + // Copy the header into the result array + header.copyInto(result, currentPos) + currentPos += header.size + + // Determine the end index of the current chunk + val endIndex = min(currentIndex + adjustedChunkSize, data.size) + val chunk = data.copyOfRange(currentIndex, endIndex) + + chunk.copyInto(result, currentPos) + currentPos += chunk.size + currentIndex = endIndex + } + + return result + } + + abstract class Response {} +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseE3Communicator.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseE3Communicator.kt new file mode 100644 index 00000000000..cc6b9976818 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseE3Communicator.kt @@ -0,0 +1,300 @@ +package com.nightscout.eversense.packets + +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper +import androidx.core.content.edit +import com.nightscout.eversense.EversenseGattCallback +import com.nightscout.eversense.callbacks.EversenseWatcher +import com.nightscout.eversense.enums.CalibrationMode +import com.nightscout.eversense.enums.EversenseType +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseState +import com.nightscout.eversense.models.EversenseTransmitterSettings +import com.nightscout.eversense.packets.e3.GetBatteryPercentagePacket +import com.nightscout.eversense.packets.e3.GetVersionPacket +import com.nightscout.eversense.packets.e3.GetVersionExtendedPacket +import com.nightscout.eversense.packets.e3.GetMmaFeaturesPacket +import com.nightscout.eversense.packets.e3.GetHighGlucoseRepeatIntervalPacket +import com.nightscout.eversense.packets.e3.GetLowGlucoseRepeatIntervalPacket +import com.nightscout.eversense.packets.e3.SetBleDisconnectPacket +import com.nightscout.eversense.packets.e3.SetAppVersionE3Packet +import com.nightscout.eversense.packets.e3.GetCalibrationDailyPacket +import com.nightscout.eversense.packets.e3.GetCalibrationPhasePacket +import com.nightscout.eversense.packets.e3.GetCalibrationReadinessPacket +import com.nightscout.eversense.packets.e3.GetCurrentDatetimePacket +import com.nightscout.eversense.packets.e3.GetCurrentGlucosePacket +import com.nightscout.eversense.packets.e3.GetInsertionDatePacket +import com.nightscout.eversense.packets.e3.GetInsertionTimePacket +import com.nightscout.eversense.packets.e3.GetLastCalibrationDatePacket +import com.nightscout.eversense.packets.e3.GetLastCalibrationTimePacket +import com.nightscout.eversense.packets.e3.GetNextCalibrationDatePacket +import com.nightscout.eversense.packets.e3.GetNextCalibrationTimePacket +import com.nightscout.eversense.packets.e3.GetSettingGlucoseHighEnabled +import com.nightscout.eversense.packets.e3.GetSettingGlucoseHighThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingGlucoseLowThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveHighEnabledPacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveHighThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveHighTimePacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveLowEnabledPacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveLowThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingPredictiveLowTimePacket +import com.nightscout.eversense.packets.e3.GetSettingRateFallingEnabledPacket +import com.nightscout.eversense.packets.e3.GetSettingRateFallingThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingRateRisingEnabledPacket +import com.nightscout.eversense.packets.e3.GetSettingRateRisingThresholdPacket +import com.nightscout.eversense.packets.e3.GetSettingVibratePacket +import com.nightscout.eversense.packets.e3.SetBloodGlucosePointPacket +import com.nightscout.eversense.packets.e3.SetCurrentDatetimePacket +import com.nightscout.eversense.packets.e3.SetSettingGlucoseHighEnablePacket +import com.nightscout.eversense.packets.e3.SetSettingGlucoseHighThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingGlucoseLowThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveHighAlarmEnabledPacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveHighThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveHighTimePacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveLowAlarmEnabledPacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveLowThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingPredictiveLowTimePacket +import com.nightscout.eversense.packets.e3.SetSettingRateFallingEnabledPacket +import com.nightscout.eversense.packets.e3.SetSettingRateFallingThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingRateRisingEnabledPacket +import com.nightscout.eversense.packets.e3.SetSettingRateRisingThresholdPacket +import com.nightscout.eversense.packets.e3.SetSettingVibratePacket +import com.nightscout.eversense.util.EselSmoothing +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.util.StorageKeys +import kotlinx.serialization.json.Json +import java.util.concurrent.TimeUnit + +class EversenseE3Communicator { + companion object { + private const val TAG = "EversenseE3Communicator" + private val JSON = Json { ignoreUnknownKeys = true } + private val handler = Handler(Looper.getMainLooper()) + + fun readGlucose(gatt: EversenseGattCallback, preferences: SharedPreferences, watchers: List) { + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + val fourHalfMinAgo = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(270) + + if (fourHalfMinAgo < state.recentGlucoseDatetime) { + EversenseLogger.warning(TAG, "Glucose data is still recent - lastReading: ${state.recentGlucoseDatetime}") + return + } + + try { + EversenseLogger.debug(TAG, "Reading current glucose...") + val glucoseData = gatt.writePacket(GetCurrentGlucosePacket()) + if (glucoseData.datetime <= state.recentGlucoseDatetime) { + EversenseLogger.warning(TAG, "Glucose data is still recent after reading - currentReading: ${glucoseData.datetime}, lastReading: ${state.recentGlucoseDatetime}") + return + } + + if (glucoseData.glucoseInMgDl > 1000) { + EversenseLogger.error(TAG, "recentGlucose exceeds range - received: ${glucoseData.glucoseInMgDl}") + return + } + + var currentGlucose = glucoseData.glucoseInMgDl + if (state.useSmoothing && state.recentGlucoseValue > 0 && state.lastGlucoseRaw > 0) { + currentGlucose = EselSmoothing.smooth(currentGlucose, state.recentGlucoseValue, state.lastGlucoseRaw) + } + + val result = mutableListOf() + state.recentGlucoseDatetime = glucoseData.datetime + state.recentGlucoseValue = currentGlucose + state.lastGlucoseRaw = glucoseData.glucoseInMgDl + result += EversenseCGMResult( + glucoseInMgDl = currentGlucose, + datetime = glucoseData.datetime, + trend = glucoseData.trend + ) + + // TODO: read history for backfill + + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + + // Read RSSI to update placement signal after each glucose reading + try { + EversenseLogger.debug(TAG, "Reading RSSI for placement signal...") + } catch (e: Exception) { + EversenseLogger.warning(TAG, "Failed to read RSSI: $e") + } + + handler.post { + watchers.forEach { + it.onCGMRead(EversenseType.EVERSENSE_E3, result) + } + } + } catch (exception: Exception) { + EversenseLogger.error(TAG, "Got exception during readGlucose - exception $exception") + } + } + + fun fullSync(gatt: EversenseGattCallback, preferences: SharedPreferences, watchers: List, force: Boolean = false) { + try { + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + val fourHalfMinAgo = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(270) + + if (!force && fourHalfMinAgo < state.lastSync) { + EversenseLogger.warning(TAG, "State is still fresh - lastSync: ${state.lastSync}") + return + } + + EversenseLogger.debug(TAG, "Reading current datetime...") + val currentDatetime = gatt.writePacket(GetCurrentDatetimePacket()) + if (currentDatetime.needsTimeSync) { + EversenseLogger.debug(TAG, "Send SetCurrentDatetimePacket...") + gatt.writePacket(SetCurrentDatetimePacket()) + } + + EversenseLogger.debug(TAG, "Reading battery percentage...") + val batteryPercentage = gatt.writePacket(GetBatteryPercentagePacket()) + state.batteryPercentage = batteryPercentage.percentage + + EversenseLogger.debug(TAG, "Reading insertion datetime...") + val insertionDate = gatt.writePacket(GetInsertionDatePacket()) + val insertionTime = gatt.writePacket(GetInsertionTimePacket()) + state.insertionDate = insertionDate.date + insertionTime.time + + EversenseLogger.debug(TAG, "Reading calibration info...") + val calibrationPhase = gatt.writePacket(GetCalibrationPhasePacket()) + val calibrationReadiness = gatt.writePacket(GetCalibrationReadinessPacket()) + val nextCalibrationDate = gatt.writePacket(GetNextCalibrationDatePacket()) + val nextCalibrationTime = gatt.writePacket(GetNextCalibrationTimePacket()) + val lastCalibrationDate = gatt.writePacket(GetLastCalibrationDatePacket()) + val lastCalibrationTime = gatt.writePacket(GetLastCalibrationTimePacket()) + + try { + // Older E3 transmitters might not have this data point, thus we are allowed to ignore the exception + val isDailyCalibration = gatt.writePacket(GetCalibrationDailyPacket()) + state.calibrationMode = if (isDailyCalibration.isDaily) CalibrationMode.DAILY_SINGLE else CalibrationMode.DAILY_DUAL + } catch(exception: Exception) { + state.calibrationMode = CalibrationMode.DEFAULT + } + + state.calibrationPhase = calibrationPhase.phase + state.calibrationReadiness = calibrationReadiness.readiness + state.nextCalibrationDate = nextCalibrationDate.date + nextCalibrationTime.time + state.lastCalibrationDate = lastCalibrationDate.date + lastCalibrationTime.time + + // Transmitter settings + EversenseLogger.debug(TAG, "Reading transmitter settings...") + val vibrateEnabled = gatt.writePacket(GetSettingVibratePacket()) + val glucoseHighEnabled = gatt.writePacket(GetSettingGlucoseHighEnabled()) + val glucoseHighThreshold = gatt.writePacket(GetSettingGlucoseHighThresholdPacket()) + val glucoseLowThreshold = gatt.writePacket(GetSettingGlucoseLowThresholdPacket()) + val rateFallingEnabled = gatt.writePacket(GetSettingRateFallingEnabledPacket()) + val rateFallingThreshold = gatt.writePacket(GetSettingRateFallingThresholdPacket()) + val rateRisingEnabled = gatt.writePacket(GetSettingRateRisingEnabledPacket()) + val rateRisingThreshold = gatt.writePacket(GetSettingRateRisingThresholdPacket()) + val predictiveHighEnabled = gatt.writePacket(GetSettingPredictiveHighEnabledPacket()) + val predictiveHighTime = gatt.writePacket(GetSettingPredictiveHighTimePacket()) + val predictiveHighThreshold = gatt.writePacket(GetSettingPredictiveHighThresholdPacket()) + val predictiveLowEnabled = gatt.writePacket(GetSettingPredictiveLowEnabledPacket()) + val predictiveLowTime = gatt.writePacket(GetSettingPredictiveLowTimePacket()) + val predictiveLowThreshold = gatt.writePacket(GetSettingPredictiveLowThresholdPacket()) + + state.settings.vibrateEnabled = vibrateEnabled.enabled + state.settings.glucoseHighAlarmEnabled = glucoseHighEnabled.enabled + state.settings.glucoseHighAlarmThreshold = glucoseHighThreshold.threshold + state.settings.glucoseLowAlarmThreshold = glucoseLowThreshold.threshold + state.settings.rateFallingAlarmEnabled = rateFallingEnabled.enabled + state.settings.rateFallingAlarmThreshold = rateFallingThreshold.threshold + state.settings.rateRisingAlarmEnabled = rateRisingEnabled.enabled + state.settings.rateRisingAlarmThreshold = rateRisingThreshold.threshold + state.settings.predictiveHighAlarmEnabled = predictiveHighEnabled.enabled + state.settings.predictiveHighAlarmMinutes = predictiveHighTime.minutes + state.settings.predictiveHighAlarmThreshold = predictiveHighThreshold.threshold + state.settings.predictiveLowAlarmEnabled = predictiveLowEnabled.enabled + state.settings.predictiveLowAlarmMinutes = predictiveLowTime.minutes + state.settings.predictiveLowAlarmThreshold = predictiveLowThreshold.threshold + + // Get firmware version — aligns with iOS GetVersionPacket + try { + val version = gatt.writePacket(GetVersionPacket()) + if (version != null) state.firmwareVersion = version.version + } catch (e: Exception) { EversenseLogger.warning(TAG, "GetVersion failed: $e") } + + // Get extended firmware version + try { + val extVersion = gatt.writePacket(GetVersionExtendedPacket()) + if (extVersion != null) state.extFirmwareVersion = extVersion.extVersion + } catch (e: Exception) { EversenseLogger.warning(TAG, "GetVersionExtended failed: $e") } + + // Get MMA features + try { + val mma = gatt.writePacket(GetMmaFeaturesPacket()) + if (mma != null) state.mmaFeatures = mma.value + } catch (e: Exception) { EversenseLogger.warning(TAG, "GetMmaFeatures failed: $e") } + + // Set app version — iOS sends 8.0.4 in every fullSync + try { gatt.writePacket(SetAppVersionE3Packet()) } catch (e: Exception) { EversenseLogger.warning(TAG, "SetAppVersionE3 failed: $e") } + + // Set BLE disconnect timeout — 300s matching iOS default + try { gatt.writePacket(SetBleDisconnectPacket(300)) } catch (e: Exception) { EversenseLogger.warning(TAG, "SetBleDisconnect E3 failed: $e") } + + state.lastSync = System.currentTimeMillis() + EversenseLogger.info(TAG, "Completed full sync - datetime: ${state.lastSync}") + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + + handler.post { + watchers.forEach { + it.onStateChanged(state) + } + } + } catch (exception: Exception) { + EversenseLogger.error(TAG, "Failed to do full sync: $exception") + } + } + + fun writeSettings(gatt: EversenseGattCallback, preferences: SharedPreferences, settings: EversenseTransmitterSettings): Boolean { + try { + gatt.writePacket(SetSettingVibratePacket(settings.vibrateEnabled)) + + gatt.writePacket(SetSettingGlucoseHighEnablePacket(settings.glucoseHighAlarmEnabled)) + gatt.writePacket(SetSettingGlucoseHighThresholdPacket(settings.glucoseHighAlarmThreshold)) + gatt.writePacket(SetSettingGlucoseLowThresholdPacket(settings.glucoseLowAlarmThreshold)) + + gatt.writePacket(SetSettingRateFallingEnabledPacket(settings.rateFallingAlarmEnabled)) + gatt.writePacket(SetSettingRateFallingThresholdPacket(settings.rateFallingAlarmThreshold)) + gatt.writePacket(SetSettingRateRisingEnabledPacket(settings.rateRisingAlarmEnabled)) + gatt.writePacket(SetSettingRateRisingThresholdPacket(settings.rateRisingAlarmThreshold)) + + gatt.writePacket(SetSettingPredictiveHighAlarmEnabledPacket(settings.predictiveHighAlarmEnabled)) + gatt.writePacket(SetSettingPredictiveHighTimePacket(settings.predictiveHighAlarmMinutes)) + gatt.writePacket(SetSettingPredictiveHighThresholdPacket(settings.predictiveHighAlarmThreshold)) + gatt.writePacket(SetSettingPredictiveLowAlarmEnabledPacket(settings.predictiveLowAlarmEnabled)) + gatt.writePacket(SetSettingPredictiveLowTimePacket(settings.predictiveLowAlarmMinutes)) + gatt.writePacket(SetSettingPredictiveLowThresholdPacket(settings.predictiveLowAlarmThreshold)) + + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + state.settings = settings + preferences.edit(commit = true) { + putString(StorageKeys.STATE, JSON.encodeToString(state)) + } + + return true + } catch (exception: Exception) { + EversenseLogger.error(TAG, "Failed to write settings: $exception") + return false + } + } + + // Send a blood glucose calibration value to the E3 transmitter. + // The transmitter must be in CalibrationReadiness.READY state. + // Throws EversenseWriteException if the packet fails. + fun sendCalibration(gatt: EversenseGattCallback, glucoseMgDl: Int) { + EversenseLogger.info(TAG, "Sending calibration value: $glucoseMgDl mg/dL") + val now = System.currentTimeMillis() + gatt.writePacket(SetBloodGlucosePointPacket(glucoseMgDl, now), 15000L) + EversenseLogger.info(TAG, "Calibration sent successfully") + } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversensePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversensePacket.kt new file mode 100644 index 00000000000..263a3e2bac5 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversensePacket.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets + +import com.nightscout.eversense.enums.EversenseSecurityType + +annotation class EversensePacket( + /** The request id for the packet */ + val requestId: Byte, + + /** The expected response id for this packet */ + val responseId: Byte, + + /** The expected response id for this packet. Only relevant for 365 packets */ + val typeId: Byte, + + /** The required security protocol */ + val securityType: EversenseSecurityType +) diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/EversenseE3Packets.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/EversenseE3Packets.kt new file mode 100644 index 00000000000..0d9d1cb75c1 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/EversenseE3Packets.kt @@ -0,0 +1,117 @@ +package com.nightscout.eversense.packets.e3 + +class EversenseE3Packets { + companion object { + const val AssertSnoozeAgainsAlarmCommandId = 20.toByte() + const val AssertSnoozeAgainsAlarmResponseId = 148.toByte() + const val CalibrationAlertPush = 77.toByte() + const val CalibrationPush = 67.toByte() + const val CalibrationSwitchPush = 76.toByte() + const val ChangeTimingParametersCommandId = 117.toByte() + const val ChangeTimingParametersResponseId = 245.toByte() + const val ClearErrorFlagsCommandId = 4.toByte() + const val ClearErrorFlagsResponseId = 132.toByte() + const val DisconnectBLESavingBondingInformationCommandId = 116.toByte() + const val DisconnectBLESavingBondingInformationResponseId = 244.toByte() + const val EnterDiagnosticModeCommandId = 118.toByte() + const val EnterDiagnosticModeResponseId = 246.toByte() + const val ErrorResponseId = 128.toByte() + const val ExerciseVibrationCommandId = 106.toByte() + const val ExerciseVibrationResponseId = 234.toByte() + const val ExitDiagnosticModeCommandId = 119.toByte() + const val ExitDiagnosticModeResponseId = 247.toByte() + const val GlucoseLevelAlarmPush = 64.toByte() + const val GlucoseLevelAlertPush = 65.toByte() + const val HardwareStatusPush = 69.toByte() + const val KeepAlivePush = 80.toByte() + const val LinkTransmitterWithSensorCommandId = 2.toByte() + const val LinkTransmitterWithSensorResponseId = 130.toByte() + const val MarkPatientEventRecordAsDeletedCommandId = 29.toByte() + const val MarkPatientEventRecordAsDeletedResponseId = 157.toByte() + const val PingCommandId = 1.toByte() + const val PingResponseId = 129.toByte() + const val RateAndPredictiveAlertPush = 66.toByte() + const val ReadAllAvailableSensorsResponseId = 134.toByte() + const val ReadAllSensorGlucoseAlertsInSpecifiedRangeCommandId = 113.toByte() + const val ReadAllSensorGlucoseAlertsInSpecifiedRangeResponseId = 241.toByte() + const val ReadAllSensorGlucoseDataInSpecifiedRangeCommandId = 112.toByte() + const val ReadAllSensorGlucoseDataInSpecifiedRangeResponseId = 240.toByte() + const val ReadCurrentTransmitterDateAndTimeCommandId = 25.toByte() + const val ReadCurrentTransmitterDateAndTimeResponseId = 153.toByte() + const val ReadFirstAndLastBloodGlucoseDataRecordNumbersCommandId = 23.toByte() + const val ReadFirstAndLastBloodGlucoseDataRecordNumbersResponseId = 151.toByte() + const val ReadFirstAndLastErrorLogRecordNumbersCommandId = 39.toByte() + const val ReadFirstAndLastErrorLogRecordNumbersResponseId = 167.toByte() + const val ReadFirstAndLastMiscEventLogRecordNumbersCommandId = 35.toByte() + const val ReadFirstAndLastMiscEventLogRecordNumbersResponseId = 163.toByte() + const val ReadFirstAndLastPatientEventRecordNumbersCommandId = 28.toByte() + const val ReadFirstAndLastPatientEventRecordNumbersResponseId = 156.toByte() + const val ReadFirstAndLastSensorGlucoseAlertRecordNumbersCommandId = 18.toByte() + const val ReadFirstAndLastSensorGlucoseAlertRecordNumbersResponseId = 146.toByte() + const val ReadFirstAndLastSensorGlucoseRecordNumbersCommandId = 14.toByte() + const val ReadFirstAndLastSensorGlucoseRecordNumbersResponseId = 142.toByte() + const val ReadFourByteSerialFlashRegisterCommandId = 46.toByte() + const val ReadFourByteSerialFlashRegisterResponseId = 174.toByte() + const val ReadLogOfBloodGlucoseDataInSpecifiedRangeCommandId = 114.toByte() + const val ReadLogOfBloodGlucoseDataInSpecifiedRangeResponseId = 242.toByte() + const val ReadLogOfPatientEventsInSpecifiedRangeCommandId = 115.toByte() + const val ReadLogOfPatientEventsInSpecifiedRangeResponseId = 243.toByte() + const val ReadNByteSerialFlashRegisterCommandId = 48.toByte() + const val ReadNByteSerialFlashRegisterResponseId = 176.toByte() + const val ReadSensorGlucoseAlertsAndStatusCommandId = 16.toByte() + const val ReadSensorGlucoseAlertsAndStatusResponseId = 144.toByte() + const val ReadSensorGlucoseCommandId = 8.toByte() + const val ReadSensorGlucoseResponseId = 136.toByte() + const val ReadSingleBloodGlucoseDataRecordCommandId = 22.toByte() + const val ReadSingleBloodGlucoseDataRecordResponseId = 150.toByte() + const val ReadSingleByteSerialFlashRegisterCommandId = 42.toByte() + const val ReadSingleByteSerialFlashRegisterResponseId = 170.toByte() + const val ReadSingleMiscEventLogCommandId = 34.toByte() + const val ReadSingleMiscEventLogResponseId = 162.toByte() + const val ReadSinglePatientEventCommandId = 27.toByte() + const val ReadSinglePatientEventResponseId = 155.toByte() + const val ReadSingleSensorGlucoseAlertRecordCommandId = 17.toByte() + const val ReadSingleSensorGlucoseAlertRecordResponseId = 145.toByte() + const val ReadSingleSensorGlucoseDataRecordResponseId = 137.toByte() + const val ReadTwoByteSerialFlashRegisterCommandId = 44.toByte() + const val ReadTwoByteSerialFlashRegisterResponseId = 172.toByte() + const val ResetTransmitterCommandId = 3.toByte() + const val ResetTransmitterResponseId = 131.toByte() + const val SaveBLEBondingInformationCommandId = 105.toByte() + const val SaveBLEBondingInformationResponseId = 233.toByte() + const val SendBloodGlucoseDataCommandId = 21.toByte() + const val SendBloodGlucoseDataResponseId = 149.toByte() + const val SendBloodGlucoseDataWithTwoTimestampsCommandId = 60.toByte() + const val SendBloodGlucoseDataWithTwoTimestampsResponseId = 188.toByte() + const val SensorReadAlertPush = 73.toByte() + const val SensorReplacement2Push = 75.toByte() + const val SensorReplacementPush = 68.toByte() + const val SetCurrentTransmitterDateAndTimeCommandId = 7.toByte() + const val SetCurrentTransmitterDateAndTimeResponseId = 135.toByte() + const val StartSelfTestSequenceCommandId = 5.toByte() + const val StartSelfTestSequenceResponseId = 133.toByte() + const val TestResponseId = 224.toByte() + const val TransmitterBatteryPush = 71.toByte() + const val TransmitterEOLPush = 74.toByte() + const val WriteFourByteSerialFlashRegisterCommandId = 47.toByte() + const val WriteFourByteSerialFlashRegisterResponseId = 175.toByte() + const val WriteNByteSerialFlashRegisterCommandId = 49.toByte() + const val WriteNByteSerialFlashRegisterResponseId = 177.toByte() + const val WritePatientEventCommandId = 26.toByte() + const val WritePatientEventResponseId = 154.toByte() + const val WriteSingleByteSerialFlashRegisterCommandId = 43.toByte() + const val WriteSingleByteSerialFlashRegisterResponseId = 171.toByte() + const val WriteSingleMiscEventLogRecordCommandId = 36.toByte() + const val WriteSingleMiscEventLogRecordResponseId = 164.toByte() + const val WriteTwoByteSerialFlashRegisterCommandId = 45.toByte() + const val WriteTwoByteSerialFlashRegisterResponseId = 173.toByte() + + fun isPushPacket(data: Byte): Boolean { + return data == KeepAlivePush + } + + fun isErrorPacket(data: Byte): Boolean { + return data == ErrorResponseId + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetAlertLogPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetAlertLogPacket.kt new file mode 100644 index 00000000000..8d6f27c5e6c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetAlertLogPacket.kt @@ -0,0 +1,42 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseAlarm +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.ReadAllSensorGlucoseAlertsInSpecifiedRangeCommandId, + responseId = EversenseE3Packets.ReadAllSensorGlucoseAlertsInSpecifiedRangeResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetAlertLogPacket(private val index: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Writer.writeInt16(index) + EversenseE3Writer.writeInt16(index) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val recordIndex = (receivedData[s].toInt() and 0xFF) or ((receivedData[s + 1].toInt() and 0xFF) shl 8) + val datetime = EversenseE3Parser.readDate(receivedData, s + 2) + EversenseE3Parser.readTime(receivedData, s + 4) + val alarmCode = receivedData[s + 7].toInt() and 0xFF + + return Response( + index = recordIndex, + datetime = datetime, + alarm = EversenseAlarm.from(alarmCode) + ) + } + + data class Response( + val index: Int, + val datetime: Long, + val alarm: EversenseAlarm + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBatteryPercentagePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBatteryPercentagePacket.kt new file mode 100644 index 00000000000..2066900aa01 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBatteryPercentagePacket.kt @@ -0,0 +1,47 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetBatteryPercentagePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.BatteryPercentage.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(percentage = parseEnum(receivedData[getStartIndex()].toInt())) + } + + private fun parseEnum(value: Int): Int { + return when(value) { + 0 -> 0 //% + 1 -> 5 //% + 2 -> 10 //% + 3 -> 25 //% + 4 -> 35 //% + 5 -> 45 //% + 6 -> 55 //% + 7 -> 65 //% + 8 -> 75 //% + 9 -> 85 //% + 10 -> 95 //% + 11 -> 100 //% + else -> -1 + } + } + + data class Response(val percentage: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBleDisconnectPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBleDisconnectPacket.kt new file mode 100644 index 00000000000..74737b01ec4 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBleDisconnectPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetBleDisconnectPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.BleDisconnect.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val intervalSeconds = (receivedData[s].toInt() and 0xFF) or ((receivedData[s + 1].toInt() and 0xFF) shl 8) + return Response(intervalSeconds = intervalSeconds) + } + + data class Response(val intervalSeconds: Int) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationDailyPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationDailyPacket.kt new file mode 100644 index 00000000000..6a300a43a84 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationDailyPacket.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.CalibrationPhase +import com.nightscout.eversense.enums.CalibrationReadiness +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCalibrationDailyPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.IsOneCalibration.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(isDaily = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val isDaily: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogPacket.kt new file mode 100644 index 00000000000..347d360b285 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogPacket.kt @@ -0,0 +1,45 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.CalibrationFlag +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.ReadLogOfBloodGlucoseDataInSpecifiedRangeCommandId, + responseId = EversenseE3Packets.ReadLogOfBloodGlucoseDataInSpecifiedRangeResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCalibrationLogPacket(private val index: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Writer.writeInt16(index) + EversenseE3Writer.writeInt16(index) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val recordIndex = (receivedData[s].toInt() and 0xFF) or ((receivedData[s + 1].toInt() and 0xFF) shl 8) + val datetime = EversenseE3Parser.readDate(receivedData, s + 2) + EversenseE3Parser.readTime(receivedData, s + 4) + val glucoseInMgDl = (receivedData[s + 6].toInt() and 0xFF) or ((receivedData[s + 8].toInt() and 0xFF) shl 7) + val flagCode = receivedData[s + 9].toInt() and 0xFF + + return Response( + index = recordIndex, + datetime = datetime, + glucoseInMgDl = glucoseInMgDl, + flag = CalibrationFlag.from(flagCode) + ) + } + + data class Response( + val index: Int, + val datetime: Long, + val glucoseInMgDl: Int, + val flag: CalibrationFlag + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogRangePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogRangePacket.kt new file mode 100644 index 00000000000..df9005e2c21 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationLogRangePacket.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +/** + * Reads the first and last record numbers for the blood glucose (calibration) log (16-bit indices). + * Use [GetGlucoseLogRangePacket] for sensor glucose log range. + */ +@EversensePacket( + requestId = EversenseE3Packets.ReadFirstAndLastBloodGlucoseDataRecordNumbersCommandId, + responseId = EversenseE3Packets.ReadFirstAndLastBloodGlucoseDataRecordNumbersResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCalibrationLogRangePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val from = (receivedData[s].toInt() and 0xFF) or ((receivedData[s + 1].toInt() and 0xFF) shl 8) + val to = (receivedData[s + 2].toInt() and 0xFF) or ((receivedData[s + 3].toInt() and 0xFF) shl 8) + + return Response(rangeFrom = from, rangeTo = to) + } + + data class Response(val rangeFrom: Int, val rangeTo: Int) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.kt new file mode 100644 index 00000000000..add48d5425a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.kt @@ -0,0 +1,31 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.CalibrationPhase +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCalibrationPhasePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.CalibrationPhase.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(phase = CalibrationPhase.fromE3(receivedData[getStartIndex()].toInt())) + } + + data class Response(val phase: CalibrationPhase) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationReadinessPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationReadinessPacket.kt new file mode 100644 index 00000000000..b85b5648695 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationReadinessPacket.kt @@ -0,0 +1,31 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.CalibrationReadiness +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCalibrationReadinessPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.CalibrationReadiness.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(readiness = CalibrationReadiness.from(receivedData[getStartIndex()].toInt())) + } + + data class Response(val readiness: CalibrationReadiness) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCompletedCalibrationsCountPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCompletedCalibrationsCountPacket.kt new file mode 100644 index 00000000000..e4bcc105e36 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCompletedCalibrationsCountPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCompletedCalibrationsCountPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.CalibrationsMadeInThisPhase.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val count = (receivedData[s].toInt() and 0xFF) or ((receivedData[s + 1].toInt() and 0xFF) shl 8) + return Response(count = count) + } + + data class Response(val count: Int) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentDatetimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentDatetimePacket.kt new file mode 100644 index 00000000000..4fbe01fe74c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentDatetimePacket.kt @@ -0,0 +1,49 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser +import java.util.TimeZone +import kotlin.math.abs + +@EversensePacket( + requestId = EversenseE3Packets.ReadCurrentTransmitterDateAndTimeCommandId, + responseId = EversenseE3Packets.ReadCurrentTransmitterDateAndTimeResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCurrentDatetimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return ByteArray(0) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + val start = getStartIndex() + val date = EversenseE3Parser.readDate(receivedData, start) + val time = EversenseE3Parser.readTime(receivedData, start + 2) + val timeZoneOffset = EversenseE3Parser.readTimezone(receivedData, start + 4) + + var needsTimeSync = false + val actualTimeZoneOffset = TimeZone.getDefault().getOffset(System.currentTimeMillis()).toLong() + + // Allow time drift <10s + if (abs(System.currentTimeMillis() - (date + time)) > 10_000) { + EversenseLogger.warning("GetCurrentDatetimePacket", "time drift detected... drift: ${abs(System.currentTimeMillis() - (date + time))} ms") + needsTimeSync = true + } else if (actualTimeZoneOffset != timeZoneOffset) { + EversenseLogger.warning("GetCurrentDatetimePacket", "timezone mismatch - received: $timeZoneOffset, actual: $actualTimeZoneOffset") + needsTimeSync = true + } + + return Response(date + time, timeZoneOffset, needsTimeSync) + } + + data class Response(val datetime: Long, val timezoneOffset: Long, val needsTimeSync: Boolean): EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentGlucosePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentGlucosePacket.kt new file mode 100644 index 00000000000..4c85b1810f1 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentGlucosePacket.kt @@ -0,0 +1,45 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.enums.EversenseTrendArrow +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadSensorGlucoseCommandId, + responseId = EversenseE3Packets.ReadSensorGlucoseResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetCurrentGlucosePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return ByteArray(0) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + datetime = EversenseE3Parser.readDate(receivedData, 4) + EversenseE3Parser.readTime(receivedData, 6), + glucoseInMgDl = EversenseE3Parser.readGlucose(receivedData, 9), + trend = parseTrend(receivedData[13].toInt()), + ) + } + + private fun parseTrend(value: Int): EversenseTrendArrow { + return when(value) { + 1 -> EversenseTrendArrow.SINGLE_DOWN + 2 -> EversenseTrendArrow.FORTY_FIVE_DOWN + 4 -> EversenseTrendArrow.FLAT + 8 -> EversenseTrendArrow.FORTY_FIVE_UP + 16 -> EversenseTrendArrow.SINGLE_UP + else -> EversenseTrendArrow.NONE + } + } + + data class Response(val datetime: Long, val glucoseInMgDl: Int, val trend: EversenseTrendArrow) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseAlertsAndStatusPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseAlertsAndStatusPacket.kt new file mode 100644 index 00000000000..3bd8ff13564 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseAlertsAndStatusPacket.kt @@ -0,0 +1,56 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.models.ActiveAlarm +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.util.MessageCoder + +@EversensePacket( + requestId = EversenseE3Packets.ReadSensorGlucoseAlertsAndStatusCommandId, + responseId = EversenseE3Packets.ReadSensorGlucoseAlertsAndStatusResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetGlucoseAlertsAndStatusPacket : EversenseBasePacket() { + + private val STATUS_FLAG_COUNT = 13 + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val rawContent = receivedData.drop(s + 1).dropLast(2).map { it.toInt() and 0xFF } + val content = IntArray(STATUS_FLAG_COUNT) + val offset = STATUS_FLAG_COUNT - rawContent.size.coerceAtMost(STATUS_FLAG_COUNT) + rawContent.take(STATUS_FLAG_COUNT).forEachIndexed { i, v -> content[offset + i] = v } + + if (content.all { it == 0 }) return Response(alarms = emptyList()) + + val alarms = mutableListOf() + + fun add(alarm: com.nightscout.eversense.enums.EversenseAlarm?) { + alarm?.let { alarms.add(ActiveAlarm(code = it, codeRaw = it.code, flag = 0, priority = 0)) } + } + + add(MessageCoder.messageCodeForGlucoseLevelAlarmFlags(content[0])) + add(MessageCoder.messageCodeForGlucoseLevelAlertFlags(content[1])) + add(MessageCoder.messageCodeForRateAlertFlags(content[2])) + add(MessageCoder.messageCodeForPredictiveAlertFlags(content[3])) + add(MessageCoder.messageCodeForSensorHardwareAndAlertFlags(content[4])) + add(MessageCoder.messageCodeForSensorReadAlertFlags(content[5])) + add(MessageCoder.messageCodeForSensorReplacementFlags(content[6])) + add(MessageCoder.messageCodeForSensorCalibrationFlags(content[7])) + add(MessageCoder.messageCodeForTransmitterStatusAlertFlags(content[8])) + add(MessageCoder.messageCodeForTransmitterBatteryAlertFlags(content[9])) + add(MessageCoder.messageCodeForTransmitterEOLAlertFlags(content[10])) + add(MessageCoder.messageCodeForSensorReplacementFlags2(content[11])) + add(MessageCoder.messageCodeForCalibrationSwitchFlags(content[12])) + + return Response(alarms = alarms) + } + + data class Response(val alarms: List) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogPacket.kt new file mode 100644 index 00000000000..064398520cc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogPacket.kt @@ -0,0 +1,50 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadAllSensorGlucoseDataInSpecifiedRangeCommandId, + responseId = EversenseE3Packets.ReadAllSensorGlucoseDataInSpecifiedRangeResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetGlucoseLogPacket(private val index: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + // 24-bit (3-byte) index sent as both from and to + return byteArrayOf( + index.toByte(), + (index shr 8).toByte(), + (index shr 16).toByte(), + index.toByte(), + (index shr 8).toByte(), + (index shr 16).toByte() + ) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val recordIndex = (receivedData[s].toInt() and 0xFF) or + ((receivedData[s + 1].toInt() and 0xFF) shl 8) or + ((receivedData[s + 2].toInt() and 0xFF) shl 16) + val datetime = EversenseE3Parser.readDate(receivedData, s + 3) + EversenseE3Parser.readTime(receivedData, s + 5) + val glucoseInMgDl = (receivedData[s + 7].toInt() and 0xFF) or ((receivedData[s + 8].toInt() and 0xFF) shl 8) + + return Response( + index = recordIndex, + datetime = datetime, + glucoseInMgDl = glucoseInMgDl + ) + } + + data class Response( + val index: Int, + val datetime: Long, + val glucoseInMgDl: Int + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogRangePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogRangePacket.kt new file mode 100644 index 00000000000..1808d2bd23c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetGlucoseLogRangePacket.kt @@ -0,0 +1,36 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +/** + * Reads the first and last record numbers for the sensor glucose log (24-bit indices). + * Use [GetCalibrationLogRangePacket] for blood glucose (calibration) log range. + */ +@EversensePacket( + requestId = EversenseE3Packets.ReadFirstAndLastSensorGlucoseRecordNumbersCommandId, + responseId = EversenseE3Packets.ReadFirstAndLastSensorGlucoseRecordNumbersResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetGlucoseLogRangePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val s = getStartIndex() + val from = (receivedData[s].toInt() and 0xFF) or + ((receivedData[s + 1].toInt() and 0xFF) shl 8) or + ((receivedData[s + 2].toInt() and 0xFF) shl 16) + val to = (receivedData[s + 3].toInt() and 0xFF) or + ((receivedData[s + 4].toInt() and 0xFF) shl 8) or + ((receivedData[s + 5].toInt() and 0xFF) shl 16) + + return Response(rangeFrom = from, rangeTo = to) + } + + data class Response(val rangeFrom: Int, val rangeTo: Int) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetHighGlucoseRepeatIntervalPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetHighGlucoseRepeatIntervalPacket.kt new file mode 100644 index 00000000000..1683e2c84c8 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetHighGlucoseRepeatIntervalPacket.kt @@ -0,0 +1,21 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetHighGlucoseRepeatIntervalPacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray = EversenseE3Memory.HighGlucoseAlarmRepeatIntervalDay.getRequestData() + override fun parseResponse(): Response? { + if (receivedData.size < getStartIndex() + 1) return null + return Response(intervalMinutes = receivedData[getStartIndex()].toInt() and 0xFF) + } + data class Response(val intervalMinutes: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionDatePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionDatePacket.kt new file mode 100644 index 00000000000..44831229544 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionDatePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetInsertionDatePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.SensorInsertionDate.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(date = EversenseE3Parser.readDate(receivedData, getStartIndex())) + } + + data class Response(val date: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionTimePacket.kt new file mode 100644 index 00000000000..17cd351bb52 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionTimePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetInsertionTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.SensorInsertionTime.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(time = EversenseE3Parser.readTime(receivedData, getStartIndex())) + } + + data class Response(val time: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetIsOneCalPhasePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetIsOneCalPhasePacket.kt new file mode 100644 index 00000000000..7f234c317f6 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetIsOneCalPhasePacket.kt @@ -0,0 +1,26 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetIsOneCalPhasePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.IsOneCalibration.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response(value = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val value: Boolean) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationDatePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationDatePacket.kt new file mode 100644 index 00000000000..8b0232c4782 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationDatePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetLastCalibrationDatePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LastCalibrationDate.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(date = EversenseE3Parser.readDate(receivedData, getStartIndex())) + } + + data class Response(val date: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationTimePacket.kt new file mode 100644 index 00000000000..4ec8be31d33 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationTimePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetLastCalibrationTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LastCalibrationTime.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(time = EversenseE3Parser.readTime(receivedData, getStartIndex())) + } + + data class Response(val time: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLowGlucoseRepeatIntervalPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLowGlucoseRepeatIntervalPacket.kt new file mode 100644 index 00000000000..f636598e3d0 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLowGlucoseRepeatIntervalPacket.kt @@ -0,0 +1,21 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetLowGlucoseRepeatIntervalPacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray = EversenseE3Memory.LowGlucoseAlarmRepeatIntervalDay.getRequestData() + override fun parseResponse(): Response? { + if (receivedData.size < getStartIndex() + 1) return null + return Response(intervalMinutes = receivedData[getStartIndex()].toInt() and 0xFF) + } + data class Response(val intervalMinutes: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetMmaFeaturesPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetMmaFeaturesPacket.kt new file mode 100644 index 00000000000..95212ddcfbe --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetMmaFeaturesPacket.kt @@ -0,0 +1,21 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetMmaFeaturesPacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray = EversenseE3Memory.MmaFeatures.getRequestData() + override fun parseResponse(): Response? { + if (receivedData.size < getStartIndex() + 1) return null + return Response(value = receivedData[getStartIndex()].toInt() and 0xFF) + } + data class Response(val value: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationDatePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationDatePacket.kt new file mode 100644 index 00000000000..4123b98fc07 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationDatePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetNextCalibrationDatePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.NextCalibrationDate.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(date = EversenseE3Parser.readDate(receivedData, getStartIndex())) + } + + data class Response(val date: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationTimePacket.kt new file mode 100644 index 00000000000..bbd36786cf7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationTimePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetNextCalibrationTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.NextCalibrationTime.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(time = EversenseE3Parser.readTime(receivedData, getStartIndex())) + } + + data class Response(val time: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighEnabled.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighEnabled.kt new file mode 100644 index 00000000000..eef9a00d909 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighEnabled.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingGlucoseHighEnabled : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmEnabled.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighThresholdPacket.kt new file mode 100644 index 00000000000..a10c7e33718 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighThresholdPacket.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingGlucoseHighThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmThreshold.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + threshold = EversenseE3Parser.readGlucose(receivedData, getStartIndex()) + ) + } + + data class Response(val threshold: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseLowThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseLowThresholdPacket.kt new file mode 100644 index 00000000000..5c630b61381 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseLowThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingGlucoseLowThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LowGlucoseAlarmThreshold.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(threshold = EversenseE3Parser.readGlucose(receivedData, getStartIndex())) + } + + data class Response(val threshold: Int): EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveAlarmEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveAlarmEnabledPacket.kt new file mode 100644 index 00000000000..a2dd0448a20 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveAlarmEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveAlarmEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighEnabledPacket.kt new file mode 100644 index 00000000000..6246df76705 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveHighEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighThresholdPacket.kt new file mode 100644 index 00000000000..6f4782d1ec4 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveHighThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighTarget.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(threshold = EversenseE3Parser.readGlucose(receivedData, getStartIndex())) + } + + data class Response(val threshold: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighTimePacket.kt new file mode 100644 index 00000000000..5a35c8e094c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighTimePacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveHighTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighTime.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(minutes = receivedData[getStartIndex()].toInt()) + } + + data class Response(val minutes: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowEnabledPacket.kt new file mode 100644 index 00000000000..f66e3316207 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveLowEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowThresholdPacket.kt new file mode 100644 index 00000000000..03af10877ba --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Parser + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveLowThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowTarget.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(threshold = EversenseE3Parser.readGlucose(receivedData, getStartIndex())) + } + + data class Response(val threshold: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowTimePacket.kt new file mode 100644 index 00000000000..13c6031d7af --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowTimePacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingPredictiveLowTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowTime.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(minutes = receivedData[getStartIndex()].toInt()) + } + + data class Response(val minutes: Int) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateEnabledPacket.kt new file mode 100644 index 00000000000..261771a233a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingRateEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingEnabledPacket.kt new file mode 100644 index 00000000000..44e95c9dc92 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingRateFallingEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateFallingAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingThresholdPacket.kt new file mode 100644 index 00000000000..15bf0112402 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingThresholdPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingRateFallingThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateFallingThreshold.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(threshold = receivedData[getStartIndex()].toDouble() / 10) + } + + data class Response(val threshold: Double): EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingEnabledPacket.kt new file mode 100644 index 00000000000..b617526b855 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingEnabledPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingRateRisingEnabledPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateRisingAlert.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(enabled = receivedData[getStartIndex()].toInt() == 0x55) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingThresholdPacket.kt new file mode 100644 index 00000000000..23c5404014a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingThresholdPacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingRateRisingThresholdPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateRisingThreshold.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response(threshold = receivedData[getStartIndex()].toDouble() / 10) + } + + data class Response(val threshold: Double) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingVibratePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingVibratePacket.kt new file mode 100644 index 00000000000..927fecd14df --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingVibratePacket.kt @@ -0,0 +1,31 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSettingVibratePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.VibrateMode.getRequestData() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + enabled = receivedData[getStartIndex()].toInt() == 0x55 + ) + } + + data class Response(val enabled: Boolean) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSignalStrengthRawPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSignalStrengthRawPacket.kt new file mode 100644 index 00000000000..afe7c58b0da --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSignalStrengthRawPacket.kt @@ -0,0 +1,45 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetSignalStrengthRawPacket : EversenseBasePacket() { + + companion object { + // Raw thresholds matching iOS EversenseKit SignalStrength.swift rawThreshold values + const val THRESHOLD_EXCELLENT = 1600 + const val THRESHOLD_GOOD = 1300 + const val THRESHOLD_LOW = 800 + const val THRESHOLD_VERY_LOW = 500 + const val THRESHOLD_POOR = 350 + } + + override fun getRequestData(): ByteArray = EversenseE3Memory.SensorFieldCurrentRaw.getRequestData() + + override fun parseResponse(): Response? { + val start = getStartIndex() + if (receivedData.size < start + 2) return null + + // Little-endian UInt16 — matches iOS: UInt16(data[start]) | (UInt16(data[start + 1]) << 8) + val raw = (receivedData[start].toInt() and 0xFF) or + ((receivedData[start + 1].toInt() and 0xFF) shl 8) + + // Scale to 0-100 matching iOS PlacementGuideViewModel: rawValue / 20 + val signalPercent = (raw / 20).coerceIn(0, 100) + + return Response(rawValue = raw, signalStrength = signalPercent) + } + + data class Response( + val rawValue: Int, + val signalStrength: Int // 0-100 scaled, matching iOS implementation + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionExtendedPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionExtendedPacket.kt new file mode 100644 index 00000000000..112b6d1f2cc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionExtendedPacket.kt @@ -0,0 +1,23 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadFourByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadFourByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetVersionExtendedPacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray = EversenseE3Memory.TransmitterSoftwareVersionExt.getRequestData() + override fun parseResponse(): Response? { + val start = getStartIndex() + if (receivedData.size < start + 4) return null + val extVersion = (start until start + 4).map { receivedData[it].toInt().toChar() }.joinToString("") + return Response(extVersion = extVersion.trim()) + } + data class Response(val extVersion: String) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionPacket.kt new file mode 100644 index 00000000000..3ee45801f71 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionPacket.kt @@ -0,0 +1,23 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.ReadFourByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadFourByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class GetVersionPacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray = EversenseE3Memory.TransmitterSoftwareVersion.getRequestData() + override fun parseResponse(): Response? { + val start = getStartIndex() + if (receivedData.size < start + 4) return null + val version = (start until start + 4).map { receivedData[it].toInt().toChar() }.joinToString("") + return Response(version = version.trim()) + } + data class Response(val version: String) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/PingPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/PingPacket.kt new file mode 100644 index 00000000000..95dc2352873 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/PingPacket.kt @@ -0,0 +1,23 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.PingCommandId, + responseId = EversenseE3Packets.PingResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class PingPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SaveBondingInformationPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SaveBondingInformationPacket.kt new file mode 100644 index 00000000000..b02063b05d9 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SaveBondingInformationPacket.kt @@ -0,0 +1,28 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.SaveBLEBondingInformationCommandId, + responseId = EversenseE3Packets.SaveBLEBondingInformationResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SaveBondingInformationPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return ByteArray(0) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return SaveBondingInformationResponse() + } + + class SaveBondingInformationResponse() : Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SendCalibrationPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SendCalibrationPacket.kt new file mode 100644 index 00000000000..0049b259ede --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SendCalibrationPacket.kt @@ -0,0 +1,40 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +/** + * Sends a blood glucose calibration value to the Eversense E3 transmitter. + * + * Uses the SendBloodGlucoseData command (ID 21) which accepts: + * - BG value as Int16 in mg/dL (little-endian) + * - Current date (2 bytes) and time (2 bytes) as packed timestamps + * CRC16 is appended by EversenseBasePacket.buildRequest() — do not add it here. + * + * @param glucoseMgDl Blood glucose value in mg/dL + */ +@EversensePacket( + requestId = EversenseE3Packets.SendBloodGlucoseDataCommandId, + responseId = EversenseE3Packets.SendBloodGlucoseDataResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SendCalibrationPacket(private val glucoseMgDl: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + val now = System.currentTimeMillis() + val bgEncoded = EversenseE3Writer.writeInt16(glucoseMgDl) + val date = EversenseE3Writer.writeDate(now) + val time = EversenseE3Writer.writeTime(now) + return bgEncoded + date + time + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetAppVersionE3Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetAppVersionE3Packet.kt new file mode 100644 index 00000000000..2d7430be969 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetAppVersionE3Packet.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteFourByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteFourByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetAppVersionE3Packet(private val appVersion: String = "8.0.4") : EversenseBasePacket() { + override fun getRequestData(): ByteArray { + val parts = appVersion.split(".") + val i0 = parts.getOrNull(0)?.toIntOrNull() ?: 0 + val i1 = parts.getOrNull(1)?.toIntOrNull() ?: 0 + val i2 = parts.getOrNull(2)?.toIntOrNull() ?: 0 + val addr = EversenseE3Memory.AppVersion.getRequestData() + return byteArrayOf(addr[0], addr[1], addr[2], + (i2 and 0xFF).toByte(), + ((i2 and 0xFF00) shr 8).toByte(), + (i1 and 0xFF).toByte(), + (i0 and 0xFF).toByte() + ) + } + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBleDisconnectPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBleDisconnectPacket.kt new file mode 100644 index 00000000000..71864525203 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBleDisconnectPacket.kt @@ -0,0 +1,24 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetBleDisconnectPacket(private val intervalSeconds: Int = 300) : EversenseBasePacket() { + override fun getRequestData(): ByteArray { + val addr = EversenseE3Memory.BleDisconnect.getRequestData() + return byteArrayOf(addr[0], addr[1], addr[2], + (intervalSeconds and 0xFF).toByte(), + ((intervalSeconds shr 8) and 0xFF).toByte() + ) + } + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBloodGlucosePointPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBloodGlucosePointPacket.kt new file mode 100644 index 00000000000..c0bbe3ae438 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBloodGlucosePointPacket.kt @@ -0,0 +1,43 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer +import com.nightscout.eversense.packets.e365.utils.toUnixArray + +/** + * Sends a blood glucose calibration point using Unix2000 timestamps. + * Accepts a specific sample timestamp plus the current time, matching the + * iOS SendBloodGlucoseDataWithTwoTimestamps protocol format. + * + * @param glucoseInMgDl Blood glucose value in mg/dL + * @param sampleTimestamp Epoch milliseconds of the blood glucose sample + */ +@EversensePacket( + requestId = EversenseE3Packets.SendBloodGlucoseDataCommandId, + responseId = EversenseE3Packets.SendBloodGlucoseDataResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetBloodGlucosePointPacket( + private val glucoseInMgDl: Int, + private val sampleTimestamp: Long = System.currentTimeMillis() +) : EversenseBasePacket() { + override val skipResponseIdValidation: Boolean = true + + override fun getRequestData(): ByteArray { + val now = System.currentTimeMillis() + return sampleTimestamp.toUnixArray() + + now.toUnixArray() + + EversenseE3Writer.writeInt16(glucoseInMgDl) + + byteArrayOf(0x55) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetCurrentDatetimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetCurrentDatetimePacket.kt new file mode 100644 index 00000000000..6c9ff2c2dcf --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetCurrentDatetimePacket.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.SetCurrentTransmitterDateAndTimeCommandId, + responseId = EversenseE3Packets.SetCurrentTransmitterDateAndTimeResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetCurrentDatetimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + val now = System.currentTimeMillis() + return EversenseE3Writer.writeDate(now) + + EversenseE3Writer.writeTime(now) + + EversenseE3Writer.writeTimezone(now) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() {} +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalDayPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalDayPacket.kt new file mode 100644 index 00000000000..c5daa83d91a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalDayPacket.kt @@ -0,0 +1,27 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetHighGlucoseRepeatIntervalDayPacket(private val intervalMinutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmRepeatIntervalDay.getRequestData() + + byteArrayOf(intervalMinutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalNightPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalNightPacket.kt new file mode 100644 index 00000000000..0d8487162f6 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetHighGlucoseRepeatIntervalNightPacket.kt @@ -0,0 +1,27 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetHighGlucoseRepeatIntervalNightPacket(private val intervalMinutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmRepeatIntervalNight.getRequestData() + + byteArrayOf(intervalMinutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalDayPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalDayPacket.kt new file mode 100644 index 00000000000..fd5c5516176 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalDayPacket.kt @@ -0,0 +1,27 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetLowGlucoseRepeatIntervalDayPacket(private val intervalMinutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LowGlucoseAlarmRepeatIntervalDay.getRequestData() + + byteArrayOf(intervalMinutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalNightPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalNightPacket.kt new file mode 100644 index 00000000000..926605c3c83 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetLowGlucoseRepeatIntervalNightPacket.kt @@ -0,0 +1,27 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetLowGlucoseRepeatIntervalNightPacket(private val intervalMinutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LowGlucoseAlarmRepeatIntervalNight.getRequestData() + + byteArrayOf(intervalMinutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighEnablePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighEnablePacket.kt new file mode 100644 index 00000000000..9d72e98e78b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighEnablePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingGlucoseHighEnablePacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmEnabled.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response() : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighThresholdPacket.kt new file mode 100644 index 00000000000..acc13c3f022 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingGlucoseHighThresholdPacket(private val threshold: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.HighGlucoseAlarmThreshold.getRequestData() + EversenseE3Writer.writeInt16(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response() : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseLowThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseLowThresholdPacket.kt new file mode 100644 index 00000000000..584b0d688fa --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseLowThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingGlucoseLowThresholdPacket(private val threshold: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.LowGlucoseAlarmThreshold.getRequestData() + EversenseE3Writer.writeInt16(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response() : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveAlarmEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveAlarmEnabledPacket.kt new file mode 100644 index 00000000000..5ba9da6b3a8 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveAlarmEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveAlarmEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighAlarmEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighAlarmEnabledPacket.kt new file mode 100644 index 00000000000..22e013df1de --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighAlarmEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveHighAlarmEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighThresholdPacket.kt new file mode 100644 index 00000000000..39d855be6ce --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveHighThresholdPacket(private val threshold: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighTarget.getRequestData() + EversenseE3Writer.writeInt16(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighTimePacket.kt new file mode 100644 index 00000000000..f03c112a6ec --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighTimePacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveHighTimePacket(private val minutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveHighTime.getRequestData() + byteArrayOf(minutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowAlarmEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowAlarmEnabledPacket.kt new file mode 100644 index 00000000000..aa8a7d9fa4b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowAlarmEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveLowAlarmEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowThresholdPacket.kt new file mode 100644 index 00000000000..5782f2495cc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteTwoByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveLowThresholdPacket(private val threshold: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowTarget.getRequestData() + EversenseE3Writer.writeInt16(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowTimePacket.kt new file mode 100644 index 00000000000..1a15d4648c6 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowTimePacket.kt @@ -0,0 +1,29 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingPredictiveLowTimePacket(private val minutes: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.PredictiveLowTime.getRequestData() + byteArrayOf(minutes.toByte()) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateEnabledPacket.kt new file mode 100644 index 00000000000..fbf7788b034 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingRateEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingEnabledPacket.kt new file mode 100644 index 00000000000..27f1db9867a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingRateFallingEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateFallingAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingThresholdPacket.kt new file mode 100644 index 00000000000..29bab7fa731 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingRateFallingThresholdPacket(private val threshold: Double) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateFallingThreshold.getRequestData() + EversenseE3Writer.writeDouble(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingEnabledPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingEnabledPacket.kt new file mode 100644 index 00000000000..7f9083dca0f --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingEnabledPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingRateRisingEnabledPacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateRisingAlert.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingThresholdPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingThresholdPacket.kt new file mode 100644 index 00000000000..0895f41b846 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingThresholdPacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingRateRisingThresholdPacket(private val threshold: Double) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.RateRisingThreshold.getRequestData() + EversenseE3Writer.writeDouble(threshold) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingVibratePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingVibratePacket.kt new file mode 100644 index 00000000000..d083c463719 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingVibratePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.EversenseE3Memory +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e3.util.EversenseE3Writer + +@EversensePacket( + requestId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.WriteSingleByteSerialFlashRegisterResponseId, + typeId = 0, + securityType = EversenseSecurityType.None +) +class SetSettingVibratePacket(private val enabled: Boolean) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return EversenseE3Memory.VibrateMode.getRequestData() + EversenseE3Writer.writeBoolean(enabled) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Parser.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Parser.kt new file mode 100644 index 00000000000..a4736487c85 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Parser.kt @@ -0,0 +1,77 @@ +package com.nightscout.eversense.packets.e3.util + +import java.util.Calendar +import java.util.TimeZone + +class EversenseE3Parser { + companion object { + fun readDate(data: UByteArray, start: Int): Long { + require(data.size >= start + 2) { "readDate: data too short (size=${data.size}, start=$start)" } + val lowByte = data[start].toInt() + val highByte = data[start + 1].toInt() + + val day = lowByte and 31 + var month = lowByte shr 5 + val year = (highByte shr 1) + 2000 + + if (highByte and 1 == 1) { + month += 8 + } + + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month - 1) + calendar.set(Calendar.DAY_OF_MONTH, day) + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + + return calendar.timeInMillis + } + + fun readTime(data: UByteArray, start: Int): Long { + require(data.size >= start + 2) { "readTime: data too short (size=${data.size}, start=$start)" } + val lowByte = data[start].toInt() + val highByte = data[start + 1].toInt() + + val hour = highByte shr 3 + val minute = ((highByte and 7) shl 3) or (lowByte shr 5) + val second = (lowByte and 31) * 2 + + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.set(Calendar.YEAR, 1970) + calendar.set(Calendar.MONTH, 0) + calendar.set(Calendar.DAY_OF_MONTH, 1) + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, second) + calendar.set(Calendar.MILLISECOND, 0) + + return calendar.timeInMillis + } + + /** + * Reads a timezone offset from 3 bytes: 2 bytes of time (HH:MM encoded) + 1 sign byte. + * Returns the offset in milliseconds (positive = east of UTC, negative = west). + */ + fun readTimezone(data: UByteArray, start: Int): Long { + require(data.size >= start + 3) { "readTimezone: data too short (size=${data.size}, start=$start)" } + val lowByte = data[start].toInt() + val highByte = data[start + 1].toInt() + + val hour = highByte shr 3 + val minute = ((highByte and 7) shl 3) or (lowByte shr 5) + val offsetMs = (hour * 60L + minute) * 60L * 1000L + + return if (data[start + 2] != 0.toUByte()) -offsetMs else offsetMs + } + + fun readGlucose(data: UByteArray, start: Int): Int { + require(data.size >= start + 2) { "readGlucose: data too short (size=${data.size}, start=$start)" } + val lowByte = data[start].toInt() and 0xFF + val highByte = (data[start + 1].toInt() and 0xFF) shl 8 + return lowByte or highByte + } + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Writer.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Writer.kt new file mode 100644 index 00000000000..49cf9ce7453 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Writer.kt @@ -0,0 +1,60 @@ +package com.nightscout.eversense.packets.e3.util +import java.util.Calendar +import java.util.TimeZone +import kotlin.math.abs +class EversenseE3Writer { + companion object { + fun generateChecksumCRC16(data: ByteArray): ByteArray { + var crc = 0xFFFF + for (byte in data) { + var currentByte = byte.toInt() and 0xFF + repeat(8) { + val xor = ((crc shr 15) and 0x01) xor ((currentByte shr 7) and 0x01) + crc = (crc shl 1) and 0xFFFF + if (xor != 0) { + crc = (crc xor 0x1021) and 0xFFFF + } + currentByte = (currentByte shl 1) and 0xFF + } + } + return writeInt16(crc) + } + fun writeDate(timestamp: Long): ByteArray { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.setTimeInMillis(timestamp) + val year = calendar.get(Calendar.YEAR) - 2000 + val month = calendar.get(Calendar.MONTH) + 1 + val day = calendar.get(Calendar.DAY_OF_MONTH) + val byte1 = (month shl 5) or day + val byte2 = (year shl 1) or (if (month >= 8) 1 else 0) + return byteArrayOf(byte1.toByte(), byte2.toByte()) + } + fun writeTime(timestamp: Long): ByteArray { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.setTimeInMillis(timestamp) + val hour = calendar.get(Calendar.HOUR_OF_DAY) + val minute = calendar.get(Calendar.MINUTE) + val second = calendar.get(Calendar.SECOND) + val byte1 = ((minute and 7) shl 5) or (second / 2) + val byte2 = (hour shl 3) or ((minute and 56) shr 3) + return byteArrayOf(byte1.toByte(), byte2.toByte()) + } + fun writeTimezone(timestamp: Long): ByteArray { + val timezoneOffset = TimeZone.getDefault().getOffset(timestamp) + val timezoneNegative = if (timezoneOffset < 0) 255 else 0 + return writeTime(abs(timezoneOffset).toLong()) + byteArrayOf(timezoneNegative.toByte()) + } + fun writeBoolean(value: Boolean): ByteArray { + return byteArrayOf(if (value) 0x55 else 0x00) + } + fun writeDouble(value: Double): ByteArray { + return writeInt16((value * 10).toInt()) + } + fun writeInt16(value: Int): ByteArray { + return byteArrayOf( + value.toByte(), + (value shr 8).toByte() + ) + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthIdentityPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthIdentityPacket.kt new file mode 100644 index 00000000000..a3185b6a2c2 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthIdentityPacket.kt @@ -0,0 +1,28 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.AuthenticateCommandId, + responseId = Eversense365Packets.AuthenticateResponseId, + typeId = Eversense365Packets.AuthenticateIdentity, + securityType = EversenseSecurityType.SecureV2 +) +class AuthIdentityPacket(val secret: ByteArray) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return secret + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response() + } + + class Response: EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthStartPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthStartPacket.kt new file mode 100644 index 00000000000..19aa855b269 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthStartPacket.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.AuthenticateCommandId, + responseId = Eversense365Packets.AuthenticateResponseId, + typeId = Eversense365Packets.AuthenticateStart, + securityType = EversenseSecurityType.SecureV2 +) +class AuthStartPacket(val secret: ByteArray) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return secret + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + receivedData.copyOfRange(2, 66).toByteArray() + ) + } + + data class Response( + val sessionPublicKey: ByteArray + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthWhoAmIPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthWhoAmIPacket.kt new file mode 100644 index 00000000000..8f69f6c3b02 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthWhoAmIPacket.kt @@ -0,0 +1,36 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.AuthenticateCommandId, + responseId = Eversense365Packets.AuthenticateResponseId, + typeId = Eversense365Packets.AuthenticateWhoAmI, + securityType = EversenseSecurityType.SecureV2 +) +class AuthWhoAmIPacket(private val clientId: ByteArray) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return clientId + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + receivedData.copyOfRange(2, 34).toByteArray(), + receivedData.copyOfRange(34, 42).toByteArray(), + ((receivedData[42].toInt() shl 8) or receivedData[43].toInt()) == 0, + ) + } + + data class Response( + val serialNumber: ByteArray, + val nonce: ByteArray, + val flags: Boolean + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Eversense365Packets.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Eversense365Packets.kt new file mode 100644 index 00000000000..49308499743 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Eversense365Packets.kt @@ -0,0 +1,71 @@ +package com.nightscout.eversense.packets.e365 + +class Eversense365Packets { + companion object { + + const val AuthenticateCommandId = 0x09.toByte() + const val AuthenticateResponseId = 0x0B.toByte() + + const val ReadCommandId = 0x02.toByte() + const val ReadResponseId = 0x42.toByte() + + const val WriteCommandId = 0x03.toByte() + const val WriteResponseId = 0x43.toByte() + + const val NotificationResponseId = 0x44.toByte() + + const val AuthenticateWhoAmI = 0x01.toByte() + const val AuthenticateIdentity = 0x02.toByte() + const val AuthenticateStart = 0x03.toByte() + + const val ReadPing = 0x01.toByte() + const val ReadLogRangeOld = 0x09.toByte() + const val ReadSignalStrength = 0x1B.toByte() + const val ReadCalibrationInfo = 0x1D.toByte() + const val ReadGlucoseData = 0x1F.toByte() + const val ReadSensorInformation = 0x20.toByte() + const val ReadPatientInformation = 0x21.toByte() + const val ReadActiveAlerts = 0x22.toByte() + const val ReadLogRange = 0x38.toByte() + const val ReadLogValue = 0x3A.toByte() + + const val WriteCurrentDateTime = 0x01.toByte() + const val WriteCalibration = 0x0C.toByte() + const val WriteAppVersion = 0x0E.toByte() + const val WriteVibrateMode = 0x10.toByte() + const val WriteBleDisconnect = 0x11.toByte() + const val WritePredictionLowThreshold = 0x12.toByte() + const val WritePredictionHighThreshold = 0x13.toByte() + const val WriteRateFallingEnabled = 0x14.toByte() + const val WriteRateFallingThreshold = 0x15.toByte() + const val WriteRateRisingEnabled = 0x16.toByte() + const val WriteRateRisingThreshold = 0x17.toByte() + const val WritePredictionLowEnabled = 0x18.toByte() + const val WritePredictionLowTime = 0x19.toByte() + const val WritePredictionHighEnabled = 0x1A.toByte() + const val WritePredictionHighTime = 0x1B.toByte() + const val WriteHighGlucoseAlarmEnable = 0x1C.toByte() + const val WriteHighGlucoseAlarm = 0x1D.toByte() + const val WriteHighGlucoseAlarmRepeat = 0x1E.toByte() + const val WriteLowGlucoseAlarm = 0x1F.toByte() + const val WriteLowGlucoseAlarmRepeat = 0x20.toByte() + + const val NotificationKeepAlive = 0x02.toByte() + const val NotificationAlarmWithData = 0x03.toByte() + + const val ReadLogsId = 0x62.toByte() + + const val LogTypeAlerts: Byte = 0 + const val LogTypeCalibrations: Byte = 6 + const val LogTypeGlucose: Byte = 13 + + + fun isNotificationPacket(value: Byte): Boolean { + return value == NotificationResponseId + } + + fun isKeepAlivePacket(value1: Byte, value2: Byte): Boolean { + return value1 == NotificationResponseId && value2 == NotificationKeepAlive + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetActiveAlarmsPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetActiveAlarmsPacket.kt new file mode 100644 index 00000000000..af54cfd3d30 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetActiveAlarmsPacket.kt @@ -0,0 +1,52 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseAlarm +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.models.ActiveAlarm +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.util.EversenseLogger + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadActiveAlerts, + securityType = EversenseSecurityType.SecureV2 +) +class GetActiveAlarmsPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = byteArrayOf() + + // 42 22 -> CmdType & CmdId + // 03 -> Active alarm count + // [code, flag, priority] * count -> 3 bytes per alarm + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val count = receivedData[2].toInt() and 0xFF + val alarms = mutableListOf() + + for (i in 0 until count) { + val offsetStart = i * 3 + 3 + if (receivedData.size < offsetStart + 3) { + EversenseLogger.warning("GetActiveAlarmsPacket", "Missing data for alarm $i") + break + } + alarms.add(ActiveAlarm( + code = EversenseAlarm.from(receivedData[offsetStart].toInt() and 0xFF), + codeRaw = receivedData[offsetStart].toInt() and 0xFF, + flag = receivedData[offsetStart + 1].toInt() and 0xFF, + priority = receivedData[offsetStart + 2].toInt() and 0xFF + )) + } + + alarms.sortBy { it.priority } + EversenseLogger.info("GetActiveAlarmsPacket", "Active alarms: ${alarms.map { it.code.title }}") + return Response(count = count, alarms = alarms) + } + + data class Response( + val count: Int, + val alarms: List + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetAlertsLogValuesPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetAlertsLogValuesPacket.kt new file mode 100644 index 00000000000..b2b291def21 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetAlertsLogValuesPacket.kt @@ -0,0 +1,64 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseAlarm +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix +import com.nightscout.eversense.util.EversenseLogger + +data class AlertHistoryItem( + val datetime: Long, + val code: EversenseAlarm +) + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadLogsId, + typeId = Eversense365Packets.ReadLogValue, + securityType = EversenseSecurityType.SecureV2 +) +class GetAlertsLogValuesPacket( + private val from: Int, + private val to: Int +) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf( + Eversense365Packets.LogTypeAlerts, + (from and 0xFF).toByte(), ((from shr 8) and 0xFF).toByte(), + ((from shr 16) and 0xFF).toByte(), ((from shr 24) and 0xFF).toByte(), + (to and 0xFF).toByte(), ((to shr 8) and 0xFF).toByte(), + ((to shr 16) and 0xFF).toByte(), ((to shr 24) and 0xFF).toByte() + ) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + if (receivedData[6].toInt() != Eversense365Packets.LogTypeAlerts.toInt()) { + EversenseLogger.error("GetAlertsLogValuesPacket", "Invalid log type: ${receivedData[6]}") + return Response(count = 0, alertHistory = emptyList()) + } + + val actualData = receivedData.drop(7).toUByteArray() + val recordLength = 60 + val history = mutableListOf() + var i = 0 + + while (i + recordLength <= actualData.size) { + val chunk = actualData.copyOfRange(i, i + recordLength) + val datetime = chunk.copyOfRange(4, 12).toUnix() + val alarmCode = chunk[12].toInt() and 0xFF + history.add(AlertHistoryItem(datetime = datetime, code = EversenseAlarm.from(alarmCode))) + i += recordLength + } + + return Response(count = history.size, alertHistory = history) + } + + data class Response( + val count: Int, + val alertHistory: List + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt new file mode 100644 index 00000000000..129ff183eb3 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt @@ -0,0 +1,60 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.CalibrationMode +import com.nightscout.eversense.enums.CalibrationPhase +import com.nightscout.eversense.enums.CalibrationReadiness +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadCalibrationInfo, + securityType = EversenseSecurityType.SecureV2 +) +class GetCalibrationInfoPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf() + } + + // Parsed message: + // 42 1D -> CmdType & CmdId + // 00 -> Current calibration phase + // 06 -> Ready for calibration (CALIBRATION_READINESS) + // 00 00 00 00 00 00 00 00 -> Next calibration datetime + // 00 -> Number of calibrations per day + // 00 -> Number of calibrations in this Phase + // 00 00 -> Minutes allowed before next calibration due + // 00 00 -> Minutes allowed after next calibration due + // 00 00 -> Number of completed calibrations + // 00 00 00 00 00 00 00 00 -> Start datetime of current phase + // 00 00 -> Sensor lifetime + // 00 00 -> Warmup duration + // 00 00 -> Minutes until next calibration + // 00 00 00 00 00 00 00 00 -> Last calibration datetime + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + val calPerDay = receivedData[12].toInt() + return Response( + currentPhase = CalibrationPhase.from365(receivedData[2].toInt(), calPerDay), + calibrationReadiness = CalibrationReadiness.from(receivedData[3].toInt()), + calibrationMode = CalibrationMode.from365(calPerDay), + nextCalibration = receivedData.copyOfRange(4, 12).toUnix(), + lastCalibration = receivedData.copyOfRange(34, 42).toUnix(), + ) + } + + data class Response( + val currentPhase: CalibrationPhase, + val calibrationReadiness: CalibrationReadiness, + val calibrationMode: CalibrationMode, + val nextCalibration: Long, + val lastCalibration: Long + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationLogValuesPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationLogValuesPacket.kt new file mode 100644 index 00000000000..791cd147c8e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationLogValuesPacket.kt @@ -0,0 +1,72 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.CalibrationFlag +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix +import com.nightscout.eversense.util.EversenseLogger + +data class CalibrationHistoryItem( + val datetime: Long, + val glucoseInMgDl: Int, + val flag: CalibrationFlag +) + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadLogsId, + typeId = Eversense365Packets.ReadLogValue, + securityType = EversenseSecurityType.SecureV2 +) +class GetCalibrationLogValuesPacket( + private val from: Int, + private val to: Int +) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf( + Eversense365Packets.LogTypeCalibrations, + (from and 0xFF).toByte(), ((from shr 8) and 0xFF).toByte(), + ((from shr 16) and 0xFF).toByte(), ((from shr 24) and 0xFF).toByte(), + (to and 0xFF).toByte(), ((to shr 8) and 0xFF).toByte(), + ((to shr 16) and 0xFF).toByte(), ((to shr 24) and 0xFF).toByte() + ) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + if (receivedData[6].toInt() != Eversense365Packets.LogTypeCalibrations.toInt()) { + EversenseLogger.error("GetCalibrationLogValuesPacket", "Invalid log type: ${receivedData[6]}") + return Response(count = 0, calibrationHistory = emptyList()) + } + + val actualData = receivedData.drop(7).toUByteArray() + val recordLength = 56 + + // Offset: recordLength(4) + datetime(8) + FsStartEndFlag(2) + ProcessingDatetime(8) + // + SampleDatetime(8) + MmaFSDatetime(8) + DecisionDatetime(8) = 46 + val offsetGlucose = 4 + 8 + 2 + 8 + 8 + 8 + 8 + val offsetFlag = offsetGlucose + 2 + + val history = mutableListOf() + var i = 0 + + while (i + recordLength <= actualData.size) { + val chunk = actualData.copyOfRange(i, i + recordLength) + val datetime = chunk.copyOfRange(4, 12).toUnix() + val glucose = (chunk[offsetGlucose].toInt() and 0xFF) or ((chunk[offsetGlucose + 1].toInt() and 0xFF) shl 8) + val flag = CalibrationFlag.from(chunk[offsetFlag].toInt() and 0xFF) + history.add(CalibrationHistoryItem(datetime = datetime, glucoseInMgDl = glucose, flag = flag)) + i += recordLength + } + + return Response(count = history.size, calibrationHistory = history) + } + + data class Response( + val count: Int, + val calibrationHistory: List + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt new file mode 100644 index 00000000000..6ea0e670ab1 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt @@ -0,0 +1,84 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.enums.EversenseTrendArrow +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toInt +import com.nightscout.eversense.util.EversenseLogger +import com.nightscout.eversense.packets.e365.utils.toUnix + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadGlucoseData, + securityType = EversenseSecurityType.SecureV2 +) +class GetGlucoseDataPacket(private val sensorIdLen: Int) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf() + } + + // 42 1F -> CmdType & CmdId + // F6 95 86 CB C1 00 00 00 -> Current datetime + // 00 -> Sensor type + // 0A -> Sensor ID length + // 00 00 00 00 00 00 00 00 00 00 -> Sensor ID + // 00 18 82 cb c1 00 00 00 -> Most recent glucose datetime + // bc 00 -> Most recent glucose value + // 32 00 -> Signal strength (transmitter-to-sensor, little-endian UInt16) + // 00 00 -> Glucose unavailable reason + // ... measurement bytes ... + // 04 -> Trend direction + // 07 -> Battery percentage + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + var sensorIdLen = receivedData[11].toInt() + if (sensorIdLen == 0x00) { + sensorIdLen = this.sensorIdLen + } + + // Signal strength at bytes 22+sensorIdLen (little-endian UInt16) + val signalRaw = (receivedData[22 + sensorIdLen].toInt() and 0xFF) or + ((receivedData[23 + sensorIdLen].toInt() and 0xFF) shl 8) + + EversenseLogger.info("GetGlucoseDataPacket", "Sensor signal strength raw: $signalRaw") + + val sensorId = receivedData.copyOfRange(12, 12 + sensorIdLen) + .toByteArray().joinToString("") { "%02x".format(it) } + val rawHex = receivedData.toByteArray().joinToString("") { "%02x".format(it) } + + return Response( + datetime = receivedData.copyOfRange(12 + sensorIdLen, 20 + sensorIdLen).toUnix(), + glucoseInMgDl = receivedData.copyOfRange(20 + sensorIdLen, 22 + sensorIdLen).toInt(), + trend = getTrend(receivedData[164 + sensorIdLen].toInt()), + signalStrength = signalRaw, + sensorId = sensorId, + rawResponseHex = rawHex + ) + } + + private fun getTrend(value: Int): EversenseTrendArrow { + return when (value) { + 1 -> EversenseTrendArrow.SINGLE_DOWN + 2 -> EversenseTrendArrow.FORTY_FIVE_DOWN + 4 -> EversenseTrendArrow.FLAT + 8 -> EversenseTrendArrow.FORTY_FIVE_UP + 16 -> EversenseTrendArrow.SINGLE_UP + 32 -> EversenseTrendArrow.SINGLE_DOWN + 64 -> EversenseTrendArrow.SINGLE_UP + else -> EversenseTrendArrow.NONE + } + } + + data class Response( + val datetime: Long, + val glucoseInMgDl: Int, + val trend: EversenseTrendArrow, + val signalStrength: Int, + val sensorId: String = "", + val rawResponseHex: String = "" + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseLogValuesPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseLogValuesPacket.kt new file mode 100644 index 00000000000..440654555fc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseLogValuesPacket.kt @@ -0,0 +1,83 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.enums.EversenseTrendArrow +import com.nightscout.eversense.models.GlucoseHistoryItem +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix +import com.nightscout.eversense.util.EversenseLogger + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = 0x62.toByte(), + typeId = Eversense365Packets.ReadLogValue, + securityType = EversenseSecurityType.SecureV2 +) +class GetGlucoseLogValuesPacket( + private val from: Int, + private val to: Int, + private val sensorIdLength: Int = 10 +) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + val logType: Byte = 13 // LogTypes.Glucose + return byteArrayOf( + logType, + (from and 0xFF).toByte(), ((from shr 8) and 0xFF).toByte(), + ((from shr 16) and 0xFF).toByte(), ((from shr 24) and 0xFF).toByte(), + (to and 0xFF).toByte(), ((to shr 8) and 0xFF).toByte(), + ((to shr 16) and 0xFF).toByte(), ((to shr 24) and 0xFF).toByte() + ) + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + if (receivedData[6].toInt() != 13) { + EversenseLogger.error("GetGlucoseLogValuesPacket", "Invalid log type: ${receivedData[6]}") + return Response(count = 0, glucoseHistory = emptyList()) + } + + val actualData = receivedData.drop(7).toUByteArray() + val recordLength = 193 + val offsetGlucose = sensorIdLength + 8 + 4 + val offsetTrend = offsetGlucose + 2 + 4 + val history = mutableListOf() + var i = 0 + + while (i + recordLength <= actualData.size) { + val chunk = actualData.copyOfRange(i, i + recordLength) + val datetime = chunk.copyOfRange(4, 12).toUnix() + val glucose = (chunk[offsetGlucose].toInt() and 0xFF) or + ((chunk[offsetGlucose + 1].toInt() and 0xFF) shl 8) + val trend = getTrend(chunk[offsetTrend].toInt() and 0xFF) + i += recordLength + + if (glucose >= 0x03E8) { + EversenseLogger.warning("GetGlucoseLogValuesPacket", "Glucose exceeds limits: $glucose — skipping") + continue + } + history.add(GlucoseHistoryItem(valueInMgDl = glucose, datetime = datetime, trend = trend)) + } + + EversenseLogger.info("GetGlucoseLogValuesPacket", "History records: ${history.size}") + return Response(count = history.size, glucoseHistory = history) + } + + private fun getTrend(value: Int): EversenseTrendArrow = when (value) { + 1 -> EversenseTrendArrow.SINGLE_DOWN + 2 -> EversenseTrendArrow.FORTY_FIVE_DOWN + 4 -> EversenseTrendArrow.FLAT + 8 -> EversenseTrendArrow.FORTY_FIVE_UP + 16 -> EversenseTrendArrow.SINGLE_UP + 32 -> EversenseTrendArrow.SINGLE_DOWN + 64 -> EversenseTrendArrow.SINGLE_UP + else -> EversenseTrendArrow.FLAT + } + + data class Response( + val count: Int, + val glucoseHistory: List + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetLogRangePacket365.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetLogRangePacket365.kt new file mode 100644 index 00000000000..d66df1010ff --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetLogRangePacket365.kt @@ -0,0 +1,47 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +enum class LogType(val value: Byte) { + ALERTS(0), + CALIBRATIONS(6), + GLUCOSE(13) +} + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadLogRange, + securityType = EversenseSecurityType.SecureV2 +) +class GetLogRangePacket365(private val logType: LogType) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = byteArrayOf(logType.value) + + // 42 38 -> CmdType & CmdId + // XX -> log type + // 00 00 00 00 -> rangeFrom (UInt32 little-endian) + // 00 00 00 00 -> rangeTo (UInt32 little-endian) + override fun parseResponse(): Response? { + if (receivedData.size < 11) return null + + val rangeFrom = ((receivedData[3].toLong() and 0xFF) or + ((receivedData[4].toLong() and 0xFF) shl 8) or + ((receivedData[5].toLong() and 0xFF) shl 16) or + ((receivedData[6].toLong() and 0xFF) shl 24)).toInt() + + val rangeTo = ((receivedData[7].toLong() and 0xFF) or + ((receivedData[8].toLong() and 0xFF) shl 8) or + ((receivedData[9].toLong() and 0xFF) shl 16) or + ((receivedData[10].toLong() and 0xFF) shl 24)).toInt() + + com.nightscout.eversense.util.EversenseLogger.info( + "GetLogRangePacket365", "Log range: $rangeFrom - $rangeTo" + ) + return Response(rangeFrom = rangeFrom, rangeTo = rangeTo) + } + + data class Response(val rangeFrom: Int, val rangeTo: Int) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetPatientSettingsPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetPatientSettingsPacket.kt new file mode 100644 index 00000000000..0392ceffd15 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetPatientSettingsPacket.kt @@ -0,0 +1,83 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toShort + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadPatientInformation, + securityType = EversenseSecurityType.SecureV2 +) +class GetPatientSettingsPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf() + } + + override fun parseResponse(): Response? { + if (receivedData.size < 65) { + return null + } + + // Message parsed: + // 42 21 -> CmdType & CmdId + // 44 33 30 36 33 36 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -> Transmitter name + // 38 2E 30 2E 34 00 00 00 00 00 00 00 00 00 00 00 -> Recent MMA Version + // 00 -> Is clinical mode enabled + // 00 -> Is do not Disturb Enabled + // 2C 01 -> BLE connect time in sec -> 300s + // 46 00 -> Low sugar target in mg/dl + // B4 00 -> High sugar target in mg/dl + // 00 -> Alarm rate falling enabled + // 19 -> Alarm rate falling threshold + // 00 -> Alarm rate rising enabled + // 19 -> Alarm rate rising threshold + // 00 -> Alarm Predictive Low enabled + // 14 -> Alarm Predictive Low Time + // 00 -> Alarm Predictive High enabled + // 14 -> Alarm Predictive High Time + // 01 -> Alarm High Glucose enabled + // FA 00 -> Alarm High Glucose Threshold + // 1E -> Alarm High Glucose Repeat Interval + // 41 00 -> Alarm Low Glucose Threshold + // 0F -> Alarm Low Glucose Repeat Interval + // 34 -> Battery Temp Thresh Mode Change + // 44 -> Battery Temp Thresh Warn + return Response( + vibrateMode = receivedData[44].toInt() != 0x00, + highGlucoseEnabled = receivedData[59].toInt() != 0x00, + lowGlucoseAlarmInMgDl = receivedData.copyOfRange(60, 62).toShort().toInt(), + highGlucoseAlarmInMgDl = receivedData.copyOfRange(63, 65).toShort().toInt(), + predictionLowEnabled = receivedData[55].toInt() != 0x00, + predictionHighEnabled = receivedData[57].toInt() != 0x00, + predictionFallingInterval = receivedData[56].toInt(), + predictionRisingInterval = receivedData[58].toInt(), + predictionFallingThreshold = receivedData.copyOfRange(47, 49).toShort().toInt(), + predictionRisingThreshold = receivedData.copyOfRange(49, 51).toShort().toInt(), + rateFallingEnabled = receivedData[51].toInt() != 0x00, + rateRisingEnabled = receivedData[53].toInt() != 0x00, + rateFallingThreshold = receivedData[52].toDouble() / 10, + rateRisingThreshold = receivedData[54].toDouble() / 10, + ) + } + + data class Response( + val vibrateMode: Boolean, + val highGlucoseEnabled: Boolean, + val lowGlucoseAlarmInMgDl: Int, + val highGlucoseAlarmInMgDl: Int, + val predictionLowEnabled: Boolean, + val predictionHighEnabled: Boolean, + val predictionFallingInterval: Int, + val predictionRisingInterval: Int, + val predictionFallingThreshold: Int, + val predictionRisingThreshold: Int, + val rateFallingEnabled: Boolean, + val rateRisingEnabled: Boolean, + val rateFallingThreshold: Double, + val rateRisingThreshold: Double + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSensorInformationPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSensorInformationPacket.kt new file mode 100644 index 00000000000..c4566793339 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSensorInformationPacket.kt @@ -0,0 +1,90 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix +import com.nightscout.eversense.packets.e365.utils.toUtfString + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadSensorInformation, + securityType = EversenseSecurityType.SecureV2 +) +class GetSensorInformationPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + return byteArrayOf() + } + + // 42 20 + // 33 30 36 33 36 36 00 00 00 00 00 00 00 00 00 00 + // 44 33 30 36 33 36 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + // 26 9f 14 b7 c4 00 00 00 + // 29 6d 23 06 + // 30352e30302e30312e30374d000030312e30360030312e30300030312e30300030312e30300000000111008c012c01010a7900000000000076000083d959f3c30000006d01790000000000007600005730352e30302e30312e30374d2d303600000000000000000030312e30302e30312e30320000000000 + // Message parsed: + // 42 20 -> CmdType & CmdId + // 33 30 36 33 36 36 00 00 00 00 00 00 00 00 00 00 -> Serial number + // 44 33 30 36 33 36 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -> Transmitter name + // 1A 44 08 CB BF 00 00 00 -> Current datetime + // 29 6D 23 06 -> Transmitter model + // 30 35 2E 30 30 2E 30 31 2E 30 37 4D 00 00 -> Current firmware version + // 30 31 2E 30 36 00 -> Comm version + // 30 31 2E 30 30 00 -> Register map version + // 30 31 2E 30 30 00 -> Log map version + // 30 31 2E 30 30 00 -> Push map version + // 00 -> Glucose algorithm version major + // 00 -> Glucose algorithm version minor + // 01 -> MMA functionality + // 11 00 -> Transmitter mode + // 8C 01 -> Transmitter life remaining + // 2C 01 -> Sensor sample interval + // 01 -> Sensor type + // 0A -> Sensor ID length + // 00 00 00 00 00 00 00 00 00 00 -> Sensor ID (size is based on previous value) + // 00 00 00 00 00 00 00 00 -> Sensor insertion date + // 00 00 -> Sensor life remaining + // 00 00 00 00 00 00 00 00 00 00 -> Detected sensor ID (length is based on Sensor ID length) + // 62 -> Battery percentage + // 30 35 2E 30 30 2E 30 31 2E 30 37 4D 2D 30 36 00 -> Firmware version + // 00 00 00 00 00 00 00 00 -> Operation start datetime + // 30 31 2E 30 30 2E 30 31 2E 30 32 00 00 00 00 00 -> Other firmware version + override fun parseResponse(): Response? { + if (receivedData.size < 104) { + return null + } + + val sensorIdLength = receivedData[103].toInt() + val sensorIdDoubleLength = 2 * sensorIdLength + if (receivedData.size < 139 + sensorIdDoubleLength) { + return null + } + return Response( + serialNumber = receivedData.copyOfRange(2, 18).toUtfString(), + transmitterName = receivedData.copyOfRange(18, 43).toUtfString(), + transmitterDatetime = receivedData.copyOfRange(43, 51).toUnix(), + insertionDate = receivedData.copyOfRange(104+sensorIdLength, 112+sensorIdLength).toUnix(), + mmaFeatures = receivedData[95].toInt(), + batteryLevel = receivedData[114+sensorIdDoubleLength].toInt(), + version = receivedData.copyOfRange(115+sensorIdDoubleLength, 131+sensorIdDoubleLength).toUtfString(), + extVersion = receivedData.copyOfRange(131+sensorIdDoubleLength, 139+sensorIdDoubleLength).toUtfString(), + sensorIdLength = sensorIdLength, + communicationProtocolVersion = receivedData.copyOfRange(69, 75).toUtfString().toDouble() + ) + } + + data class Response( + val serialNumber: String, + val transmitterName: String, + val transmitterDatetime: Long, + val insertionDate: Long, + val mmaFeatures: Int, + val batteryLevel: Int, + val version: String, + val extVersion: String, + val sensorIdLength: Int, + val communicationProtocolVersion: Double + ) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSignalStrengthPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSignalStrengthPacket.kt new file mode 100644 index 00000000000..da3f33ba986 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSignalStrengthPacket.kt @@ -0,0 +1,63 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import java.nio.ByteBuffer +import java.nio.ByteOrder + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadSignalStrength, + securityType = EversenseSecurityType.SecureV2 +) +class GetSignalStrengthPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = byteArrayOf() + + // Parsed message: + // 42 1B -> CmdType & CmdId + // 01 -> SensorType + // 0A -> Sensor ID length + // 00 00 00 00 00 00 00 00 00 00 -> Sensor ID (length = byte[3]) + // XX XX XX XX XX XX XX XX -> Timestamp + // XX XX -> Signal Strength (raw) + // XX XX XX XX -> Signal Coupling (little-endian float, multiply by 100 for percentage) + // XX -> Placement + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) return null + + val hex = receivedData.joinToString(" ") { it.toString(16).padStart(2, '0') } + com.nightscout.eversense.util.EversenseLogger.info("GetSignalStrengthPacket", "Raw response: $hex") + + // Byte [3] = sensor ID length + val sensorIdLength = receivedData[3].toInt() and 0xFF + + // Signal coupling float starts at byte 14 + sensorIdLength + val couplingStart = 14 + sensorIdLength + if (receivedData.size < couplingStart + 4) { + com.nightscout.eversense.util.EversenseLogger.warning("GetSignalStrengthPacket", "Response too short: ${receivedData.size} bytes, need ${couplingStart + 4}") + return Response(signalStrength = 0) + } + + // Read little-endian float and multiply by 100 for percentage + val floatBytes = byteArrayOf( + receivedData[couplingStart].toByte(), + receivedData[couplingStart + 1].toByte(), + receivedData[couplingStart + 2].toByte(), + receivedData[couplingStart + 3].toByte() + ) + val signalFloat = ByteBuffer.wrap(floatBytes) + .order(ByteOrder.LITTLE_ENDIAN) + .float + val signalPercent = (signalFloat * 100).toInt().coerceIn(0, 100) + + com.nightscout.eversense.util.EversenseLogger.info("GetSignalStrengthPacket", "sensorIdLength: $sensorIdLength, signalFloat: $signalFloat -> $signalPercent%") + return Response(signalStrength = signalPercent) + } + + data class Response( + val signalStrength: Int // 0-100, transmitter-to-sensor placement signal + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/KeepAlivePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/KeepAlivePacket.kt new file mode 100644 index 00000000000..1886bac3efc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/KeepAlivePacket.kt @@ -0,0 +1,30 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix + +@EversensePacket( + requestId = -1, // Can only be received + responseId = Eversense365Packets.NotificationResponseId, + typeId = Eversense365Packets.NotificationKeepAlive, + securityType = EversenseSecurityType.SecureV2 +) +class KeepAlivePacket : EversenseBasePacket() { + override fun getRequestData(): ByteArray { + return byteArrayOf() + } + + override fun parseResponse(): Response? { + if (receivedData.isEmpty()) { + return null + } + + return Response( + glucoseDatetime = receivedData.copyOfRange(11, 19).toUnix(), + ) + } + + data class Response(val glucoseDatetime: Long) : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Ping365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Ping365Packet.kt new file mode 100644 index 00000000000..e76408127d7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Ping365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.ReadCommandId, + responseId = Eversense365Packets.ReadResponseId, + typeId = Eversense365Packets.ReadPing, + securityType = EversenseSecurityType.SecureV2 +) +class Ping365Packet : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf() + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushAlarmWithDataPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushAlarmWithDataPacket.kt new file mode 100644 index 00000000000..ade5e3719d4 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushAlarmWithDataPacket.kt @@ -0,0 +1,48 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseAlarm +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.models.ActiveAlarm +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix + +/** + * Push notification packet for alarms with data payload. + * + * Packet format: + * [0] = 0x44 (NotificationResponseId) + * [1] = 0x03 (AlarmWithData) + * [2] = reserved + * [3] = alarm code + * [4..11] = alarm datetime (Unix2000) + * [12..] = alarm data + */ +@EversensePacket( + requestId = Eversense365Packets.NotificationResponseId, + responseId = Eversense365Packets.NotificationAlarmWithData, + typeId = 0, + securityType = EversenseSecurityType.SecureV2 +) +class PushAlarmWithDataPacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.size < 12) return null + + val alarmCode = receivedData[3].toInt() and 0xFF + val alarm = EversenseAlarm.from(alarmCode) + val datetime = receivedData.copyOfRange(4, 12).toUnix() + + return Response( + alarm = ActiveAlarm(code = alarm, codeRaw = alarmCode, flag = 0, priority = 0), + datetime = datetime + ) + } + + data class Response( + val alarm: ActiveAlarm, + val datetime: Long + ) : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushKeepAlive365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushKeepAlive365Packet.kt new file mode 100644 index 00000000000..c65c7a9e018 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/PushKeepAlive365Packet.kt @@ -0,0 +1,50 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnix + +/** + * Push keep-alive packet that includes battery level and most recent glucose datetime. + * + * Offsets: + * [10] = battery level (raw enum value) + * [11..18] = most recent glucose datetime (Unix2000) + */ +@EversensePacket( + requestId = Eversense365Packets.NotificationResponseId, + responseId = Eversense365Packets.NotificationKeepAlive, + typeId = 0, + securityType = EversenseSecurityType.SecureV2 +) +class PushKeepAlive365Packet : EversenseBasePacket() { + + override fun getRequestData(): ByteArray = ByteArray(0) + + override fun parseResponse(): Response? { + if (receivedData.size < 19) return null + + val batteryLevel = receivedData[OFFSET_BATTERY_LEVEL].toInt() and 0xFF + val glucoseDatetime = receivedData.copyOfRange( + OFFSET_GLUCOSE_DATETIME_START, + OFFSET_GLUCOSE_DATETIME_END + ).toUnix() + + return Response( + batteryLevelRaw = batteryLevel, + mostRecentGlucoseDatetime = glucoseDatetime + ) + } + + data class Response( + val batteryLevelRaw: Int, + val mostRecentGlucoseDatetime: Long + ) : EversenseBasePacket.Response() + + companion object { + private const val OFFSET_BATTERY_LEVEL = 10 + private const val OFFSET_GLUCOSE_DATETIME_START = 11 + private const val OFFSET_GLUCOSE_DATETIME_END = 19 + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetAppVersion365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetAppVersion365Packet.kt new file mode 100644 index 00000000000..46261babcff --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetAppVersion365Packet.kt @@ -0,0 +1,24 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteAppVersion, + securityType = EversenseSecurityType.SecureV2 +) +class SetAppVersion365Packet(private val appVersion: String = "8.0.4") : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + val data = ByteArray(18) + val versionBytes = appVersion.toByteArray(Charsets.US_ASCII) + versionBytes.copyInto(data, 0, 0, minOf(versionBytes.size, 18)) + return data + } + + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBleDisconnect365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBleDisconnect365Packet.kt new file mode 100644 index 00000000000..8e47c9e5783 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBleDisconnect365Packet.kt @@ -0,0 +1,25 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteBleDisconnect, + securityType = EversenseSecurityType.SecureV2 +) +class SetBleDisconnect365Packet(private val intervalSeconds: Int = 300) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + // UInt16 little-endian interval in seconds + return byteArrayOf( + (intervalSeconds and 0xFF).toByte(), + ((intervalSeconds shr 8) and 0xFF).toByte() + ) + } + + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBloodGlucosePointPacket365.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBloodGlucosePointPacket365.kt new file mode 100644 index 00000000000..5fa3bbf88d7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBloodGlucosePointPacket365.kt @@ -0,0 +1,32 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toUnixArray + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteCalibration, + securityType = EversenseSecurityType.SecureV2 +) +class SetBloodGlucosePointPacket365(private val glucoseInMgDl: Int, private val timestampMs: Long = System.currentTimeMillis()) : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + var data = timestampMs.toUnixArray() // fingerstick measurement timestamp + data += System.currentTimeMillis().toUnixArray() // current time + data += byteArrayOf( + (glucoseInMgDl and 0xFF).toByte(), + ((glucoseInMgDl shr 8) and 0xFF).toByte() + ) + data += byteArrayOf(1, 0, 0) + return data + } + + override fun parseResponse(): Response { + return Response() + } + + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetCurrentDateTimePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetCurrentDateTimePacket.kt new file mode 100644 index 00000000000..eb73eac0f49 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetCurrentDateTimePacket.kt @@ -0,0 +1,38 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import com.nightscout.eversense.packets.e365.utils.toByteArray +import com.nightscout.eversense.packets.e365.utils.toTimeZone +import com.nightscout.eversense.packets.e365.utils.toUnixArray +import java.time.ZonedDateTime +import java.util.TimeZone +import kotlin.math.abs + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteCurrentDateTime, + securityType = EversenseSecurityType.SecureV2 +) +class SetCurrentDateTimePacket : EversenseBasePacket() { + + override fun getRequestData(): ByteArray { + val now = System.currentTimeMillis() + val timezoneOffset = TimeZone.getDefault().getOffset(now) + val timezoneNegative = if (timezoneOffset < 0) 255.toByte() else 0.toByte() + + var request = now.toUnixArray() + request += abs(timezoneOffset).toTimeZone() + request += byteArrayOf(timezoneNegative) + + return request + } + + override fun parseResponse(): Response { + return Response() + } + + class Response : EversenseBasePacket.Response() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarm365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarm365Packet.kt new file mode 100644 index 00000000000..3733676dd5e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarm365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteHighGlucoseAlarm, + securityType = EversenseSecurityType.SecureV2 +) +class SetHighGlucoseAlarm365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarmEnabled365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarmEnabled365Packet.kt new file mode 100644 index 00000000000..15531d2a72a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarmEnabled365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteHighGlucoseAlarmEnable, + securityType = EversenseSecurityType.SecureV2 +) +class SetHighGlucoseAlarmEnabled365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetLowGlucoseAlarm365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetLowGlucoseAlarm365Packet.kt new file mode 100644 index 00000000000..65076a5c3a7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetLowGlucoseAlarm365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteLowGlucoseAlarm, + securityType = EversenseSecurityType.SecureV2 +) +class SetLowGlucoseAlarm365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighEnabled365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighEnabled365Packet.kt new file mode 100644 index 00000000000..e021423ff0a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighEnabled365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionHighEnabled, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionHighEnabled365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighThreshold365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighThreshold365Packet.kt new file mode 100644 index 00000000000..b14d9ec752e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighThreshold365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionHighThreshold, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionHighThreshold365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighTime365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighTime365Packet.kt new file mode 100644 index 00000000000..0eb5ed0c562 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighTime365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionHighTime, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionHighTime365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowEnabled365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowEnabled365Packet.kt new file mode 100644 index 00000000000..260854f473a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowEnabled365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionLowEnabled, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionLowEnabled365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowThreshold365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowThreshold365Packet.kt new file mode 100644 index 00000000000..e123847df0e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowThreshold365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionLowThreshold, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionLowThreshold365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowTime365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowTime365Packet.kt new file mode 100644 index 00000000000..96360e24253 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowTime365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WritePredictionLowTime, + securityType = EversenseSecurityType.SecureV2 +) +class SetPredictionLowTime365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingEnabled365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingEnabled365Packet.kt new file mode 100644 index 00000000000..a835b425c1f --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingEnabled365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteRateFallingEnabled, + securityType = EversenseSecurityType.SecureV2 +) +class SetRateFallingEnabled365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingThreshold365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingThreshold365Packet.kt new file mode 100644 index 00000000000..d8128c82733 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingThreshold365Packet.kt @@ -0,0 +1,23 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import java.nio.ByteBuffer +import java.nio.ByteOrder + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteRateFallingThreshold, + securityType = EversenseSecurityType.SecureV2 +) +class SetRateFallingThreshold365Packet(private val value: Double) : EversenseBasePacket() { + override fun getRequestData(): ByteArray { + val buf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) + buf.putFloat(value.toFloat()) + return buf.array() + } + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingEnabled365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingEnabled365Packet.kt new file mode 100644 index 00000000000..43044404ba4 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingEnabled365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteRateRisingEnabled, + securityType = EversenseSecurityType.SecureV2 +) +class SetRateRisingEnabled365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingThreshold365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingThreshold365Packet.kt new file mode 100644 index 00000000000..77bd1dca0dd --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingThreshold365Packet.kt @@ -0,0 +1,23 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket +import java.nio.ByteBuffer +import java.nio.ByteOrder + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteRateRisingThreshold, + securityType = EversenseSecurityType.SecureV2 +) +class SetRateRisingThreshold365Packet(private val value: Double) : EversenseBasePacket() { + override fun getRequestData(): ByteArray { + val buf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) + buf.putFloat(value.toFloat()) + return buf.array() + } + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatHighGlucose365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatHighGlucose365Packet.kt new file mode 100644 index 00000000000..3adc7ce29e5 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatHighGlucose365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteHighGlucoseAlarmRepeat, + securityType = EversenseSecurityType.SecureV2 +) +class SetRepeatHighGlucose365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatLowGlucose365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatLowGlucose365Packet.kt new file mode 100644 index 00000000000..a1d7a39a578 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatLowGlucose365Packet.kt @@ -0,0 +1,20 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteLowGlucoseAlarmRepeat, + securityType = EversenseSecurityType.SecureV2 +) +class SetRepeatLowGlucose365Packet(private val value: Int) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf( + (value and 0xFF).toByte(), + ((value shr 8) and 0xFF).toByte() + ) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetVibrateMode365Packet.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetVibrateMode365Packet.kt new file mode 100644 index 00000000000..281fc2fb376 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetVibrateMode365Packet.kt @@ -0,0 +1,17 @@ +package com.nightscout.eversense.packets.e365 + +import com.nightscout.eversense.enums.EversenseSecurityType +import com.nightscout.eversense.packets.EversenseBasePacket +import com.nightscout.eversense.packets.EversensePacket + +@EversensePacket( + requestId = Eversense365Packets.WriteCommandId, + responseId = Eversense365Packets.WriteResponseId, + typeId = Eversense365Packets.WriteVibrateMode, + securityType = EversenseSecurityType.SecureV2 +) +class SetVibrateMode365Packet(private val enabled: Boolean) : EversenseBasePacket() { + override fun getRequestData(): ByteArray = byteArrayOf(if (enabled) 1 else 0) + override fun parseResponse(): Response = Response() + class Response : EversenseBasePacket.Response() +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/utils/ByteArrayUtil.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/utils/ByteArrayUtil.kt new file mode 100644 index 00000000000..9e30a154edc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/utils/ByteArrayUtil.kt @@ -0,0 +1,70 @@ +package com.nightscout.eversense.packets.e365.utils + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.experimental.or + +fun UByteArray.toUtfString(): String { + val sb = StringBuilder() + for (i in this) { + if (i == 0.toUByte()) { + continue + } + + sb.append(i.toInt().toChar()) + } + + return sb.toString() +} + +fun Short.toByteArray(): ByteArray { + val allocate = ByteBuffer.allocate(2) + allocate.order(ByteOrder.LITTLE_ENDIAN) + allocate.putShort(this) + return allocate.array() +} + +fun Int.toTimeZone(): ByteArray { + val totalMinutes = this / (60 * 1000) + val hour = totalMinutes / 60 + val minute = totalMinutes % 60 + + val byte1: Byte = ((minute and 7) shl 5).toByte() + val byte2: Byte = ((hour shl 3) or ((minute and 56) shr 3) ).toByte() + + return byteArrayOf(byte1, byte2) +} + +fun UByteArray.toShort(): Short { + return ((this[0].toInt() and 0xFF) or ((this[1].toInt() and 0xFF) shl 8)).toShort() +} + +fun UByteArray.toInt(): Int { + return (this[0].toInt() and 0xFF) or ((this[1].toInt() and 0xFF) shl 8) +} + +fun UByteArray.toLong(): Long { + var result = 0L + for (i in indices) { + val shifted = (this[i].toLong() and 0xFF) shl (8 * i) + result = result or shifted + } + return result +} +fun ByteArray.toLong(): Long { + return this.toUByteArray().toLong() +} + +const val UNIX = 946_684_800_000 +fun UByteArray.toUnix(): Long { + val offset = this.toLong() / 1024 * 1000 + return UNIX + offset +} + +fun Long.toUnixArray(): ByteArray { + val offset = (this - UNIX) / 1000 * 1024 + + val allocate = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN) + allocate.putLong(offset) + return allocate.array() +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EselSmoothing.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EselSmoothing.kt new file mode 100644 index 00000000000..a06951b1f4a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EselSmoothing.kt @@ -0,0 +1,33 @@ +package com.nightscout.eversense.util + +import kotlin.math.min +import kotlin.math.round + +class EselSmoothing { + companion object { + private const val factor: Double = 0.3 + private const val correction: Double = 0.5 + private const val descent_factor: Double = 0.0 + + fun smooth(currentRaw: Int, lastSmooth: Int, lastRaw: Int): Int { + val value = currentRaw.toDouble() + + // exponential smoothing, see https://en.wikipedia.org/wiki/Exponential_smoothing + // y'[t]=y'[t-1] + (a*(y-y'[t-1])) = a*y+(1-a)*y'[t-1] + // factor is a, value is y, lastSmooth y'[t-1], smooth y' + // factor between 0 and 1, default 0.3 + // factor = 0: always last smooth (constant) + // factor = 1: no smoothing + var smooth: Double = lastSmooth + (factor * (value - lastSmooth)) + + // correction: average of delta between raw and smooth value, added to smooth with correction factor + // correction between 0 and 1, default 0.5 + // correction = 0: no correction, full smoothing + // correction > 0: less smoothing + smooth += (correction * ((lastRaw - lastSmooth) + (value - smooth)) / 2.0) + smooth -= descent_factor * (smooth - min(value, smooth)) + + return round(smooth).toInt() + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseCrypto365Util.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseCrypto365Util.kt new file mode 100644 index 00000000000..5752a0c9546 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseCrypto365Util.kt @@ -0,0 +1,303 @@ +package com.nightscout.eversense.util + +import android.content.SharedPreferences +import android.security.keystore.KeyProperties +import androidx.core.content.edit +import com.nightscout.eversense.models.EversenseSecureState +import com.nightscout.eversense.packets.e365.utils.toByteArray +import com.nightscout.eversense.packets.e365.utils.toLong +import kotlinx.serialization.json.Json +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DLSequence +import org.bouncycastle.crypto.digests.SHA256Digest +import org.bouncycastle.crypto.engines.AESEngine +import org.bouncycastle.crypto.generators.HKDFBytesGenerator +import org.bouncycastle.crypto.modes.CCMBlockCipher +import org.bouncycastle.crypto.params.AEADParameters +import org.bouncycastle.crypto.params.HKDFParameters +import org.bouncycastle.crypto.params.KeyParameter +import org.bouncycastle.util.BigIntegers +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.AlgorithmParameters +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.Signature +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.security.spec.PKCS8EncodedKeySpec +import javax.crypto.KeyAgreement +import kotlin.collections.toByteArray + +class EversenseCrypto365Util(val preference: SharedPreferences) { + private var ephemPrivate: ECPrivateKey? = null + private var ephemPublic: ECPublicKey? = null + private var ephemSalt: ByteArray? = null + + private var messageSequenceNumber = 1 + private var sessionKey: ByteArray? = null + + fun canUseShortcut(): Boolean { + return getState(preference).canUseShortcut + } + + fun allowUseShortcut() { + val state = getState(preference) + state.canUseShortcut = true + saveState(state, preference) + } + + fun disallowUseShortcut() { + val state = getState(preference) + state.canUseShortcut = false + saveState(state, preference) + } + + fun getClientPublicKey(): ByteArray { + val state = getState(preference) + return state.publicKey.hexToByteArray() + } + + fun generateKeyPairIfNotExists(): Boolean { + val state = getState(preference) + if (state.clientId.isNotEmpty() && state.publicKey.isNotEmpty() && state.privateKey.isNotEmpty()) { + EversenseLogger.debug(TAG, "Already generated keypair") + return true + } + + val keyPair = generatePrivateKeyPair() ?: return false + state.clientId = generateRandomBytes(32).toHexString() + state.privateKey = keyPair.private.encoded.toHexString() + state.publicKey = keyPair.public.encoded.toHexString() + + saveState(state, preference) + EversenseLogger.debug(TAG, "Generated keypair!") + return true + } + + fun getClientId(): ByteArray { + val state = getState(preference) + return state.clientId.hexToByteArray() + } + + fun getStartSecret(signature: ByteArray): ByteArray { + val public = ephemPublic?.encoded ?: return byteArrayOf() + val salt = ephemSalt ?: return byteArrayOf() + + var secret = byteArrayOf(128.toByte(), 0) + secret += getState(preference).clientId.hexToByteArray() + secret += public.copyOfRange(27, public.count()) + secret += salt + secret += signature + + return secret + } + + fun generateEphem(): ByteArray? { + val keyPair = generatePrivateKeyPair() ?:run { + EversenseLogger.error(TAG, "Failed to generate keypair...") + return null + } + + val privateKey = getState(preference).privateKey.hexToByteArray() + try { + val privateKey = KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(privateKey)) + val publicKey = keyPair.public.encoded + val v2Salt = generateRandomBytes(8) + + ephemPrivate = keyPair.private as ECPrivateKey + ephemPublic = keyPair.public as ECPublicKey + ephemSalt = v2Salt + + val data = publicKey.copyOfRange(27, publicKey.count()) + v2Salt + return ecdsaSign(privateKey, data) + } catch (e: Exception) { + e.printStackTrace() + EversenseLogger.error(TAG, "Got exception during generateEphem - exception: $e") + return null + } + } + + fun generateSessionKey(encodedPublicKey: ByteArray) { + try { + val salt = ephemSalt ?: return + + val ecPoint = ECPoint( + BigInteger(1, encodedPublicKey.copyOfRange(0, 32)), + BigInteger(1, encodedPublicKey.copyOfRange(32, 64)) + ) + val algorithmParameters = AlgorithmParameters.getInstance(KeyProperties.KEY_ALGORITHM_EC).run { + init(ECGenParameterSpec("secp256r1")) + getParameterSpec(ECParameterSpec::class.java) + } + + val publicKey = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_EC).generatePublic( + ECPublicKeySpec(ecPoint, algorithmParameters) + ) + + val sharedSecret = + KeyAgreement.getInstance("ECDH").run { + init(ephemPrivate) + doPhase(publicKey, true) + generateSecret() + } + + val symmetricKey = HKDFBytesGenerator(SHA256Digest()).run { + init(HKDFParameters(sharedSecret, null, salt)) + + val arr = ByteArray(16) + generateBytes(arr, 0, 16) + arr + } + + EversenseLogger.info(TAG, "SessionKey = ${symmetricKey.toHexString()}") + sessionKey = symmetricKey + } catch (e: Exception) { + e.printStackTrace() + EversenseLogger.error(TAG, "Failed to generate sessionKey: $e") + } + } + + fun encrypt(data: ByteArray): ByteArray { + val ephemSalt = ephemSalt ?:run { + EversenseLogger.error(TAG, "No salt available...") + return byteArrayOf() + } + + val sessionKey = sessionKey ?:run { + EversenseLogger.error(TAG, "No sessionKey available...") + return byteArrayOf() + } + + val i = (messageSequenceNumber and 0x3FFF).toLong() + val prefix = (i shl 2).toShort().toByteArray() + + val salt = generateEncryptionSalt(ephemSalt, i) + messageSequenceNumber++ + + val encryptedData = aeadCCM(salt, data, prefix, true, sessionKey) ?: return byteArrayOf() + return ByteBuffer.allocate(encryptedData.count() + 2).run { + put(prefix) + put(encryptedData) + array() + } + } + + fun decrypt(response: ByteArray): ByteArray { + val ephemSalt = ephemSalt ?:run { + EversenseLogger.error(TAG, "No salt available...") + return byteArrayOf() + } + + val sessionKey = sessionKey ?:run { + EversenseLogger.error(TAG, "No sessionKey available...") + return byteArrayOf() + } + + val cypherText = response.copyOfRange(2, response.size) + val prefix = response.copyOfRange(0, 2) + val i = (prefix.toLong() shr 2) and 0x3FFF + val salt = generateEncryptionSalt(ephemSalt, i) + + return aeadCCM(salt, cypherText, prefix, false, sessionKey) ?: byteArrayOf() + } + + companion object { + private const val TAG = "EversenseCrypto365Handler" + private val JSON = Json { ignoreUnknownKeys = true } + + private fun generateRandomBytes(i: Int): ByteArray { + val bArr = ByteArray(i) + SecureRandom().nextBytes(bArr) + return bArr + } + + private fun generatePrivateKeyPair(): KeyPair? { + try { + return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC).run { + initialize(ECGenParameterSpec("secp256r1")) + generateKeyPair() + } + } catch (e: Exception) { + EversenseLogger.error(TAG, "Error generating key pair: $e") + return null + } + } + + private fun ecdsaSign(privateKey: PrivateKey, data: ByteArray): ByteArray? { + try { + val signature = Signature.getInstance("SHA256withECDSA").run { + initSign(privateKey) + update(data) + sign() + } + + val dLSequence = ASN1InputStream(signature).readObject() as DLSequence + val value1 = (dLSequence.getObjectAt(0) as ASN1Integer).value + val value2 = (dLSequence.getObjectAt(1) as ASN1Integer).value + val bigInt1 = BigIntegers.asUnsignedByteArray(32, value1) + val bigInt2 = BigIntegers.asUnsignedByteArray(32, value2) + + return bigInt1 + bigInt2 + } catch(e: Exception) { + EversenseLogger.error(TAG, "Got exception during ecdsaSign - exception: $e") + return null + } + } + + private fun generateEncryptionSalt(salt: ByteArray, i: Long): ByteArray { + val wrapLong = ByteBuffer.wrap(salt).run { + order(ByteOrder.LITTLE_ENDIAN) + getLong() + } + + val wrapLongLong = ((wrapLong and (-16384)) or i) + val wrapByteArray = longToBytes(wrapLongLong) + return wrapByteArray.reversed().toByteArray() + } + + private fun getState(preference: SharedPreferences): EversenseSecureState { + val stateJson = preference.getString(StorageKeys.SECURE_STATE, null) ?: "{}" + return JSON.decodeFromString(stateJson) + } + + private fun saveState(state: EversenseSecureState, preference: SharedPreferences) { + preference.edit(commit = true) { + putString(StorageKeys.SECURE_STATE, JSON.encodeToString(state)) + } + } + + private fun aeadCCM(salt: ByteArray, data: ByteArray, prefix: ByteArray, forEncryption: Boolean, sessionKey: ByteArray): ByteArray? { + try { + return CCMBlockCipher.newInstance(AESEngine.newInstance()).run { + init(forEncryption, + AEADParameters(KeyParameter(sessionKey), salt.count() * 8, salt, prefix) + ) + + val bArr5 = ByteArray(getOutputSize(data.count())) + doFinal(bArr5, processBytes(data, 0, data.count(), bArr5, 0)) + bArr5 + } + } catch (e: Exception) { + EversenseLogger.error(TAG, "AEAD-CCM encryption/decryption error: $e"); + e.printStackTrace() + return null; + } + } + + private fun longToBytes(j: Long): ByteArray { + val allocate = ByteBuffer.allocate(8) + allocate.putLong(j) + return allocate.array() + } + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseHttp365Util.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseHttp365Util.kt new file mode 100644 index 00000000000..3ea46b3c28e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseHttp365Util.kt @@ -0,0 +1,253 @@ +package com.nightscout.eversense.util + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import androidx.core.content.edit +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseSecureState +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.Base64 +import java.util.Date +import java.util.Locale + +class EversenseHttp365Util { + companion object { + private val TAG = "EversenseHttp365Util" + private val JSON = Json { ignoreUnknownKeys = true } + + private val CLIENT_ID = "eversenseMMAAndroid" + private val CLIENT_SECRET = "6ksPx#]~wQ3U" + private val CLIENT_NO = 2 + private val CLIENT_TYPE = 128 + + // Overridable for unit tests + internal var tokenBaseUrl = "https://usiamapi.eversensedms.com/" + internal var uploadBaseUrl = "https://usmobileappmsprod.eversensedms.com/" + + fun login(preference: SharedPreferences): LoginResponseModel? { + val state = getState(preference) + try { + val formBody = listOf( + "grant_type=password", + "client_id=$CLIENT_ID", + "client_secret=$CLIENT_SECRET", + "username=${state.username}", + "password=${state.password}" + ).joinToString("&") + + val url = URL("${tokenBaseUrl}connect/token") + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.doOutput = true + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") + + val stream = conn.outputStream + val outputStreamWriter = OutputStreamWriter(stream, "UTF-8") + outputStreamWriter.write(formBody) + outputStreamWriter.flush() + outputStreamWriter.close() + stream.close() + conn.connect() + + val bufferStream = BufferedInputStream(conn.inputStream) + val buffer = ByteArrayOutputStream() + var data = bufferStream.read() + while (data != -1) { + buffer.write(data) + data = bufferStream.read() + } + + val dataJson = buffer.toString() + + if (conn.responseCode >= 400) { + EversenseLogger.error(TAG, "Failed to do login - status: ${conn.responseCode}, data: $dataJson") + return null + } + + EversenseLogger.info(TAG, "Login success - status: ${conn.responseCode}") + return Json.decodeFromString(LoginResponseModel.serializer(), dataJson) + } catch (e: Exception) { + EversenseLogger.error(TAG, "Got exception during login - exception: $e") + return null + } + } + + fun getFleetSecretV2(accessToken: String, serialNumber: ByteArray, nonce: ByteArray, flags: Boolean, publicKey: ByteArray): FleetSecretV2ResponseModel? { + try { + val publicKeyStr = Base64.getUrlEncoder().withoutPadding() + .encodeToString(publicKey.copyOfRange(27, publicKey.count())) + val serialNumberStr = + Base64.getUrlEncoder().withoutPadding().encodeToString(serialNumber) + val nonceStr = Base64.getUrlEncoder().withoutPadding().encodeToString(nonce) + val query = listOf( + "tx_flags=$flags", + "txSerialNumber=$serialNumberStr", + "nonce=$nonceStr", + "clientNo=$CLIENT_NO", + "clientType=$CLIENT_TYPE", + "kp_client_unique_id=$publicKeyStr" + ).joinToString("&") + + val url = + URL("https://deviceauthorization.eversensedms.com/api/vault/GetTxCertificate?$query") + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "GET" + conn.setRequestProperty("Authorization", "Bearer $accessToken") + conn.connect() + + val bufferStream = BufferedInputStream(conn.inputStream) + val buffer = ByteArrayOutputStream() + var data = bufferStream.read() + while (data != -1) { + buffer.write(data) + data = bufferStream.read() + } + + val dataJson = buffer.toString() + + if (conn.responseCode >= 400) { + EversenseLogger.error(TAG, "Failed to do login - status: ${conn.responseCode}, data: $dataJson") + return null + } + + val response = Json.decodeFromString(FleetSecretV2ResponseModel.serializer(), dataJson) + if (response.Status != "Success" || response.Result.Certificate == null) { + EversenseLogger.error(TAG, "Received invalid response - message: $dataJson") + return null + } + + return response + } catch (e: Exception) { + EversenseLogger.error(TAG, "Failed to get fleetSecretV2 - exception: $e") + return null + } + } + + private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { + timeZone = java.util.TimeZone.getTimeZone("UTC") + } + + fun getOrRefreshToken(preferences: SharedPreferences): String? { + val expiry = preferences.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0) + val cached = preferences.getString(StorageKeys.ACCESS_TOKEN, null) + // Use cached token if it has more than 5 minutes remaining + if (cached != null && System.currentTimeMillis() < expiry - 300_000L) { + return cached + } + // Re-login to get a fresh token + val fresh = login(preferences) ?: return null + val newExpiry = System.currentTimeMillis() + (fresh.expires_in * 1000L) + preferences.edit(commit = true) { + putString(StorageKeys.ACCESS_TOKEN, fresh.access_token) + putLong(StorageKeys.ACCESS_TOKEN_EXPIRY, newExpiry) + } + return fresh.access_token + } + + /** + * Upload glucose readings to the Eversense DMS cloud. + * Returns true if the server accepted the upload (HTTP 2xx), false on any error. + */ + fun uploadGlucoseReadings( + preferences: SharedPreferences, + readings: List, + transmitterSerialNumber: String, + firmwareVersion: String + ): Boolean { + // Only upload readings that have raw BLE data — backfill history entries have empty rawResponseHex + val uploadable = readings.filter { it.rawResponseHex.isNotEmpty() } + if (uploadable.isEmpty()) { + EversenseLogger.info(TAG, "No readings with raw BLE data to upload — skipping") + return true + } + val token = getOrRefreshToken(preferences) ?: run { + EversenseLogger.error(TAG, "Cannot upload glucose — no valid access token") + return false + } + EversenseLogger.info(TAG, "Uploading ${uploadable.size} reading(s) — TransmitterId='$transmitterSerialNumber'") + + return try { + // EssentialLog must be base64-encoded bytes (System.Byte[] in .NET JSON serialization) + // Body must be a bare JSON array — server deserializes directly to List + val jsonBody = uploadable.joinToString(prefix = "[", postfix = "]") { r -> + val rawBytes = r.rawResponseHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() + val essentialLogBase64 = Base64.getEncoder().encodeToString(rawBytes) + val ts = dateFormatter.format(Date(r.datetime)) + "Z" + EversenseLogger.info(TAG, " Reading: sensorId='${r.sensorId}' glucose=${r.glucoseInMgDl} ts=$ts rawBytes=${rawBytes.size}") + """{"SensorId":"${r.sensorId}","TransmitterId":"$transmitterSerialNumber","Timestamp":"$ts","CurrentGlucoseValue":${r.glucoseInMgDl},"CurrentGlucoseDateTime":"$ts","FWVersion":"$firmwareVersion","EssentialLog":"$essentialLogBase64"}""" + } + + val url = URL("${uploadBaseUrl}api/v1.0/DiagnosticLog/PostEssentialLogs") + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Authorization", "Bearer $token") + conn.setRequestProperty("Content-Type", "application/json") + conn.doOutput = true + + val writer = OutputStreamWriter(conn.outputStream, "UTF-8") + writer.write(jsonBody) + writer.flush() + writer.close() + conn.connect() + + val responseCode = conn.responseCode + if (responseCode >= 400) { + val error = try { BufferedInputStream(conn.errorStream).readBytes().toString(Charsets.UTF_8) } catch (e: Exception) { "" } + EversenseLogger.error(TAG, "Glucose upload failed — status: $responseCode, body: $error") + false + } else { + EversenseLogger.info(TAG, "Glucose upload success — status: $responseCode, readings: ${readings.size}") + true + } + } catch (e: Exception) { + EversenseLogger.error(TAG, "Glucose upload exception: $e") + false + } + } + + private fun getState(preference: SharedPreferences): EversenseSecureState { + val stateJson = preference.getString(StorageKeys.SECURE_STATE, null) ?: "{}" + return JSON.decodeFromString(stateJson) + } + } + + + @Serializable + @SuppressLint("UnsafeOptInUsageError") + data class LoginResponseModel( + val access_token: String, + val expires_in: Int, + val token_type: String, + val expires: String, + val lastLogin: String + ) + + @Serializable + @SuppressLint("UnsafeOptInUsageError") + data class FleetSecretV2ResponseModel( + val Status: String, + val StatusCode: Int, + val Result: FleetSecretV2Result + ) + + @Serializable + @SuppressLint("UnsafeOptInUsageError") + data class FleetSecretV2Result( + val Certificate: String? = null, + val Digital_Signature: String? = null, + val IsKeyAvailable: Boolean, + val KpAuthKey: String? = null, + val KpTxId: String? = null, + val KpTxUniqueId: String? = null, + val tx_flag: Boolean? = null, + val TxFleetKey: String? = null, + val TxKeyVersion: String? = null + ) +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseLogger.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseLogger.kt new file mode 100644 index 00000000000..16a5ba7eda7 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseLogger.kt @@ -0,0 +1,118 @@ +package com.nightscout.eversense.util + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.joran.JoranConfigurator +import ch.qos.logback.core.joran.spi.JoranException +import java.io.ByteArrayInputStream +import java.io.InputStream + +class EversenseLogger { + private val lc = LoggerContext() + private var isEnabled: Boolean = true + + init { + val config = JoranConfigurator() + config.setContext(lc) + + val stream: InputStream = ByteArrayInputStream(LOGBACK_XML.toByteArray()) + try { + config.doConfigure(stream) + } catch (e: JoranException) { + e.printStackTrace() + } + } + + private fun debug(tag: String, message: String) { + if (!isEnabled) { return } + + lc.getLogger(tag).debug(logLocationPrefix() + message) + } + + private fun info(tag: String, message: String) { + if (!isEnabled) { return } + lc.getLogger(tag).info(logLocationPrefix() + message) + } + + private fun warning(tag: String, message: String) { + if (!isEnabled) { return } + lc.getLogger(tag).warn(logLocationPrefix() + message) + } + + private fun error(tag: String, message: String) { + if (!isEnabled) { return } + lc.getLogger(tag).error(logLocationPrefix() + message) + } + + fun enableLogging(value: Boolean) { + this.isEnabled = value + } + + private fun logLocationPrefix(): String { + val stackInfo = Throwable().stackTrace[4] + val className = stackInfo.className.substringAfterLast(".") + val methodName = stackInfo.methodName + val lineNumber = stackInfo.lineNumber + + return "$className.$methodName():$lineNumber]: " + } + + companion object { + val instance = EversenseLogger() + + fun debug(tag: String, message: String) { + instance.debug(tag, message) + } + + fun info(tag: String, message: String) { + instance.info(tag, message) + } + + fun warning(tag: String, message: String) { + instance.warning(tag, message) + } + + fun error(tag: String, message: String) { + instance.error(tag, message) + } + + private const val LOGBACK_XML: String = "\n" + + " \n" + + " \n" + + " \n" + + " \${EXT_FILES_DIR}/Eversense.log\n" + + " \n" + + " \n" + + " \${EXT_FILES_DIR}/Eversense._%d{yyyy-MM-dd}_%d{HH-mm-ss, aux}_.%i.zip\n" + + " \n" + + "\n" + + " \n" + + " 5MB\n" + + " \n" + + " \n" + + " 120\n" + + " \n" + + " \n" + + " [%d{HH:mm:ss.SSS} %.-1level/%logger %msg%n\n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " %logger{0}\n" + + " \n" + + " \n" + + " [%d{HH:mm:ss.SSS} %msg%n\n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "" + + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseScanner.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseScanner.kt new file mode 100644 index 00000000000..d9e5f31301b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseScanner.kt @@ -0,0 +1,35 @@ +package com.nightscout.eversense.util + +import android.annotation.SuppressLint +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import com.nightscout.eversense.callbacks.EversenseScanCallback +import com.nightscout.eversense.models.EversenseScanResult + +class EversenseScanner(private val callback: EversenseScanCallback): ScanCallback() { + + @SuppressLint("MissingPermission") + override fun onScanResult(callbackType: Int, scanRecord: ScanResult) { + // Allow devices with null names — use address as fallback label. + // Some Eversense transmitters may advertise without a device name. + val deviceName = scanRecord.device?.name ?: scanRecord.device?.address ?: return + + // Filter to only show Eversense transmitters. + // E3 transmitters advertise as "T" followed by a serial number (e.g. "T0214389", "T3xxxxxx"). + // E365 transmitters advertise starting with "365". + if (!deviceName.startsWith("T") && !deviceName.startsWith("365") && !deviceName.contains("versense", ignoreCase = true)) { + return + } + + EversenseLogger.info(TAG, "Found Eversense device: $deviceName (address: ${scanRecord.device?.address})") + callback.onResult(EversenseScanResult(deviceName, scanRecord.rssi, scanRecord.device)) + } + + override fun onScanFailed(errorCode: Int) { + EversenseLogger.error(TAG, "BLE scan failed with error code: $errorCode") + } + + companion object { + private const val TAG = "EversenseScanner" + } +} \ No newline at end of file diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/MessageCoder.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/MessageCoder.kt new file mode 100644 index 00000000000..a7fcc821dc0 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/MessageCoder.kt @@ -0,0 +1,114 @@ +package com.nightscout.eversense.util + +import com.nightscout.eversense.enums.EversenseAlarm + +object MessageCoder { + + fun messageCodeForGlucoseLevelAlarmFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.LOW_GLUCOSE + 2 -> EversenseAlarm.HIGH_GLUCOSE + else -> null + } + + fun messageCodeForGlucoseLevelAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.LOW_GLUCOSE + 2 -> EversenseAlarm.HIGH_GLUCOSE + else -> null + } + + fun messageCodeForRateAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.RATE_FALLING + 2 -> EversenseAlarm.RATE_RISING + else -> null + } + + fun messageCodeForPredictiveAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.PREDICTIVE_LOW + 4 -> EversenseAlarm.PREDICTIVE_HIGH + else -> null + } + + fun messageCodeForSensorHardwareAndAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.UNKNOWN + 2 -> EversenseAlarm.SENSOR_AWOL + 4 -> EversenseAlarm.UNKNOWN + 8 -> EversenseAlarm.UNKNOWN + 16 -> EversenseAlarm.UNKNOWN + 32 -> EversenseAlarm.UNKNOWN + 64 -> EversenseAlarm.UNKNOWN + 128 -> EversenseAlarm.UNKNOWN + else -> null + } + + fun messageCodeForSensorReadAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.SERIOUSLY_HIGH + 2 -> EversenseAlarm.SERIOUSLY_LOW + 4 -> EversenseAlarm.UNKNOWN + 8 -> EversenseAlarm.UNKNOWN + 16 -> EversenseAlarm.SENSOR_TEMPERATURE + 32 -> EversenseAlarm.SENSOR_LOW_TEMPERATURE + 64 -> EversenseAlarm.READER_TEMPERATURE + 128 -> EversenseAlarm.MSP_ALARM + else -> null + } + + fun messageCodeForSensorReplacementFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.SENSOR_RETIRED + 2 -> EversenseAlarm.SENSOR_RETIRING_SOON_1 + 4 -> EversenseAlarm.SENSOR_RETIRING_SOON_1 + 8 -> EversenseAlarm.SENSOR_RETIRING_SOON_3 + 16 -> EversenseAlarm.SENSOR_RETIRING_SOON_4 + 32 -> EversenseAlarm.SENSOR_RETIRING_SOON_5 + 64 -> EversenseAlarm.SENSOR_RETIRING_SOON_6 + 128 -> EversenseAlarm.UNKNOWN + else -> null + } + + fun messageCodeForSensorCalibrationFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + 2 -> EversenseAlarm.CALIBRATION_EXPIRED + 4 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + 16 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + 32 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + 64 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + 128 -> EversenseAlarm.CALIBRATION_GRACE_PERIOD + else -> null + } + + fun messageCodeForTransmitterStatusAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.CRITICAL_FAULT + 4 -> EversenseAlarm.INVALID_SENSOR + 8 -> EversenseAlarm.INVALID_CLOCK + 32 -> EversenseAlarm.VIBRATION_CURRENT + 64 -> EversenseAlarm.UNKNOWN + 128 -> EversenseAlarm.UNKNOWN + else -> null + } + + fun messageCodeForTransmitterBatteryAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.EMPTY_BATTERY + 2 -> EversenseAlarm.VERY_LOW_BATTERY + 4 -> EversenseAlarm.EMPTY_BATTERY + 8 -> EversenseAlarm.BATTERY_ERROR + else -> null + } + + fun messageCodeForTransmitterEOLAlertFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.TRANSMITTER_EOL_396 + 2 -> EversenseAlarm.TRANSMITTER_EOL_366 + 4 -> EversenseAlarm.TRANSMITTER_EOL_330 + 8 -> EversenseAlarm.TRANSMITTER_EOL_395 + else -> null + } + + fun messageCodeForSensorReplacementFlags2(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.SENSOR_RETIRING_SOON_7 + else -> null + } + + fun messageCodeForCalibrationSwitchFlags(value: Int): EversenseAlarm? = when (value) { + 1 -> EversenseAlarm.ONE_CAL + 2 -> EversenseAlarm.TWO_CAL + else -> null + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/RangeCalculator.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/RangeCalculator.kt new file mode 100644 index 00000000000..49a6a5650c3 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/RangeCalculator.kt @@ -0,0 +1,21 @@ +package com.nightscout.eversense.util + +import kotlin.math.min + +data class RangeCalculation(val from: Int, val to: Int) + +object RangeCalculator { + fun calculateGlucoseRange(rangeFrom: Int, rangeTo: Int, lastGlucoseTimestampMs: Long): RangeCalculation { + val timeDiffMs = System.currentTimeMillis() - lastGlucoseTimestampMs + val fiveMinMs = 5 * 60 * 1000L + val pageCount = min(((timeDiffMs / fiveMinMs) + 2).toInt(), 20) + val from = maxOf(rangeTo - pageCount, rangeFrom) + return RangeCalculation(from = from, to = rangeTo) + } + + fun calculateRange(rangeFrom: Int, rangeTo: Int): RangeCalculation { + val count = min(rangeTo - rangeFrom, 20) + val from = maxOf(rangeTo - count, rangeFrom) + return RangeCalculation(from = from, to = rangeTo) + } +} diff --git a/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/StorageKeys.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/StorageKeys.kt new file mode 100644 index 00000000000..e4fb4f2ce30 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/StorageKeys.kt @@ -0,0 +1,11 @@ +package com.nightscout.eversense.util + +class StorageKeys { + companion object { + const val REMOTE_DEVICE_KEY = "eversense_remote_device" + const val STATE = "eversense_state" + const val SECURE_STATE = "eversense_state_secure" + const val ACCESS_TOKEN = "eversense_access_token" + const val ACCESS_TOKEN_EXPIRY = "eversense_access_token_expiry" + } +} \ No newline at end of file diff --git a/plugins/eversense/src/test/kotlin/com/nightscout/eversense/packets/e3/CalibrationPacketTest.kt b/plugins/eversense/src/test/kotlin/com/nightscout/eversense/packets/e3/CalibrationPacketTest.kt new file mode 100644 index 00000000000..cd5cfaac5ce --- /dev/null +++ b/plugins/eversense/src/test/kotlin/com/nightscout/eversense/packets/e3/CalibrationPacketTest.kt @@ -0,0 +1,181 @@ +package com.nightscout.eversense.packets.e3 + +import com.nightscout.eversense.enums.CalibrationPhase +import com.nightscout.eversense.enums.CalibrationReadiness +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class GetCalibrationPhasePacketTest { + + private fun makePacket(vararg bytes: Int): GetCalibrationPhasePacket { + val packet = GetCalibrationPhasePacket() + packet.appendData(bytes.map { it.toUByte() }.toUByteArray()) + return packet + } + + @Test + fun `empty receivedData returns null`() { + val packet = GetCalibrationPhasePacket() + assertNull(packet.parseResponse()) + } + + @Test + fun `value 1 parses to WARMING_UP`() { + val packet = makePacket(0, 0, 0, 0, 1) + assertEquals(CalibrationPhase.WARMING_UP, packet.parseResponse()?.phase) + } + + @Test + fun `value 2 parses to DAILY_CALIBRATION`() { + val packet = makePacket(0, 0, 0, 0, 2) + assertEquals(CalibrationPhase.DAILY_CALIBRATION, packet.parseResponse()?.phase) + } + + @Test + fun `value 3 parses to INITIALIZATION`() { + val packet = makePacket(0, 0, 0, 0, 3) + assertEquals(CalibrationPhase.INITIALIZATION, packet.parseResponse()?.phase) + } + + @Test + fun `value 4 parses to SUSPICIOUS`() { + val packet = makePacket(0, 0, 0, 0, 4) + assertEquals(CalibrationPhase.SUSPICIOUS, packet.parseResponse()?.phase) + } + + @Test + fun `value 5 parses to UNKNOWN`() { + val packet = makePacket(0, 0, 0, 0, 5) + assertEquals(CalibrationPhase.UNKNOWN, packet.parseResponse()?.phase) + } + + @Test + fun `value 6 parses to DEBUG`() { + val packet = makePacket(0, 0, 0, 0, 6) + assertEquals(CalibrationPhase.DEBUG, packet.parseResponse()?.phase) + } + + @Test + fun `value 7 parses to DROPOUT`() { + val packet = makePacket(0, 0, 0, 0, 7) + assertEquals(CalibrationPhase.DROPOUT, packet.parseResponse()?.phase) + } + + @Test + fun `unknown value falls back to UNKNOWN`() { + val packet = makePacket(0, 0, 0, 0, 99) + assertEquals(CalibrationPhase.UNKNOWN, packet.parseResponse()?.phase) + } + + @Test + fun `annotation uses SingleByte command id`() { + val annotation = GetCalibrationPhasePacket().getAnnotation()!! + assertEquals(EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, annotation.requestId) + } + + @Test + fun `annotation uses SingleByte response id`() { + val annotation = GetCalibrationPhasePacket().getAnnotation()!! + assertEquals(EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, annotation.responseId) + } +} + +class GetCalibrationReadinessPacketTest { + + private fun makePacket(vararg bytes: Int): GetCalibrationReadinessPacket { + val packet = GetCalibrationReadinessPacket() + packet.appendData(bytes.map { it.toUByte() }.toUByteArray()) + return packet + } + + @Test + fun `empty receivedData returns null`() { + val packet = GetCalibrationReadinessPacket() + assertNull(packet.parseResponse()) + } + + @Test + fun `value 0 parses to READY`() { + val packet = makePacket(0, 0, 0, 0, 0) + assertEquals(CalibrationReadiness.READY, packet.parseResponse()?.readiness) + } + + @Test + fun `value 1 parses to NOT_ENOUGH_DATA`() { + val packet = makePacket(0, 0, 0, 0, 1) + assertEquals(CalibrationReadiness.NOT_ENOUGH_DATA, packet.parseResponse()?.readiness) + } + + @Test + fun `value 2 parses to GLUCOSE_TOO_HIGH`() { + val packet = makePacket(0, 0, 0, 0, 2) + assertEquals(CalibrationReadiness.GLUCOSE_TOO_HIGH, packet.parseResponse()?.readiness) + } + + @Test + fun `value 3 parses to TOO_SOON`() { + val packet = makePacket(0, 0, 0, 0, 3) + assertEquals(CalibrationReadiness.TOO_SOON, packet.parseResponse()?.readiness) + } + + @Test + fun `value 4 parses to DROPOUT_PHASE`() { + val packet = makePacket(0, 0, 0, 0, 4) + assertEquals(CalibrationReadiness.DROPOUT_PHASE, packet.parseResponse()?.readiness) + } + + @Test + fun `value 5 parses to SENSOR_EOL`() { + val packet = makePacket(0, 0, 0, 0, 5) + assertEquals(CalibrationReadiness.SENSOR_EOL, packet.parseResponse()?.readiness) + } + + @Test + fun `value 6 parses to NO_SENSOR_LINKED`() { + val packet = makePacket(0, 0, 0, 0, 6) + assertEquals(CalibrationReadiness.NO_SENSOR_LINKED, packet.parseResponse()?.readiness) + } + + @Test + fun `value 7 parses to UNSUPPORTED_MODE`() { + val packet = makePacket(0, 0, 0, 0, 7) + assertEquals(CalibrationReadiness.UNSUPPORTED_MODE, packet.parseResponse()?.readiness) + } + + @Test + fun `value 8 parses to CALIBRATING`() { + val packet = makePacket(0, 0, 0, 0, 8) + assertEquals(CalibrationReadiness.CALIBRATING, packet.parseResponse()?.readiness) + } + + @Test + fun `value 9 parses to LED_DISCONNECT_DETECTED`() { + val packet = makePacket(0, 0, 0, 0, 9) + assertEquals(CalibrationReadiness.LED_DISCONNECT_DETECTED, packet.parseResponse()?.readiness) + } + + @Test + fun `value 10 parses to TRANSMITTER_EOL`() { + val packet = makePacket(0, 0, 0, 0, 10) + assertEquals(CalibrationReadiness.TRANSMITTER_EOL, packet.parseResponse()?.readiness) + } + + @Test + fun `unknown value falls back to UNKNOWN`() { + val packet = makePacket(0, 0, 0, 0, 99) + assertEquals(CalibrationReadiness.UNKNOWN, packet.parseResponse()?.readiness) + } + + @Test + fun `annotation uses SingleByte command id`() { + val annotation = GetCalibrationReadinessPacket().getAnnotation()!! + assertEquals(EversenseE3Packets.ReadSingleByteSerialFlashRegisterCommandId, annotation.requestId) + } + + @Test + fun `annotation uses SingleByte response id`() { + val annotation = GetCalibrationReadinessPacket().getAnnotation()!! + assertEquals(EversenseE3Packets.ReadSingleByteSerialFlashRegisterResponseId, annotation.responseId) + } +} diff --git a/plugins/eversense/src/test/kotlin/com/nightscout/eversense/util/EversenseHttp365UtilTest.kt b/plugins/eversense/src/test/kotlin/com/nightscout/eversense/util/EversenseHttp365UtilTest.kt new file mode 100644 index 00000000000..c7094caaeb4 --- /dev/null +++ b/plugins/eversense/src/test/kotlin/com/nightscout/eversense/util/EversenseHttp365UtilTest.kt @@ -0,0 +1,315 @@ +package com.nightscout.eversense.util + +import android.content.SharedPreferences +import com.nightscout.eversense.enums.EversenseTrendArrow +import com.nightscout.eversense.models.EversenseCGMResult +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class EversenseHttp365UtilTest { + + private lateinit var mockWebServer: MockWebServer + private lateinit var prefs: SharedPreferences + private lateinit var editor: SharedPreferences.Editor + + private val validTokenJson = """ + { + "access_token": "test_access_token_abc123", + "expires_in": 3600, + "token_type": "Bearer", + "expires": "2099-01-01T00:00:00Z", + "lastLogin": "2026-04-10T00:00:00Z" + } + """.trimIndent() + + @BeforeEach + fun setUp() { + mockWebServer = MockWebServer() + mockWebServer.start() + + val baseUrl = mockWebServer.url("/").toString() + EversenseHttp365Util.tokenBaseUrl = baseUrl + EversenseHttp365Util.uploadBaseUrl = baseUrl + + prefs = mock() + editor = mock() + whenever(prefs.edit()).thenReturn(editor) + whenever(editor.putString(any(), anyOrNull())).thenReturn(editor) + whenever(editor.putLong(any(), any())).thenReturn(editor) + whenever(editor.commit()).thenReturn(true) + whenever(editor.apply()).then { } + + // Default: no stored state (empty secure state) + whenever(prefs.getString(StorageKeys.SECURE_STATE, null)).thenReturn(null) + } + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + // Restore production URLs + EversenseHttp365Util.tokenBaseUrl = "https://usiamapi.eversensedms.com/" + EversenseHttp365Util.uploadBaseUrl = "https://usmobileappmsprod.eversensedms.com/" + } + + // ─── getOrRefreshToken ──────────────────────────────────────────────────── + + @Test + fun `getOrRefreshToken returns cached token when not expired`() { + val futureExpiry = System.currentTimeMillis() + 600_000L // 10 minutes from now + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("cached_token_xyz") + + val token = EversenseHttp365Util.getOrRefreshToken(prefs) + + assertEquals("cached_token_xyz", token) + // No requests should have been made to the server + assertEquals(0, mockWebServer.requestCount) + } + + @Test + fun `getOrRefreshToken fetches new token when cache is expired`() { + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(0L) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn(null) + whenever(prefs.getString(StorageKeys.SECURE_STATE, null)).thenReturn( + """{"username":"user@example.com","password":"testpass"}""" + ) + + mockWebServer.enqueue(MockResponse().setBody(validTokenJson).setResponseCode(200)) + + val token = EversenseHttp365Util.getOrRefreshToken(prefs) + + assertEquals("test_access_token_abc123", token) + assertEquals(1, mockWebServer.requestCount) + val request = mockWebServer.takeRequest() + assertEquals("/connect/token", request.path) + assertEquals("POST", request.method) + assertTrue(request.body.readUtf8().contains("grant_type=password")) + } + + @Test + fun `getOrRefreshToken returns null when login fails`() { + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(0L) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn(null) + + mockWebServer.enqueue(MockResponse().setResponseCode(401).setBody("""{"error":"invalid_client"}""")) + + val token = EversenseHttp365Util.getOrRefreshToken(prefs) + + assertNull(token) + } + + @Test + fun `getOrRefreshToken refreshes token within 5 minutes of expiry`() { + // Token expires in 4 minutes — within the 5-minute refresh window + val nearExpiryMs = System.currentTimeMillis() + 240_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(nearExpiryMs) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("old_token") + whenever(prefs.getString(StorageKeys.SECURE_STATE, null)).thenReturn( + """{"username":"user@example.com","password":"testpass"}""" + ) + + mockWebServer.enqueue(MockResponse().setBody(validTokenJson).setResponseCode(200)) + + val token = EversenseHttp365Util.getOrRefreshToken(prefs) + + assertEquals("test_access_token_abc123", token) + assertEquals(1, mockWebServer.requestCount) + } + + // ─── uploadGlucoseReadings ──────────────────────────────────────────────── + + @Test + fun `uploadGlucoseReadings posts to correct endpoint with bearer token`() { + val futureExpiry = System.currentTimeMillis() + 600_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("my_bearer_token") + + mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("[]")) + + val readings = listOf( + EversenseCGMResult( + glucoseInMgDl = 120, + datetime = 1700000000000L, + trend = EversenseTrendArrow.FLAT, + sensorId = "sensor_001", + rawResponseHex = "deadbeef" + ) + ) + + val result = EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TX-12345", "1.2.3") + + assertTrue(result, "Expected upload to return true on HTTP 200") + assertEquals(1, mockWebServer.requestCount) + val request = mockWebServer.takeRequest() + assertEquals("/api/v1.0/DiagnosticLog/PostEssentialLogs", request.path) + assertEquals("POST", request.method) + assertEquals("Bearer my_bearer_token", request.getHeader("Authorization")) + assertEquals("application/json", request.getHeader("Content-Type")) + } + + @Test + fun `uploadGlucoseReadings sends correct JSON body fields`() { + val futureExpiry = System.currentTimeMillis() + 600_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("my_bearer_token") + + mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("[]")) + + val readings = listOf( + EversenseCGMResult( + glucoseInMgDl = 95, + datetime = 1700000000000L, + trend = EversenseTrendArrow.FLAT, + sensorId = "abc123", + rawResponseHex = "cafebabe" + ) + ) + + EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TXSERIAL", "2.0.1") + + val body = mockWebServer.takeRequest().body.readUtf8() + + assertTrue(body.startsWith("[") && body.endsWith("]"), "Body must be a bare JSON array") + assertTrue(body.contains("\"SensorId\":\"abc123\""), "Missing SensorId") + assertTrue(body.contains("\"TransmitterId\":\"TXSERIAL\""), "Missing TransmitterId") + assertTrue(body.contains("\"CurrentGlucoseValue\":95"), "Missing CurrentGlucoseValue") + assertTrue(body.contains("\"FWVersion\":\"2.0.1\""), "Missing FWVersion") + // EssentialLog must be base64-encoded bytes, not a hex string + val expectedBase64 = java.util.Base64.getEncoder().encodeToString(byteArrayOf(0xca.toByte(), 0xfe.toByte(), 0xba.toByte(), 0xbe.toByte())) + assertTrue(body.contains("\"EssentialLog\":\"$expectedBase64\""), "EssentialLog must be base64 bytes, got: $body") + } + + @Test + fun `uploadGlucoseReadings sends multiple readings in one request`() { + val futureExpiry = System.currentTimeMillis() + 600_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("token") + + mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("[]")) + + val readings = listOf( + EversenseCGMResult(100, 1700000000000L, EversenseTrendArrow.FLAT, "s1", "aa"), + EversenseCGMResult(110, 1700000300000L, EversenseTrendArrow.SINGLE_UP, "s1", "bb"), + EversenseCGMResult(105, 1700000600000L, EversenseTrendArrow.SINGLE_DOWN, "s1", "cc") + ) + + EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TX99", "3.0") + + assertEquals(1, mockWebServer.requestCount) + val body = mockWebServer.takeRequest().body.readUtf8() + assertTrue(body.contains("\"CurrentGlucoseValue\":100")) + assertTrue(body.contains("\"CurrentGlucoseValue\":110")) + assertTrue(body.contains("\"CurrentGlucoseValue\":105")) + } + + @Test + fun `uploadGlucoseReadings does nothing when readings list is empty`() { + EversenseHttp365Util.uploadGlucoseReadings(prefs, emptyList(), "TX99", "1.0") + + assertEquals(0, mockWebServer.requestCount) + } + + @Test + fun `uploadGlucoseReadings does not throw on 4xx server error`() { + val futureExpiry = System.currentTimeMillis() + 600_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("token") + + mockWebServer.enqueue(MockResponse().setResponseCode(400).setBody("""{"error":"bad request"}""")) + + val readings = listOf( + EversenseCGMResult(120, System.currentTimeMillis(), EversenseTrendArrow.FLAT, "s1", "ff") + ) + + // Should not throw — errors are logged internally, returns false + val result = EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TX1", "1.0") + + assertFalse(result, "Expected upload to return false on HTTP 400") + assertEquals(1, mockWebServer.requestCount) + } + + @Test + fun `uploadGlucoseReadings does not throw on 5xx server error`() { + val futureExpiry = System.currentTimeMillis() + 600_000L + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(futureExpiry) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn("token") + + mockWebServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal Server Error")) + + val readings = listOf( + EversenseCGMResult(80, System.currentTimeMillis(), EversenseTrendArrow.FLAT, "s1", "01") + ) + + val result = EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TX1", "1.0") + + assertFalse(result, "Expected upload to return false on HTTP 500") + assertEquals(1, mockWebServer.requestCount) + } + + @Test + fun `uploadGlucoseReadings skips upload when no valid token available`() { + // Token expired and login fails + whenever(prefs.getLong(StorageKeys.ACCESS_TOKEN_EXPIRY, 0)).thenReturn(0L) + whenever(prefs.getString(StorageKeys.ACCESS_TOKEN, null)).thenReturn(null) + + mockWebServer.enqueue(MockResponse().setResponseCode(401).setBody("""{"error":"unauthorized"}""")) + + val readings = listOf( + EversenseCGMResult(100, System.currentTimeMillis(), EversenseTrendArrow.FLAT, "s1", "ab") + ) + + EversenseHttp365Util.uploadGlucoseReadings(prefs, readings, "TX1", "1.0") + + // Only the login attempt should have been made, not the upload + assertEquals(1, mockWebServer.requestCount) + assertEquals("/connect/token", mockWebServer.takeRequest().path) + } + + // ─── login ─────────────────────────────────────────────────────────────── + + @Test + fun `login sends correct form-encoded body`() { + whenever(prefs.getString(StorageKeys.SECURE_STATE, null)).thenReturn( + """{"username":"testuser@test.com","password":"secret123"}""" + ) + + mockWebServer.enqueue(MockResponse().setBody(validTokenJson).setResponseCode(200)) + + val result = EversenseHttp365Util.login(prefs) + + assertNotNull(result) + assertEquals("test_access_token_abc123", result!!.access_token) + assertEquals(3600, result.expires_in) + + val request = mockWebServer.takeRequest() + val body = request.body.readUtf8() + assertTrue(body.contains("grant_type=password")) + assertTrue(body.contains("client_id=eversenseMMAAndroid")) + assertTrue(body.contains("username=testuser%40test.com") || body.contains("username=testuser@test.com")) + } + + @Test + fun `login returns null on 401 response`() { + whenever(prefs.getString(StorageKeys.SECURE_STATE, null)).thenReturn( + """{"username":"bad@user.com","password":"wrong"}""" + ) + + mockWebServer.enqueue(MockResponse().setResponseCode(401).setBody("""{"error":"invalid_grant"}""")) + + val result = EversenseHttp365Util.login(prefs) + + assertNull(result) + } +} diff --git a/plugins/source/build.gradle.kts b/plugins/source/build.gradle.kts index 060590be1c7..8095ac250f6 100644 --- a/plugins/source/build.gradle.kts +++ b/plugins/source/build.gradle.kts @@ -18,6 +18,7 @@ android { dependencies { + implementation(project(":plugins:eversense")) implementation(project(":core:data")) implementation(project(":core:interfaces")) implementation(project(":core:keys")) diff --git a/plugins/source/src/main/AndroidManifest.xml b/plugins/source/src/main/AndroidManifest.xml index 607a2dbf65d..17aa22eaea9 100644 --- a/plugins/source/src/main/AndroidManifest.xml +++ b/plugins/source/src/main/AndroidManifest.xml @@ -18,6 +18,14 @@ + + + \ No newline at end of file diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/EversensePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/EversensePlugin.kt new file mode 100644 index 00000000000..c924651886b --- /dev/null +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/EversensePlugin.kt @@ -0,0 +1,762 @@ +package app.aaps.plugins.source + +import android.Manifest +import android.content.Intent +import android.content.Context +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreference +import app.aaps.core.interfaces.configuration.Config +import app.aaps.core.interfaces.notifications.NotificationId +import app.aaps.core.interfaces.notifications.NotificationLevel +import app.aaps.core.interfaces.notifications.NotificationManager +import com.nightscout.eversense.models.ActiveAlarm +import app.aaps.core.data.model.GV +import app.aaps.core.data.model.SourceSensor +import app.aaps.core.data.model.TrendArrow +import app.aaps.core.data.plugin.PluginType +import app.aaps.core.data.ue.Sources +import app.aaps.core.interfaces.db.PersistenceLayer +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.plugin.PluginDescription +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.source.BgSource +import app.aaps.core.keys.IntKey +import app.aaps.core.keys.interfaces.Preferences +import com.nightscout.eversense.EversenseCGMPlugin +import com.nightscout.eversense.callbacks.EversenseScanCallback +import com.nightscout.eversense.callbacks.EversenseWatcher +import app.aaps.plugins.source.compose.BgSourceComposeContent +import com.nightscout.eversense.enums.CalibrationReadiness +import com.nightscout.eversense.enums.EversenseAlarm +import com.nightscout.eversense.enums.EversenseType +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseScanResult +import com.nightscout.eversense.models.EversenseSecureState +import com.nightscout.eversense.models.EversenseState +import com.nightscout.eversense.util.StorageKeys +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject + +class EversensePlugin @Inject constructor( + rh: ResourceHelper, + private val context: Context, + aapsLogger: AAPSLogger, + preferences: Preferences, + config: Config, + private val notificationManager: NotificationManager +) : AbstractBgSourcePlugin( + PluginDescription() + .mainType(PluginType.BGSOURCE) + .composeContent { _ -> + BgSourceComposeContent( + title = rh.gs(R.string.source_eversense) + ) + } + .pluginIcon(app.aaps.core.objects.R.drawable.ic_blooddrop_48) + .preferencesId(PluginDescription.PREFERENCE_SCREEN) + .pluginName(R.string.source_eversense) + .preferencesVisibleInSimpleMode(false) + .description(R.string.description_source_eversense), + ownPreferences = emptyList(), + aapsLogger, rh, preferences, config +), BgSource, EversenseWatcher { + + @Inject lateinit var persistenceLayer: PersistenceLayer + + private val mainHandler = Handler(Looper.getMainLooper()) + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + private val json = Json { ignoreUnknownKeys = true } + + private val securePrefs by lazy { + context.getSharedPreferences("EversenseCGMManager", Context.MODE_PRIVATE) + } + + private fun cloudUploadEnabled() = securePrefs.getBoolean("eversense_cloud_upload_enabled", true) + private fun cloudUploadToastEnabled() = securePrefs.getBoolean("eversense_notif_cloud_upload_toast", true) + + private var connectedPreference: Preference? = null + private var batteryPreference: Preference? = null + private var placementSignalPreference: Preference? = null + private var insertionPreference: Preference? = null + private var lastSyncPreference: Preference? = null + private var currentPhasePreference: Preference? = null + private var lastCalibrationPreference: Preference? = null + private var nextCalibrationPreference: Preference? = null + private var calibrationActionPreference: Preference? = null + private val lastNotifiedFirmwareVersion: String get() = securePrefs.getString("last_notified_firmware_version", "") ?: "" + private fun setLastNotifiedFirmwareVersion(version: String) = securePrefs.edit(commit = true) { putString("last_notified_firmware_version", version) } + private fun isSensorExpiryDismissed(insertionDate: Long, days: Int): Boolean = + securePrefs.getBoolean("eversense_expiry_dismissed_${insertionDate}_${days}", false) + private fun setSensorExpiryDismissed(insertionDate: Long, days: Int) = + securePrefs.edit(commit = true) { putBoolean("eversense_expiry_dismissed_${insertionDate}_${days}", true) } + private fun isCalibrationDueDismissed(nextCalibrationDate: Long): Boolean = + securePrefs.getBoolean("eversense_cal_due_dismissed_${nextCalibrationDate}", false) + private fun setCalibrationDueDismissed(nextCalibrationDate: Long) = + securePrefs.edit(commit = true) { putBoolean("eversense_cal_due_dismissed_${nextCalibrationDate}", true) } + private fun isBatteryLowDismissed(): Boolean = + securePrefs.getBoolean("eversense_battery_low_dismissed", false) + private fun setBatteryLowDismissed() = + securePrefs.edit(commit = true) { putBoolean("eversense_battery_low_dismissed", true) } + private var consecutiveNoSignalReadings: Int = 0 + private val NO_SIGNAL_WARNING_THRESHOLD = 3 + private var releaseForOfficialApp: Boolean = false + @Volatile private var placementNotificationSnoozed: Boolean = false + private var releasePreference: Preference? = null + + init { + eversense.setContext(context, true) + } + + override fun onStart() { + super.onStart() + eversense.addWatcher(this) + if (hasBluetoothPermissions()) { + aapsLogger.debug(LTag.BGSOURCE, "onStart — permissions granted, attempting auto-reconnect") + ioScope.launch { + eversense.connect(null) + } + } else { + aapsLogger.warn(LTag.BGSOURCE, "Bluetooth permissions not granted — requesting permissions") + requestBluetoothPermissions() + } + mainHandler.post { + connectedPreference?.summary = if (eversense.isConnected()) "✅" else "❌" + } + } + + override fun onStop() { + super.onStop() + eversense.removeWatcher(this) + } + + private fun requestBluetoothPermissions() { + val intent = Intent(context, app.aaps.plugins.source.activities.RequestEversensePermissionActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + private fun hasBluetoothPermissions(): Boolean { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED + } else { + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED + } + } + + private fun getSecureState(): EversenseSecureState { + val stateJson = securePrefs.getString(StorageKeys.SECURE_STATE, null) ?: "{}" + return json.decodeFromString(stateJson) + } + + private fun saveSecureState(state: EversenseSecureState) { + securePrefs.edit(commit = true) { + putString(StorageKeys.SECURE_STATE, json.encodeToString(EversenseSecureState.serializer(), state)) + } + } + + override fun addPreferenceScreen( + preferenceManager: PreferenceManager, + parent: PreferenceScreen, + context: Context, + requiredKey: String? + ) { + val state = eversense.getCurrentState() + val notConnected = rh.gs(R.string.eversense_not_connected) + val secureState = getSecureState() + + super.addPreferenceScreen(preferenceManager, parent, context, requiredKey) + + val bgSourceCategory = parent.findPreference("bg_source_upload_settings") + bgSourceCategory?.let { category -> + val eselSmoothing = SwitchPreference(context) + eselSmoothing.key = "eversense_use_smoothing" + eselSmoothing.title = rh.gs(R.string.eversense_use_smoothing) + eselSmoothing.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + eversense.setSmoothing(newValue as Boolean) + true + } + category.addPreference(eselSmoothing) + + } + + // Credentials section — E365 only + val credentials = PreferenceCategory(context) + parent.addPreference(credentials) + credentials.apply { + title = rh.gs(R.string.eversense_credentials_title) + initialExpandedChildrenCount = 0 + isVisible = eversense.is365() + + val uploadEnabled = SwitchPreference(context) + uploadEnabled.key = "eversense_cloud_upload_enabled" + uploadEnabled.title = "Enable Eversense Data Upload" + uploadEnabled.summary = "Automatically upload BG readings to the Eversense cloud" + uploadEnabled.isChecked = securePrefs.getBoolean("eversense_cloud_upload_enabled", true) + uploadEnabled.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v -> + securePrefs.edit(commit = true) { putBoolean("eversense_cloud_upload_enabled", v as Boolean) } + true + } + addPreference(uploadEnabled) + + val username = EditTextPreference(context) + username.key = "eversense_credentials_username" + username.title = rh.gs(R.string.eversense_credentials_username) + username.summary = if (secureState.username.isNotEmpty()) secureState.username + else rh.gs(R.string.eversense_credentials_not_set) + username.text = secureState.username + username.dialogTitle = rh.gs(R.string.eversense_credentials_username) + username.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue -> + val value = newValue as String + val updated = getSecureState().also { it.username = value } + saveSecureState(updated) + pref.summary = if (value.isNotEmpty()) value + else rh.gs(R.string.eversense_credentials_not_set) + aapsLogger.info(LTag.BGSOURCE, "Eversense username updated") + true + } + addPreference(username) + + val password = EditTextPreference(context) + password.key = "eversense_credentials_password" + password.title = rh.gs(R.string.eversense_credentials_password) + password.summary = if (secureState.password.isNotEmpty()) rh.gs(R.string.eversense_credentials_password_set) + else rh.gs(R.string.eversense_credentials_not_set) + password.text = secureState.password + password.dialogTitle = rh.gs(R.string.eversense_credentials_password) + password.setOnBindEditTextListener { editText -> + editText.inputType = android.text.InputType.TYPE_CLASS_TEXT or + android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + val toggleButton = android.widget.Button(editText.context) + toggleButton.text = "Show" + toggleButton.textSize = 12f + toggleButton.setOnClickListener { + if (editText.inputType == (android.text.InputType.TYPE_CLASS_TEXT or + android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD)) { + editText.inputType = android.text.InputType.TYPE_CLASS_TEXT or + android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + toggleButton.text = "Hide" + } else { + editText.inputType = android.text.InputType.TYPE_CLASS_TEXT or + android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + toggleButton.text = "Show" + } + editText.setSelection(editText.text.length) + } + val parentLayout = editText.parent as? android.widget.LinearLayout + parentLayout?.addView(toggleButton) + } + password.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue -> + val value = newValue as String + val updated = getSecureState().also { it.password = value } + saveSecureState(updated) + pref.summary = if (value.isNotEmpty()) rh.gs(R.string.eversense_credentials_password_set) + else rh.gs(R.string.eversense_credentials_not_set) + aapsLogger.info(LTag.BGSOURCE, "Eversense password updated") + true + } + addPreference(password) + + // Sign Out button + val signOut = Preference(context) + signOut.key = "eversense_credentials_sign_out" + signOut.title = "Sign Out" + signOut.summary = "Clear saved username and password" + signOut.onPreferenceClickListener = Preference.OnPreferenceClickListener { + AlertDialog.Builder(preferenceManager.context) + .setTitle("Sign Out") + .setMessage("Are you sure you want to clear your Eversense credentials?") + .setPositiveButton("Sign Out") { _, _ -> + val cleared = getSecureState().also { + it.username = "" + it.password = "" + } + saveSecureState(cleared) + username.summary = rh.gs(R.string.eversense_credentials_not_set) + username.text = "" + password.summary = rh.gs(R.string.eversense_credentials_not_set) + password.text = "" + aapsLogger.info(LTag.BGSOURCE, "Eversense credentials cleared by user") + } + .setNegativeButton("Cancel", null) + .show() + true + } + addPreference(signOut) + + } + + // Calibration section + val calibration = PreferenceCategory(context) + parent.addPreference(calibration) + calibration.apply { + title = rh.gs(R.string.eversense_calibration_title) + initialExpandedChildrenCount = 0 + // Calibration is not supported on E3 transmitters + isEnabled = eversense.is365() + + val currentPhase = Preference(context) + currentPhase.key = "eversense_calibration_phase" + currentPhase.title = rh.gs(R.string.eversense_calibration_phase) + currentPhase.summary = state?.calibrationPhase?.name ?: notConnected + addPreference(currentPhase) + currentPhasePreference = currentPhase + + val lastCalibration = Preference(context) + lastCalibration.key = "eversense_calibration_last" + lastCalibration.title = rh.gs(R.string.eversense_calibration_last) + lastCalibration.summary = state?.let { dateFormatter.format(Date(it.lastCalibrationDate)) } ?: notConnected + addPreference(lastCalibration) + lastCalibrationPreference = lastCalibration + + val nextCalibration = Preference(context) + nextCalibration.key = "eversense_calibration_next" + nextCalibration.title = rh.gs(R.string.eversense_calibration_next) + nextCalibration.summary = state?.let { dateFormatter.format(Date(it.nextCalibrationDate)) } ?: notConnected + addPreference(nextCalibration) + nextCalibrationPreference = nextCalibration + + val calibrationAction = Preference(context) + calibrationAction.key = "eversense_calibration_action" + calibrationAction.title = rh.gs(R.string.eversense_calibration_action) + calibrationAction.summary = when { + state == null -> notConnected + state.calibrationReadiness == CalibrationReadiness.TOO_SOON -> + "⏳ " + rh.gs(R.string.eversense_calibration_too_soon) + state.calibrationReadiness != CalibrationReadiness.READY -> state.calibrationReadiness.name + else -> "" + } + calibrationAction.isEnabled = state?.calibrationReadiness == CalibrationReadiness.READY + calibrationAction.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val latestState = eversense.getCurrentState() + if (latestState == null) { + aapsLogger.warn(LTag.BGSOURCE, "Calibration tapped but state is null") + return@OnPreferenceClickListener false + } + if (latestState.calibrationReadiness != CalibrationReadiness.READY) { + aapsLogger.warn(LTag.BGSOURCE, "Calibration tapped but readiness is ${latestState.calibrationReadiness}") + return@OnPreferenceClickListener false + } + val intent = Intent(context, app.aaps.plugins.source.activities.EversenseCalibrationActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + return@OnPreferenceClickListener true + } + addPreference(calibrationAction) + calibrationActionPreference = calibrationAction + } + + // Information section + val information = PreferenceCategory(context) + parent.addPreference(information) + information.apply { + title = rh.gs(R.string.eversense_information_title) + initialExpandedChildrenCount = 0 + + val connected = Preference(context) + connected.key = "eversense_information_connected" + connected.title = rh.gs(R.string.eversense_information_connected) + connected.summary = if (eversense.isConnected()) "✅" else "❌" + connected.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val activityContext = preferenceManager.context + if (eversense.isConnected()) { + AlertDialog.Builder(activityContext) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setMessage("Disconnect from transmitter?") + .setPositiveButton("Disconnect") { _, _ -> + eversense.clearStoredDevice() + eversense.disconnect() + aapsLogger.info(LTag.BGSOURCE, "User disconnected transmitter") + connectedPreference?.summary = "❌" + } + .setNegativeButton("Cancel", null) + .show() + return@OnPreferenceClickListener true + } + if (!hasBluetoothPermissions()) { + aapsLogger.warn(LTag.BGSOURCE, "Cannot start scan — requesting Bluetooth permissions") + requestBluetoothPermissions() + return@OnPreferenceClickListener false + } + val hasStoredDevice = context.getSharedPreferences("EversenseCGMManager", android.content.Context.MODE_PRIVATE).getString("eversense_remote_device", null) != null + if (hasStoredDevice) { + aapsLogger.debug(LTag.BGSOURCE, "User tapped connect — reconnecting to stored device") + ioScope.launch { eversense.connect(null) } + } else { + aapsLogger.debug(LTag.BGSOURCE, "User tapped connect — no stored device, starting BLE scan") + showDeviceSelectionDialog(activityContext) + } + return@OnPreferenceClickListener true + } + addPreference(connected) + connectedPreference = connected + + val battery = Preference(context) + battery.key = "eversense_information_battery" + battery.title = rh.gs(R.string.eversense_information_battery) + battery.summary = state?.let { "${it.batteryPercentage}%" } ?: notConnected + addPreference(battery) + batteryPreference = battery + + val placementSignal = Preference(context) + placementSignal.key = "eversense_information_placement_signal" + placementSignal.title = rh.gs(R.string.eversense_placement_signal) + placementSignal.summary = state?.let { signalToLabel(it.sensorSignalStrength) } ?: notConnected + placementSignal.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val intent = Intent(context, app.aaps.plugins.source.activities.EversensePlacementActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + return@OnPreferenceClickListener true + } + addPreference(placementSignal) + placementSignalPreference = placementSignal + + val insertion = Preference(context) + insertion.key = "eversense_information_insertion_date" + insertion.title = rh.gs(R.string.eversense_information_insertion_date) + insertion.summary = state?.let { dateFormatter.format(Date(it.insertionDate)) } ?: notConnected + addPreference(insertion) + insertionPreference = insertion + + val lastSync = Preference(context) + lastSync.key = "eversense_information_last_sync" + lastSync.title = rh.gs(R.string.eversense_information_last_sync) + lastSync.summary = state?.let { dateFormatter.format(Date(it.lastSync)) } ?: notConnected + lastSync.onPreferenceClickListener = Preference.OnPreferenceClickListener { + aapsLogger.debug(LTag.BGSOURCE, "User tapped Last Sync — triggering full sync and glucose read") + if (!eversense.isConnected()) { + aapsLogger.warn(LTag.BGSOURCE, "Cannot sync — not connected") + return@OnPreferenceClickListener false + } + ioScope.launch { + eversense.triggerFullSync(force = true) + } + return@OnPreferenceClickListener true + } + addPreference(lastSync) + lastSyncPreference = lastSync + + } + + // Notifications section — E365 only + val notifications = PreferenceCategory(context) + parent.addPreference(notifications) + notifications.apply { + title = "Notifications" + initialExpandedChildrenCount = 0 + isVisible = eversense.is365() + + val cloudUploadToast = SwitchPreference(context) + cloudUploadToast.key = "eversense_notif_cloud_upload_toast" + cloudUploadToast.title = "Show cloud upload result" + cloudUploadToast.summary = "Display a toast after each BG upload to the Eversense cloud" + cloudUploadToast.isChecked = securePrefs.getBoolean("eversense_notif_cloud_upload_toast", true) + cloudUploadToast.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v -> + securePrefs.edit(commit = true) { putBoolean("eversense_notif_cloud_upload_toast", v as Boolean) } + true + } + addPreference(cloudUploadToast) + } + } + + private fun startOfficialAppReleaseReconnectLoop() { + + if (false) return + if (!releaseForOfficialApp) return + aapsLogger.info(LTag.BGSOURCE, "Release mode — attempting reconnect") + ioScope.launch { + eversense.connect(null) + mainHandler.postDelayed({ + if (eversense.isConnected()) { + aapsLogger.info(LTag.BGSOURCE, "Reconnected after official app release") + releaseForOfficialApp = false + mainHandler.post { + releasePreference?.summary = rh.gs(R.string.eversense_release_summary) + notificationManager.dismiss(NotificationId.EVERSENSE_RELEASE) + } + } else { + aapsLogger.info(LTag.BGSOURCE, "Reconnect failed — retrying in 5 minutes") + mainHandler.postDelayed({ startOfficialAppReleaseReconnectLoop() }, 300000L) + } + }, 10000L) + } + } + + private fun signalToLabel(strength: Int): String = when { + strength >= 75 -> "Excellent" + strength >= 48 -> "Good" + strength >= 30 -> "Low" + strength >= 25 -> "Poor" + strength > 0 -> "Very Poor" + else -> rh.gs(R.string.eversense_not_connected) + } + + override fun onStateChanged(state: EversenseState) { + aapsLogger.info(LTag.BGSOURCE, "New state received: ${Json.encodeToString(state)}") + + // Sync SAGE color thresholds to match Eversense sensor lifetime and notification days + if (state.insertionDate > 0) { + val lifetimeDays = if (eversense.is365()) 365 else 180 + val warnHours = (lifetimeDays - 30) * 24 // orange when 30 days remaining + val urgentHours = (lifetimeDays - 10) * 24 // red when 10 days remaining + preferences.put(IntKey.OverviewSageWarning, warnHours) + preferences.put(IntKey.OverviewSageCritical, urgentHours) + } + + // Check for persistent no-signal — indicates transmitter not placed over sensor + if (state.sensorSignalStrength == 0) { + consecutiveNoSignalReadings++ + aapsLogger.warn(LTag.BGSOURCE, "No signal reading $consecutiveNoSignalReadings of $NO_SIGNAL_WARNING_THRESHOLD") + if (consecutiveNoSignalReadings >= NO_SIGNAL_WARNING_THRESHOLD) { + if (!placementNotificationSnoozed) { + onTransmitterNotPlaced() + } + consecutiveNoSignalReadings = 0 + } + } else { + consecutiveNoSignalReadings = 0 + placementNotificationSnoozed = false + notificationManager.dismiss(NotificationId.EVERSENSE_PLACEMENT) + } + + // Show sensor expiry notifications at 60, 30, and 10 days remaining — once each, at noon, keyed to insertionDate + if (state.insertionDate > 0) { + val sensorLifetimeMs = if (eversense.is365()) 365L * 24 * 60 * 60 * 1000 else 180L * 24 * 60 * 60 * 1000 + val expiryMs = state.insertionDate + sensorLifetimeMs + val daysRemaining = ((expiryMs - System.currentTimeMillis()) / (24 * 60 * 60 * 1000)).toInt() + val isAfterNoon = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) >= 12 + + if (isAfterNoon && daysRemaining in 31..60 && !isSensorExpiryDismissed(state.insertionDate, 60)) { + setSensorExpiryDismissed(state.insertionDate, 60) + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + "Eversense sensor expires in $daysRemaining days — plan your sensor replacement.", + level = NotificationLevel.INFO + ) + } else if (isAfterNoon && daysRemaining in 11..30 && !isSensorExpiryDismissed(state.insertionDate, 30)) { + setSensorExpiryDismissed(state.insertionDate, 30) + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + "Eversense sensor expires in $daysRemaining days — replace your sensor soon.", + level = NotificationLevel.NORMAL + ) + } else if (isAfterNoon && daysRemaining in 1..10 && !isSensorExpiryDismissed(state.insertionDate, daysRemaining)) { + setSensorExpiryDismissed(state.insertionDate, daysRemaining) + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + "Eversense sensor expires in $daysRemaining days — replace your sensor immediately.", + level = NotificationLevel.URGENT + ) + } + } + + // Battery low notification — fires once at noon when battery < 11% + val isAfterNoonBattery = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) >= 12 + if (isAfterNoonBattery && state.batteryPercentage in 1..10 && !isBatteryLowDismissed()) { + setBatteryLowDismissed() + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + "Eversense transmitter battery low: ${state.batteryPercentage}% — please charge your transmitter.", + level = NotificationLevel.NORMAL + ) + } + + // Calibration due notification — E365 only, fires once at noon per nextCalibrationDate + val isAfterNoonCal = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) >= 12 + if (eversense.is365() && isAfterNoonCal && state.nextCalibrationDate > 0 + && System.currentTimeMillis() >= state.nextCalibrationDate + && !isCalibrationDueDismissed(state.nextCalibrationDate)) { + setCalibrationDueDismissed(state.nextCalibrationDate) + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + "Eversense calibration is due — open AAPS to calibrate your sensor.", + level = NotificationLevel.NORMAL + ) + } + + // Show firmware notification only once per unique firmware version + if (state.firmwareVersion.isNotEmpty() && state.firmwareVersion != lastNotifiedFirmwareVersion) { + setLastNotifiedFirmwareVersion(state.firmwareVersion) + aapsLogger.info(LTag.BGSOURCE, "Transmitter firmware: ${state.firmwareVersion}") + notificationManager.post( + NotificationId.EVERSENSE_FIRMWARE, + "Eversense firmware: ${state.firmwareVersion} — open the official Eversense app to check for updates", + level = NotificationLevel.INFO + ) + } + } + + override fun onTransmitterNotPlaced() { + aapsLogger.warn(LTag.BGSOURCE, "Transmitter not placed — firing placement warning notification") + mainHandler.post { + notificationManager.post( + NotificationId.EVERSENSE_PLACEMENT, + rh.gs(R.string.eversense_transmitter_not_placed), + level = NotificationLevel.URGENT + ) + } + } + + override fun onConnectionChanged(connected: Boolean) { + aapsLogger.info(LTag.BGSOURCE, "Connection changed — connected: $connected") + mainHandler.post { + connectedPreference?.summary = if (connected) "✅" else "❌" + } + } + + override fun onAlarmReceived(alarm: ActiveAlarm) { + aapsLogger.info(LTag.BGSOURCE, "Eversense alarm received: ${alarm.code.title}") + // CRITICAL_FAULT (code 0) is sent for both hardware faults and calibration-overdue events. + // If the stored next calibration date has already passed, treat it as a calibration alarm. + val title = if (alarm.code == EversenseAlarm.CRITICAL_FAULT) { + val stateJson = securePrefs.getString(StorageKeys.STATE, null) + val state = stateJson?.let { json.decodeFromString(it) } + if (state != null && state.nextCalibrationDate > 0 && state.nextCalibrationDate < System.currentTimeMillis()) { + "Eversense Calibration Due Now" + } else { + alarm.code.title + } + } else { + alarm.code.title + } + val level = when { + alarm.code.isCritical -> NotificationLevel.URGENT + alarm.code.isWarning -> NotificationLevel.NORMAL + else -> NotificationLevel.INFO + } + mainHandler.post { + notificationManager.post( + NotificationId.EVERSENSE_ALARM, + title, + level = level + ) + } + } + + override fun onCGMRead(type: EversenseType, readings: List) { + val glucoseValues = readings.map { reading -> + GV( + timestamp = reading.datetime, + value = reading.glucoseInMgDl.toDouble(), + noise = null, + raw = null, + trendArrow = TrendArrow.fromString(reading.trend.type), + sourceSensor = when (type) { + EversenseType.EVERSENSE_365 -> SourceSensor.EVERSENSE_365 + EversenseType.EVERSENSE_E3 -> SourceSensor.EVERSENSE_E3 + } + ) + } + + ioScope.launch { + val state = eversense.getCurrentState() + val insertionDate = state?.insertionDate?.takeIf { it > 0 } + val result = persistenceLayer.insertCgmSourceData( + Sources.Eversense, + glucoseValues, + listOf(), + insertionDate + ) + aapsLogger.info(LTag.BGSOURCE, "CGM insert complete — inserted: ${result.inserted}, updated: ${result.updated}") + + // Upload E365 readings to Eversense cloud so official app sees data without needing BLE + if (type == EversenseType.EVERSENSE_365 && state != null && cloudUploadEnabled()) { + val prefs = context.getSharedPreferences("EversenseCGMManager", android.content.Context.MODE_PRIVATE) + val uploadOk = com.nightscout.eversense.util.EversenseHttp365Util.uploadGlucoseReadings( + preferences = prefs, + readings = readings, + transmitterSerialNumber = state.transmitterName.ifEmpty { state.transmitterSerialNumber }, + firmwareVersion = state.firmwareVersion + ) + val msg = if (uploadOk) + "Eversense cloud upload: ✅ ${readings.size} reading(s) sent" + else + "Eversense cloud upload: ❌ failed — check credentials and internet" + aapsLogger.info(LTag.BGSOURCE, msg) + if (cloudUploadToastEnabled()) { + mainHandler.post { + android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_LONG).show() + } + } + } + } + } + + private fun showDeviceSelectionDialog(context: Context) { + val foundDevices = mutableListOf() + var isCancelled = false + var dialog: AlertDialog? = null + + val scanCallback = object : EversenseScanCallback { + override fun onResult(item: EversenseScanResult) { + val isEversenseTransmitter = item.name.matches(Regex("T\\d+.*")) + if (!isCancelled && isEversenseTransmitter && foundDevices.none { it.name == item.name }) { + foundDevices.add(item) + aapsLogger.info(LTag.BGSOURCE, "Scan found device: ${item.name}") + } + } + } + + eversense.startScan(scanCallback) + + mainHandler.postDelayed({ + if (isCancelled) return@postDelayed + eversense.stopScan() + dialog?.dismiss() + + if (foundDevices.isEmpty()) { + AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setMessage("No Eversense transmitters found. Make sure the transmitter is nearby and try again.") + .setPositiveButton("OK", null) + .show() + } else { + val items = foundDevices.map { it.name }.toTypedArray() + AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setItems(items) { _, position -> + val selected = foundDevices[position] + aapsLogger.info(LTag.BGSOURCE, "User selected device: ${selected.name}") + eversense.connect(selected.device) + } + .setNegativeButton(rh.gs(R.string.eversense_scan_cancel), null) + .show() + } + }, 10000) + + dialog = AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setMessage("Scanning for Eversense devices (10 seconds)...") + .setNegativeButton(rh.gs(R.string.eversense_scan_cancel)) { _, _ -> + isCancelled = true + eversense.stopScan() + } + .setCancelable(false) + .show() + } + + companion object { + private val eversense get() = EversenseCGMPlugin.instance + } +} + + diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversenseCalibrationActivity.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversenseCalibrationActivity.kt new file mode 100644 index 00000000000..081a23c1292 --- /dev/null +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversenseCalibrationActivity.kt @@ -0,0 +1,175 @@ +package app.aaps.plugins.source.activities + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.InputType +import android.view.MenuItem +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.widget.Toolbar +import app.aaps.core.interfaces.profile.ProfileUtil +import app.aaps.core.ui.activities.TranslatedDaggerAppCompatActivity +import app.aaps.plugins.source.R +import com.nightscout.eversense.EversenseCGMPlugin +import com.nightscout.eversense.callbacks.EversenseWatcher +import com.nightscout.eversense.enums.CalibrationReadiness +import com.nightscout.eversense.enums.EversenseType +import com.nightscout.eversense.models.ActiveAlarm +import com.nightscout.eversense.models.EversenseCGMResult +import com.nightscout.eversense.models.EversenseState +import com.nightscout.eversense.util.EversenseLogger +import javax.inject.Inject + +class EversenseCalibrationActivity : TranslatedDaggerAppCompatActivity() { + + @Inject lateinit var profileUtil: ProfileUtil + + companion object { + private const val TAG = "EversenseCalibration" + private const val RECONNECT_TIMEOUT_MS = 30000L + } + + private val mainHandler = Handler(Looper.getMainLooper()) + private var connectionWatcher: EversenseWatcher? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_eversense_calibration) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = getString(R.string.eversense_calibration_action) + + val state = EversenseCGMPlugin.instance.getCurrentState() + EversenseLogger.info(TAG, "Activity opened — readiness: ${state?.calibrationReadiness}, phase: ${state?.calibrationPhase}, connected: ${EversenseCGMPlugin.instance.isConnected()}") + + val statusText = findViewById(R.id.calibration_status) + statusText.text = when { + state == null -> getString(R.string.eversense_not_connected) + state.calibrationReadiness == CalibrationReadiness.READY -> getString(R.string.eversense_calibration_ready) + else -> state.calibrationReadiness.name + } + + val unitLabel = findViewById(R.id.calibration_unit_label) + unitLabel.text = profileUtil.units.asText + + val bgInput = findViewById(R.id.calibration_bg_input) + bgInput.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + bgInput.isEnabled = state?.calibrationReadiness == CalibrationReadiness.READY + + val submitButton = findViewById