Skip to content

Commit 3cd55f6

Browse files
committed
feat: add Eversense 365 support
1 parent 5f7d032 commit 3cd55f6

32 files changed

Lines changed: 1223 additions & 61 deletions

plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import android.bluetooth.le.ScanSettings
88
import android.content.Context
99
import android.content.SharedPreferences
1010
import android.os.ParcelUuid
11+
import androidx.core.content.edit
1112
import com.nightscout.eversense.callbacks.EversenseScanCallback
1213
import com.nightscout.eversense.callbacks.EversenseWatcher
1314
import com.nightscout.eversense.models.EversenseState
1415
import com.nightscout.eversense.models.EversenseTransmitterSettings
16+
import com.nightscout.eversense.packets.Eversense365Communicator
1517
import com.nightscout.eversense.packets.EversenseE3Communicator
1618
import com.nightscout.eversense.util.EversenseLogger
1719
import com.nightscout.eversense.util.EversenseScanner
@@ -39,10 +41,24 @@ class EversenseCGMPlugin {
3941
gattCallback = EversenseGattCallback(this, preference)
4042
}
4143

44+
fun setSmoothing(value: Boolean) {
45+
val state = getCurrentState() ?: return
46+
state.useSmoothing = value
47+
48+
preferences?.edit(commit = true) {
49+
putString(StorageKeys.STATE, JSON.encodeToString(state))
50+
}
51+
}
52+
53+
4254
fun addWatcher(watcher: EversenseWatcher) {
4355
this.watchers += watcher
4456
}
4557

58+
fun removeWatcher(watcher: EversenseWatcher) {
59+
this.watchers -= watcher
60+
}
61+
4662
fun isConnected(): Boolean {
4763
val gattCallback = this.gattCallback ?:run {
4864
return false
@@ -58,7 +74,7 @@ class EversenseCGMPlugin {
5874
}
5975

6076
val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}"
61-
return Json.decodeFromString<EversenseState>(stateJson)
77+
return JSON.decodeFromString<EversenseState>(stateJson)
6278
}
6379

6480
@SuppressLint("MissingPermission")
@@ -99,7 +115,7 @@ class EversenseCGMPlugin {
99115

100116
if (device != null) {
101117
EversenseLogger.info(TAG, "Connecting to ${device.name}")
102-
device.connectGatt(context, true, gattCallback)
118+
device.connectGatt(context, false, gattCallback)
103119
return true
104120
}
105121

@@ -113,7 +129,7 @@ class EversenseCGMPlugin {
113129
return false
114130
}
115131

116-
remoteDevice.connectGatt(context, true, gattCallback)
132+
remoteDevice.connectGatt(context, false, gattCallback)
117133
return true
118134
}
119135

@@ -139,6 +155,8 @@ class EversenseCGMPlugin {
139155
companion object {
140156
private const val TAG = "EversenseCGMManager"
141157

158+
private val JSON = Json { ignoreUnknownKeys = true }
159+
142160
val instance:EversenseCGMPlugin by lazy {
143161
EversenseCGMPlugin()
144162
}

plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import android.bluetooth.BluetoothGattDescriptor
88
import android.bluetooth.BluetoothGattService
99
import android.bluetooth.BluetoothProfile
1010
import android.content.SharedPreferences
11+
import android.os.Handler
12+
import android.os.Looper
1113
import com.nightscout.eversense.enums.EversenseSecurityType
1214
import com.nightscout.eversense.exceptions.EversenseWriteException
1315
import com.nightscout.eversense.packets.EversenseBasePacket
@@ -22,10 +24,13 @@ import com.nightscout.eversense.packets.Eversense365Communicator
2224
import com.nightscout.eversense.packets.e365.AuthIdentityPacket
2325
import com.nightscout.eversense.packets.e365.AuthStartPacket
2426
import com.nightscout.eversense.packets.e365.AuthWhoAmIPacket
27+
import com.nightscout.eversense.packets.e365.Eversense365Packets
28+
import com.nightscout.eversense.packets.e365.KeepAlivePacket
2529
import com.nightscout.eversense.util.EversenseCrypto365Util
2630
import com.nightscout.eversense.util.EversenseHttp365Util
2731
import com.nightscout.eversense.util.EversenseLogger
2832
import com.nightscout.eversense.util.StorageKeys
33+
import java.util.concurrent.TimeUnit
2934

3035
class EversenseGattCallback(
3136
private val plugin: EversenseCGMPlugin,
@@ -46,6 +51,7 @@ class EversenseGattCallback(
4651
}
4752

4853
private val executor = Executors.newSingleThreadExecutor()
54+
private val handler = Handler(Looper.getMainLooper())
4955
private var bluetoothGatt: BluetoothGatt? = null
5056
private var eversenseBluetoothService: BluetoothGattService? = null
5157
private var requestCharacteristic: BluetoothGattCharacteristic? = null
@@ -62,7 +68,7 @@ class EversenseGattCallback(
6268

6369
@SuppressLint("MissingPermission")
6470
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
65-
EversenseLogger.info(TAG, "Connection state changed - state: $status, newState: $newState")
71+
EversenseLogger.info(TAG, "Connection state changed - state: $status, newState: $newState, ${gatt.device.name}")
6672

6773
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
6874
bluetoothGatt = gatt
@@ -71,8 +77,10 @@ class EversenseGattCallback(
7177
putString(StorageKeys.REMOTE_DEVICE_KEY, gatt.device.address)
7278
}
7379

74-
for (watcher in plugin.watchers) {
75-
watcher.onConnectionChanged(true)
80+
handler.post {
81+
plugin.watchers.forEach {
82+
it.onConnectionChanged(true)
83+
}
7684
}
7785

7886
if (!gatt.requestMtu(512)) {
@@ -93,14 +101,14 @@ class EversenseGattCallback(
93101

94102
@SuppressLint("MissingPermission")
95103
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
96-
if (status == 0) {
97-
payloadSize = mtu - 3
98-
EversenseLogger.debug(TAG, "New payload size: $payloadSize")
99-
} else {
100-
payloadSize = 20
104+
if (status != 0) {
101105
EversenseLogger.error(TAG, "Failed to set payload size - status: $status")
106+
return
102107
}
103108

109+
payloadSize = mtu - 3
110+
EversenseLogger.debug(TAG, "New payload size: $payloadSize")
111+
104112
val success = gatt?.discoverServices()
105113
EversenseLogger.info(TAG, "Trigger discover services - success: $success")
106114
}
@@ -173,13 +181,28 @@ class EversenseGattCallback(
173181
}
174182
}
175183

184+
@Deprecated("")
185+
@SuppressLint("MissingPermission")
176186
@OptIn(ExperimentalStdlibApi::class)
177187
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
178188
EversenseLogger.debug(TAG, "Received data: ${characteristic.value.toHexString()}")
179189

180190
var data = characteristic.value
181191
if (security == EversenseSecurityType.SecureV2) {
182192
data = data.drop(3).toByteArray()
193+
194+
if (data[0] != Eversense365Packets.AuthenticateResponseId) {
195+
data = cryptoUtil.decrypt(data)
196+
EversenseLogger.debug(TAG, "Decrypted data -> ${data.toHexString()}")
197+
if (data.isEmpty()) {
198+
EversenseLogger.error(TAG, "Failed do decrypt data...")
199+
200+
// Delete keys to redo security handshake on next connection
201+
cryptoUtil.disallowUseShortcut()
202+
gatt.disconnect()
203+
return
204+
}
205+
}
183206
}
184207

185208
if (EversenseE3Packets.isPushPacket(data[0])) {
@@ -191,6 +214,27 @@ class EversenseGattCallback(
191214
return
192215
}
193216

217+
if (Eversense365Packets.isKeepAlivePacket(data[0], data[1])) {
218+
EversenseLogger.debug(TAG, "Keep Alive packet received!")
219+
220+
val packet = KeepAlivePacket()
221+
packet.appendData(data.toUByteArray())
222+
val response = packet.parseResponse() ?: return
223+
224+
val fourHalfMinAgo = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(270)
225+
if (response.glucoseDatetime > fourHalfMinAgo) {
226+
executor.submit {
227+
Eversense365Communicator.readGlucose(this, preferences, plugin.watchers)
228+
Eversense365Communicator.fullSync(this, preferences, plugin.watchers)
229+
}
230+
}
231+
232+
return
233+
} else if (Eversense365Packets.isNotificationPacket(data[0])) {
234+
EversenseLogger.warning(TAG, "Unknown notification packet received...")
235+
return
236+
}
237+
194238
val packet = currentPacket ?:run {
195239
EversenseLogger.warning(TAG, "currentPacket is empty -> Skip packet...")
196240
return
@@ -208,15 +252,20 @@ class EversenseGattCallback(
208252
return
209253
}
210254

211-
if (packetAnnotation.responseId != data[0]) {
212-
EversenseLogger.warning(TAG, "Incorrect responseId received - Expected: ${packetAnnotation.responseId}, got: ${data[0]}")
213-
return
214-
}
215-
216255
if (security == EversenseSecurityType.None) {
256+
if (packetAnnotation.responseId != data[0]) {
257+
EversenseLogger.warning(TAG, "Incorrect responseId received - Expected: ${packetAnnotation.responseId}, got: ${data[0]}")
258+
return
259+
}
260+
217261
packet.appendData(data.toUByteArray())
218262
packet.notifyAll()
219263
} else {
264+
if (packetAnnotation.responseId != data[0]) {
265+
EversenseLogger.warning(TAG, "Incorrect responseId received - Expected: ${packetAnnotation.responseId}, got: ${data[0]}")
266+
return
267+
}
268+
220269
if (packetAnnotation.typeId != data[1]) {
221270
EversenseLogger.warning(TAG, "Incorrect responseType received - Expected: ${packetAnnotation.typeId}, got: ${data[1]}")
222271
return
@@ -241,7 +290,7 @@ class EversenseGattCallback(
241290
throw EversenseWriteException("requestCharacteristic is empty")
242291
}
243292

244-
val requestData = packet.buildRequest(cryptoUtil) ?:run {
293+
val requestData = packet.buildRequest(cryptoUtil, payloadSize) ?:run {
245294
throw EversenseWriteException("Failed to build request data...")
246295
}
247296

@@ -264,7 +313,7 @@ class EversenseGattCallback(
264313
return try {
265314
val response = packet.parseResponse()
266315
currentPacket = null
267-
response as? T ?: throw EversenseWriteException("Unable to cast response")
316+
response as? T ?: throw EversenseWriteException("Unable to cast response - packet: ${packet.getAnnotation()?.responseId}")
268317
} catch(e: Exception) {
269318
throw EversenseWriteException("Failed to parse response - exception: $e")
270319
}
@@ -293,7 +342,8 @@ class EversenseGattCallback(
293342
}
294343

295344
if (!cryptoUtil.canUseShortcut()) {
296-
val whoAmI = writePacket<AuthWhoAmIPacket.Response>(AuthWhoAmIPacket())
345+
val clientId = cryptoUtil.getClientId()
346+
val whoAmI = writePacket<AuthWhoAmIPacket.Response>(AuthWhoAmIPacket(clientId))
297347

298348
val authSession = EversenseHttp365Util.login(preferences) ?:run {
299349
bluetoothGatt?.disconnect()
@@ -330,6 +380,7 @@ class EversenseGattCallback(
330380
Eversense365Communicator.fullSync(this, preferences, plugin.watchers)
331381
} catch(exception: Exception) {
332382
EversenseLogger.error(TAG, "[365] Failed to do authV2 - $exception")
383+
exception.printStackTrace()
333384
bluetoothGatt?.disconnect()
334385
}
335386
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.nightscout.eversense.enums
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
enum class CalibrationMode(private val value: Int) {
8+
@SerialName("DAILY_SINGLE")
9+
DAILY_SINGLE(0x01),
10+
11+
@SerialName("DAILY_DUAL")
12+
DAILY_DUAL(0x02),
13+
14+
@SerialName("WEEKLY_SINGLE")
15+
WEEKLY_SINGLE(0x03),
16+
17+
@SerialName("DEFAULT")
18+
DEFAULT(0x04);
19+
20+
companion object {
21+
fun from365(value: Int): CalibrationMode {
22+
return when(value) {
23+
0 -> DAILY_SINGLE
24+
1 -> WEEKLY_SINGLE
25+
else -> DEFAULT
26+
}
27+
}
28+
}
29+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.nightscout.eversense.enums
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
enum class CalibrationPhase(private val value: Int) {
8+
9+
@SerialName("WARMING_UP")
10+
WARMING_UP(0x01),
11+
12+
@SerialName("DAILY_CALIBRATION")
13+
DAILY_CALIBRATION(0x02),
14+
15+
@SerialName("INITIALIZATION")
16+
INITIALIZATION(0x03),
17+
18+
@SerialName("SUSPICIOUS")
19+
SUSPICIOUS(0x04),
20+
21+
@SerialName("UNKNOWN")
22+
UNKNOWN(0x05),
23+
24+
@SerialName("DEBUG")
25+
DEBUG(0x06),
26+
27+
@SerialName("DROPOUT")
28+
DROPOUT(0x07);
29+
30+
companion object {
31+
fun fromE3(value: Int): CalibrationPhase {
32+
return when(value) {
33+
1 -> WARMING_UP
34+
2 -> DAILY_CALIBRATION
35+
3 -> INITIALIZATION
36+
4 -> SUSPICIOUS
37+
5 -> UNKNOWN
38+
6 -> DEBUG
39+
7 -> DROPOUT
40+
else -> UNKNOWN
41+
}
42+
}
43+
44+
fun from365(value: Int): CalibrationPhase {
45+
return when(value) {
46+
0 -> UNKNOWN
47+
1 -> WARMING_UP
48+
2, 3 -> DAILY_CALIBRATION
49+
4 -> SUSPICIOUS
50+
5 -> DROPOUT
51+
6 -> DEBUG
52+
else -> UNKNOWN
53+
}
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)