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