From ffdc969a15280fc05bab85887cd0d55d286c0a31 Mon Sep 17 00:00:00 2001 From: Craig Gordon Date: Wed, 1 Apr 2026 19:13:27 -0400 Subject: [PATCH 1/5] Add Eversense E3/365 CGM plugin - Add plugins:eversense module with full BLE driver for E3 and 365 transmitters - Add EversensePlugin integrating with AAPS BgSource interface - Add placement guide activity with 500ms signal polling - Add calibration activity - Add Bluetooth permission request activity - Add EVERSENSE_E3 and EVERSENSE_365 to SourceSensor and GlucoseValue enums - Add Eversense notification IDs to NotificationId enum - Register plugin in SourceModule, PluginsListModule and settings.gradle - Use new AAPS NotificationManager API for all notifications - Support both Eversense E3 (180-day) and Eversense 365 (1-year) transmitters --- .../kotlin/app/aaps/di/PluginsListModule.kt | 2 + .../app/aaps/core/data/model/SourceSensor.kt | 2 + .../notifications/NotificationId.kt | 4 + .../aaps/database/entities/GlucoseValue.kt | 2 + .../converters/SourceSensorExtension.kt | 4 + plugins/eversense/build.gradle.kts | 18 + .../eversense/EversenseCGMPlugin.kt | 321 ++++++++++ .../eversense/EversenseGattCallback.kt | 568 ++++++++++++++++++ .../callbacks/EversenseScanCallback.kt | 7 + .../eversense/callbacks/EversenseWatcher.kt | 14 + .../eversense/enums/CalibrationMode.kt | 29 + .../eversense/enums/CalibrationPhase.kt | 56 ++ .../eversense/enums/CalibrationReadiness.kt | 73 +++ .../eversense/enums/EversenseAlarm.kt | 82 +++ .../eversense/enums/EversenseE3Memory.kt | 48 ++ .../eversense/enums/EversenseSecurityType.kt | 10 + .../eversense/enums/EversenseTrendArrow.kt | 10 + .../eversense/enums/EversenseType.kt | 6 + .../exceptions/EversenseWriteException.kt | 4 + .../eversense/models/ActiveAlarm.kt | 12 + .../eversense/models/EversenseCGMResult.kt | 9 + .../eversense/models/EversenseScanResult.kt | 5 + .../eversense/models/EversenseSecureState.kt | 13 + .../eversense/models/EversenseState.kt | 47 ++ .../eversense/models/GlucoseHistoryItem.kt | 9 + .../packets/Eversense365Communicator.kt | 221 +++++++ .../eversense/packets/EversenseBasePacket.kt | 104 ++++ .../packets/EversenseE3Communicator.kt | 299 +++++++++ .../eversense/packets/EversensePacket.kt | 17 + .../packets/e3/EversenseE3Packets.kt | 117 ++++ .../packets/e3/GetBatteryPercentagePacket.kt | 47 ++ .../packets/e3/GetCalibrationDailyPacket.kt | 32 + .../packets/e3/GetCalibrationPhasePacket.kt | 32 + .../e3/GetCalibrationReadinessPacket.kt | 31 + .../packets/e3/GetCurrentDatetimePacket.kt | 49 ++ .../packets/e3/GetCurrentGlucosePacket.kt | 45 ++ .../e3/GetHighGlucoseRepeatIntervalPacket.kt | 21 + .../packets/e3/GetInsertionDatePacket.kt | 30 + .../packets/e3/GetInsertionTimePacket.kt | 30 + .../e3/GetLastCalibrationDatePacket.kt | 30 + .../e3/GetLastCalibrationTimePacket.kt | 30 + .../e3/GetLowGlucoseRepeatIntervalPacket.kt | 21 + .../packets/e3/GetMmaFeaturesPacket.kt | 21 + .../e3/GetNextCalibrationDatePacket.kt | 30 + .../e3/GetNextCalibrationTimePacket.kt | 30 + .../e3/GetSettingGlucoseHighEnabled.kt | 29 + .../GetSettingGlucoseHighThresholdPacket.kt | 32 + .../e3/GetSettingGlucoseLowThresholdPacket.kt | 30 + .../GetSettingPredictiveAlarmEnabledPacket.kt | 29 + .../GetSettingPredictiveHighEnabledPacket.kt | 29 + ...GetSettingPredictiveHighThresholdPacket.kt | 30 + .../e3/GetSettingPredictiveHighTimePacket.kt | 29 + .../GetSettingPredictiveLowEnabledPacket.kt | 29 + .../GetSettingPredictiveLowThresholdPacket.kt | 30 + .../e3/GetSettingPredictiveLowTimePacket.kt | 29 + .../packets/e3/GetSettingRateEnabledPacket.kt | 29 + .../e3/GetSettingRateFallingEnabledPacket.kt | 29 + .../GetSettingRateFallingThresholdPacket.kt | 29 + .../e3/GetSettingRateRisingEnabledPacket.kt | 29 + .../e3/GetSettingRateRisingThresholdPacket.kt | 29 + .../packets/e3/GetSettingVibratePacket.kt | 31 + .../packets/e3/GetSignalStrengthRawPacket.kt | 45 ++ .../packets/e3/GetVersionExtendedPacket.kt | 23 + .../eversense/packets/e3/GetVersionPacket.kt | 23 + .../e3/SaveBondingInformationPacket.kt | 28 + .../packets/e3/SendCalibrationPacket.kt | 52 ++ .../packets/e3/SetAppVersionE3Packet.kt | 30 + .../packets/e3/SetBleDisconnectPacket.kt | 24 + .../packets/e3/SetCurrentDatetimePacket.kt | 32 + .../e3/SetSettingGlucoseHighEnablePacket.kt | 30 + .../SetSettingGlucoseHighThresholdPacket.kt | 30 + .../e3/SetSettingGlucoseLowThresholdPacket.kt | 30 + .../SetSettingPredictiveAlarmEnabledPacket.kt | 30 + ...SettingPredictiveHighAlarmEnabledPacket.kt | 30 + ...SetSettingPredictiveHighThresholdPacket.kt | 30 + .../e3/SetSettingPredictiveHighTimePacket.kt | 29 + ...tSettingPredictiveLowAlarmEnabledPacket.kt | 30 + .../SetSettingPredictiveLowThresholdPacket.kt | 30 + .../e3/SetSettingPredictiveLowTimePacket.kt | 29 + .../packets/e3/SetSettingRateEnabledPacket.kt | 30 + .../e3/SetSettingRateFallingEnabledPacket.kt | 30 + .../SetSettingRateFallingThresholdPacket.kt | 30 + .../e3/SetSettingRateRisingEnabledPacket.kt | 30 + .../e3/SetSettingRateRisingThresholdPacket.kt | 30 + .../packets/e3/SetSettingVibratePacket.kt | 30 + .../packets/e3/util/EversenseE3Parser.kt | 69 +++ .../packets/e3/util/EversenseE3Writer.kt | 78 +++ .../packets/e365/AuthIdentityPacket.kt | 28 + .../eversense/packets/e365/AuthStartPacket.kt | 32 + .../packets/e365/AuthWhoAmIPacket.kt | 36 ++ .../packets/e365/Eversense365Packets.kt | 64 ++ .../packets/e365/GetActiveAlarmsPacket.kt | 52 ++ .../packets/e365/GetCalibrationInfoPacket.kt | 59 ++ .../packets/e365/GetGlucoseDataPacket.kt | 75 +++ .../packets/e365/GetGlucoseLogValuesPacket.kt | 83 +++ .../packets/e365/GetLogRangePacket365.kt | 47 ++ .../packets/e365/GetPatientSettingsPacket.kt | 83 +++ .../e365/GetSensorInformationPacket.kt | 87 +++ .../packets/e365/GetSignalStrengthPacket.kt | 63 ++ .../eversense/packets/e365/KeepAlivePacket.kt | 30 + .../eversense/packets/e365/Ping365Packet.kt | 17 + .../packets/e365/SetAppVersion365Packet.kt | 24 + .../packets/e365/SetBleDisconnect365Packet.kt | 25 + .../e365/SetBloodGlucosePointPacket365.kt | 32 + .../packets/e365/SetCurrentDateTimePacket.kt | 39 ++ .../e365/SetHighGlucoseAlarm365Packet.kt | 20 + .../SetHighGlucoseAlarmEnabled365Packet.kt | 17 + .../e365/SetLowGlucoseAlarm365Packet.kt | 20 + .../e365/SetPredictionHighEnabled365Packet.kt | 17 + .../SetPredictionHighThreshold365Packet.kt | 20 + .../e365/SetPredictionHighTime365Packet.kt | 20 + .../e365/SetPredictionLowEnabled365Packet.kt | 17 + .../SetPredictionLowThreshold365Packet.kt | 20 + .../e365/SetPredictionLowTime365Packet.kt | 20 + .../e365/SetRateFallingEnabled365Packet.kt | 17 + .../e365/SetRateFallingThreshold365Packet.kt | 23 + .../e365/SetRateRisingEnabled365Packet.kt | 17 + .../e365/SetRateRisingThreshold365Packet.kt | 23 + .../e365/SetRepeatHighGlucose365Packet.kt | 20 + .../e365/SetRepeatLowGlucose365Packet.kt | 20 + .../packets/e365/SetVibrateMode365Packet.kt | 17 + .../packets/e365/utils/ByteArrayUtil.kt | 70 +++ .../eversense/util/EselSmoothing.kt | 33 + .../eversense/util/EversenseCrypto365Util.kt | 304 ++++++++++ .../eversense/util/EversenseHttp365Util.kt | 161 +++++ .../eversense/util/EversenseLogger.kt | 118 ++++ .../eversense/util/EversenseScanner.kt | 33 + .../eversense/util/RangeCalculator.kt | 21 + .../nightscout/eversense/util/StorageKeys.kt | 9 + plugins/source/build.gradle.kts | 1 + plugins/source/src/main/AndroidManifest.xml | 8 + .../aaps/plugins/source/EversensePlugin.kt | 453 ++++++++++++++ .../EversenseCalibrationActivity.kt | 85 +++ .../activities/EversensePlacementActivity.kt | 148 +++++ .../RequestEversensePermissionActivity.kt | 40 ++ .../aaps/plugins/source/di/SourceModule.kt | 8 + .../res/drawable/eversense_signal_bar.xml | 6 + .../layout/activity_eversense_calibration.xml | 81 +++ .../layout/activity_eversense_placement.xml | 117 ++++ .../src/main/res/layout/source_fragment.xml | 16 + .../src/main/res/layout/source_item.xml | 94 +++ plugins/source/src/main/res/values/colors.xml | 8 + .../source/src/main/res/values/strings.xml | 62 ++ settings.gradle | 1 + 144 files changed, 7007 insertions(+) create mode 100644 plugins/eversense/build.gradle.kts create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseScanCallback.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseWatcher.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationMode.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationPhase.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationReadiness.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseAlarm.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseE3Memory.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseSecurityType.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseTrendArrow.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseType.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/exceptions/EversenseWriteException.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/ActiveAlarm.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseCGMResult.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseScanResult.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseSecureState.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseState.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/GlucoseHistoryItem.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/Eversense365Communicator.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseBasePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseE3Communicator.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversensePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/EversenseE3Packets.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetBatteryPercentagePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationDailyPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationReadinessPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentDatetimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCurrentGlucosePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetHighGlucoseRepeatIntervalPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionDatePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetInsertionTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationDatePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLastCalibrationTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetLowGlucoseRepeatIntervalPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetMmaFeaturesPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationDatePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetNextCalibrationTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighEnabled.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseHighThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingGlucoseLowThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveAlarmEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveHighTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingPredictiveLowTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateFallingThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingRateRisingThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSettingVibratePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetSignalStrengthRawPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionExtendedPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetVersionPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SaveBondingInformationPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SendCalibrationPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetAppVersionE3Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetBleDisconnectPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetCurrentDatetimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighEnablePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseHighThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingGlucoseLowThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveAlarmEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighAlarmEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveHighTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowAlarmEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingPredictiveLowTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateFallingThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingEnabledPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingRateRisingThresholdPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SetSettingVibratePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Parser.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Writer.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthIdentityPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthStartPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/AuthWhoAmIPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Eversense365Packets.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetActiveAlarmsPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseLogValuesPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetLogRangePacket365.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetPatientSettingsPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSensorInformationPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSignalStrengthPacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/KeepAlivePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Ping365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetAppVersion365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBleDisconnect365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetBloodGlucosePointPacket365.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetCurrentDateTimePacket.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarm365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetHighGlucoseAlarmEnabled365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetLowGlucoseAlarm365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighEnabled365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighThreshold365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionHighTime365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowEnabled365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowThreshold365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetPredictionLowTime365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingEnabled365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateFallingThreshold365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingEnabled365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRateRisingThreshold365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatHighGlucose365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetRepeatLowGlucose365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetVibrateMode365Packet.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/utils/ByteArrayUtil.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EselSmoothing.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseCrypto365Util.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseHttp365Util.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseLogger.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseScanner.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/RangeCalculator.kt create mode 100644 plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/StorageKeys.kt create mode 100644 plugins/source/src/main/kotlin/app/aaps/plugins/source/EversensePlugin.kt create mode 100644 plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversenseCalibrationActivity.kt create mode 100644 plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversensePlacementActivity.kt create mode 100644 plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/RequestEversensePermissionActivity.kt create mode 100644 plugins/source/src/main/res/drawable/eversense_signal_bar.xml create mode 100644 plugins/source/src/main/res/layout/activity_eversense_calibration.xml create mode 100644 plugins/source/src/main/res/layout/activity_eversense_placement.xml create mode 100644 plugins/source/src/main/res/layout/source_fragment.xml create mode 100644 plugins/source/src/main/res/layout/source_item.xml create mode 100644 plugins/source/src/main/res/values/colors.xml 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/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/build.gradle.kts b/plugins/eversense/build.gradle.kts new file mode 100644 index 00000000000..0c6e1f6b6ae --- /dev/null +++ b/plugins/eversense/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.ksp) + id("kotlinx-serialization") + id("android-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") +} \ 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..48e4763c978 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseCGMPlugin.kt @@ -0,0 +1,321 @@ +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) + } + + // FIX 3: setSmoothing now returns Boolean and logs on failure. + fun setCoexistenceMode(enabled: Boolean) { + gattCallback?.coexistenceMode = enabled + EversenseLogger.info(TAG, "Coexistence mode: $enabled") + } + + 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 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() { + 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()) + 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() + } + } +} \ No newline at end of file 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..65f417a5ff5 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/EversenseGattCallback.kt @@ -0,0 +1,568 @@ +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 + } + + // 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 failed connection attempts to detect transmitter placement issues + @Volatile + private var failedConnectionAttempts: Int = 0 + private val PLACEMENT_WARNING_THRESHOLD = 3 + + // When true, AAPS disconnects after each sync to give the official Eversense app + // a 2-minute window to connect and upload data to the cloud. Reconnects automatically. + @Volatile + var coexistenceMode: Boolean = false + + // True during a planned coexistence disconnect — suppresses normal auto-reconnect + @Volatile + private var plannedDisconnect: Boolean = false + + 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 + + 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.error(TAG, "Failed to request MTU") + } + 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 && !plannedDisconnect) { + val delayMs = if (status == BluetoothGatt.GATT_SUCCESS) 5000L else 10000L + EversenseLogger.info(TAG, "Scheduling auto-reconnect in ${delayMs/1000}s (status: $status)") + handler.postDelayed({ + EversenseLogger.info(TAG, "Attempting auto-reconnect...") + plugin.connect(null) + }, delayMs) + } else { + if (!plannedDisconnect) { EversenseLogger.warning(TAG, "No stored device address — skipping auto-reconnect") } else { EversenseLogger.info(TAG, "Planned coexistence disconnect — suppressing 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) + if (coexistenceMode) { + EversenseLogger.info(TAG, "Coexistence — disconnecting to give official app 2-minute window") + plannedDisconnect = true + disconnect() + handler.postDelayed({ + EversenseLogger.info(TAG, "Coexistence reconnect — official app window ended") + plannedDisconnect = false + plugin.connect(null) + }, 120000L) + } + } + 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) + } + if (coexistenceMode && !plannedDisconnect) { + EversenseLogger.info(TAG, "Coexistence — disconnecting to give official app 2-minute window") + plannedDisconnect = true + disconnect() + handler.postDelayed({ + EversenseLogger.info(TAG, "Coexistence reconnect — official app window ended") + plannedDisconnect = false + plugin.connect(null) + }, 120000L) + } + } + 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.notifyAll() + return + } + + if (security == EversenseSecurityType.None) { + if (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): 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(WRITE_TIMEOUT_MS) + val elapsed = System.currentTimeMillis() - start + if (elapsed >= WRITE_TIMEOUT_MS) { + currentPacket.set(null) + throw EversenseWriteException("Timed out waiting for response after ${WRITE_TIMEOUT_MS}ms — 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) + } + + @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 + } + + 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) + + } 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..015b591bfdb --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/callbacks/EversenseWatcher.kt @@ -0,0 +1,14 @@ +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() {} +} 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..2d999631e35 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/CalibrationPhase.kt @@ -0,0 +1,56 @@ +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("DAILY_CALIBRATION") + DAILY_CALIBRATION(0x02), + + @SerialName("INITIALIZATION") + INITIALIZATION(0x03), + + @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 + } + } + + fun from365(value: Int): CalibrationPhase { + return when(value) { + 0 -> UNKNOWN + 1 -> WARMING_UP + 2, 3 -> 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/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..5aff0d8ad36 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/enums/EversenseE3Memory.kt @@ -0,0 +1,48 @@ +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); + + 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/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..c364e55b18e --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseCGMResult.kt @@ -0,0 +1,9 @@ +package com.nightscout.eversense.models + +import com.nightscout.eversense.enums.EversenseTrendArrow + +data class EversenseCGMResult( + val glucoseInMgDl: Int, + val datetime: Long, + val trend: EversenseTrendArrow +) 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..9a4d70dac0c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/models/EversenseState.kt @@ -0,0 +1,47 @@ +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 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..f72402059f9 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/Eversense365Communicator.kt @@ -0,0 +1,221 @@ +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}") + + result += EversenseCGMResult( + glucoseInMgDl = currentGlucose, + datetime = glucoseData.datetime, + trend = glucoseData.trend + ) + + // 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 + 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..38ab95dac22 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseBasePacket.kt @@ -0,0 +1,104 @@ +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) + + 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 + + // 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..3c26044954a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/EversenseE3Communicator.kt @@ -0,0 +1,299 @@ +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.SendCalibrationPacket +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) { + try { + val stateJson = preferences.getString(StorageKeys.STATE, null) ?: "{}" + val state = JSON.decodeFromString(stateJson) + val fourHalfMinAgo = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(270) + + if (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") + gatt.writePacket(SendCalibrationPacket(glucoseMgDl)) + EversenseLogger.info(TAG, "Calibration sent successfully") + } + } +} \ No newline at end of file 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/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/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/GetCalibrationPhasePacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.kt new file mode 100644 index 00000000000..beea5d25fa6 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/GetCalibrationPhasePacket.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 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() +} \ No newline at end of file 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..8a2ad1d30a5 --- /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.ReadTwoByteSerialFlashRegisterCommandId, + responseId = EversenseE3Packets.ReadTwoByteSerialFlashRegisterResponseId, + 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() +} \ No newline at end of file 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/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/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/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..b6dd12f5b7c --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/SendCalibrationPacket.kt @@ -0,0 +1,52 @@ +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 x10, e.g. 100 mg/dL = 1000) + * - Current date (2 bytes) and time (2 bytes) as timestamps + * - CRC16 checksum of the payload + * + * The transmitter must be in CalibrationReadiness.READY state before calling this. + * + * @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() + + // BG value is sent as mg/dL * 10 to preserve one decimal place + val bgEncoded = EversenseE3Writer.writeInt16(glucoseMgDl * 10) + val date = EversenseE3Writer.writeDate(now) + val time = EversenseE3Writer.writeTime(now) + + val payload = bgEncoded + date + time + + // Append CRC16 checksum of the payload for data integrity + val crc = EversenseE3Writer.generateChecksumCRC16(payload) + + return payload + crc + } + + 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/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/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..bfc6fd339d5 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Parser.kt @@ -0,0 +1,69 @@ +package com.nightscout.eversense.packets.e3.util + +import android.util.Log +import java.util.Calendar +import java.util.TimeZone + +class EversenseE3Parser { + companion object { + fun readDate(data: UByteArray, start: Int): Long { + val lowBit = data[start].toInt() + val highBit = data[start+1].toInt() + + val day = lowBit and 31 + var month = lowBit shr 5 + val year = (highBit shr 1) + 2000 + + if (highBit 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 { + val lowBit = data[start].toInt() + val highBit = data[start+1].toInt() + + val hour = highBit shr 3 + val minute = ((highBit and 7) shl 3) or (lowBit shr 5) + val second = (lowBit 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 + } + + fun readTimezone(data: UByteArray, start: Int): Long { + var timezoneOffset = readTime(data, start) + if (data[start + 2] != 0.toUByte()) { + timezoneOffset *= -1 + } + + return timezoneOffset + } + + fun readGlucose(data: UByteArray, start: Int): Int { + val lowBit = data[start].toInt() + val highBit = data[start+1].toInt() shl 8 + return lowBit or highBit + } + } +} \ No newline at end of file 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..b8365acc9dc --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e3/util/EversenseE3Writer.kt @@ -0,0 +1,78 @@ +package com.nightscout.eversense.packets.e3.util + +import java.util.Calendar +import java.util.TimeZone +import kotlin.math.PI +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() + 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..c0f2c5e9cd2 --- /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) and 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..a80839f3ec8 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/Eversense365Packets.kt @@ -0,0 +1,64 @@ +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() + + + 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/GetCalibrationInfoPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt new file mode 100644 index 00000000000..e827f41b30b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetCalibrationInfoPacket.kt @@ -0,0 +1,59 @@ +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 + } + + return Response( + currentPhase = CalibrationPhase.from365(receivedData[2].toInt()), + calibrationReadiness = CalibrationReadiness.from(receivedData[3].toInt()), + calibrationMode = CalibrationMode.from365(receivedData[12].toInt()), + 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/GetGlucoseDataPacket.kt b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt new file mode 100644 index 00000000000..b9c2426bd95 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetGlucoseDataPacket.kt @@ -0,0 +1,75 @@ +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") + + 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 + ) + } + + private fun getTrend(value: Int): EversenseTrendArrow { + return when (value) { + 0 -> EversenseTrendArrow.FLAT + 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, + val signalStrength: Int + ) : 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..533a7336dd4 --- /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.isEmpty()) { + 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..7483d962a7a --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/GetSensorInformationPacket.kt @@ -0,0 +1,87 @@ +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.isEmpty()) { + return null + } + + val sensorIdLength = receivedData[103].toInt() + val sensorIdDoubleLength = 2 * sensorIdLength + 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/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..4d8aa61ef28 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/packets/e365/SetCurrentDateTimePacket.kt @@ -0,0 +1,39 @@ +package com.nightscout.eversense.packets.e365 + +import android.util.Log +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..14ba579ae91 --- /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].toShort() or (this[1].toInt() shr 8).toShort() +} + +fun UByteArray.toInt(): Int { + return this[0].toInt() or (this[1].toInt() shr 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..0b7f2629568 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseCrypto365Util.kt @@ -0,0 +1,304 @@ +package com.nightscout.eversense.util + +import android.content.SharedPreferences +import android.security.keystore.KeyProperties +import android.util.Log +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..8cfc6d1ac25 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseHttp365Util.kt @@ -0,0 +1,161 @@ +package com.nightscout.eversense.util + +import android.annotation.SuppressLint +import android.content.SharedPreferences +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.util.Base64 + +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 + + 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("https://usiamapi.eversensedms.com/connect/token") + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + 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 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..8699e29f4a8 --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/EversenseScanner.kt @@ -0,0 +1,33 @@ +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 — they start with "T3" (E3) or "365" + if (!deviceName.startsWith("T3") && !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/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..cb2d2bb4f7b --- /dev/null +++ b/plugins/eversense/src/main/kotlin/com/nightscout/eversense/util/StorageKeys.kt @@ -0,0 +1,9 @@ +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" + } +} \ No newline at end of file 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..7dc68ecf375 --- /dev/null +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/EversensePlugin.kt @@ -0,0 +1,453 @@ +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.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.configuration.Config +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.notifications.NotificationId +import app.aaps.core.interfaces.notifications.NotificationLevel +import app.aaps.core.interfaces.notifications.NotificationManager +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.interfaces.Preferences +import com.nightscout.eversense.EversenseCGMPlugin +import com.nightscout.eversense.callbacks.EversenseScanCallback +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.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 +import javax.inject.Singleton + +@Singleton +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) + .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 var connectedPreference: Preference? = null + private var releasePreference: Preference? = null + private var lastNotifiedFirmwareVersion: String = "" + private var consecutiveNoSignalReadings: Int = 0 + private val NO_SIGNAL_WARNING_THRESHOLD = 3 + + private val eversense get() = EversenseCGMPlugin.instance + + override fun onStart() { + super.onStart() + eversense.setContext(context, true) + eversense.addWatcher(this) + if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { + eversense.connect(null) + } + } + + override fun onStop() { + super.onStop() + eversense.removeWatcher(this) + } + + override fun addPreferenceScreen(preferenceManager: PreferenceManager, parent: PreferenceScreen, context: Context, requiredKey: String?) { + if (requiredKey != null) return + + 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 + } + + val credentialsCategory = PreferenceCategory(context) + credentialsCategory.title = rh.gs(R.string.eversense_credentials_title) + + val username = EditTextPreference(context) + username.key = "eversense_credentials_username" + username.title = rh.gs(R.string.eversense_credentials_username) + val secureState = getSecureState() + username.summary = if (secureState.username.isNotEmpty()) secureState.username + else rh.gs(R.string.eversense_credentials_not_set) + username.dialogTitle = rh.gs(R.string.eversense_credentials_username) + username.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue -> + val state = getSecureState() + val updated = EversenseSecureState().also { it.canUseShortcut = state.canUseShortcut; it.username = newValue as String; it.password = state.password; it.clientId = state.clientId; it.privateKey = state.privateKey; it.publicKey = state.publicKey } + saveSecureState(updated) + val newStr = newValue as String + pref.summary = if (newStr.isNotEmpty()) newStr else rh.gs(R.string.eversense_credentials_not_set) + aapsLogger.info(LTag.BGSOURCE, "Eversense username updated") + true + } + + 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.dialogTitle = rh.gs(R.string.eversense_credentials_password) + password.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue -> + val state = getSecureState() + val updated = EversenseSecureState().also { it.canUseShortcut = state.canUseShortcut; it.username = state.username; it.password = newValue as String; it.clientId = state.clientId; it.privateKey = state.privateKey; it.publicKey = state.publicKey } + saveSecureState(updated) + pref.summary = if ((newValue as String).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 + } + + val signOut = Preference(context) + signOut.key = "eversense_credentials_sign_out" + signOut.title = "Sign out" + signOut.onPreferenceClickListener = Preference.OnPreferenceClickListener { + AlertDialog.Builder(context) + .setTitle("Sign out") + .setMessage("Are you sure you want to clear your Eversense credentials?") + .setPositiveButton("Yes") { _, _ -> + saveSecureState(EversenseSecureState()) + username.summary = rh.gs(R.string.eversense_credentials_not_set) + password.summary = rh.gs(R.string.eversense_credentials_not_set) + aapsLogger.info(LTag.BGSOURCE, "Eversense credentials cleared by user") + } + .setNegativeButton("No", null) + .show() + true + } + + val calibrationCategory = PreferenceCategory(context) + calibrationCategory.title = rh.gs(R.string.eversense_calibration_title) + + val currentPhase = Preference(context) + currentPhase.key = "eversense_calibration_phase" + currentPhase.title = rh.gs(R.string.eversense_calibration_phase) + val state = eversense.getCurrentState() + currentPhase.summary = state?.calibrationPhase?.name ?: rh.gs(R.string.eversense_not_connected) + + val lastCalibration = Preference(context) + lastCalibration.key = "eversense_calibration_last" + lastCalibration.title = rh.gs(R.string.eversense_calibration_last) + lastCalibration.summary = if (state != null && state.lastCalibrationDate > 0) + dateFormatter.format(Date(state.lastCalibrationDate)) + else rh.gs(R.string.eversense_not_connected) + + val nextCalibration = Preference(context) + nextCalibration.key = "eversense_calibration_next" + nextCalibration.title = rh.gs(R.string.eversense_calibration_next) + nextCalibration.summary = if (state != null && state.nextCalibrationDate > 0) + dateFormatter.format(Date(state.nextCalibrationDate)) + else rh.gs(R.string.eversense_not_connected) + + val calibrationAction = Preference(context) + calibrationAction.key = "eversense_calibration_action" + calibrationAction.title = rh.gs(R.string.eversense_calibration_action) + calibrationAction.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val latestState = eversense.getCurrentState() + if (latestState?.calibrationReadiness == CalibrationReadiness.READY) { + val intent = Intent(context, app.aaps.plugins.source.activities.EversenseCalibrationActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } else { + AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_calibration_title)) + .setMessage(rh.gs(R.string.eversense_calibration_not_supported)) + .setPositiveButton("OK", null) + .show() + } + true + } + + val informationCategory = PreferenceCategory(context) + informationCategory.title = rh.gs(R.string.eversense_information_title) + + val connected = Preference(context) + connected.key = "eversense_information_connected" + connected.title = rh.gs(R.string.eversense_information_connected) + connected.summary = if (eversense.isConnected()) "\u2705" else "\u274C" + connected.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (eversense.isConnected()) { + showDeviceScanDialog(context) + } else { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + val intent = Intent(context, app.aaps.plugins.source.activities.RequestEversensePermissionActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } else { + showDeviceScanDialog(context) + } + } + true + } + connectedPreference = connected + + val battery = Preference(context) + battery.key = "eversense_information_battery" + battery.title = rh.gs(R.string.eversense_information_battery) + battery.summary = if (state != null && state.batteryPercentage >= 0) "${state.batteryPercentage}%" else rh.gs(R.string.eversense_not_connected) + + val placementSignal = Preference(context) + placementSignal.key = "eversense_information_placement_signal" + placementSignal.title = rh.gs(R.string.eversense_placement_signal) + placementSignal.summary = if (state != null && state.sensorSignalStrength > 0) "${state.sensorSignalStrength}%" else rh.gs(R.string.eversense_not_connected) + 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) + true + } + + val insertion = Preference(context) + insertion.key = "eversense_information_insertion_date" + insertion.title = rh.gs(R.string.eversense_information_insertion_date) + insertion.summary = if (state != null && state.insertionDate > 0) dateFormatter.format(Date(state.insertionDate)) else rh.gs(R.string.eversense_not_connected) + + val lastSync = Preference(context) + lastSync.key = "eversense_information_last_sync" + lastSync.title = rh.gs(R.string.eversense_information_last_sync) + lastSync.summary = if (state != null && state.lastSync > 0) dateFormatter.format(Date(state.lastSync)) else rh.gs(R.string.eversense_not_connected) + lastSync.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (!eversense.isConnected()) { + aapsLogger.warn(LTag.BGSOURCE, "Cannot trigger sync — not connected") + } else { + eversense.triggerFullSync() + } + true + } + + val releaseForApp = Preference(context) + releaseForApp.key = "eversense_release_for_official_app" + releaseForApp.title = rh.gs(R.string.eversense_release_for_official_app) + releaseForApp.summary = rh.gs(R.string.eversense_release_summary) + releaseForApp.onPreferenceClickListener = Preference.OnPreferenceClickListener { + eversense.disconnect() + releaseForApp.summary = rh.gs(R.string.eversense_release_active) + notificationManager.post(NotificationId.EVERSENSE_RELEASE, "Eversense transmitter released — open the official Eversense app to connect", NotificationLevel.INFO) + val handler = Handler(Looper.getMainLooper()) + val reconnectRunnable = object : Runnable { + override fun run() { + if (!eversense.isConnected()) { + eversense.connect(null) + if (!eversense.isConnected()) { + handler.postDelayed(this, 300000L) + } else { + releasePreference?.summary = rh.gs(R.string.eversense_release_summary) + } + } else { + releasePreference?.summary = rh.gs(R.string.eversense_release_summary) + } + } + } + handler.postDelayed(reconnectRunnable, 300000L) + true + } + releasePreference = releaseForApp + + parent.addPreference(eselSmoothing) + parent.addPreference(credentialsCategory) + credentialsCategory.addPreference(username) + credentialsCategory.addPreference(password) + credentialsCategory.addPreference(signOut) + parent.addPreference(calibrationCategory) + calibrationCategory.addPreference(currentPhase) + calibrationCategory.addPreference(lastCalibration) + calibrationCategory.addPreference(nextCalibration) + calibrationCategory.addPreference(calibrationAction) + parent.addPreference(informationCategory) + informationCategory.addPreference(connected) + informationCategory.addPreference(battery) + informationCategory.addPreference(placementSignal) + informationCategory.addPreference(insertion) + informationCategory.addPreference(lastSync) + informationCategory.addPreference(releaseForApp) + } + + private fun getSecureState(): EversenseSecureState { + val json = Json { ignoreUnknownKeys = true } + val stored = securePrefs.getString(StorageKeys.SECURE_STATE, null) + return if (stored != null) { + try { json.decodeFromString(EversenseSecureState.serializer(), stored) } + catch (e: Exception) { EversenseSecureState() } + } else EversenseSecureState() + } + + private fun saveSecureState(state: EversenseSecureState) { + val json = Json { ignoreUnknownKeys = true } + securePrefs.edit { + putString(StorageKeys.SECURE_STATE, json.encodeToString(EversenseSecureState.serializer(), state)) + } + } + + private fun showDeviceScanDialog(context: Context) { + val foundDevices = mutableListOf() + val scanCallback = object : EversenseScanCallback { + override fun onResult(item: EversenseScanResult) { + if (foundDevices.none { it.device.address == item.device.address }) { + foundDevices.add(item) + } + } + } + eversense.startScan(scanCallback) + mainHandler.postDelayed({ + eversense.stopScan() + mainHandler.post { + 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 names = foundDevices.map { it.name ?: rh.gs(R.string.eversense_scan_unknown_device) }.toTypedArray() + AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setItems(names) { _, which -> + val selected = foundDevices[which] + eversense.connect(selected.device) + } + .setNegativeButton(rh.gs(R.string.eversense_scan_cancel), null) + .show() + } + } + }, 5000L) + AlertDialog.Builder(context) + .setTitle(rh.gs(R.string.eversense_scan_title)) + .setMessage("Scanning for Eversense devices...") + .setNegativeButton(rh.gs(R.string.eversense_scan_cancel)) { _, _ -> eversense.stopScan() } + .show() + } + + override fun onTransmitterNotPlaced() { + aapsLogger.warn(LTag.BGSOURCE, "Transmitter not placed — firing placement warning notification") + mainHandler.post { + notificationManager.post( + id = NotificationId.EVERSENSE_PLACEMENT, + text = rh.gs(R.string.eversense_transmitter_not_placed), + level = NotificationLevel.URGENT + ) + } + } + + override fun onStateChanged(state: EversenseState) { + mainHandler.post { + connectedPreference?.summary = if (eversense.isConnected()) "\u2705" else "\u274C" + } + + if (state.sensorSignalStrength == 0) { + consecutiveNoSignalReadings++ + aapsLogger.warn(LTag.BGSOURCE, "No signal reading $consecutiveNoSignalReadings of $NO_SIGNAL_WARNING_THRESHOLD") + if (consecutiveNoSignalReadings >= NO_SIGNAL_WARNING_THRESHOLD) { + onTransmitterNotPlaced() + consecutiveNoSignalReadings = 0 + } + } else { + consecutiveNoSignalReadings = 0 + notificationManager.dismiss(NotificationId.EVERSENSE_PLACEMENT) + } + + if (state.firmwareVersion.isNotEmpty() && state.firmwareVersion != lastNotifiedFirmwareVersion) { + lastNotifiedFirmwareVersion = state.firmwareVersion + notificationManager.post( + id = NotificationId.EVERSENSE_FIRMWARE, + text = "Eversense firmware: ${state.firmwareVersion} — open the official Eversense app to check for updates", + level = NotificationLevel.INFO + ) + } + } + + override fun onConnectionChanged(connected: Boolean) { + aapsLogger.info(LTag.BGSOURCE, "Connection changed — connected: $connected") + mainHandler.post { + connectedPreference?.summary = if (connected) "\u2705" else "\u274C" + } + } + + override fun onAlarmReceived(alarm: ActiveAlarm) { + aapsLogger.info(LTag.BGSOURCE, "Eversense alarm received: ${alarm.code.title}") + val level = when { + alarm.code.isCritical -> NotificationLevel.URGENT + alarm.code.isWarning -> NotificationLevel.NORMAL + else -> NotificationLevel.INFO + } + mainHandler.post { + notificationManager.post( + id = NotificationId.EVERSENSE_ALARM, + text = alarm.code.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 + } + ) + } + if (glucoseValues.isNotEmpty()) { + ioScope.launch { + persistenceLayer.insertCgmSourceData(Sources.Eversense, glucoseValues, emptyList(), null) + } + } + } +} \ No newline at end of file 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..280d586ed7d --- /dev/null +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/activities/EversenseCalibrationActivity.kt @@ -0,0 +1,85 @@ +package app.aaps.plugins.source.activities + +import android.os.Bundle +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.enums.CalibrationReadiness +import javax.inject.Inject + +class EversenseCalibrationActivity : TranslatedDaggerAppCompatActivity() { + + @Inject lateinit var profileUtil: ProfileUtil + + 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() + + 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