From db653de40533d7020953f6c3b059274819f9202f Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Tue, 25 Feb 2025 21:56:14 +0100 Subject: [PATCH 1/6] wip: start BLE flow --- MedtrumKit.xcodeproj/project.pbxproj | 28 ++++++ MedtrumKit/PumpManager/BluetoothManager.swift | 89 ++++++++++++++++++ .../PumpManager/MedtrumPumpManager.swift | 24 ++++- MedtrumKit/PumpManager/MedtrumPumpState.swift | 21 +++++ .../PumpManager/Models/ConnectResult.swift | 16 ++++ .../PumpManager/Models/ScanResult.swift | 18 ++++ .../PumpManager/PeripheralManager.swift | 90 +++++++++++++++++++ 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 MedtrumKit/PumpManager/BluetoothManager.swift create mode 100644 MedtrumKit/PumpManager/MedtrumPumpState.swift create mode 100644 MedtrumKit/PumpManager/Models/ConnectResult.swift create mode 100644 MedtrumKit/PumpManager/Models/ScanResult.swift create mode 100644 MedtrumKit/PumpManager/PeripheralManager.swift diff --git a/MedtrumKit.xcodeproj/project.pbxproj b/MedtrumKit.xcodeproj/project.pbxproj index 399d345..3c19977 100644 --- a/MedtrumKit.xcodeproj/project.pbxproj +++ b/MedtrumKit.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */; }; + 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */; }; + 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390142D6E5C5200A146C1 /* ScanResult.swift */; }; + 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */; }; + 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */; }; 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CFD2D67B192004B1971 /* LocalizedString.swift */; }; 3E767D032D67B319004B1971 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CE42D67AF35004B1971 /* OSLog.swift */; }; 3E767D042D67B3FC004B1971 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3E767CFB2D67B13C004B1971 /* Localizable.strings */; }; @@ -60,6 +65,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothManager.swift; sourceTree = ""; }; + 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumPumpState.swift; sourceTree = ""; }; + 3E1390142D6E5C5200A146C1 /* ScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResult.swift; sourceTree = ""; }; + 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectResult.swift; sourceTree = ""; }; + 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManager.swift; sourceTree = ""; }; 3E767CE22D67ADFA004B1971 /* MedtrumKitPumpManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MedtrumKitPumpManager+UI.swift"; sourceTree = ""; }; 3E767CE42D67AF35004B1971 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 3E767CE52D67B13C004B1971 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ../Localization/ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -132,6 +142,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3E1390132D6E5C4900A146C1 /* Models */ = { + isa = PBXGroup; + children = ( + 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */, + 3E1390142D6E5C5200A146C1 /* ScanResult.swift */, + ); + path = Models; + sourceTree = ""; + }; 3E767CE12D67AD91004B1971 /* MedtrumKitUI */ = { isa = PBXGroup; children = ( @@ -164,6 +183,10 @@ B7189AE62C15A52800703DE2 /* PumpManager */ = { isa = PBXGroup; children = ( + 3E1390132D6E5C4900A146C1 /* Models */, + 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */, + 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */, + 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */, B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */, ); path = PumpManager; @@ -420,14 +443,19 @@ buildActionMask = 2147483647; files = ( 3E767D562D67BCD6004B1971 /* MedtrumKitUICoordinator.swift in Sources */, + 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */, 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */, 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */, 3E767D522D67BC2E004B1971 /* MedtrumKitPumpManager+UI.swift in Sources */, + 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */, + 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */, 3E767D032D67B319004B1971 /* OSLog.swift in Sources */, + 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */, B7D3A8F12C14F0EF002EE003 /* MedtrumKitPlugin.swift in Sources */, B7189AE72C15A52800703DE2 /* MedtrumPumpManager.swift in Sources */, 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */, B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */, + 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MedtrumKit/PumpManager/BluetoothManager.swift b/MedtrumKit/PumpManager/BluetoothManager.swift new file mode 100644 index 0000000..00f2183 --- /dev/null +++ b/MedtrumKit/PumpManager/BluetoothManager.swift @@ -0,0 +1,89 @@ +// +// BluetoothManager.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 25/02/2025. +// + +import CoreBluetooth + +class BluetoothManager: NSObject, CBCentralManagerDelegate { + public var pumpManager: MedtrumPumpManager? + + let log = MedtrumLogger(category: "BluetoothManager") + + var manager: CBCentralManager! + let managerQueue = DispatchQueue(label: "com.nightscout.MedtrumKit.bluetoothManagerQueue", qos: .unspecified) + + var scanCompletion: ((ScanResult) -> Void)? + var connectCompletion: ((ConnectResult) -> Void)? + + override init() { + super.init() + + managerQueue.sync { + self.manager = CBCentralManager(delegate: self, queue: managerQueue) + } + } + + func startScan(_ completion: @escaping (_ result: ScanResult) -> Void) { + guard manager.state == .poweredOn else { + completion(.failure(error: .invalidBluetoothState(state: manager.state))) + return + } + + guard !manager.isScanning else { + completion(.failure(error: .alreadyScanning)) + return + } + + scanCompletion = completion + manager.scanForPeripherals(withServices: []) + + log.info("Started scanning") + } + + func connect(peripheral: CBPeripheral, _ completion: @escaping (ConnectResult) -> Void) { + if manager.isScanning { + manager.stopScan() + scanCompletion = nil + } + + log.info("Connecting to \(peripheral)") + + connectCompletion = completion + manager.connect(peripheral) + } +} + +extension BluetoothManager { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + log.info("\(String(describing: central.state.rawValue))") + } + + func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi _: NSNumber) { + guard let deviceName = peripheral.name, !deviceName.isEmpty else { + return + } + + // TODO: Need to validate if the device name is always MT + log.info("Found device: \(deviceName), \(advertisementData)!") + guard deviceName == "MT" else { + return + } + + scanCompletion?(.success(peripheral: peripheral)) + } + + func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { + log.info("Connected to pump!") + } + + func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + log.info("Device disconnected, name: \(peripheral.name ?? "")") + } + + func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + log.info("Device connect error, name: \(peripheral.name ?? ""), error: \(error!.localizedDescription)") + } +} diff --git a/MedtrumKit/PumpManager/MedtrumPumpManager.swift b/MedtrumKit/PumpManager/MedtrumPumpManager.swift index 465e55e..314c92f 100644 --- a/MedtrumKit/PumpManager/MedtrumPumpManager.swift +++ b/MedtrumKit/PumpManager/MedtrumPumpManager.swift @@ -6,12 +6,26 @@ public class MedtrumPumpManager: DeviceManager { public static let pluginIdentifier = "Medtrum" public let localizedTitle = LocalizedString("Medtrum", comment: "Generic title of the Medtrum pump manager") public let managerIdentifier: String = "MedtrumKit" - public var rawState: RawStateValue + private let log = MedtrumLogger(category: "MedtrumPumpManager") public let pumpDelegate = WeakSynchronizedDelegate() + + var state: MedtrumPumpState + public var rawState: PumpManager.RawStateValue { + state.rawValue + } + + private let bluetooth: BluetoothManager - public required init?(rawState _: RawStateValue) { - nil + init(state: MedtrumPumpState) { + self.state = state + self.bluetooth = BluetoothManager() + + self.bluetooth.pumpManager = self + } + + public required convenience init?(rawState: RawStateValue) { + self.init(state: MedtrumPumpState(rawValue: rawState)) } public var isOnboarded: Bool { @@ -67,7 +81,9 @@ public class MedtrumPumpManager: DeviceManager { TimeInterval(minutes: 30) } - public var debugDescription: String + public var debugDescription: String { + "" + } public func acknowledgeAlert(alertIdentifier _: LoopKit.Alert.AlertIdentifier, completion: @escaping ((any Error)?) -> Void) { completion(nil) diff --git a/MedtrumKit/PumpManager/MedtrumPumpState.swift b/MedtrumKit/PumpManager/MedtrumPumpState.swift new file mode 100644 index 0000000..003632e --- /dev/null +++ b/MedtrumKit/PumpManager/MedtrumPumpState.swift @@ -0,0 +1,21 @@ +// +// MedtrumPumpState.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 25/02/2025. +// +import LoopKit + +class MedtrumPumpState: RawRepresentable { + public typealias RawValue = PumpManager.RawStateValue + + required public init(rawValue: RawValue) { + } + + public var rawValue: RawValue { + var value: [String: Any] = [:] + + return value + } + +} diff --git a/MedtrumKit/PumpManager/Models/ConnectResult.swift b/MedtrumKit/PumpManager/Models/ConnectResult.swift new file mode 100644 index 0000000..75d0c78 --- /dev/null +++ b/MedtrumKit/PumpManager/Models/ConnectResult.swift @@ -0,0 +1,16 @@ +// +// ConnectResult.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 25/02/2025. +// + +enum ConnectResult { + case success + case failure(error: ConnectError) +} + +enum ConnectError { + case failedToDiscoverServices(localizedError: String) + case failedToDiscoverCharacteristics(localizedError: String) +} diff --git a/MedtrumKit/PumpManager/Models/ScanResult.swift b/MedtrumKit/PumpManager/Models/ScanResult.swift new file mode 100644 index 0000000..9af0a8b --- /dev/null +++ b/MedtrumKit/PumpManager/Models/ScanResult.swift @@ -0,0 +1,18 @@ +// +// ScanResult.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 25/02/2025. +// + +import CoreBluetooth + +enum ScanResult { + case failure(error: ScanError) + case success(peripheral: CBPeripheral) +} + +enum ScanError { + case invalidBluetoothState(state: CBManagerState) + case alreadyScanning +} diff --git a/MedtrumKit/PumpManager/PeripheralManager.swift b/MedtrumKit/PumpManager/PeripheralManager.swift new file mode 100644 index 0000000..bb1f85f --- /dev/null +++ b/MedtrumKit/PumpManager/PeripheralManager.swift @@ -0,0 +1,90 @@ +// +// PeripheralManager.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 25/02/2025. +// + +import CoreBluetooth + +class PeripheralManager : NSObject { + private let log = MedtrumLogger(category: "PeripheralManager") + + private let connectedDevice: CBPeripheral + private let bluetoothManager: BluetoothManager + private let pumpManager: MedtrumPumpManager + private var completion: ((ConnectResult) -> Void)? + + private static let SERVICE_UUID = CBUUID(string: "669A9001-0008-968F-E311-6050405558B3") + private static let READ_UUID = CBUUID(string: "669a9120-0008-968f-e311-6050405558b3") + private var readCharacteristic: CBCharacteristic! + private static let WRITE_UUID = CBUUID(string: "669a9101-0008-968f-e311-6050405558b3") + private var writeCharacteristic: CBCharacteristic! + private static let CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") + private var configCharacteristic: CBCharacteristic! + + public init(_ peripheral: CBPeripheral, _ bluetoothManager: BluetoothManager, _ pumpManager: MedtrumPumpManager,_ completion: @escaping (ConnectResult) -> Void) { + self.connectedDevice = peripheral + self.bluetoothManager = bluetoothManager + self.pumpManager = pumpManager + self.completion = completion + + super.init() + + peripheral.delegate = self + } +} + +extension PeripheralManager : CBPeripheralDelegate { + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + guard error == nil else { + log.error("\(error!.localizedDescription)") + completion?(.failure(error: .failedToDiscoverServices(localizedError: error?.localizedDescription ?? ""))) + return + } + + let service = peripheral.services?.first(where: { $0.uuid == PeripheralManager.SERVICE_UUID }) + guard let service = service else { + let localizedError = "No Metrum service found - " + (peripheral.services?.map { $0.uuid.uuidString }.joined(separator: ", ") ?? "No services discovered") + log.error(localizedError) + completion?(.failure(error: .failedToDiscoverServices(localizedError: localizedError))) + return + } + + peripheral.discoverCharacteristics([PeripheralManager.READ_UUID, PeripheralManager.WRITE_UUID, PeripheralManager.CONFIG_UUID], for: service) + } + + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + guard error == nil else { + log.error("\(error!.localizedDescription)") + completion?(.failure(error: .failedToDiscoverCharacteristics(localizedError: error?.localizedDescription ?? ""))) + return + } + + let service = peripheral.services!.first(where: { $0.uuid == PeripheralManager.SERVICE_UUID })! + self.readCharacteristic = service.characteristics?.first(where: { $0.uuid == PeripheralManager.READ_UUID }) + self.writeCharacteristic = service.characteristics?.first(where: { $0.uuid == PeripheralManager.WRITE_UUID }) + self.configCharacteristic = service.characteristics?.first(where: { $0.uuid == PeripheralManager.CONFIG_UUID }) + + guard (self.readCharacteristic != nil), (self.writeCharacteristic != nil), (self.configCharacteristic != nil) else { + let localizedError = "Failed to discover read, write or config characteristic - " + (service.characteristics?.map { $0.uuid.uuidString }.joined(separator: ", ") ?? "No characteristics discovered") + + log.error(localizedError) + completion?(.failure(error: .failedToDiscoverCharacteristics(localizedError: localizedError))) + return + } + + // Subscribe on all characteristics with notifying abilities + service.characteristics?.forEach { characteristic in + guard characteristic.properties.contains(.notify) else { + return + } + + peripheral.setNotifyValue(true, for: characteristic) + } + + log.info("Notify enabled and ready to start auth flow!") + // TODO: Send AuthPacket + + } +} From cac0439eded8b6409f57d7b8d6e86734acea6216 Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Tue, 25 Feb 2025 22:01:58 +0100 Subject: [PATCH 2/6] fix: init peripheralManager --- MedtrumKit/PumpManager/BluetoothManager.swift | 11 ++++++++++- MedtrumKit/PumpManager/PeripheralManager.swift | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/MedtrumKit/PumpManager/BluetoothManager.swift b/MedtrumKit/PumpManager/BluetoothManager.swift index 00f2183..cedb41e 100644 --- a/MedtrumKit/PumpManager/BluetoothManager.swift +++ b/MedtrumKit/PumpManager/BluetoothManager.swift @@ -15,6 +15,8 @@ class BluetoothManager: NSObject, CBCentralManagerDelegate { var manager: CBCentralManager! let managerQueue = DispatchQueue(label: "com.nightscout.MedtrumKit.bluetoothManagerQueue", qos: .unspecified) + private var peripheralManager: PeripheralManager? + var scanCompletion: ((ScanResult) -> Void)? var connectCompletion: ((ConnectResult) -> Void)? @@ -76,7 +78,14 @@ extension BluetoothManager { } func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { - log.info("Connected to pump!") + log.info("Connected to pump: \(peripheral.name ?? "")!") + + guard let completion = connectCompletion, let pumpManager = pumpManager else { + return + } + + peripheralManager = PeripheralManager(peripheral, self, pumpManager, completion) + peripheral.discoverServices([PeripheralManager.SERVICE_UUID]) } func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { diff --git a/MedtrumKit/PumpManager/PeripheralManager.swift b/MedtrumKit/PumpManager/PeripheralManager.swift index bb1f85f..8093e2a 100644 --- a/MedtrumKit/PumpManager/PeripheralManager.swift +++ b/MedtrumKit/PumpManager/PeripheralManager.swift @@ -15,7 +15,7 @@ class PeripheralManager : NSObject { private let pumpManager: MedtrumPumpManager private var completion: ((ConnectResult) -> Void)? - private static let SERVICE_UUID = CBUUID(string: "669A9001-0008-968F-E311-6050405558B3") + public static let SERVICE_UUID = CBUUID(string: "669A9001-0008-968F-E311-6050405558B3") private static let READ_UUID = CBUUID(string: "669a9120-0008-968f-e311-6050405558b3") private var readCharacteristic: CBCharacteristic! private static let WRITE_UUID = CBUUID(string: "669a9101-0008-968f-e311-6050405558b3") From 41f494b6d00f63a9f881e199039137399d704533 Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Wed, 26 Feb 2025 21:25:46 +0100 Subject: [PATCH 3/6] wip: start authPackage --- Common/Data.swift | 22 + Common/UInt64.swift | 17 + MedtrumKit.xcodeproj/project.pbxproj | 38 +- MedtrumKit/Packets/AuthorizePacket.swift | 45 ++ MedtrumKit/Packets/BasePacket.swift | 15 + MedtrumKit/Packets/CommandType.swift | 31 + MedtrumKit/Packets/Crypto.swift | 596 ++++++++++++++++++ MedtrumKit/Packets/WritePacket.swift | 316 ++++++++++ MedtrumKit/PumpManager/BluetoothManager.swift | 9 +- MedtrumKit/PumpManager/MedtrumPumpState.swift | 7 + .../PumpManager/Models/ConnectResult.swift | 1 + .../PumpManager/Models/ScanResult.swift | 2 +- .../PumpManager/PeripheralManager.swift | 43 +- 13 files changed, 1132 insertions(+), 10 deletions(-) create mode 100644 Common/Data.swift create mode 100644 Common/UInt64.swift create mode 100644 MedtrumKit/Packets/AuthorizePacket.swift create mode 100644 MedtrumKit/Packets/BasePacket.swift create mode 100644 MedtrumKit/Packets/CommandType.swift create mode 100644 MedtrumKit/Packets/Crypto.swift create mode 100644 MedtrumKit/Packets/WritePacket.swift diff --git a/Common/Data.swift b/Common/Data.swift new file mode 100644 index 0000000..dc205b9 --- /dev/null +++ b/Common/Data.swift @@ -0,0 +1,22 @@ +// +// Data.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +extension Data { + func toUInt64() -> UInt64 { + guard self.count <= 8 else { + preconditionFailure("Cannot convert Data to UInt64, size too long") + } + + var result: UInt64 = 0 + for i in 0.. Data { + var output = Data(count: length) + for i in 0...length { + output[i] = UInt8(self << (i * 8) & 0xFF) + } + + return output + } +} diff --git a/MedtrumKit.xcodeproj/project.pbxproj b/MedtrumKit.xcodeproj/project.pbxproj index 3c19977..69cb7ff 100644 --- a/MedtrumKit.xcodeproj/project.pbxproj +++ b/MedtrumKit.xcodeproj/project.pbxproj @@ -12,6 +12,13 @@ 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390142D6E5C5200A146C1 /* ScanResult.swift */; }; 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */; }; 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */; }; + 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532412D6F93E70020F015 /* BasePacket.swift */; }; + 3E1532442D6F94E30020F015 /* AuthorizePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */; }; + 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532452D6F95A70020F015 /* CommandType.swift */; }; + 3E1532482D6F98D20020F015 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532472D6F98CF0020F015 /* Crypto.swift */; }; + 3E15324A2D6FA0080020F015 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532492D6FA0060020F015 /* Data.swift */; }; + 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324B2D6FA4250020F015 /* UInt64.swift */; }; + 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324D2D6FA7E70020F015 /* WritePacket.swift */; }; 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CFD2D67B192004B1971 /* LocalizedString.swift */; }; 3E767D032D67B319004B1971 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CE42D67AF35004B1971 /* OSLog.swift */; }; 3E767D042D67B3FC004B1971 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3E767CFB2D67B13C004B1971 /* Localizable.strings */; }; @@ -70,6 +77,13 @@ 3E1390142D6E5C5200A146C1 /* ScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResult.swift; sourceTree = ""; }; 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectResult.swift; sourceTree = ""; }; 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManager.swift; sourceTree = ""; }; + 3E1532412D6F93E70020F015 /* BasePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePacket.swift; sourceTree = ""; }; + 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizePacket.swift; sourceTree = ""; }; + 3E1532452D6F95A70020F015 /* CommandType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandType.swift; sourceTree = ""; }; + 3E1532472D6F98CF0020F015 /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; + 3E1532492D6FA0060020F015 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; + 3E15324B2D6FA4250020F015 /* UInt64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64.swift; sourceTree = ""; }; + 3E15324D2D6FA7E70020F015 /* WritePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WritePacket.swift; sourceTree = ""; }; 3E767CE22D67ADFA004B1971 /* MedtrumKitPumpManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MedtrumKitPumpManager+UI.swift"; sourceTree = ""; }; 3E767CE42D67AF35004B1971 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 3E767CE52D67B13C004B1971 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ../Localization/ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -96,7 +110,7 @@ 3E767CFA2D67B13C004B1971 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "../Localization/zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 3E767CFD2D67B192004B1971 /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; 3E767CFE2D67B1A9004B1971 /* MedtrumHUDProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumHUDProvider.swift; sourceTree = ""; }; - 3E767D052D67B4A0004B1971 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 3E767D052D67B4A0004B1971 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3E767D072D67B63B004B1971 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 3E767D432D67B790004B1971 /* MedtrumKitPlugin.loopplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MedtrumKitPlugin.loopplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 3E767D4C2D67BA71004B1971 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -151,6 +165,18 @@ path = Models; sourceTree = ""; }; + 3E1532402D6F93DD0020F015 /* Packets */ = { + isa = PBXGroup; + children = ( + 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */, + 3E15324D2D6FA7E70020F015 /* WritePacket.swift */, + 3E1532472D6F98CF0020F015 /* Crypto.swift */, + 3E1532452D6F95A70020F015 /* CommandType.swift */, + 3E1532412D6F93E70020F015 /* BasePacket.swift */, + ); + path = Packets; + sourceTree = ""; + }; 3E767CE12D67AD91004B1971 /* MedtrumKitUI */ = { isa = PBXGroup; children = ( @@ -165,6 +191,8 @@ 3E767CE32D67AF2B004B1971 /* Common */ = { isa = PBXGroup; children = ( + 3E15324B2D6FA4250020F015 /* UInt64.swift */, + 3E1532492D6FA0060020F015 /* Data.swift */, 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */, 3E767CFD2D67B192004B1971 /* LocalizedString.swift */, 3E767CE42D67AF35004B1971 /* OSLog.swift */, @@ -228,6 +256,7 @@ isa = PBXGroup; children = ( 3E767D052D67B4A0004B1971 /* Info.plist */, + 3E1532402D6F93DD0020F015 /* Packets */, B7189AE62C15A52800703DE2 /* PumpManager */, B7D3A8D32C148CC4002EE003 /* MedtrumKit.h */, B7D3A8D42C148CC4002EE003 /* MedtrumKit.docc */, @@ -445,15 +474,22 @@ 3E767D562D67BCD6004B1971 /* MedtrumKitUICoordinator.swift in Sources */, 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */, 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */, + 3E1532442D6F94E30020F015 /* AuthorizePacket.swift in Sources */, 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */, 3E767D522D67BC2E004B1971 /* MedtrumKitPumpManager+UI.swift in Sources */, 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */, + 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */, 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */, + 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */, + 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */, 3E767D032D67B319004B1971 /* OSLog.swift in Sources */, 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */, + 3E1532482D6F98D20020F015 /* Crypto.swift in Sources */, B7D3A8F12C14F0EF002EE003 /* MedtrumKitPlugin.swift in Sources */, B7189AE72C15A52800703DE2 /* MedtrumPumpManager.swift in Sources */, 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */, + 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */, + 3E15324A2D6FA0080020F015 /* Data.swift in Sources */, B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */, 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */, ); diff --git a/MedtrumKit/Packets/AuthorizePacket.swift b/MedtrumKit/Packets/AuthorizePacket.swift new file mode 100644 index 0000000..5959ee4 --- /dev/null +++ b/MedtrumKit/Packets/AuthorizePacket.swift @@ -0,0 +1,45 @@ +// +// AuthorizePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +struct AuthorizeResponse { + let deviceType: UInt8 + let swVersion: String +} + +class AuthorizePacket: MedtrumBasePacket { + typealias T = AuthorizeResponse + + let commandType: UInt8 = CommandType.AUTH_REQ + + private let role: UInt8 = 2 + private let pumpSN: Data + private let sessionToken: Data + + init(pumpSN: Data, sessionToken: Data) { + self.pumpSN = pumpSN + self.sessionToken = sessionToken + } + + func getRequestBytes() -> Data { + let key = Crypto.genKey(self.pumpSN) + + var output = Data([role]) + output.append(self.sessionToken) + output.append(key) + + return output + } + + static func parseResponse(data: Data) throws -> AuthorizeResponse { + return AuthorizeResponse( + deviceType: data[7], + swVersion: "\(data[8]).\(data[9]).\(data[10])" + ) + } +} + + diff --git a/MedtrumKit/Packets/BasePacket.swift b/MedtrumKit/Packets/BasePacket.swift new file mode 100644 index 0000000..995c535 --- /dev/null +++ b/MedtrumKit/Packets/BasePacket.swift @@ -0,0 +1,15 @@ +// +// BasePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +protocol MedtrumBasePacket { + associatedtype T + + var commandType: UInt8 { get } + + func getRequestBytes() -> Data + static func parseResponse(data: Data) throws -> T +} diff --git a/MedtrumKit/Packets/CommandType.swift b/MedtrumKit/Packets/CommandType.swift new file mode 100644 index 0000000..7bf809d --- /dev/null +++ b/MedtrumKit/Packets/CommandType.swift @@ -0,0 +1,31 @@ +// +// OpCodes.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +struct CommandType { + static let SYNCHRONIZE: UInt8 = 3 + static let SUBSCRIBE: UInt8 = 4 + static let AUTH_REQ: UInt8 = 5 + static let GET_DEVICE_TYPE: UInt8 = 6 + static let SET_TIME: UInt8 = 10 + static let GET_TIME: UInt8 = 11 + static let SET_TIME_ZONE: UInt8 = 12 + static let PRIME: UInt8 = 16 + static let ACTIVATE: UInt8 = 18 + static let SET_BOLUS: UInt8 = 19 + static let CANCEL_BOLUS: UInt8 = 20 + static let SET_BASAL_PROFILE: UInt8 = 21 + static let SET_TEMP_BASAL: UInt8 = 24 + static let CANCEL_TEMP_BASAL: UInt8 = 25 + static let RESUME_PUMP: UInt8 = 29 + static let POLL_PATCH: UInt8 = 30 + static let STOP_PATCH: UInt8 = 31 + static let READ_BOLUS_STATE: UInt8 = 34 + static let SET_PATCH: UInt8 = 35 + static let SET_BOLUS_MOTOR: UInt8 = 36 + static let GET_RECORD: UInt8 = 99 + static let CLEAR_ALARM: UInt8 = 115 +} diff --git a/MedtrumKit/Packets/Crypto.swift b/MedtrumKit/Packets/Crypto.swift new file mode 100644 index 0000000..fcd3a7a --- /dev/null +++ b/MedtrumKit/Packets/Crypto.swift @@ -0,0 +1,596 @@ +// +// Crypto.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +class Crypto { + private static let MEDTRUM_CIPHER: UInt64 = 1344751489 + + static func genKey(_ pumpSN: Data) -> Data { + let sn = pumpSN.toUInt64() + let key = randomGen(randomGen(MEDTRUM_CIPHER ^ sn)) + + return Data() + } + + static func genSessionToken() -> Data { + var bytes = [UInt8](repeating: 0, count: 4) + let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + + guard status == 0 else { + return Data() + } + return Data(bytes) + } + + static func simpleDecrypt(_ input: UInt64) -> UInt64 { + var temp = input + for i in 0..<32 { + temp = rotateRight(changeByTable(temp, RIJNDEAL_S_BOX), 32, 1) + } + + return temp ^ MEDTRUM_CIPHER + } + + private static func randomGen(_ input: UInt64) -> UInt64 { + let a: UInt64 = 16807 + let q: UInt64 = 127773 + let r: UInt64 = 2836 + + let tmp1 = input / q + var ret = (input - (tmp1 * q)) * a - (tmp1 * r) + if (ret < 0) { + ret &+= 2147483647 + } + + return ret + } + + private static func simpleCrypt(_ input: UInt64) -> UInt64 { + var temp = input ^ MEDTRUM_CIPHER + for i in 0..<32 { + temp = changeByTable(rotateLeft(temp, 32, 1), RIJNDEAL_S_BOX) + } + return temp + } + + private static func rotateLeft(_ x: UInt64, _ s: UInt8, _ n: UInt8) -> UInt64 { + return (x << n) | (x >> (s - n)) + } + + private static func rotateRight(_ x: UInt64, _ s: UInt8, _ n: UInt8) -> UInt64 { + return UInt64(x >> n | (x << (s - n))) + } + + private static func changeByTable(_ input: UInt64, _ tableData: [UInt8]) -> UInt64 { + let value = input.toData(length: 4) + var result = Data(count: 4) + + for i in 0..<4 { + result[i] = tableData[Int(value[i])] + } + + return result.toUInt64() + } + + + private static let RIJNDEAL_S_BOX: [UInt8] = [ + 99, + 124, + 119, + 123, + 242, + 107, + 111, + 197, + 48, + 1, + 103, + 43, + 254, + 215, + 171, + 118, + 202, + 130, + 201, + 125, + 250, + 89, + 71, + 240, + 173, + 212, + 162, + 175, + 156, + 164, + 114, + 192, + 183, + 253, + 147, + 38, + 54, + 63, + 247, + 204, + 52, + 165, + 229, + 241, + 113, + 216, + 49, + 21, + 4, + 199, + 35, + 195, + 24, + 150, + 5, + 154, + 7, + 18, + 128, + 226, + 235, + 39, + 178, + 117, + 9, + 131, + 44, + 26, + 27, + 110, + 90, + 160, + 82, + 59, + 214, + 179, + 41, + 227, + 47, + 132, + 83, + 209, + 0, + 237, + 32, + 252, + 177, + 91, + 106, + 203, + 190, + 57, + 74, + 76, + 88, + 207, + 208, + 239, + 170, + 251, + 67, + 77, + 51, + 133, + 69, + 249, + 2, + 127, + 80, + 60, + 159, + 168, + 81, + 163, + 64, + 143, + 146, + 157, + 56, + 245, + 188, + 182, + 218, + 33, + 16, + 255, + 243, + 210, + 205, + 12, + 19, + 236, + 95, + 151, + 68, + 23, + 196, + 167, + 126, + 61, + 100, + 93, + 25, + 115, + 96, + 129, + 79, + 220, + 34, + 42, + 144, + 136, + 70, + 238, + 184, + 20, + 222, + 94, + 11, + 219, + 224, + 50, + 58, + 10, + 73, + 6, + 36, + 92, + 194, + 211, + 172, + 98, + 145, + 149, + 228, + 121, + 231, + 200, + 55, + 109, + 141, + 213, + 78, + 169, + 108, + 86, + 244, + 234, + 101, + 122, + 174, + 8, + 186, + 120, + 37, + 46, + 28, + 166, + 180, + 198, + 232, + 221, + 116, + 31, + 75, + 189, + 139, + 138, + 112, + 62, + 181, + 102, + 72, + 3, + 246, + 14, + 97, + 53, + 87, + 185, + 134, + 193, + 29, + 158, + 225, + 248, + 152, + 17, + 105, + 217, + 142, + 148, + 155, + 30, + 135, + 233, + 206, + 85, + 40, + 223, + 140, + 161, + 137, + 13, + 191, + 230, + 66, + 104, + 65, + 153, + 45, + 15, + 176, + 84, + 187, + 22 + ] + + private static let RIJNDEAL_INVERSE_S_BOX: [UInt8] = [ + 82, + 9, + 106, + 213, + 48, + 54, + 165, + 56, + 191, + 64, + 163, + 158, + 129, + 243, + 215, + 251, + 124, + 227, + 57, + 130, + 155, + 47, + 255, + 135, + 52, + 142, + 67, + 68, + 196, + 222, + 233, + 203, + 84, + 123, + 148, + 50, + 166, + 194, + 35, + 61, + 238, + 76, + 149, + 11, + 66, + 250, + 195, + 78, + 8, + 46, + 161, + 102, + 40, + 217, + 36, + 178, + 118, + 91, + 162, + 73, + 109, + 139, + 209, + 37, + 114, + 248, + 246, + 100, + 134, + 104, + 152, + 22, + 212, + 164, + 92, + 204, + 93, + 101, + 182, + 146, + 108, + 112, + 72, + 80, + 253, + 237, + 185, + 218, + 94, + 21, + 70, + 87, + 167, + 141, + 157, + 132, + 144, + 216, + 171, + 0, + 140, + 188, + 211, + 10, + 247, + 228, + 88, + 5, + 184, + 179, + 69, + 6, + 208, + 44, + 30, + 143, + 202, + 63, + 15, + 2, + 193, + 175, + 189, + 3, + 1, + 19, + 138, + 107, + 58, + 145, + 17, + 65, + 79, + 103, + 220, + 234, + 151, + 242, + 207, + 206, + 240, + 180, + 230, + 115, + 150, + 172, + 116, + 34, + 231, + 173, + 53, + 133, + 226, + 249, + 55, + 232, + 28, + 117, + 223, + 110, + 71, + 241, + 26, + 113, + 29, + 41, + 197, + 137, + 111, + 183, + 98, + 14, + 170, + 24, + 190, + 27, + 252, + 86, + 62, + 75, + 198, + 210, + 121, + 32, + 154, + 219, + 192, + 254, + 120, + 205, + 90, + 244, + 31, + 221, + 168, + 51, + 136, + 7, + 199, + 49, + 177, + 18, + 16, + 89, + 39, + 128, + 236, + 95, + 96, + 81, + 127, + 169, + 25, + 181, + 74, + 13, + 45, + 229, + 122, + 159, + 147, + 201, + 156, + 239, + 160, + 224, + 59, + 77, + 174, + 42, + 245, + 176, + 200, + 235, + 187, + 60, + 131, + 83, + 153, + 97, + 23, + 43, + 4, + 126, + 186, + 119, + 214, + 38, + 225, + 105, + 20, + 99, + 85, + 33, + 12, + 125 + ] +} diff --git a/MedtrumKit/Packets/WritePacket.swift b/MedtrumKit/Packets/WritePacket.swift new file mode 100644 index 0000000..011a229 --- /dev/null +++ b/MedtrumKit/Packets/WritePacket.swift @@ -0,0 +1,316 @@ +// +// WritePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 26/02/2025. +// + +class WritePacket { + static func encode(_ data: any MedtrumBasePacket, sequenceNumber: UInt8) -> [Data] { + let content = data.getRequestBytes() + var header = Data([ + UInt8(content.count + 4), + data.commandType, + sequenceNumber, + 0, // pkgIndex + ]) + + let tmp = header + content + let totalCommand = tmp + calcCrc8(tmp) + + if (totalCommand.count - header.count) <= 15 { + return [totalCommand] + } + + // We need to split up the command in multiple packages + var packages: [Data] = [] + + var pkgIndex: UInt8 = 1 + var remainingCommand = totalCommand[4...] + + while remainingCommand.count > 15 { + header[3] = pkgIndex + + let tmp2 = header + remainingCommand[0..<15] + packages.append(tmp2 + calcCrc8(tmp2)) + + remainingCommand = remainingCommand[15...] + pkgIndex = UInt8(pkgIndex + 1) + } + + header[3] = pkgIndex + let tmp3 = header + remainingCommand + + packages.append(tmp + calcCrc8(tmp3)) + return packages + } + + private static func calcCrc8(_ data: Data) -> Data { + var crc8: UInt8 = 0 + for i in 0..= 5 else { + log.warning("No ManufacturerData or too short - " + advertisementData.keys.joined(separator: ", ")) + return + } + + scanCompletion?(.success(peripheral: peripheral, pumpSN: manufacturerData[0...4], deviceType: manufacturerData[4], version: manufacturerData[5])) } func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { diff --git a/MedtrumKit/PumpManager/MedtrumPumpState.swift b/MedtrumKit/PumpManager/MedtrumPumpState.swift index 003632e..df90647 100644 --- a/MedtrumKit/PumpManager/MedtrumPumpState.swift +++ b/MedtrumKit/PumpManager/MedtrumPumpState.swift @@ -10,12 +10,19 @@ class MedtrumPumpState: RawRepresentable { public typealias RawValue = PumpManager.RawStateValue required public init(rawValue: RawValue) { + pumpSN = rawValue["pumpSN"] as? Data ?? Data() + sessionToken = rawValue["sessionToken"] as? Data ?? Data() } public var rawValue: RawValue { var value: [String: Any] = [:] + value["pumpSN"] = pumpSN + value["sessionToken"] = sessionToken + return value } + public var pumpSN: Data + public var sessionToken: Data } diff --git a/MedtrumKit/PumpManager/Models/ConnectResult.swift b/MedtrumKit/PumpManager/Models/ConnectResult.swift index 75d0c78..f31b67c 100644 --- a/MedtrumKit/PumpManager/Models/ConnectResult.swift +++ b/MedtrumKit/PumpManager/Models/ConnectResult.swift @@ -13,4 +13,5 @@ enum ConnectResult { enum ConnectError { case failedToDiscoverServices(localizedError: String) case failedToDiscoverCharacteristics(localizedError: String) + case failedToEnableNotify(localizedError: String) } diff --git a/MedtrumKit/PumpManager/Models/ScanResult.swift b/MedtrumKit/PumpManager/Models/ScanResult.swift index 9af0a8b..ad31899 100644 --- a/MedtrumKit/PumpManager/Models/ScanResult.swift +++ b/MedtrumKit/PumpManager/Models/ScanResult.swift @@ -9,7 +9,7 @@ import CoreBluetooth enum ScanResult { case failure(error: ScanError) - case success(peripheral: CBPeripheral) + case success(peripheral: CBPeripheral, pumpSN: Data, deviceType: UInt8, version: UInt8) } enum ScanError { diff --git a/MedtrumKit/PumpManager/PeripheralManager.swift b/MedtrumKit/PumpManager/PeripheralManager.swift index 8093e2a..03d954f 100644 --- a/MedtrumKit/PumpManager/PeripheralManager.swift +++ b/MedtrumKit/PumpManager/PeripheralManager.swift @@ -23,6 +23,8 @@ class PeripheralManager : NSObject { private static let CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") private var configCharacteristic: CBCharacteristic! + private var writeSequence: UInt8 = 0 + public init(_ peripheral: CBPeripheral, _ bluetoothManager: BluetoothManager, _ pumpManager: MedtrumPumpManager,_ completion: @escaping (ConnectResult) -> Void) { self.connectedDevice = peripheral self.bluetoothManager = bluetoothManager @@ -33,13 +35,25 @@ class PeripheralManager : NSObject { peripheral.delegate = self } + + func writePacket(_ packet: any MedtrumBasePacket) { + let packages = WritePacket.encode(packet, sequenceNumber: self.writeSequence) + self.writeSequence = UInt8(self.writeSequence + 1) + + for package in packages { + self.connectedDevice.writeValue(package, for: self.writeCharacteristic, type: .withResponse) + } + + + // TODO: Add async flow to wait for response + } } extension PeripheralManager : CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - guard error == nil else { - log.error("\(error!.localizedDescription)") - completion?(.failure(error: .failedToDiscoverServices(localizedError: error?.localizedDescription ?? ""))) + if let error = error { + log.error("\(error.localizedDescription)") + completion?(.failure(error: .failedToDiscoverServices(localizedError: error.localizedDescription))) return } @@ -55,9 +69,9 @@ extension PeripheralManager : CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - guard error == nil else { - log.error("\(error!.localizedDescription)") - completion?(.failure(error: .failedToDiscoverCharacteristics(localizedError: error?.localizedDescription ?? ""))) + if let error = error { + log.error("\(error.localizedDescription)") + completion?(.failure(error: .failedToDiscoverCharacteristics(localizedError: error.localizedDescription))) return } @@ -84,7 +98,22 @@ extension PeripheralManager : CBPeripheralDelegate { } log.info("Notify enabled and ready to start auth flow!") - // TODO: Send AuthPacket + writePacket(AuthorizePacket(pumpSN: self.pumpManager.state.pumpSN, sessionToken: self.pumpManager.state.sessionToken)) + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + if let error = error { + log.error("\(error.localizedDescription)") + if let connectCompletion = self.completion { + connectCompletion(.failure(error: .failedToEnableNotify(localizedError: error.localizedDescription))) + } + return + } + + guard let data = characteristic.value else { + return + } + // TODO: Process message } } From d6479874d2f46e5799b22e4cc093b6bd39e8bbe2 Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Thu, 27 Feb 2025 21:38:14 +0100 Subject: [PATCH 4/6] wip: completed auth/connect flow --- Common/Date.swift | 19 ++ MedtrumKit.xcodeproj/project.pbxproj | 68 ++++- .../Crc8.swift} | 47 +--- .../{Packets => Encryption}/Crypto.swift | 0 MedtrumKit/Encryption/ReadPacket.swift | 46 +++ MedtrumKit/Encryption/WritePacket.swift | 48 ++++ MedtrumKit/Packets/AuthorizePacket.swift | 2 +- MedtrumKit/Packets/BasePacket.swift | 2 +- MedtrumKit/Packets/GetDeviceTypePacket.swift | 27 ++ MedtrumKit/Packets/GetTimePacket.swift | 27 ++ MedtrumKit/Packets/SetTimePacket.swift | 25 ++ MedtrumKit/Packets/SetTimeZonePacket.swift | 30 ++ MedtrumKit/Packets/SubscribePacket.swift | 22 ++ MedtrumKit/Packets/SynchronizePacket.swift | 61 ++++ MedtrumKit/PumpManager/BluetoothManager.swift | 8 +- MedtrumKit/PumpManager/MedtrumPumpState.swift | 33 +++ ...esult.swift => MedtrumConnectResult.swift} | 7 +- ...anResult.swift => MedtrumScanResult.swift} | 6 +- .../Models/MedtrumWriteResult.swift | 22 ++ .../PumpManager/PeripheralManager.swift | 264 +++++++++++++++++- 20 files changed, 686 insertions(+), 78 deletions(-) create mode 100644 Common/Date.swift rename MedtrumKit/{Packets/WritePacket.swift => Encryption/Crc8.swift} (72%) rename MedtrumKit/{Packets => Encryption}/Crypto.swift (100%) create mode 100644 MedtrumKit/Encryption/ReadPacket.swift create mode 100644 MedtrumKit/Encryption/WritePacket.swift create mode 100644 MedtrumKit/Packets/GetDeviceTypePacket.swift create mode 100644 MedtrumKit/Packets/GetTimePacket.swift create mode 100644 MedtrumKit/Packets/SetTimePacket.swift create mode 100644 MedtrumKit/Packets/SetTimeZonePacket.swift create mode 100644 MedtrumKit/Packets/SubscribePacket.swift create mode 100644 MedtrumKit/Packets/SynchronizePacket.swift rename MedtrumKit/PumpManager/Models/{ConnectResult.swift => MedtrumConnectResult.swift} (63%) rename MedtrumKit/PumpManager/Models/{ScanResult.swift => MedtrumScanResult.swift} (76%) create mode 100644 MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift diff --git a/Common/Date.swift b/Common/Date.swift new file mode 100644 index 0000000..aa75514 --- /dev/null +++ b/Common/Date.swift @@ -0,0 +1,19 @@ +// +// Date.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +let baseUnix: TimeInterval = .seconds(1388530800) //2014-01-01T00:00:00+0000 + +extension Date { + static func fromMedtrumSeconds(_ seconds: UInt64) -> Date { + return Date(timeIntervalSince1970: baseUnix + Double(seconds)) + } + + static func toMedtrumSeconds() -> Data { + let data = UInt64(Date.now.timeIntervalSince1970 - baseUnix) + return data.toData(length: 4) + } +} diff --git a/MedtrumKit.xcodeproj/project.pbxproj b/MedtrumKit.xcodeproj/project.pbxproj index 69cb7ff..1cab5a3 100644 --- a/MedtrumKit.xcodeproj/project.pbxproj +++ b/MedtrumKit.xcodeproj/project.pbxproj @@ -9,8 +9,8 @@ /* Begin PBXBuildFile section */ 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */; }; 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */; }; - 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390142D6E5C5200A146C1 /* ScanResult.swift */; }; - 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */; }; + 3E1390152D6E5C5E00A146C1 /* MedtrumScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */; }; + 3E1390172D6E5EA000A146C1 /* MedtrumConnectResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390162D6E5E9A00A146C1 /* MedtrumConnectResult.swift */; }; 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */; }; 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532412D6F93E70020F015 /* BasePacket.swift */; }; 3E1532442D6F94E30020F015 /* AuthorizePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */; }; @@ -19,6 +19,16 @@ 3E15324A2D6FA0080020F015 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532492D6FA0060020F015 /* Data.swift */; }; 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324B2D6FA4250020F015 /* UInt64.swift */; }; 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324D2D6FA7E70020F015 /* WritePacket.swift */; }; + 3E5060142D70E5CB00376609 /* Crc8.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060132D70E5C900376609 /* Crc8.swift */; }; + 3E5060162D70E62E00376609 /* ReadPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060152D70E62B00376609 /* ReadPacket.swift */; }; + 3E5060182D70EAF800376609 /* MedtrumWriteResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */; }; + 3E50601A2D70F2F500376609 /* GetDeviceTypePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */; }; + 3E50601C2D70F69900376609 /* GetTimePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E50601B2D70F69800376609 /* GetTimePacket.swift */; }; + 3E50601E2D70F7A900376609 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E50601D2D70F7A700376609 /* Date.swift */; }; + 3E5060202D70F97500376609 /* SetTimePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E50601F2D70F97500376609 /* SetTimePacket.swift */; }; + 3E5060222D70FDA100376609 /* SetTimeZonePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060212D70FDA000376609 /* SetTimeZonePacket.swift */; }; + 3E5060242D70FED500376609 /* SynchronizePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060232D70FED500376609 /* SynchronizePacket.swift */; }; + 3E5060262D71035900376609 /* SubscribePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060252D71035900376609 /* SubscribePacket.swift */; }; 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CFD2D67B192004B1971 /* LocalizedString.swift */; }; 3E767D032D67B319004B1971 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CE42D67AF35004B1971 /* OSLog.swift */; }; 3E767D042D67B3FC004B1971 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3E767CFB2D67B13C004B1971 /* Localizable.strings */; }; @@ -74,8 +84,8 @@ /* Begin PBXFileReference section */ 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothManager.swift; sourceTree = ""; }; 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumPumpState.swift; sourceTree = ""; }; - 3E1390142D6E5C5200A146C1 /* ScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResult.swift; sourceTree = ""; }; - 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectResult.swift; sourceTree = ""; }; + 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumScanResult.swift; sourceTree = ""; }; + 3E1390162D6E5E9A00A146C1 /* MedtrumConnectResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumConnectResult.swift; sourceTree = ""; }; 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManager.swift; sourceTree = ""; }; 3E1532412D6F93E70020F015 /* BasePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePacket.swift; sourceTree = ""; }; 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizePacket.swift; sourceTree = ""; }; @@ -84,6 +94,16 @@ 3E1532492D6FA0060020F015 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 3E15324B2D6FA4250020F015 /* UInt64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64.swift; sourceTree = ""; }; 3E15324D2D6FA7E70020F015 /* WritePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WritePacket.swift; sourceTree = ""; }; + 3E5060132D70E5C900376609 /* Crc8.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crc8.swift; sourceTree = ""; }; + 3E5060152D70E62B00376609 /* ReadPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadPacket.swift; sourceTree = ""; }; + 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumWriteResult.swift; sourceTree = ""; }; + 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetDeviceTypePacket.swift; sourceTree = ""; }; + 3E50601B2D70F69800376609 /* GetTimePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTimePacket.swift; sourceTree = ""; }; + 3E50601D2D70F7A700376609 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 3E50601F2D70F97500376609 /* SetTimePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTimePacket.swift; sourceTree = ""; }; + 3E5060212D70FDA000376609 /* SetTimeZonePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTimeZonePacket.swift; sourceTree = ""; }; + 3E5060232D70FED500376609 /* SynchronizePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizePacket.swift; sourceTree = ""; }; + 3E5060252D71035900376609 /* SubscribePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribePacket.swift; sourceTree = ""; }; 3E767CE22D67ADFA004B1971 /* MedtrumKitPumpManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MedtrumKitPumpManager+UI.swift"; sourceTree = ""; }; 3E767CE42D67AF35004B1971 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 3E767CE52D67B13C004B1971 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ../Localization/ar.lproj/Localizable.strings; sourceTree = ""; }; @@ -159,8 +179,9 @@ 3E1390132D6E5C4900A146C1 /* Models */ = { isa = PBXGroup; children = ( - 3E1390162D6E5E9A00A146C1 /* ConnectResult.swift */, - 3E1390142D6E5C5200A146C1 /* ScanResult.swift */, + 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */, + 3E1390162D6E5E9A00A146C1 /* MedtrumConnectResult.swift */, + 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */, ); path = Models; sourceTree = ""; @@ -168,15 +189,30 @@ 3E1532402D6F93DD0020F015 /* Packets */ = { isa = PBXGroup; children = ( + 3E5060252D71035900376609 /* SubscribePacket.swift */, + 3E5060232D70FED500376609 /* SynchronizePacket.swift */, + 3E5060212D70FDA000376609 /* SetTimeZonePacket.swift */, + 3E50601F2D70F97500376609 /* SetTimePacket.swift */, + 3E50601B2D70F69800376609 /* GetTimePacket.swift */, + 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */, 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */, - 3E15324D2D6FA7E70020F015 /* WritePacket.swift */, - 3E1532472D6F98CF0020F015 /* Crypto.swift */, 3E1532452D6F95A70020F015 /* CommandType.swift */, 3E1532412D6F93E70020F015 /* BasePacket.swift */, ); path = Packets; sourceTree = ""; }; + 3E5060122D70E5B000376609 /* Encryption */ = { + isa = PBXGroup; + children = ( + 3E5060152D70E62B00376609 /* ReadPacket.swift */, + 3E15324D2D6FA7E70020F015 /* WritePacket.swift */, + 3E5060132D70E5C900376609 /* Crc8.swift */, + 3E1532472D6F98CF0020F015 /* Crypto.swift */, + ); + path = Encryption; + sourceTree = ""; + }; 3E767CE12D67AD91004B1971 /* MedtrumKitUI */ = { isa = PBXGroup; children = ( @@ -191,6 +227,7 @@ 3E767CE32D67AF2B004B1971 /* Common */ = { isa = PBXGroup; children = ( + 3E50601D2D70F7A700376609 /* Date.swift */, 3E15324B2D6FA4250020F015 /* UInt64.swift */, 3E1532492D6FA0060020F015 /* Data.swift */, 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */, @@ -255,6 +292,7 @@ B7D3A8D22C148CC4002EE003 /* MedtrumKit */ = { isa = PBXGroup; children = ( + 3E5060122D70E5B000376609 /* Encryption */, 3E767D052D67B4A0004B1971 /* Info.plist */, 3E1532402D6F93DD0020F015 /* Packets */, B7189AE62C15A52800703DE2 /* PumpManager */, @@ -472,13 +510,18 @@ buildActionMask = 2147483647; files = ( 3E767D562D67BCD6004B1971 /* MedtrumKitUICoordinator.swift in Sources */, - 3E1390172D6E5EA000A146C1 /* ConnectResult.swift in Sources */, + 3E5060262D71035900376609 /* SubscribePacket.swift in Sources */, + 3E5060202D70F97500376609 /* SetTimePacket.swift in Sources */, + 3E1390172D6E5EA000A146C1 /* MedtrumConnectResult.swift in Sources */, 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */, + 3E5060142D70E5CB00376609 /* Crc8.swift in Sources */, + 3E5060222D70FDA100376609 /* SetTimeZonePacket.swift in Sources */, 3E1532442D6F94E30020F015 /* AuthorizePacket.swift in Sources */, 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */, 3E767D522D67BC2E004B1971 /* MedtrumKitPumpManager+UI.swift in Sources */, 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */, 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */, + 3E50601C2D70F69900376609 /* GetTimePacket.swift in Sources */, 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */, 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */, 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */, @@ -487,11 +530,16 @@ 3E1532482D6F98D20020F015 /* Crypto.swift in Sources */, B7D3A8F12C14F0EF002EE003 /* MedtrumKitPlugin.swift in Sources */, B7189AE72C15A52800703DE2 /* MedtrumPumpManager.swift in Sources */, + 3E5060162D70E62E00376609 /* ReadPacket.swift in Sources */, 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */, 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */, 3E15324A2D6FA0080020F015 /* Data.swift in Sources */, + 3E50601A2D70F2F500376609 /* GetDeviceTypePacket.swift in Sources */, B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */, - 3E1390152D6E5C5E00A146C1 /* ScanResult.swift in Sources */, + 3E5060242D70FED500376609 /* SynchronizePacket.swift in Sources */, + 3E5060182D70EAF800376609 /* MedtrumWriteResult.swift in Sources */, + 3E50601E2D70F7A900376609 /* Date.swift in Sources */, + 3E1390152D6E5C5E00A146C1 /* MedtrumScanResult.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MedtrumKit/Packets/WritePacket.swift b/MedtrumKit/Encryption/Crc8.swift similarity index 72% rename from MedtrumKit/Packets/WritePacket.swift rename to MedtrumKit/Encryption/Crc8.swift index 011a229..0195bdb 100644 --- a/MedtrumKit/Packets/WritePacket.swift +++ b/MedtrumKit/Encryption/Crc8.swift @@ -1,51 +1,12 @@ // -// WritePacket.swift +// Crc8.swift // MedtrumKit // -// Created by Bastiaan Verhaar on 26/02/2025. +// Created by Bastiaan Verhaar on 27/02/2025. // -class WritePacket { - static func encode(_ data: any MedtrumBasePacket, sequenceNumber: UInt8) -> [Data] { - let content = data.getRequestBytes() - var header = Data([ - UInt8(content.count + 4), - data.commandType, - sequenceNumber, - 0, // pkgIndex - ]) - - let tmp = header + content - let totalCommand = tmp + calcCrc8(tmp) - - if (totalCommand.count - header.count) <= 15 { - return [totalCommand] - } - - // We need to split up the command in multiple packages - var packages: [Data] = [] - - var pkgIndex: UInt8 = 1 - var remainingCommand = totalCommand[4...] - - while remainingCommand.count > 15 { - header[3] = pkgIndex - - let tmp2 = header + remainingCommand[0..<15] - packages.append(tmp2 + calcCrc8(tmp2)) - - remainingCommand = remainingCommand[15...] - pkgIndex = UInt8(pkgIndex + 1) - } - - header[3] = pkgIndex - let tmp3 = header + remainingCommand - - packages.append(tmp + calcCrc8(tmp3)) - return packages - } - - private static func calcCrc8(_ data: Data) -> Data { +class Crc8 { + public static func calculate(_ data: Data) -> Data { var crc8: UInt8 = 0 for i in 0.. [Data] { + let content = data.getRequestBytes() + var header = Data([ + UInt8(content.count + 4), + data.commandType, + sequenceNumber, + 0, // pkgIndex + ]) + + let tmp = header + content + let totalCommand = tmp + Crc8.calculate(tmp) + + if (totalCommand.count - header.count) <= 15 { + return [totalCommand] + } + + // We need to split up the command in multiple packages + var packages: [Data] = [] + + var pkgIndex: UInt8 = 1 + var remainingCommand = totalCommand[4...] + + while remainingCommand.count > 15 { + header[3] = pkgIndex + + let tmp2 = header + remainingCommand[0..<15] + packages.append(tmp2 + Crc8.calculate(tmp2)) + + remainingCommand = remainingCommand[15...] + pkgIndex = UInt8(pkgIndex + 1) + } + + header[3] = pkgIndex + let tmp3 = header + remainingCommand + + packages.append(tmp + Crc8.calculate(tmp3)) + return packages + } + +} diff --git a/MedtrumKit/Packets/AuthorizePacket.swift b/MedtrumKit/Packets/AuthorizePacket.swift index 5959ee4..408988e 100644 --- a/MedtrumKit/Packets/AuthorizePacket.swift +++ b/MedtrumKit/Packets/AuthorizePacket.swift @@ -34,7 +34,7 @@ class AuthorizePacket: MedtrumBasePacket { return output } - static func parseResponse(data: Data) throws -> AuthorizeResponse { + static func parseResponse(data: Data) -> AuthorizeResponse { return AuthorizeResponse( deviceType: data[7], swVersion: "\(data[8]).\(data[9]).\(data[10])" diff --git a/MedtrumKit/Packets/BasePacket.swift b/MedtrumKit/Packets/BasePacket.swift index 995c535..29c5cb1 100644 --- a/MedtrumKit/Packets/BasePacket.swift +++ b/MedtrumKit/Packets/BasePacket.swift @@ -11,5 +11,5 @@ protocol MedtrumBasePacket { var commandType: UInt8 { get } func getRequestBytes() -> Data - static func parseResponse(data: Data) throws -> T + static func parseResponse(data: Data) -> T } diff --git a/MedtrumKit/Packets/GetDeviceTypePacket.swift b/MedtrumKit/Packets/GetDeviceTypePacket.swift new file mode 100644 index 0000000..d786406 --- /dev/null +++ b/MedtrumKit/Packets/GetDeviceTypePacket.swift @@ -0,0 +1,27 @@ +// +// GetDeviceTypePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct GetDeviceTypeResponse { + let deviceType: UInt8 + let deviceSN: Data +} + +class GetDeviceTypePacket: MedtrumBasePacket { + typealias T = GetDeviceTypeResponse + let commandType: UInt8 = CommandType.GET_DEVICE_TYPE + + func getRequestBytes() -> Data { + return Data() + } + + static func parseResponse(data: Data) -> GetDeviceTypeResponse { + return GetDeviceTypeResponse( + deviceType: data[6], + deviceSN: data[7..<11] + ) + } +} diff --git a/MedtrumKit/Packets/GetTimePacket.swift b/MedtrumKit/Packets/GetTimePacket.swift new file mode 100644 index 0000000..fe05095 --- /dev/null +++ b/MedtrumKit/Packets/GetTimePacket.swift @@ -0,0 +1,27 @@ +// +// GetTimePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct GetTimePacketResponse { + let time: Date +} + +class GetTimePacket : MedtrumBasePacket { + typealias T = GetTimePacketResponse + + let commandType: UInt8 = CommandType.GET_TIME + + func getRequestBytes() -> Data { + return Data() + } + + static func parseResponse(data: Data) -> GetTimePacketResponse { + let secondsPassed = data[6..<10].toUInt64() + return GetTimePacketResponse( + time: Date.fromMedtrumSeconds(secondsPassed) + ) + } +} diff --git a/MedtrumKit/Packets/SetTimePacket.swift b/MedtrumKit/Packets/SetTimePacket.swift new file mode 100644 index 0000000..b19e878 --- /dev/null +++ b/MedtrumKit/Packets/SetTimePacket.swift @@ -0,0 +1,25 @@ +// +// SetTimePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct SetTimePacketResponse {} + +class SetTimePacket : MedtrumBasePacket { + typealias T = SetTimePacketResponse + + let commandType: UInt8 = CommandType.SET_TIME + + func getRequestBytes() -> Data { + var output = Data([2]) + output.append(Date.toMedtrumSeconds()) + + return output + } + + static func parseResponse(data: Data) -> SetTimePacketResponse { + return SetTimePacketResponse() + } +} diff --git a/MedtrumKit/Packets/SetTimeZonePacket.swift b/MedtrumKit/Packets/SetTimeZonePacket.swift new file mode 100644 index 0000000..3162fbd --- /dev/null +++ b/MedtrumKit/Packets/SetTimeZonePacket.swift @@ -0,0 +1,30 @@ +// +// SetTimeZonePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct SetTimeZonePacketResponse {} + +class SetTimeZonePacket : MedtrumBasePacket { + typealias T = SetTimeZonePacketResponse + + let commandType: UInt8 = CommandType.SET_TIME_ZONE + + func getRequestBytes() -> Data { + var offsetInSeconds = TimeZone.current.secondsFromGMT(for: Date.now) + if offsetInSeconds < 0 { + offsetInSeconds += 65536 + } + + let offsetData = UInt64(offsetInSeconds).toData(length: 2) + let timeData = Date.toMedtrumSeconds() + + return offsetData + timeData + } + + static func parseResponse(data: Data) -> SetTimeZonePacketResponse { + return SetTimeZonePacketResponse() + } +} diff --git a/MedtrumKit/Packets/SubscribePacket.swift b/MedtrumKit/Packets/SubscribePacket.swift new file mode 100644 index 0000000..d4fdd6d --- /dev/null +++ b/MedtrumKit/Packets/SubscribePacket.swift @@ -0,0 +1,22 @@ +// +// SubscribePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct SubscribePacketResponse {} + +class SubscribePacket : MedtrumBasePacket { + typealias T = SubscribePacketResponse + + let commandType: UInt8 = CommandType.SUBSCRIBE + + func getRequestBytes() -> Data { + return UInt64(4095).toData(length: 2) + } + + static func parseResponse(data: Data) -> SubscribePacketResponse { + return SubscribePacketResponse() + } +} diff --git a/MedtrumKit/Packets/SynchronizePacket.swift b/MedtrumKit/Packets/SynchronizePacket.swift new file mode 100644 index 0000000..e8be60c --- /dev/null +++ b/MedtrumKit/Packets/SynchronizePacket.swift @@ -0,0 +1,61 @@ +// +// SynchronizePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +struct SynchronizePacketResponse { + let state: MedtrumState + let fieldMask: UInt16 + let syncData: Data +} + +class SynchronizePacket : MedtrumBasePacket { + typealias T = SynchronizePacketResponse + + let commandType: UInt8 = CommandType.SYNCHRONIZE + + func getRequestBytes() -> Data { + return Data() + } + + static func parseResponse(data: Data) -> SynchronizePacketResponse { + let fieldMask = data[7..<9] + let syncData = data[9...] + + return SynchronizePacketResponse( + state: MedtrumState(rawValue: data[6]) ?? .none, + fieldMask: UInt16((fieldMask[0] << 8) | fieldMask[1]), + syncData: syncData + ) + } + +} + +enum MedtrumState: UInt8 { + case none = 0 + case idle = 1 + case filled = 2 + case priming = 3 + case ejecting = 5 + case ejected = 6 + case active = 32 + case active_alt = 33 + case lowBgSuspended = 64 + case lowBgSuspended2 = 65 + case autoSuspended = 66 + case hourlyMaxSuspended = 67 + case dailyMaxSuspended = 68 + case suspended = 69 + case paused = 70 + case occlusion = 96 + case expired = 97 + case reservoirEmpty = 98 + case patchFault = 99 + case patchFaultd2 = 100 + case baseFault = 101 + case batteryOut = 102 + case noCalibration = 103 + case stopped = 128 +} diff --git a/MedtrumKit/PumpManager/BluetoothManager.swift b/MedtrumKit/PumpManager/BluetoothManager.swift index 5aa157a..3dbd3b8 100644 --- a/MedtrumKit/PumpManager/BluetoothManager.swift +++ b/MedtrumKit/PumpManager/BluetoothManager.swift @@ -17,8 +17,8 @@ class BluetoothManager: NSObject, CBCentralManagerDelegate { private var peripheralManager: PeripheralManager? - var scanCompletion: ((ScanResult) -> Void)? - var connectCompletion: ((ConnectResult) -> Void)? + var scanCompletion: ((MedtrumScanResult) -> Void)? + var connectCompletion: ((MedtrumConnectResult) -> Void)? override init() { super.init() @@ -28,7 +28,7 @@ class BluetoothManager: NSObject, CBCentralManagerDelegate { } } - func startScan(_ completion: @escaping (_ result: ScanResult) -> Void) { + func startScan(_ completion: @escaping (_ result: MedtrumScanResult) -> Void) { guard manager.state == .poweredOn else { completion(.failure(error: .invalidBluetoothState(state: manager.state))) return @@ -45,7 +45,7 @@ class BluetoothManager: NSObject, CBCentralManagerDelegate { log.info("Started scanning") } - func connect(peripheral: CBPeripheral, _ completion: @escaping (ConnectResult) -> Void) { + func connect(peripheral: CBPeripheral, _ completion: @escaping (MedtrumConnectResult) -> Void) { if manager.isScanning { manager.stopScan() scanCompletion = nil diff --git a/MedtrumKit/PumpManager/MedtrumPumpState.swift b/MedtrumKit/PumpManager/MedtrumPumpState.swift index df90647..fa98e77 100644 --- a/MedtrumKit/PumpManager/MedtrumPumpState.swift +++ b/MedtrumKit/PumpManager/MedtrumPumpState.swift @@ -12,6 +12,26 @@ class MedtrumPumpState: RawRepresentable { required public init(rawValue: RawValue) { pumpSN = rawValue["pumpSN"] as? Data ?? Data() sessionToken = rawValue["sessionToken"] as? Data ?? Data() + deviceType = rawValue["deviceType"] as? UInt8 ?? 0 + swVersion = rawValue["swVersion"] as? String ?? "0.0.0" + pumpTime = rawValue["pumpTime"] as? Date ?? Date() + pumpTimeSyncedAt = rawValue["pumpTimeSyncedAt"] as? Date ?? Date() + + if let pumpStateRaw = rawValue["pumpState"] as? MedtrumState.RawValue { + pumpState = MedtrumState(rawValue: pumpStateRaw) ?? .none + } else { + pumpState = .none + } + } + + public init() { + pumpSN = Data() + sessionToken = Data() + deviceType = 0 + swVersion = "0.0.0" + pumpTime = Date() + pumpTimeSyncedAt = Date() + pumpState = .none } public var rawValue: RawValue { @@ -19,10 +39,23 @@ class MedtrumPumpState: RawRepresentable { value["pumpSN"] = pumpSN value["sessionToken"] = sessionToken + value["deviceType"] = deviceType + value["swVersion"] = swVersion + value["pumpTime"] = pumpTime + value["pumpTimeSyncedAt"] = pumpTimeSyncedAt + value["pumpState"] = pumpState.rawValue return value } public var pumpSN: Data public var sessionToken: Data + + public var deviceType: UInt8 + public var swVersion: String + + public var pumpTime: Date + public var pumpTimeSyncedAt: Date + + public var pumpState: MedtrumState } diff --git a/MedtrumKit/PumpManager/Models/ConnectResult.swift b/MedtrumKit/PumpManager/Models/MedtrumConnectResult.swift similarity index 63% rename from MedtrumKit/PumpManager/Models/ConnectResult.swift rename to MedtrumKit/PumpManager/Models/MedtrumConnectResult.swift index f31b67c..c77cc94 100644 --- a/MedtrumKit/PumpManager/Models/ConnectResult.swift +++ b/MedtrumKit/PumpManager/Models/MedtrumConnectResult.swift @@ -5,13 +5,14 @@ // Created by Bastiaan Verhaar on 25/02/2025. // -enum ConnectResult { +enum MedtrumConnectResult { case success - case failure(error: ConnectError) + case failure(error: MedtrumConnectError) } -enum ConnectError { +enum MedtrumConnectError { case failedToDiscoverServices(localizedError: String) case failedToDiscoverCharacteristics(localizedError: String) case failedToEnableNotify(localizedError: String) + case failedToCompleteAuthorizationFlow(localizedError: String) } diff --git a/MedtrumKit/PumpManager/Models/ScanResult.swift b/MedtrumKit/PumpManager/Models/MedtrumScanResult.swift similarity index 76% rename from MedtrumKit/PumpManager/Models/ScanResult.swift rename to MedtrumKit/PumpManager/Models/MedtrumScanResult.swift index ad31899..0669f92 100644 --- a/MedtrumKit/PumpManager/Models/ScanResult.swift +++ b/MedtrumKit/PumpManager/Models/MedtrumScanResult.swift @@ -7,12 +7,12 @@ import CoreBluetooth -enum ScanResult { - case failure(error: ScanError) +enum MedtrumScanResult { case success(peripheral: CBPeripheral, pumpSN: Data, deviceType: UInt8, version: UInt8) + case failure(error: MedtrumScanError) } -enum ScanError { +enum MedtrumScanError { case invalidBluetoothState(state: CBManagerState) case alreadyScanning } diff --git a/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift b/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift new file mode 100644 index 0000000..6583032 --- /dev/null +++ b/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift @@ -0,0 +1,22 @@ +// +// WriteResult.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 27/02/2025. +// + +enum MedtrumWriteResult { + case success(data: Data) + case failure(error: MedtrumWriteError) +} + +enum MedtrumWriteError { + case timeout + + func toString() -> String { + switch self { + case .timeout: + return "Timeout hit" + } + } +} diff --git a/MedtrumKit/PumpManager/PeripheralManager.swift b/MedtrumKit/PumpManager/PeripheralManager.swift index 03d954f..a8b5655 100644 --- a/MedtrumKit/PumpManager/PeripheralManager.swift +++ b/MedtrumKit/PumpManager/PeripheralManager.swift @@ -13,7 +13,7 @@ class PeripheralManager : NSObject { private let connectedDevice: CBPeripheral private let bluetoothManager: BluetoothManager private let pumpManager: MedtrumPumpManager - private var completion: ((ConnectResult) -> Void)? + private var completion: ((MedtrumConnectResult) -> Void)? public static let SERVICE_UUID = CBUUID(string: "669A9001-0008-968F-E311-6050405558B3") private static let READ_UUID = CBUUID(string: "669a9120-0008-968f-e311-6050405558b3") @@ -24,8 +24,13 @@ class PeripheralManager : NSObject { private var configCharacteristic: CBCharacteristic! private var writeSequence: UInt8 = 0 + private var readPacket: ReadPacket? - public init(_ peripheral: CBPeripheral, _ bluetoothManager: BluetoothManager, _ pumpManager: MedtrumPumpManager,_ completion: @escaping (ConnectResult) -> Void) { + private var writeQueue: Dictionary> = [:] + private var writeTimeoutTask: Task<(), Never>? + private let writeSemaphore = DispatchSemaphore(value: 1) + + public init(_ peripheral: CBPeripheral, _ bluetoothManager: BluetoothManager, _ pumpManager: MedtrumPumpManager,_ completion: @escaping (MedtrumConnectResult) -> Void) { self.connectedDevice = peripheral self.bluetoothManager = bluetoothManager self.pumpManager = pumpManager @@ -36,16 +41,217 @@ class PeripheralManager : NSObject { peripheral.delegate = self } - func writePacket(_ packet: any MedtrumBasePacket) { - let packages = WritePacket.encode(packet, sequenceNumber: self.writeSequence) - self.writeSequence = UInt8(self.writeSequence + 1) - - for package in packages { - self.connectedDevice.writeValue(package, for: self.writeCharacteristic, type: .withResponse) + func writePacket(_ packet: any MedtrumBasePacket) async throws -> MedtrumWriteResult { + return try await withCheckedThrowingContinuation { continuation in + // Wait for the other write to complete... + self.writeSemaphore.wait() + + writeQueue[packet.commandType] = continuation + + let packages = WritePacket.encode(packet, sequenceNumber: self.writeSequence) + self.writeSequence = UInt8(self.writeSequence + 1) + + for package in packages { + self.connectedDevice.writeValue(package, for: self.writeCharacteristic, type: .withResponse) + } + + self.writeTimeoutTask = Task { + do { + try await Task.sleep(nanoseconds: UInt64(.seconds(5)) * 1_000_000_000) + guard let queueItem = self.writeQueue[packet.commandType] else { + // We did what we must! + return + } + + // We hit a timeout... + self.bluetoothManager.manager.cancelPeripheralConnection(self.connectedDevice) + queueItem.resume(returning: .failure(error: .timeout)) + + self.writeQueue[packet.commandType] = nil + self.writeTimeoutTask = nil + self.writeSemaphore.signal() + } catch { + // Task was cancelled because message has been received + } + } } - - // TODO: Add async flow to wait for response + } +} + +extension PeripheralManager { + + // Connect step 1 + private func doAuthorize() async { + do { + let authData = try await writePacket( + AuthorizePacket(pumpSN: self.pumpManager.state.pumpSN, sessionToken: self.pumpManager.state.sessionToken) + ) + + switch authData { + case .failure(let error): + log.error("Failed to complete authorization flow: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + let authResponse = AuthorizePacket.parseResponse(data: data) + pumpManager.state.deviceType = authResponse.deviceType + pumpManager.state.swVersion = authResponse.swVersion + + await getDeviceType() + } + } catch { + let localizedError = "Failed to write authorization packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 2 + private func getDeviceType() async { + do { + let deviceTypeData = try await writePacket(GetDeviceTypePacket()) + + switch deviceTypeData { + case .failure(let error): + log.error("Failed to get device type: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + await getTime() + } + } catch { + let localizedError = "Failed to write GetDeviceType packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 3 + private func getTime() async { + do { + let timeData = try await writePacket(GetTimePacket()) + + switch timeData { + case .failure(let error): + log.error("Failed to get time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + let timeResponse = GetTimePacket.parseResponse(data: data) + + // Allow 10sec time drift + if abs(Date.now.timeIntervalSince1970 - timeResponse.time.timeIntervalSince1970) < .seconds(10) { + pumpManager.state.pumpTime = timeResponse.time + pumpManager.state.pumpTimeSyncedAt = Date.now + + await synchronize() + } else { + await setTime() + } + } + } catch { + let localizedError = "Failed to write GetTime packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 3.1 -> Fix timedrift + private func setTime() async { + do { + let timeData = try await writePacket(SetTimePacket()) + + switch timeData { + case .failure(let error): + log.error("Failed to set time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + log.info("Successfully set time") + await setTimeZone() + } + } catch { + let localizedError = "Failed to write SetTime packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 3.2 -> Fix timezone + private func setTimeZone() async { + do { + let timeZoneData = try await writePacket(SetTimeZonePacket()) + + switch timeZoneData { + case .failure(let error): + log.error("Failed to set time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + log.info("Successfully set time") + + pumpManager.state.pumpTime = Date.now + pumpManager.state.pumpTimeSyncedAt = Date.now + + await synchronize() + } + } catch { + let localizedError = "Failed to write SetTime packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 4 + private func synchronize() async { + do { + let syncData = try await writePacket(SynchronizePacket()) + + switch syncData { + case .failure(let error): + log.error("Failed to synchronize: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + let syncResponse = SynchronizePacket.parseResponse(data: Data(data)) + pumpManager.state.pumpState = syncResponse.state + + await subscribe() + } + } catch { + let localizedError = "Failed to write Synchronize packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } + } + + // Connect step 5 (last) + private func subscribe() async { + do { + let subscribeData = try await writePacket(SubscribePacket()) + + switch subscribeData { + case .failure(let error): + log.error("Failed to subscribe: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break + + case .success(let data): + log.info("Connected to pump!") + completion?(.success) + } + } catch { + let localizedError = "Failed to write Synchronize packet: \(error.localizedDescription)" + log.error(localizedError) + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + } } } @@ -97,8 +303,10 @@ extension PeripheralManager : CBPeripheralDelegate { peripheral.setNotifyValue(true, for: characteristic) } - log.info("Notify enabled and ready to start auth flow!") - writePacket(AuthorizePacket(pumpSN: self.pumpManager.state.pumpSN, sessionToken: self.pumpManager.state.sessionToken)) + Task { + self.log.info("Notify enabled and ready to start auth flow!") + await doAuthorize() + } } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { @@ -114,6 +322,36 @@ extension PeripheralManager : CBPeripheralDelegate { return } - // TODO: Process message + if peripheral.identifier.uuidString == PeripheralManager.READ_UUID.uuidString { + // TODO: Handle state notification + return + } + + if peripheral.identifier.uuidString != PeripheralManager.WRITE_UUID.uuidString { + // Ensure only write characteristic is processed futher on + return + } + + // Processing data + if let readPacket = self.readPacket { + readPacket.addData(data) + } else { + readPacket = ReadPacket(data) + } + + guard let readPacket = readPacket, readPacket.isComplete else { + // Wait for more data + return + } + + guard let writeCallback = writeQueue[readPacket.commandType] else { + // Timeout is hit... + self.writeSemaphore.signal() + return + } + + writeCallback.resume(returning: .success(data: readPacket.totalData)) + writeQueue[readPacket.commandType] = nil + self.writeSemaphore.signal() } } From 1c2c4f5cc200d62f2fee1f191c0af9fb962c26c1 Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Sun, 2 Mar 2025 17:06:21 +0100 Subject: [PATCH 5/6] wip: process feedback plus start unit test --- Common/Data.swift | 24 ++ Common/Int64.swift | 28 ++ Common/UInt64.swift | 17 -- MedtrumKit.xcodeproj/project.pbxproj | 70 +++-- .../xcschemes/MedtrumKitTests.xcscheme | 55 ++++ MedtrumKit/Encryption/Crypto.swift | 38 +-- MedtrumKit/Encryption/ReadPacket.swift | 46 --- MedtrumKit/Encryption/WritePacket.swift | 48 ---- MedtrumKit/Packets/ActivatePacket.swift | 79 ++++++ MedtrumKit/Packets/AuthorizePacket.swift | 8 +- MedtrumKit/Packets/BasePacket.swift | 93 +++++- MedtrumKit/Packets/Enums/AlarmSettings.swift | 17 ++ MedtrumKit/Packets/Enums/BasalType.swift | 72 +++++ MedtrumKit/Packets/GetDeviceTypePacket.swift | 8 +- MedtrumKit/Packets/GetTimePacket.swift | 6 +- MedtrumKit/Packets/SetTimePacket.swift | 4 +- MedtrumKit/Packets/SetTimeZonePacket.swift | 4 +- MedtrumKit/Packets/SubscribePacket.swift | 4 +- MedtrumKit/Packets/SynchronizePacket.swift | 10 +- MedtrumKit/PumpManager/MedtrumPumpState.swift | 11 + .../Models/MedtrumWriteResult.swift | 7 +- .../PumpManager/PeripheralManager.swift | 266 ++++++++---------- MedtrumKitTests/Encryption/CryptoTests.swift | 30 ++ MedtrumKitTests/MedtrumKitTests.swift | 27 -- .../Packets/MedtrumBasePacketTests.swift | 22 ++ 25 files changed, 643 insertions(+), 351 deletions(-) create mode 100644 Common/Int64.swift delete mode 100644 Common/UInt64.swift create mode 100644 MedtrumKit.xcodeproj/xcshareddata/xcschemes/MedtrumKitTests.xcscheme delete mode 100644 MedtrumKit/Encryption/ReadPacket.swift delete mode 100644 MedtrumKit/Encryption/WritePacket.swift create mode 100644 MedtrumKit/Packets/ActivatePacket.swift create mode 100644 MedtrumKit/Packets/Enums/AlarmSettings.swift create mode 100644 MedtrumKit/Packets/Enums/BasalType.swift create mode 100644 MedtrumKitTests/Encryption/CryptoTests.swift delete mode 100644 MedtrumKitTests/MedtrumKitTests.swift create mode 100644 MedtrumKitTests/Packets/MedtrumBasePacketTests.swift diff --git a/Common/Data.swift b/Common/Data.swift index dc205b9..0bd9238 100644 --- a/Common/Data.swift +++ b/Common/Data.swift @@ -6,6 +6,16 @@ // extension Data { + struct HexEncodingOptions: OptionSet { + let rawValue: Int + static let upperCase = HexEncodingOptions(rawValue: 1 << 0) + } + + func hexEncodedString(options: HexEncodingOptions = []) -> String { + let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" + return self.map { String(format: format, $0) }.joined() + } + func toUInt64() -> UInt64 { guard self.count <= 8 else { preconditionFailure("Cannot convert Data to UInt64, size too long") @@ -19,4 +29,18 @@ extension Data { return result } + + func toInt64() -> Int64 { + guard self.count <= 8 else { + preconditionFailure("Cannot convert Data to Int64, size too long") + } + + var result: Int64 = 0 + for i in 0.. Data { + var output = Data(count: length) + for i in 0..> (i * 8)) & 0xFF) + } + + return output + } +} + +extension Int64 { + func toData(length: Int) -> Data { + var output = Data(count: length) + for i in 0..> (i * 8)) & 0xFF) + } + + return output + } +} diff --git a/Common/UInt64.swift b/Common/UInt64.swift deleted file mode 100644 index bdd709b..0000000 --- a/Common/UInt64.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// UInt64.swift -// MedtrumKit -// -// Created by Bastiaan Verhaar on 26/02/2025. -// - -extension UInt64 { - func toData(length: Int) -> Data { - var output = Data(count: length) - for i in 0...length { - output[i] = UInt8(self << (i * 8) & 0xFF) - } - - return output - } -} diff --git a/MedtrumKit.xcodeproj/project.pbxproj b/MedtrumKit.xcodeproj/project.pbxproj index 1cab5a3..6fa00a4 100644 --- a/MedtrumKit.xcodeproj/project.pbxproj +++ b/MedtrumKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 3E0489052D74B82000E0B75A /* BasalType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0489042D74B81D00E0B75A /* BasalType.swift */; }; 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */; }; 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */; }; 3E1390152D6E5C5E00A146C1 /* MedtrumScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */; }; @@ -17,10 +18,8 @@ 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532452D6F95A70020F015 /* CommandType.swift */; }; 3E1532482D6F98D20020F015 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532472D6F98CF0020F015 /* Crypto.swift */; }; 3E15324A2D6FA0080020F015 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1532492D6FA0060020F015 /* Data.swift */; }; - 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324B2D6FA4250020F015 /* UInt64.swift */; }; - 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324D2D6FA7E70020F015 /* WritePacket.swift */; }; + 3E15324C2D6FA4280020F015 /* Int64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15324B2D6FA4250020F015 /* Int64.swift */; }; 3E5060142D70E5CB00376609 /* Crc8.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060132D70E5C900376609 /* Crc8.swift */; }; - 3E5060162D70E62E00376609 /* ReadPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060152D70E62B00376609 /* ReadPacket.swift */; }; 3E5060182D70EAF800376609 /* MedtrumWriteResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */; }; 3E50601A2D70F2F500376609 /* GetDeviceTypePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */; }; 3E50601C2D70F69900376609 /* GetTimePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E50601B2D70F69800376609 /* GetTimePacket.swift */; }; @@ -38,6 +37,10 @@ 3E767D522D67BC2E004B1971 /* MedtrumKitPumpManager+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CE22D67ADFA004B1971 /* MedtrumKitPumpManager+UI.swift */; }; 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767CFE2D67B1A9004B1971 /* MedtrumHUDProvider.swift */; }; 3E767D562D67BCD6004B1971 /* MedtrumKitUICoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E767D552D67BCC9004B1971 /* MedtrumKitUICoordinator.swift */; }; + 3E77FEBA2D723AD400A15134 /* CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E77FEB92D723AD200A15134 /* CryptoTests.swift */; }; + 3E77FEBC2D72F90000A15134 /* MedtrumBasePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E77FEBB2D72F8FC00A15134 /* MedtrumBasePacketTests.swift */; }; + 3E77FEBE2D7364AC00A15134 /* ActivatePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E77FEBD2D7364A300A15134 /* ActivatePacket.swift */; }; + 3E77FEC22D74B39800A15134 /* AlarmSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E77FEC12D74B39300A15134 /* AlarmSettings.swift */; }; 3E98B84B2D6BB12C00DD5123 /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */; platformFilter = ios; }; 3E98B84C2D6BB12C00DD5123 /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */; }; @@ -45,7 +48,6 @@ B7189AEA2C15A8CE00703DE2 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7189AE92C15A8CE00703DE2 /* LoopKit.framework */; }; B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */ = {isa = PBXBuildFile; fileRef = B7D3A8D42C148CC4002EE003 /* MedtrumKit.docc */; }; B7D3A8DB2C148CC4002EE003 /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */; }; - B7D3A8E02C148CC4002EE003 /* MedtrumKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D3A8DF2C148CC4002EE003 /* MedtrumKitTests.swift */; }; B7D3A8E12C148CC4002EE003 /* MedtrumKit.h in Headers */ = {isa = PBXBuildFile; fileRef = B7D3A8D32C148CC4002EE003 /* MedtrumKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7D3A8F12C14F0EF002EE003 /* MedtrumKitPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D3A8ED2C14F0EF002EE003 /* MedtrumKitPlugin.swift */; }; /* End PBXBuildFile section */ @@ -82,6 +84,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3E0489042D74B81D00E0B75A /* BasalType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalType.swift; sourceTree = ""; }; 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothManager.swift; sourceTree = ""; }; 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumPumpState.swift; sourceTree = ""; }; 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumScanResult.swift; sourceTree = ""; }; @@ -92,10 +95,8 @@ 3E1532452D6F95A70020F015 /* CommandType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandType.swift; sourceTree = ""; }; 3E1532472D6F98CF0020F015 /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; 3E1532492D6FA0060020F015 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; - 3E15324B2D6FA4250020F015 /* UInt64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64.swift; sourceTree = ""; }; - 3E15324D2D6FA7E70020F015 /* WritePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WritePacket.swift; sourceTree = ""; }; + 3E15324B2D6FA4250020F015 /* Int64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int64.swift; sourceTree = ""; }; 3E5060132D70E5C900376609 /* Crc8.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crc8.swift; sourceTree = ""; }; - 3E5060152D70E62B00376609 /* ReadPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadPacket.swift; sourceTree = ""; }; 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumWriteResult.swift; sourceTree = ""; }; 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetDeviceTypePacket.swift; sourceTree = ""; }; 3E50601B2D70F69800376609 /* GetTimePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTimePacket.swift; sourceTree = ""; }; @@ -136,6 +137,10 @@ 3E767D4C2D67BA71004B1971 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3E767D502D67BBF9004B1971 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 3E767D552D67BCC9004B1971 /* MedtrumKitUICoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumKitUICoordinator.swift; sourceTree = ""; }; + 3E77FEB92D723AD200A15134 /* CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoTests.swift; sourceTree = ""; }; + 3E77FEBB2D72F8FC00A15134 /* MedtrumBasePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumBasePacketTests.swift; sourceTree = ""; }; + 3E77FEBD2D7364A300A15134 /* ActivatePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivatePacket.swift; sourceTree = ""; }; + 3E77FEC12D74B39300A15134 /* AlarmSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettings.swift; sourceTree = ""; }; 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MedtrumPumpManager.swift; sourceTree = ""; }; B7189AE92C15A8CE00703DE2 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -143,7 +148,6 @@ B7D3A8D32C148CC4002EE003 /* MedtrumKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedtrumKit.h; sourceTree = ""; }; B7D3A8D42C148CC4002EE003 /* MedtrumKit.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = MedtrumKit.docc; sourceTree = ""; }; B7D3A8DA2C148CC4002EE003 /* MedtrumKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MedtrumKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B7D3A8DF2C148CC4002EE003 /* MedtrumKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumKitTests.swift; sourceTree = ""; }; B7D3A8ED2C14F0EF002EE003 /* MedtrumKitPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MedtrumKitPlugin.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,6 +193,7 @@ 3E1532402D6F93DD0020F015 /* Packets */ = { isa = PBXGroup; children = ( + 3E77FEBD2D7364A300A15134 /* ActivatePacket.swift */, 3E5060252D71035900376609 /* SubscribePacket.swift */, 3E5060232D70FED500376609 /* SynchronizePacket.swift */, 3E5060212D70FDA000376609 /* SetTimeZonePacket.swift */, @@ -198,6 +203,7 @@ 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */, 3E1532452D6F95A70020F015 /* CommandType.swift */, 3E1532412D6F93E70020F015 /* BasePacket.swift */, + 3E77FEC02D74B38B00A15134 /* Enums */, ); path = Packets; sourceTree = ""; @@ -205,8 +211,6 @@ 3E5060122D70E5B000376609 /* Encryption */ = { isa = PBXGroup; children = ( - 3E5060152D70E62B00376609 /* ReadPacket.swift */, - 3E15324D2D6FA7E70020F015 /* WritePacket.swift */, 3E5060132D70E5C900376609 /* Crc8.swift */, 3E1532472D6F98CF0020F015 /* Crypto.swift */, ); @@ -228,7 +232,7 @@ isa = PBXGroup; children = ( 3E50601D2D70F7A700376609 /* Date.swift */, - 3E15324B2D6FA4250020F015 /* UInt64.swift */, + 3E15324B2D6FA4250020F015 /* Int64.swift */, 3E1532492D6FA0060020F015 /* Data.swift */, 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */, 3E767CFD2D67B192004B1971 /* LocalizedString.swift */, @@ -245,6 +249,31 @@ path = ViewController; sourceTree = ""; }; + 3E77FEB82D723ABF00A15134 /* Encryption */ = { + isa = PBXGroup; + children = ( + 3E77FEB92D723AD200A15134 /* CryptoTests.swift */, + ); + path = Encryption; + sourceTree = ""; + }; + 3E77FEBF2D74B16100A15134 /* Packets */ = { + isa = PBXGroup; + children = ( + 3E77FEBB2D72F8FC00A15134 /* MedtrumBasePacketTests.swift */, + ); + path = Packets; + sourceTree = ""; + }; + 3E77FEC02D74B38B00A15134 /* Enums */ = { + isa = PBXGroup; + children = ( + 3E0489042D74B81D00E0B75A /* BasalType.swift */, + 3E77FEC12D74B39300A15134 /* AlarmSettings.swift */, + ); + path = Enums; + sourceTree = ""; + }; B7189AE62C15A52800703DE2 /* PumpManager */ = { isa = PBXGroup; children = ( @@ -293,9 +322,9 @@ isa = PBXGroup; children = ( 3E5060122D70E5B000376609 /* Encryption */, - 3E767D052D67B4A0004B1971 /* Info.plist */, 3E1532402D6F93DD0020F015 /* Packets */, B7189AE62C15A52800703DE2 /* PumpManager */, + 3E767D052D67B4A0004B1971 /* Info.plist */, B7D3A8D32C148CC4002EE003 /* MedtrumKit.h */, B7D3A8D42C148CC4002EE003 /* MedtrumKit.docc */, ); @@ -305,7 +334,8 @@ B7D3A8DE2C148CC4002EE003 /* MedtrumKitTests */ = { isa = PBXGroup; children = ( - B7D3A8DF2C148CC4002EE003 /* MedtrumKitTests.swift */, + 3E77FEBF2D74B16100A15134 /* Packets */, + 3E77FEB82D723ABF00A15134 /* Encryption */, ); path = MedtrumKitTests; sourceTree = ""; @@ -518,21 +548,22 @@ 3E5060222D70FDA100376609 /* SetTimeZonePacket.swift in Sources */, 3E1532442D6F94E30020F015 /* AuthorizePacket.swift in Sources */, 3E767D022D67B315004B1971 /* LocalizedString.swift in Sources */, + 3E77FEC22D74B39800A15134 /* AlarmSettings.swift in Sources */, 3E767D522D67BC2E004B1971 /* MedtrumKitPumpManager+UI.swift in Sources */, 3E1390192D6E5F3600A146C1 /* PeripheralManager.swift in Sources */, 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */, 3E50601C2D70F69900376609 /* GetTimePacket.swift in Sources */, 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */, - 3E15324C2D6FA4280020F015 /* UInt64.swift in Sources */, + 3E0489052D74B82000E0B75A /* BasalType.swift in Sources */, + 3E15324C2D6FA4280020F015 /* Int64.swift in Sources */, 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */, 3E767D032D67B319004B1971 /* OSLog.swift in Sources */, 3E1390122D6E59B500A146C1 /* MedtrumPumpState.swift in Sources */, 3E1532482D6F98D20020F015 /* Crypto.swift in Sources */, B7D3A8F12C14F0EF002EE003 /* MedtrumKitPlugin.swift in Sources */, B7189AE72C15A52800703DE2 /* MedtrumPumpManager.swift in Sources */, - 3E5060162D70E62E00376609 /* ReadPacket.swift in Sources */, 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */, - 3E15324E2D6FA7EE0020F015 /* WritePacket.swift in Sources */, + 3E77FEBE2D7364AC00A15134 /* ActivatePacket.swift in Sources */, 3E15324A2D6FA0080020F015 /* Data.swift in Sources */, 3E50601A2D70F2F500376609 /* GetDeviceTypePacket.swift in Sources */, B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */, @@ -547,7 +578,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B7D3A8E02C148CC4002EE003 /* MedtrumKitTests.swift in Sources */, + 3E77FEBC2D72F90000A15134 /* MedtrumBasePacketTests.swift in Sources */, + 3E77FEBA2D723AD400A15134 /* CryptoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -895,7 +927,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = A754VLF58M; - GENERATE_INFOPLIST_FILE = NO; + GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.MedtrumKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -911,7 +943,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = A754VLF58M; - GENERATE_INFOPLIST_FILE = NO; + GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.MedtrumKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/MedtrumKit.xcodeproj/xcshareddata/xcschemes/MedtrumKitTests.xcscheme b/MedtrumKit.xcodeproj/xcshareddata/xcschemes/MedtrumKitTests.xcscheme new file mode 100644 index 0000000..a18044e --- /dev/null +++ b/MedtrumKit.xcodeproj/xcshareddata/xcschemes/MedtrumKitTests.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/MedtrumKit/Encryption/Crypto.swift b/MedtrumKit/Encryption/Crypto.swift index fcd3a7a..5a4825e 100644 --- a/MedtrumKit/Encryption/Crypto.swift +++ b/MedtrumKit/Encryption/Crypto.swift @@ -6,13 +6,13 @@ // class Crypto { - private static let MEDTRUM_CIPHER: UInt64 = 1344751489 + private static let MEDTRUM_CIPHER: Int64 = 1344751489 static func genKey(_ pumpSN: Data) -> Data { - let sn = pumpSN.toUInt64() + let sn = pumpSN.toInt64() let key = randomGen(randomGen(MEDTRUM_CIPHER ^ sn)) - return Data() + return simpleCrypt(key).toData(length: 4) } static func genSessionToken() -> Data { @@ -25,19 +25,19 @@ class Crypto { return Data(bytes) } - static func simpleDecrypt(_ input: UInt64) -> UInt64 { - var temp = input - for i in 0..<32 { - temp = rotateRight(changeByTable(temp, RIJNDEAL_S_BOX), 32, 1) + static func simpleDecrypt(_ input: Data) -> Data { + var temp = input.toInt64() + for _ in 0..<32 { + temp = rotateRight(changeByTable(temp, RIJNDEAL_INVERSE_S_BOX), 32, 1) } - return temp ^ MEDTRUM_CIPHER + return (temp ^ MEDTRUM_CIPHER).toData(length: 4) } - private static func randomGen(_ input: UInt64) -> UInt64 { - let a: UInt64 = 16807 - let q: UInt64 = 127773 - let r: UInt64 = 2836 + private static func randomGen(_ input: Int64) -> Int64 { + let a: Int64 = 16807 + let q: Int64 = 127773 + let r: Int64 = 2836 let tmp1 = input / q var ret = (input - (tmp1 * q)) * a - (tmp1 * r) @@ -48,23 +48,23 @@ class Crypto { return ret } - private static func simpleCrypt(_ input: UInt64) -> UInt64 { + private static func simpleCrypt(_ input: Int64) -> Int64 { var temp = input ^ MEDTRUM_CIPHER - for i in 0..<32 { + for _ in 0..<32 { temp = changeByTable(rotateLeft(temp, 32, 1), RIJNDEAL_S_BOX) } return temp } - private static func rotateLeft(_ x: UInt64, _ s: UInt8, _ n: UInt8) -> UInt64 { + private static func rotateLeft(_ x: Int64, _ s: Int8, _ n: Int8) -> Int64 { return (x << n) | (x >> (s - n)) } - private static func rotateRight(_ x: UInt64, _ s: UInt8, _ n: UInt8) -> UInt64 { - return UInt64(x >> n | (x << (s - n))) + private static func rotateRight(_ x: Int64, _ s: Int8, _ n: Int8) -> Int64 { + return Int64(x >> n | (x << (s - n))) } - private static func changeByTable(_ input: UInt64, _ tableData: [UInt8]) -> UInt64 { + private static func changeByTable(_ input: Int64, _ tableData: [UInt8]) -> Int64 { let value = input.toData(length: 4) var result = Data(count: 4) @@ -72,7 +72,7 @@ class Crypto { result[i] = tableData[Int(value[i])] } - return result.toUInt64() + return result.toInt64() } diff --git a/MedtrumKit/Encryption/ReadPacket.swift b/MedtrumKit/Encryption/ReadPacket.swift deleted file mode 100644 index 5c500b0..0000000 --- a/MedtrumKit/Encryption/ReadPacket.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ReadPacket.swift -// MedtrumKit -// -// Created by Bastiaan Verhaar on 27/02/2025. -// - -class ReadPacket { - private let dataSize: UInt8 - - private(set) var totalData: Data - private var sequenceNumber: UInt8 - private(set) var failed: Bool = false - - var commandType: UInt8 { - totalData[1] - } - - var isComplete: Bool { - totalData.count == dataSize - } - - init(_ data: Data) { - totalData = data - dataSize = data[0] - sequenceNumber = data[3] - - let initialCrc = Crc8.calculate(data[0.. [Data] { - let content = data.getRequestBytes() - var header = Data([ - UInt8(content.count + 4), - data.commandType, - sequenceNumber, - 0, // pkgIndex - ]) - - let tmp = header + content - let totalCommand = tmp + Crc8.calculate(tmp) - - if (totalCommand.count - header.count) <= 15 { - return [totalCommand] - } - - // We need to split up the command in multiple packages - var packages: [Data] = [] - - var pkgIndex: UInt8 = 1 - var remainingCommand = totalCommand[4...] - - while remainingCommand.count > 15 { - header[3] = pkgIndex - - let tmp2 = header + remainingCommand[0..<15] - packages.append(tmp2 + Crc8.calculate(tmp2)) - - remainingCommand = remainingCommand[15...] - pkgIndex = UInt8(pkgIndex + 1) - } - - header[3] = pkgIndex - let tmp3 = header + remainingCommand - - packages.append(tmp + Crc8.calculate(tmp3)) - return packages - } - -} diff --git a/MedtrumKit/Packets/ActivatePacket.swift b/MedtrumKit/Packets/ActivatePacket.swift new file mode 100644 index 0000000..5294ba1 --- /dev/null +++ b/MedtrumKit/Packets/ActivatePacket.swift @@ -0,0 +1,79 @@ +// +// ActivatePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 01/03/2025. +// + +struct AuthorizePacketResponse { + let patchId: Data + let time: Date + let basalType: BasalType + let basalValue: Double + let basalSequence: Double + let basalPatchId: Data + let basalStartTime: Date +} + +class ActivatePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = AuthorizePacketResponse + + let commandType: UInt8 = CommandType.ACTIVATE + + let autoSuspendEnable: UInt8 = 0 + let autoSuspendTime: UInt8 = 12 // unknown why this value needs to be this + let expirationTimer: UInt8 + let alarmSetting: AlarmSettings + let lowSuspend: UInt8 = 0 + let predictiveLowSuspend: UInt8 = 0 + let predictiveLowSuspendRange: UInt8 = 30 // Not sure why, but pump needs this in order to activate + let hourlyMaxInsulin: Double + let dailyMaxInsulin: Double + let currentTDD: Double + let basalProfile: Data + + init(expirationTimer: UInt8, alarmSetting: AlarmSettings, hourlyMaxInsulin: Double, dailyMaxInsulin: Double, currentTDD: Double, basalProfile: Data) { + self.expirationTimer = expirationTimer + self.alarmSetting = alarmSetting + self.hourlyMaxInsulin = hourlyMaxInsulin + self.dailyMaxInsulin = dailyMaxInsulin + self.currentTDD = currentTDD + self.basalProfile = basalProfile + } + + /** + * byte 1: autoSuspendEnable -> Value for auto mode, not used for LoopKit + * byte 2: autoSuspendTime -> Value for auto mode, not used for LoopKit + * byte 3: expirationTimer -> Expiration timer, 0 = no expiration 1 = 12 hour reminder and expiration after 3 days + * byte 4: alarmSetting -> see AlarmSetting + * byte 5: lowSuspend -> Value for auto mode, not used for LoopKit + * byte 6: predictiveLowSuspend -> Value for auto mode, not used for LoopKit + * byte 7: predictiveLowSuspendRange -> Value for auto mode, not used for LoopKit + * byte 8-9: hourlyMaxInsulin -> Max hourly dose of insulin, divided by 0.05 + * byte 10-11: dailyMaxSet -> Max daily dose of insulin, divided by 0.05 + * byte 12-13: tddToday -> Current TDD (of present day), divided by 0.05 + * byte 14: 1 -> Always 1 + * bytes 15 - end -> Basal profile + */ + func getRequestBytes() -> Data { + let base = Data([ + autoSuspendEnable, + autoSuspendTime, + expirationTimer, + alarmSetting.rawValue, + lowSuspend, + predictiveLowSuspend, + predictiveLowSuspendRange, + UInt8(round(hourlyMaxInsulin / 0.05)), + UInt8(round(dailyMaxInsulin / 0.05)), + UInt8(round(currentTDD / 0.05)), + 1, + ]) + + return base + basalProfile + } + + func parseResponse() -> AuthorizePacketResponse { + <#code#> + } +} diff --git a/MedtrumKit/Packets/AuthorizePacket.swift b/MedtrumKit/Packets/AuthorizePacket.swift index 408988e..0bf54ac 100644 --- a/MedtrumKit/Packets/AuthorizePacket.swift +++ b/MedtrumKit/Packets/AuthorizePacket.swift @@ -10,7 +10,7 @@ struct AuthorizeResponse { let swVersion: String } -class AuthorizePacket: MedtrumBasePacket { +class AuthorizePacket: MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = AuthorizeResponse let commandType: UInt8 = CommandType.AUTH_REQ @@ -34,10 +34,10 @@ class AuthorizePacket: MedtrumBasePacket { return output } - static func parseResponse(data: Data) -> AuthorizeResponse { + func parseResponse() -> AuthorizeResponse { return AuthorizeResponse( - deviceType: data[7], - swVersion: "\(data[8]).\(data[9]).\(data[10])" + deviceType: totalData[7], + swVersion: "\(totalData[8]).\(totalData[9]).\(totalData[10])" ) } } diff --git a/MedtrumKit/Packets/BasePacket.swift b/MedtrumKit/Packets/BasePacket.swift index 29c5cb1..6515ad3 100644 --- a/MedtrumKit/Packets/BasePacket.swift +++ b/MedtrumKit/Packets/BasePacket.swift @@ -5,11 +5,100 @@ // Created by Bastiaan Verhaar on 26/02/2025. // -protocol MedtrumBasePacket { +protocol MedtrumBasePacketProtocol { associatedtype T var commandType: UInt8 { get } + // Needed to parse + var dataSize: UInt8 { get set } + var totalData: Data { get set } + var sequenceNumber: UInt8 { get set } + var failed: Bool { get set } + func getRequestBytes() -> Data - static func parseResponse(data: Data) -> T + func parseResponse() -> T +} + +class MedtrumBasePacket { + var dataSize: UInt8 = 0 + var totalData: Data = Data() + var sequenceNumber: UInt8 = 0 + var failed: Bool = false +} + +extension MedtrumBasePacketProtocol { + func encode(sequenceNumber: UInt8) -> [Data] { + let content = self.getRequestBytes() + var header = Data([ + UInt8(content.count + 5), + self.commandType, + sequenceNumber, + 0, // pkgIndex + ]) + + let tmp = header + content + let totalCommand = tmp + Crc8.calculate(tmp) + + if (totalCommand.count - header.count) <= 15 { + let output = totalCommand + Data([0]) + return [output] + } + + // We need to split up the command in multiple packages + var packages: [Data] = [] + + var pkgIndex: UInt8 = 1 + var remainingCommand = totalCommand[4...] + + while remainingCommand.count > 15 { + header[3] = pkgIndex + + let tmp2 = header + remainingCommand[0..<15] + packages.append(tmp2 + Crc8.calculate(tmp2)) + + remainingCommand = remainingCommand[15...] + pkgIndex = UInt8(pkgIndex + 1) + } + + header[3] = pkgIndex + let tmp3 = header + remainingCommand + + packages.append(tmp + Crc8.calculate(tmp3)) + return packages + } + + mutating func decode(_ data: Data) { + if totalData.isEmpty { + if totalData[1] != self.commandType { + failed = true + } + + totalData = data + dataSize = data[0] + sequenceNumber = data[3] + + let initialCrc = Crc8.calculate(data[0.. Bool { + switch self { + case .ABSOLUTE_TEMP, .RELATIVE_TEMP: + return true + default: + return false + } + } + + func isSuspendedByPump() -> Bool { + switch self { + case .SUSPEND_LOW_GLUCOSE, .SUSPEND_PREDICT_LOW_GLUCOSE, .SUSPEND_AUTO, .SUSPEND_MORE_THAN_MAX_PER_HOUR, + .SUSPEND_MORE_THAN_MAX_PER_DAY, .SUSPEND_MANUAL, .SUSPEND_KEY_LOST, .STOP_OCCLUSION, .STOP_EXPIRED, + .STOP_EMPTY, .STOP_PATCH_FAULT, .STOP_PATCH_FAULT2, .STOP_BASE_FAULT, .STOP_DISCARD, .STOP_BATTERY_EMPTY, + .STOP: + return true + default: + return false + } + } +} diff --git a/MedtrumKit/Packets/GetDeviceTypePacket.swift b/MedtrumKit/Packets/GetDeviceTypePacket.swift index d786406..a517799 100644 --- a/MedtrumKit/Packets/GetDeviceTypePacket.swift +++ b/MedtrumKit/Packets/GetDeviceTypePacket.swift @@ -10,7 +10,7 @@ struct GetDeviceTypeResponse { let deviceSN: Data } -class GetDeviceTypePacket: MedtrumBasePacket { +class GetDeviceTypePacket: MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = GetDeviceTypeResponse let commandType: UInt8 = CommandType.GET_DEVICE_TYPE @@ -18,10 +18,10 @@ class GetDeviceTypePacket: MedtrumBasePacket { return Data() } - static func parseResponse(data: Data) -> GetDeviceTypeResponse { + func parseResponse() -> GetDeviceTypeResponse { return GetDeviceTypeResponse( - deviceType: data[6], - deviceSN: data[7..<11] + deviceType: totalData[6], + deviceSN: totalData[7..<11] ) } } diff --git a/MedtrumKit/Packets/GetTimePacket.swift b/MedtrumKit/Packets/GetTimePacket.swift index fe05095..d743926 100644 --- a/MedtrumKit/Packets/GetTimePacket.swift +++ b/MedtrumKit/Packets/GetTimePacket.swift @@ -9,7 +9,7 @@ struct GetTimePacketResponse { let time: Date } -class GetTimePacket : MedtrumBasePacket { +class GetTimePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = GetTimePacketResponse let commandType: UInt8 = CommandType.GET_TIME @@ -18,8 +18,8 @@ class GetTimePacket : MedtrumBasePacket { return Data() } - static func parseResponse(data: Data) -> GetTimePacketResponse { - let secondsPassed = data[6..<10].toUInt64() + func parseResponse() -> GetTimePacketResponse { + let secondsPassed = totalData[6..<10].toUInt64() return GetTimePacketResponse( time: Date.fromMedtrumSeconds(secondsPassed) ) diff --git a/MedtrumKit/Packets/SetTimePacket.swift b/MedtrumKit/Packets/SetTimePacket.swift index b19e878..ea20de1 100644 --- a/MedtrumKit/Packets/SetTimePacket.swift +++ b/MedtrumKit/Packets/SetTimePacket.swift @@ -7,7 +7,7 @@ struct SetTimePacketResponse {} -class SetTimePacket : MedtrumBasePacket { +class SetTimePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = SetTimePacketResponse let commandType: UInt8 = CommandType.SET_TIME @@ -19,7 +19,7 @@ class SetTimePacket : MedtrumBasePacket { return output } - static func parseResponse(data: Data) -> SetTimePacketResponse { + func parseResponse() -> SetTimePacketResponse { return SetTimePacketResponse() } } diff --git a/MedtrumKit/Packets/SetTimeZonePacket.swift b/MedtrumKit/Packets/SetTimeZonePacket.swift index 3162fbd..b4be51b 100644 --- a/MedtrumKit/Packets/SetTimeZonePacket.swift +++ b/MedtrumKit/Packets/SetTimeZonePacket.swift @@ -7,7 +7,7 @@ struct SetTimeZonePacketResponse {} -class SetTimeZonePacket : MedtrumBasePacket { +class SetTimeZonePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = SetTimeZonePacketResponse let commandType: UInt8 = CommandType.SET_TIME_ZONE @@ -24,7 +24,7 @@ class SetTimeZonePacket : MedtrumBasePacket { return offsetData + timeData } - static func parseResponse(data: Data) -> SetTimeZonePacketResponse { + func parseResponse() -> SetTimeZonePacketResponse { return SetTimeZonePacketResponse() } } diff --git a/MedtrumKit/Packets/SubscribePacket.swift b/MedtrumKit/Packets/SubscribePacket.swift index d4fdd6d..8f3fbe0 100644 --- a/MedtrumKit/Packets/SubscribePacket.swift +++ b/MedtrumKit/Packets/SubscribePacket.swift @@ -7,7 +7,7 @@ struct SubscribePacketResponse {} -class SubscribePacket : MedtrumBasePacket { +class SubscribePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = SubscribePacketResponse let commandType: UInt8 = CommandType.SUBSCRIBE @@ -16,7 +16,7 @@ class SubscribePacket : MedtrumBasePacket { return UInt64(4095).toData(length: 2) } - static func parseResponse(data: Data) -> SubscribePacketResponse { + func parseResponse() -> SubscribePacketResponse { return SubscribePacketResponse() } } diff --git a/MedtrumKit/Packets/SynchronizePacket.swift b/MedtrumKit/Packets/SynchronizePacket.swift index e8be60c..85f2b4b 100644 --- a/MedtrumKit/Packets/SynchronizePacket.swift +++ b/MedtrumKit/Packets/SynchronizePacket.swift @@ -11,7 +11,7 @@ struct SynchronizePacketResponse { let syncData: Data } -class SynchronizePacket : MedtrumBasePacket { +class SynchronizePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { typealias T = SynchronizePacketResponse let commandType: UInt8 = CommandType.SYNCHRONIZE @@ -20,12 +20,12 @@ class SynchronizePacket : MedtrumBasePacket { return Data() } - static func parseResponse(data: Data) -> SynchronizePacketResponse { - let fieldMask = data[7..<9] - let syncData = data[9...] + func parseResponse() -> SynchronizePacketResponse { + let fieldMask = totalData[7..<9] + let syncData = totalData[9...] return SynchronizePacketResponse( - state: MedtrumState(rawValue: data[6]) ?? .none, + state: MedtrumState(rawValue: totalData[6]) ?? .none, fieldMask: UInt16((fieldMask[0] << 8) | fieldMask[1]), syncData: syncData ) diff --git a/MedtrumKit/PumpManager/MedtrumPumpState.swift b/MedtrumKit/PumpManager/MedtrumPumpState.swift index fa98e77..ec57151 100644 --- a/MedtrumKit/PumpManager/MedtrumPumpState.swift +++ b/MedtrumKit/PumpManager/MedtrumPumpState.swift @@ -16,6 +16,8 @@ class MedtrumPumpState: RawRepresentable { swVersion = rawValue["swVersion"] as? String ?? "0.0.0" pumpTime = rawValue["pumpTime"] as? Date ?? Date() pumpTimeSyncedAt = rawValue["pumpTimeSyncedAt"] as? Date ?? Date() + maxHourlyInsulin = rawValue["maxHourlyInsulin"] as? Double ?? 20 + maxDailyInsulin = rawValue["maxDailyInsulin"] as? Double ?? 100 if let pumpStateRaw = rawValue["pumpState"] as? MedtrumState.RawValue { pumpState = MedtrumState(rawValue: pumpStateRaw) ?? .none @@ -32,6 +34,9 @@ class MedtrumPumpState: RawRepresentable { pumpTime = Date() pumpTimeSyncedAt = Date() pumpState = .none + + maxHourlyInsulin = 20 + maxDailyInsulin = 100 } public var rawValue: RawValue { @@ -44,6 +49,8 @@ class MedtrumPumpState: RawRepresentable { value["pumpTime"] = pumpTime value["pumpTimeSyncedAt"] = pumpTimeSyncedAt value["pumpState"] = pumpState.rawValue + value["maxHourlyInsulin"] = maxHourlyInsulin + value["maxDailyInsulin"] = maxDailyInsulin return value } @@ -58,4 +65,8 @@ class MedtrumPumpState: RawRepresentable { public var pumpTimeSyncedAt: Date public var pumpState: MedtrumState + + // Patch limits + public var maxHourlyInsulin: Double + public var maxDailyInsulin: Double } diff --git a/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift b/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift index 6583032..4f43427 100644 --- a/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift +++ b/MedtrumKit/PumpManager/Models/MedtrumWriteResult.swift @@ -5,18 +5,21 @@ // Created by Bastiaan Verhaar on 27/02/2025. // -enum MedtrumWriteResult { - case success(data: Data) +enum MedtrumWriteResult { + case success(data: T) case failure(error: MedtrumWriteError) } enum MedtrumWriteError { case timeout + case invalidResponse func toString() -> String { switch self { case .timeout: return "Timeout hit" + case .invalidResponse: + return "Invalid response" } } } diff --git a/MedtrumKit/PumpManager/PeripheralManager.swift b/MedtrumKit/PumpManager/PeripheralManager.swift index a8b5655..21c93b1 100644 --- a/MedtrumKit/PumpManager/PeripheralManager.swift +++ b/MedtrumKit/PumpManager/PeripheralManager.swift @@ -24,9 +24,9 @@ class PeripheralManager : NSObject { private var configCharacteristic: CBCharacteristic! private var writeSequence: UInt8 = 0 - private var readPacket: ReadPacket? + private var currentPacket: (any MedtrumBasePacketProtocol)? - private var writeQueue: Dictionary> = [:] + private var writeQueue: Dictionary, Never>> = [:] private var writeTimeoutTask: Task<(), Never>? private let writeSemaphore = DispatchSemaphore(value: 1) @@ -41,14 +41,15 @@ class PeripheralManager : NSObject { peripheral.delegate = self } - func writePacket(_ packet: any MedtrumBasePacket) async throws -> MedtrumWriteResult { - return try await withCheckedThrowingContinuation { continuation in + func writePacket(_ packet: any MedtrumBasePacketProtocol) async -> MedtrumWriteResult { + return await withCheckedContinuation { continuation in // Wait for the other write to complete... self.writeSemaphore.wait() writeQueue[packet.commandType] = continuation + currentPacket = packet - let packages = WritePacket.encode(packet, sequenceNumber: self.writeSequence) + let packages = packet.encode(sequenceNumber: self.writeSequence) self.writeSequence = UInt8(self.writeSequence + 1) for package in packages { @@ -83,174 +84,131 @@ extension PeripheralManager { // Connect step 1 private func doAuthorize() async { - do { - let authData = try await writePacket( - AuthorizePacket(pumpSN: self.pumpManager.state.pumpSN, sessionToken: self.pumpManager.state.sessionToken) - ) + let authData = await writePacket( + AuthorizePacket(pumpSN: self.pumpManager.state.pumpSN, sessionToken: self.pumpManager.state.sessionToken) + ) + + switch authData { + case .failure(let error): + log.error("Failed to complete authorization flow: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch authData { - case .failure(let error): - log.error("Failed to complete authorization flow: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - let authResponse = AuthorizePacket.parseResponse(data: data) - pumpManager.state.deviceType = authResponse.deviceType - pumpManager.state.swVersion = authResponse.swVersion - - await getDeviceType() + case .success(let data): + guard let authResponse = data as? AuthorizeResponse else { + log.error("Failed to complete authorization flow: invalid response") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: "invalid response"))) + return } - } catch { - let localizedError = "Failed to write authorization packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + + pumpManager.state.deviceType = authResponse.deviceType + pumpManager.state.swVersion = authResponse.swVersion + + await getTime() } } // Connect step 2 - private func getDeviceType() async { - do { - let deviceTypeData = try await writePacket(GetDeviceTypePacket()) + private func getTime() async { + let timeData = await writePacket(GetTimePacket()) + + switch timeData { + case .failure(let error): + log.error("Failed to get time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch deviceTypeData { - case .failure(let error): - log.error("Failed to get device type: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - await getTime() + case .success(let data): + guard let timeResponse = data as? GetTimePacketResponse else { + log.error("Failed to get time: invalid response") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: "invalid response"))) + return } - } catch { - let localizedError = "Failed to write GetDeviceType packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) - } - } - - // Connect step 3 - private func getTime() async { - do { - let timeData = try await writePacket(GetTimePacket()) - switch timeData { - case .failure(let error): - log.error("Failed to get time: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - let timeResponse = GetTimePacket.parseResponse(data: data) + // Allow 10sec time drift + if abs(Date.now.timeIntervalSince1970 - timeResponse.time.timeIntervalSince1970) < .seconds(10) { + pumpManager.state.pumpTime = timeResponse.time + pumpManager.state.pumpTimeSyncedAt = Date.now - // Allow 10sec time drift - if abs(Date.now.timeIntervalSince1970 - timeResponse.time.timeIntervalSince1970) < .seconds(10) { - pumpManager.state.pumpTime = timeResponse.time - pumpManager.state.pumpTimeSyncedAt = Date.now - - await synchronize() - } else { - await setTime() - } + await synchronize() + } else { + await setTime() } - } catch { - let localizedError = "Failed to write GetTime packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) } } - // Connect step 3.1 -> Fix timedrift + // Connect step 2.1 -> Fix timedrift private func setTime() async { - do { - let timeData = try await writePacket(SetTimePacket()) + let timeData = await writePacket(SetTimePacket()) + + switch timeData { + case .failure(let error): + log.error("Failed to set time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch timeData { - case .failure(let error): - log.error("Failed to set time: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - log.info("Successfully set time") - await setTimeZone() - } - } catch { - let localizedError = "Failed to write SetTime packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + case .success: + log.info("Successfully set time") + await setTimeZone() } } - // Connect step 3.2 -> Fix timezone + // Connect step 2.2 -> Fix timezone private func setTimeZone() async { - do { - let timeZoneData = try await writePacket(SetTimeZonePacket()) + let timeZoneData = await writePacket(SetTimeZonePacket()) + + switch timeZoneData { + case .failure(let error): + log.error("Failed to set time: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch timeZoneData { - case .failure(let error): - log.error("Failed to set time: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - log.info("Successfully set time") - - pumpManager.state.pumpTime = Date.now - pumpManager.state.pumpTimeSyncedAt = Date.now - - await synchronize() - } - } catch { - let localizedError = "Failed to write SetTime packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + case .success: + log.info("Successfully set time") + + pumpManager.state.pumpTime = Date.now + pumpManager.state.pumpTimeSyncedAt = Date.now + + await synchronize() } } - // Connect step 4 + // Connect step 3 private func synchronize() async { - do { - let syncData = try await writePacket(SynchronizePacket()) + let syncData = await writePacket(SynchronizePacket()) + + switch syncData { + case .failure(let error): + log.error("Failed to synchronize: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch syncData { - case .failure(let error): - log.error("Failed to synchronize: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - let syncResponse = SynchronizePacket.parseResponse(data: Data(data)) - pumpManager.state.pumpState = syncResponse.state - - await subscribe() + case .success(let data): + guard let syncResponse = data as? SynchronizePacketResponse else { + log.error("Failed to Synchronize packet: invalid response") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: "invalid response"))) + return } - } catch { - let localizedError = "Failed to write Synchronize packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + + pumpManager.state.pumpState = syncResponse.state + // TODO: Map other data here + await subscribe() } } - // Connect step 5 (last) + // Connect step 4 (last) private func subscribe() async { - do { - let subscribeData = try await writePacket(SubscribePacket()) + let subscribeData = await writePacket(SubscribePacket()) + + switch subscribeData { + case .failure(let error): + log.error("Failed to subscribe: \(error.toString())") + completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) + break - switch subscribeData { - case .failure(let error): - log.error("Failed to subscribe: \(error.toString())") - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: error.toString()))) - break - - case .success(let data): - log.info("Connected to pump!") - completion?(.success) - } - } catch { - let localizedError = "Failed to write Synchronize packet: \(error.localizedDescription)" - log.error(localizedError) - completion?(.failure(error: .failedToCompleteAuthorizationFlow(localizedError: localizedError))) + case .success: + log.info("Connected to pump!") + completion?(.success) } } } @@ -333,25 +291,35 @@ extension PeripheralManager : CBPeripheralDelegate { } // Processing data - if let readPacket = self.readPacket { - readPacket.addData(data) - } else { - readPacket = ReadPacket(data) + guard var packet = self.currentPacket else { + // No packet available to validate against + return } - guard let readPacket = readPacket, readPacket.isComplete else { + packet.decode(data) + self.currentPacket = packet + + guard packet.isComplete else { // Wait for more data return } - guard let writeCallback = writeQueue[readPacket.commandType] else { + guard let writeCallback = writeQueue[packet.commandType] else { // Timeout is hit... + self.currentPacket = nil self.writeSemaphore.signal() return } - writeCallback.resume(returning: .success(data: readPacket.totalData)) - writeQueue[readPacket.commandType] = nil + if packet.failed { + writeCallback.resume(returning: .failure(error: .invalidResponse)) + } else { + writeCallback.resume(returning: .success(data: packet.parseResponse())) + } + + + self.writeQueue[packet.commandType] = nil + self.currentPacket = nil self.writeSemaphore.signal() } } diff --git a/MedtrumKitTests/Encryption/CryptoTests.swift b/MedtrumKitTests/Encryption/CryptoTests.swift new file mode 100644 index 0000000..9c2eaba --- /dev/null +++ b/MedtrumKitTests/Encryption/CryptoTests.swift @@ -0,0 +1,30 @@ +// +// Crypto.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 28/02/2025. +// + +@testable import MedtrumKit +import XCTest + +final class CryptoTests : XCTestCase { + + func testKeyGen() throws { + let input = Data([217, 249, 118, 170]) // 2859923929 + let expected = Data([235, 57, 134, 200]) //3364239851 + + + let result = Crypto.genKey(input) + XCTAssertEqual(result, expected, "Failed to generate correct key based on SN") + } + + func testKeyDecrypt() throws { + let input = Data([217, 249, 118, 170]) //2859923929 + let expected = Data([33, 191, 130, 7]) //126009121 + + + let result = Crypto.simpleDecrypt(input) + XCTAssertEqual(result, expected, "Failed to decrypt key to correct SN") + } +} diff --git a/MedtrumKitTests/MedtrumKitTests.swift b/MedtrumKitTests/MedtrumKitTests.swift deleted file mode 100644 index f58defb..0000000 --- a/MedtrumKitTests/MedtrumKitTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -@testable import MedtrumKit -import XCTest - -final class MedtrumKitTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } -} diff --git a/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift b/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift new file mode 100644 index 0000000..a259892 --- /dev/null +++ b/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift @@ -0,0 +1,22 @@ +// +// WritePacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 01/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class MedtrumBasePacketTests : XCTestCase { + func testWriteAuthorizeCommandExpectOnePacket() throws { + let input = AuthorizePacket(pumpSN: Data([217, 249, 118, 170]), sessionToken: Data([0, 0, 0, 0])) + let expected = Data([14, 5, 0, 0, 2, 0, 0, 0, 0, 235, 57, 134, 200, 163, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } +} From f1430a3c77880d72ae0fea32d170fd7ccd80f27b Mon Sep 17 00:00:00 2001 From: "bastiaan.verhaar" Date: Thu, 6 Mar 2025 21:47:28 +0100 Subject: [PATCH 6/6] wip: continue unit tests --- Common/Data.swift | 4 + Common/Date.swift | 2 +- MedtrumKit.xcodeproj/project.pbxproj | 80 +++++++++++++- MedtrumKit/Packets/ActivatePacket.swift | 45 ++++++-- MedtrumKit/Packets/BasePacket.swift | 10 +- MedtrumKit/Packets/CancelBolusPacket.swift | 31 ++++++ .../Packets/CancelTempBasalPacket.swift | 34 ++++++ MedtrumKit/Packets/ClearPumpAlarmPacket.swift | 28 +++++ MedtrumKit/Packets/Enums/BasalType.swift | 2 +- MedtrumKit/Packets/Enums/ClearAlarmType.swift | 13 +++ MedtrumKit/Packets/GetRecordPacket.swift | 48 +++++++++ MedtrumKit/Packets/GetTimePacket.swift | 2 +- MedtrumKit/Packets/PollPatchPacket.swift | 22 ++++ MedtrumKit/Packets/PrimePacket.swift | 22 ++++ MedtrumKit/PumpManager/MedtrumPumpState.swift | 15 +++ .../PumpManager/Models/BasalSchedule.swift | 100 ++++++++++++++++++ .../Packets/ActivatePacketTests.swift | 71 +++++++++++++ .../Packets/AuthorizePacketTests.swift | 43 ++++++++ .../Packets/CancelBolusPacketTests.swift | 23 ++++ .../Packets/CancelTempBasalPacketTests.swift | 46 ++++++++ .../Packets/ClearPumpAlarmPacketTests.swift | 23 ++++ .../Packets/GetDeviceTypePacketTests.swift | 43 ++++++++ .../Packets/GetRecordPacketTests.swift | 42 ++++++++ .../Packets/GetTimePacketTest.swift | 42 ++++++++ .../Packets/MedtrumBasePacketTests.swift | 23 ++++ .../Packets/PollPatchPacketTests.swift | 23 ++++ .../Packets/PrimePacketTests.swift | 23 ++++ 27 files changed, 837 insertions(+), 23 deletions(-) create mode 100644 MedtrumKit/Packets/CancelBolusPacket.swift create mode 100644 MedtrumKit/Packets/CancelTempBasalPacket.swift create mode 100644 MedtrumKit/Packets/ClearPumpAlarmPacket.swift create mode 100644 MedtrumKit/Packets/Enums/ClearAlarmType.swift create mode 100644 MedtrumKit/Packets/GetRecordPacket.swift create mode 100644 MedtrumKit/Packets/PollPatchPacket.swift create mode 100644 MedtrumKit/Packets/PrimePacket.swift create mode 100644 MedtrumKit/PumpManager/Models/BasalSchedule.swift create mode 100644 MedtrumKitTests/Packets/ActivatePacketTests.swift create mode 100644 MedtrumKitTests/Packets/AuthorizePacketTests.swift create mode 100644 MedtrumKitTests/Packets/CancelBolusPacketTests.swift create mode 100644 MedtrumKitTests/Packets/CancelTempBasalPacketTests.swift create mode 100644 MedtrumKitTests/Packets/ClearPumpAlarmPacketTests.swift create mode 100644 MedtrumKitTests/Packets/GetDeviceTypePacketTests.swift create mode 100644 MedtrumKitTests/Packets/GetRecordPacketTests.swift create mode 100644 MedtrumKitTests/Packets/GetTimePacketTest.swift create mode 100644 MedtrumKitTests/Packets/PollPatchPacketTests.swift create mode 100644 MedtrumKitTests/Packets/PrimePacketTests.swift diff --git a/Common/Data.swift b/Common/Data.swift index 0bd9238..20aae69 100644 --- a/Common/Data.swift +++ b/Common/Data.swift @@ -16,6 +16,10 @@ extension Data { return self.map { String(format: format, $0) }.joined() } + func toDouble() -> Double { + return Double(self.toInt64()) + } + func toUInt64() -> UInt64 { guard self.count <= 8 else { preconditionFailure("Cannot convert Data to UInt64, size too long") diff --git a/Common/Date.swift b/Common/Date.swift index aa75514..28dd944 100644 --- a/Common/Date.swift +++ b/Common/Date.swift @@ -5,7 +5,7 @@ // Created by Bastiaan Verhaar on 27/02/2025. // -let baseUnix: TimeInterval = .seconds(1388530800) //2014-01-01T00:00:00+0000 +let baseUnix: TimeInterval = .seconds(1388534400) //2014-01-01T00:00:00+0000 extension Date { static func fromMedtrumSeconds(_ seconds: UInt64) -> Date { diff --git a/MedtrumKit.xcodeproj/project.pbxproj b/MedtrumKit.xcodeproj/project.pbxproj index 6fa00a4..b4ac508 100644 --- a/MedtrumKit.xcodeproj/project.pbxproj +++ b/MedtrumKit.xcodeproj/project.pbxproj @@ -44,6 +44,24 @@ 3E98B84B2D6BB12C00DD5123 /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */; platformFilter = ios; }; 3E98B84C2D6BB12C00DD5123 /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */; }; + 3EA87D4B2D7A2165007DD959 /* ActivatePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D4A2D7A214B007DD959 /* ActivatePacketTests.swift */; }; + 3EA87D4D2D7A25ED007DD959 /* AuthorizePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D4C2D7A25E5007DD959 /* AuthorizePacketTests.swift */; }; + 3EA87D4F2D7A2930007DD959 /* CancelBolusPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D4E2D7A292A007DD959 /* CancelBolusPacket.swift */; }; + 3EA87D512D7A29A4007DD959 /* CancelBolusPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D502D7A29A1007DD959 /* CancelBolusPacketTests.swift */; }; + 3EA87D532D7A2A31007DD959 /* CancelTempBasalPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D522D7A2A2B007DD959 /* CancelTempBasalPacket.swift */; }; + 3EA87D552D7A33AD007DD959 /* CancelTempBasalPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D542D7A33A6007DD959 /* CancelTempBasalPacketTests.swift */; }; + 3EA87D572D7A3573007DD959 /* ClearPumpAlarmPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D562D7A356E007DD959 /* ClearPumpAlarmPacket.swift */; }; + 3EA87D592D7A35D1007DD959 /* ClearAlarmType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D582D7A35C7007DD959 /* ClearAlarmType.swift */; }; + 3EA87D5B2D7A3632007DD959 /* ClearPumpAlarmPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D5A2D7A3627007DD959 /* ClearPumpAlarmPacketTests.swift */; }; + 3EA87D5F2D7A36CB007DD959 /* GetDeviceTypePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D5E2D7A36C4007DD959 /* GetDeviceTypePacketTests.swift */; }; + 3EA87D612D7A3813007DD959 /* GetRecordPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D602D7A380D007DD959 /* GetRecordPacket.swift */; }; + 3EA87D632D7A3B9B007DD959 /* GetRecordPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D622D7A3B95007DD959 /* GetRecordPacketTests.swift */; }; + 3EA87D652D7A3DCE007DD959 /* GetTimePacketTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D642D7A3DCA007DD959 /* GetTimePacketTest.swift */; }; + 3EA87D672D7A3F8C007DD959 /* PollPatchPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D662D7A3F88007DD959 /* PollPatchPacket.swift */; }; + 3EA87D692D7A3FC2007DD959 /* PollPatchPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D682D7A3FBE007DD959 /* PollPatchPacketTests.swift */; }; + 3EA87D6B2D7A402C007DD959 /* PrimePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D6A2D7A4027007DD959 /* PrimePacket.swift */; }; + 3EA87D6D2D7A404C007DD959 /* PrimePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA87D6C2D7A4046007DD959 /* PrimePacketTests.swift */; }; + 3EB1A3B12D7784410031C044 /* BasalSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB1A3B02D77843E0031C044 /* BasalSchedule.swift */; }; B7189AE72C15A52800703DE2 /* MedtrumPumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */; }; B7189AEA2C15A8CE00703DE2 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B7189AE92C15A8CE00703DE2 /* LoopKit.framework */; }; B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */ = {isa = PBXBuildFile; fileRef = B7D3A8D42C148CC4002EE003 /* MedtrumKit.docc */; }; @@ -142,6 +160,24 @@ 3E77FEBD2D7364A300A15134 /* ActivatePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivatePacket.swift; sourceTree = ""; }; 3E77FEC12D74B39300A15134 /* AlarmSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettings.swift; sourceTree = ""; }; 3E98B8502D6BB2B900DD5123 /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; + 3EA87D4A2D7A214B007DD959 /* ActivatePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivatePacketTests.swift; sourceTree = ""; }; + 3EA87D4C2D7A25E5007DD959 /* AuthorizePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizePacketTests.swift; sourceTree = ""; }; + 3EA87D4E2D7A292A007DD959 /* CancelBolusPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelBolusPacket.swift; sourceTree = ""; }; + 3EA87D502D7A29A1007DD959 /* CancelBolusPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelBolusPacketTests.swift; sourceTree = ""; }; + 3EA87D522D7A2A2B007DD959 /* CancelTempBasalPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelTempBasalPacket.swift; sourceTree = ""; }; + 3EA87D542D7A33A6007DD959 /* CancelTempBasalPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelTempBasalPacketTests.swift; sourceTree = ""; }; + 3EA87D562D7A356E007DD959 /* ClearPumpAlarmPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPumpAlarmPacket.swift; sourceTree = ""; }; + 3EA87D582D7A35C7007DD959 /* ClearAlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAlarmType.swift; sourceTree = ""; }; + 3EA87D5A2D7A3627007DD959 /* ClearPumpAlarmPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPumpAlarmPacketTests.swift; sourceTree = ""; }; + 3EA87D5E2D7A36C4007DD959 /* GetDeviceTypePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetDeviceTypePacketTests.swift; sourceTree = ""; }; + 3EA87D602D7A380D007DD959 /* GetRecordPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRecordPacket.swift; sourceTree = ""; }; + 3EA87D622D7A3B95007DD959 /* GetRecordPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRecordPacketTests.swift; sourceTree = ""; }; + 3EA87D642D7A3DCA007DD959 /* GetTimePacketTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTimePacketTest.swift; sourceTree = ""; }; + 3EA87D662D7A3F88007DD959 /* PollPatchPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollPatchPacket.swift; sourceTree = ""; }; + 3EA87D682D7A3FBE007DD959 /* PollPatchPacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollPatchPacketTests.swift; sourceTree = ""; }; + 3EA87D6A2D7A4027007DD959 /* PrimePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimePacket.swift; sourceTree = ""; }; + 3EA87D6C2D7A4046007DD959 /* PrimePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimePacketTests.swift; sourceTree = ""; }; + 3EB1A3B02D77843E0031C044 /* BasalSchedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalSchedule.swift; sourceTree = ""; }; B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MedtrumPumpManager.swift; sourceTree = ""; }; B7189AE92C15A8CE00703DE2 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B7D3A8D02C148CC4002EE003 /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -183,6 +219,7 @@ 3E1390132D6E5C4900A146C1 /* Models */ = { isa = PBXGroup; children = ( + 3EB1A3B02D77843E0031C044 /* BasalSchedule.swift */, 3E5060172D70EAF400376609 /* MedtrumWriteResult.swift */, 3E1390162D6E5E9A00A146C1 /* MedtrumConnectResult.swift */, 3E1390142D6E5C5200A146C1 /* MedtrumScanResult.swift */, @@ -194,13 +231,19 @@ isa = PBXGroup; children = ( 3E77FEBD2D7364A300A15134 /* ActivatePacket.swift */, + 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */, + 3EA87D4E2D7A292A007DD959 /* CancelBolusPacket.swift */, + 3EA87D522D7A2A2B007DD959 /* CancelTempBasalPacket.swift */, + 3EA87D562D7A356E007DD959 /* ClearPumpAlarmPacket.swift */, + 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */, + 3EA87D602D7A380D007DD959 /* GetRecordPacket.swift */, + 3E50601B2D70F69800376609 /* GetTimePacket.swift */, + 3EA87D662D7A3F88007DD959 /* PollPatchPacket.swift */, + 3EA87D6A2D7A4027007DD959 /* PrimePacket.swift */, 3E5060252D71035900376609 /* SubscribePacket.swift */, 3E5060232D70FED500376609 /* SynchronizePacket.swift */, 3E5060212D70FDA000376609 /* SetTimeZonePacket.swift */, 3E50601F2D70F97500376609 /* SetTimePacket.swift */, - 3E50601B2D70F69800376609 /* GetTimePacket.swift */, - 3E5060192D70F2F300376609 /* GetDeviceTypePacket.swift */, - 3E1532432D6F94E20020F015 /* AuthorizePacket.swift */, 3E1532452D6F95A70020F015 /* CommandType.swift */, 3E1532412D6F93E70020F015 /* BasePacket.swift */, 3E77FEC02D74B38B00A15134 /* Enums */, @@ -260,6 +303,16 @@ 3E77FEBF2D74B16100A15134 /* Packets */ = { isa = PBXGroup; children = ( + 3EA87D4A2D7A214B007DD959 /* ActivatePacketTests.swift */, + 3EA87D4C2D7A25E5007DD959 /* AuthorizePacketTests.swift */, + 3EA87D502D7A29A1007DD959 /* CancelBolusPacketTests.swift */, + 3EA87D542D7A33A6007DD959 /* CancelTempBasalPacketTests.swift */, + 3EA87D5A2D7A3627007DD959 /* ClearPumpAlarmPacketTests.swift */, + 3EA87D5E2D7A36C4007DD959 /* GetDeviceTypePacketTests.swift */, + 3EA87D622D7A3B95007DD959 /* GetRecordPacketTests.swift */, + 3EA87D642D7A3DCA007DD959 /* GetTimePacketTest.swift */, + 3EA87D682D7A3FBE007DD959 /* PollPatchPacketTests.swift */, + 3EA87D6C2D7A4046007DD959 /* PrimePacketTests.swift */, 3E77FEBB2D72F8FC00A15134 /* MedtrumBasePacketTests.swift */, ); path = Packets; @@ -268,6 +321,7 @@ 3E77FEC02D74B38B00A15134 /* Enums */ = { isa = PBXGroup; children = ( + 3EA87D582D7A35C7007DD959 /* ClearAlarmType.swift */, 3E0489042D74B81D00E0B75A /* BasalType.swift */, 3E77FEC12D74B39300A15134 /* AlarmSettings.swift */, ); @@ -279,9 +333,9 @@ children = ( 3E1390132D6E5C4900A146C1 /* Models */, 3E1390112D6E59B000A146C1 /* MedtrumPumpState.swift */, + B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */, 3E13900F2D6E534600A146C1 /* BluetoothManager.swift */, 3E1390182D6E5F2B00A146C1 /* PeripheralManager.swift */, - B7189AE52C15A52800703DE2 /* MedtrumPumpManager.swift */, ); path = PumpManager; sourceTree = ""; @@ -542,6 +596,8 @@ 3E767D562D67BCD6004B1971 /* MedtrumKitUICoordinator.swift in Sources */, 3E5060262D71035900376609 /* SubscribePacket.swift in Sources */, 3E5060202D70F97500376609 /* SetTimePacket.swift in Sources */, + 3EA87D612D7A3813007DD959 /* GetRecordPacket.swift in Sources */, + 3EA87D4F2D7A2930007DD959 /* CancelBolusPacket.swift in Sources */, 3E1390172D6E5EA000A146C1 /* MedtrumConnectResult.swift in Sources */, 3E98B8512D6BB2BC00DD5123 /* TimeInterval.swift in Sources */, 3E5060142D70E5CB00376609 /* Crc8.swift in Sources */, @@ -554,6 +610,7 @@ 3E1532422D6F93EF0020F015 /* BasePacket.swift in Sources */, 3E50601C2D70F69900376609 /* GetTimePacket.swift in Sources */, 3E1390102D6E534C00A146C1 /* BluetoothManager.swift in Sources */, + 3EA87D6B2D7A402C007DD959 /* PrimePacket.swift in Sources */, 3E0489052D74B82000E0B75A /* BasalType.swift in Sources */, 3E15324C2D6FA4280020F015 /* Int64.swift in Sources */, 3E1532462D6F95AC0020F015 /* CommandType.swift in Sources */, @@ -565,12 +622,17 @@ 3E767D532D67BC33004B1971 /* MedtrumHUDProvider.swift in Sources */, 3E77FEBE2D7364AC00A15134 /* ActivatePacket.swift in Sources */, 3E15324A2D6FA0080020F015 /* Data.swift in Sources */, + 3EA87D672D7A3F8C007DD959 /* PollPatchPacket.swift in Sources */, 3E50601A2D70F2F500376609 /* GetDeviceTypePacket.swift in Sources */, B7D3A8D52C148CC4002EE003 /* MedtrumKit.docc in Sources */, 3E5060242D70FED500376609 /* SynchronizePacket.swift in Sources */, 3E5060182D70EAF800376609 /* MedtrumWriteResult.swift in Sources */, + 3EA87D572D7A3573007DD959 /* ClearPumpAlarmPacket.swift in Sources */, + 3EA87D592D7A35D1007DD959 /* ClearAlarmType.swift in Sources */, 3E50601E2D70F7A900376609 /* Date.swift in Sources */, 3E1390152D6E5C5E00A146C1 /* MedtrumScanResult.swift in Sources */, + 3EB1A3B12D7784410031C044 /* BasalSchedule.swift in Sources */, + 3EA87D532D7A2A31007DD959 /* CancelTempBasalPacket.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -578,8 +640,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3EA87D512D7A29A4007DD959 /* CancelBolusPacketTests.swift in Sources */, + 3EA87D6D2D7A404C007DD959 /* PrimePacketTests.swift in Sources */, + 3EA87D652D7A3DCE007DD959 /* GetTimePacketTest.swift in Sources */, + 3EA87D632D7A3B9B007DD959 /* GetRecordPacketTests.swift in Sources */, + 3EA87D692D7A3FC2007DD959 /* PollPatchPacketTests.swift in Sources */, + 3EA87D4B2D7A2165007DD959 /* ActivatePacketTests.swift in Sources */, + 3EA87D552D7A33AD007DD959 /* CancelTempBasalPacketTests.swift in Sources */, 3E77FEBC2D72F90000A15134 /* MedtrumBasePacketTests.swift in Sources */, + 3EA87D5B2D7A3632007DD959 /* ClearPumpAlarmPacketTests.swift in Sources */, 3E77FEBA2D723AD400A15134 /* CryptoTests.swift in Sources */, + 3EA87D5F2D7A36CB007DD959 /* GetDeviceTypePacketTests.swift in Sources */, + 3EA87D4D2D7A25ED007DD959 /* AuthorizePacketTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MedtrumKit/Packets/ActivatePacket.swift b/MedtrumKit/Packets/ActivatePacket.swift index 5294ba1..d412c80 100644 --- a/MedtrumKit/Packets/ActivatePacket.swift +++ b/MedtrumKit/Packets/ActivatePacket.swift @@ -5,18 +5,18 @@ // Created by Bastiaan Verhaar on 01/03/2025. // -struct AuthorizePacketResponse { +struct ActivatePacketResponse { let patchId: Data let time: Date let basalType: BasalType let basalValue: Double let basalSequence: Double - let basalPatchId: Data + let basalPatchId: Double let basalStartTime: Date } class ActivatePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { - typealias T = AuthorizePacketResponse + typealias T = ActivatePacketResponse let commandType: UInt8 = CommandType.ACTIVATE @@ -56,24 +56,47 @@ class ActivatePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { * bytes 15 - end -> Basal profile */ func getRequestBytes() -> Data { - let base = Data([ + var base = Data([ autoSuspendEnable, autoSuspendTime, expirationTimer, alarmSetting.rawValue, lowSuspend, predictiveLowSuspend, - predictiveLowSuspendRange, - UInt8(round(hourlyMaxInsulin / 0.05)), - UInt8(round(dailyMaxInsulin / 0.05)), - UInt8(round(currentTDD / 0.05)), - 1, + predictiveLowSuspendRange ]) + let calcHourlyInsulin = UInt16(round(hourlyMaxInsulin / 0.05)) + base.append(Data([ + UInt8(calcHourlyInsulin & 0xFF), + UInt8(calcHourlyInsulin >> 8) + ])) + + let calcDailyMaxInsulin = UInt16(round(dailyMaxInsulin / 0.05)) + base.append(Data([ + UInt8(calcDailyMaxInsulin & 0xFF), + UInt8(calcDailyMaxInsulin >> 8) + ])) + + let calcCurrentTDD = UInt16(round(currentTDD / 0.05)) + base.append(Data([ + UInt8(calcCurrentTDD & 0xFF), + UInt8(calcCurrentTDD >> 8), + 1 + ])) + return base + basalProfile } - func parseResponse() -> AuthorizePacketResponse { - <#code#> + func parseResponse() -> ActivatePacketResponse { + return ActivatePacketResponse( + patchId: totalData.subdata(in: 6..<10), + time: Date.fromMedtrumSeconds(totalData.subdata(in: 10..<14).toUInt64()), + basalType: BasalType(rawValue: totalData[14]) ?? .NONE, + basalValue: totalData.subdata(in: 15..<17).toDouble() * 0.05, + basalSequence: totalData.subdata(in: 17..<19).toDouble(), + basalPatchId: totalData.subdata(in: 19..<21).toDouble(), + basalStartTime:Date.fromMedtrumSeconds(totalData.subdata(in: 21..<25).toUInt64()) + ) } } diff --git a/MedtrumKit/Packets/BasePacket.swift b/MedtrumKit/Packets/BasePacket.swift index 6515ad3..cb7eba6 100644 --- a/MedtrumKit/Packets/BasePacket.swift +++ b/MedtrumKit/Packets/BasePacket.swift @@ -49,28 +49,28 @@ extension MedtrumBasePacketProtocol { var packages: [Data] = [] var pkgIndex: UInt8 = 1 - var remainingCommand = totalCommand[4...] + var remainingCommand = totalCommand.subdata(in: 4.. 15 { header[3] = pkgIndex - let tmp2 = header + remainingCommand[0..<15] + let tmp2 = header + remainingCommand.subdata(in: 0..<15) packages.append(tmp2 + Crc8.calculate(tmp2)) - remainingCommand = remainingCommand[15...] + remainingCommand = remainingCommand.subdata(in: 15.. Normal bolus + 2 -> Extended bolus + 3 -> Combi bolus + */ + private let bolusType: UInt8 = 1 + + func getRequestBytes() -> Data { + return Data([bolusType]) + } + + func parseResponse() -> CancelBolusPacketResponse { + return CancelBolusPacketResponse() + } +} + + diff --git a/MedtrumKit/Packets/CancelTempBasalPacket.swift b/MedtrumKit/Packets/CancelTempBasalPacket.swift new file mode 100644 index 0000000..54ab685 --- /dev/null +++ b/MedtrumKit/Packets/CancelTempBasalPacket.swift @@ -0,0 +1,34 @@ +// +// CancelTempBasalPacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +struct CancelTempBasalPacketResponse { + let basalType: BasalType + let basalValue: Double + let basalSequence: Double + let basalPatchId: Double + let basalStartTime: Date +} + +class CancelTempBasalPacket: MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = CancelTempBasalPacketResponse + + let commandType: UInt8 = CommandType.CANCEL_TEMP_BASAL + + func getRequestBytes() -> Data { + return Data([]) + } + + func parseResponse() -> CancelTempBasalPacketResponse { + return CancelTempBasalPacketResponse( + basalType: BasalType(rawValue: totalData[6]) ?? .NONE, + basalValue: totalData.subdata(in: 7..<9).toDouble() * 0.05, + basalSequence: totalData.subdata(in: 9..<11).toDouble(), + basalPatchId: totalData.subdata(in: 11..<13).toDouble(), + basalStartTime: Date.fromMedtrumSeconds(totalData.subdata(in: 13..<17).toUInt64()) + ) + } +} diff --git a/MedtrumKit/Packets/ClearPumpAlarmPacket.swift b/MedtrumKit/Packets/ClearPumpAlarmPacket.swift new file mode 100644 index 0000000..1f7ebac --- /dev/null +++ b/MedtrumKit/Packets/ClearPumpAlarmPacket.swift @@ -0,0 +1,28 @@ +// +// ClearPumpAlarmPacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +struct ClearPumpAlarmResponse{} + +class ClearPumpAlarmPacket: MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = ClearPumpAlarmResponse + + let commandType: UInt8 = CommandType.CLEAR_ALARM + + private let alarmType: ClearAlarmType + + init(alarmType: ClearAlarmType) { + self.alarmType = alarmType + } + + func getRequestBytes() -> Data { + return Data([alarmType.rawValue]) + } + + func parseResponse() -> ClearPumpAlarmResponse { + return ClearPumpAlarmResponse() + } +} diff --git a/MedtrumKit/Packets/Enums/BasalType.swift b/MedtrumKit/Packets/Enums/BasalType.swift index a6588ec..f505524 100644 --- a/MedtrumKit/Packets/Enums/BasalType.swift +++ b/MedtrumKit/Packets/Enums/BasalType.swift @@ -5,7 +5,7 @@ // Created by Bastiaan Verhaar on 02/03/2025. // -enum BasalType { +enum BasalType : UInt8 { case NONE case STANDARD case EXERCISE diff --git a/MedtrumKit/Packets/Enums/ClearAlarmType.swift b/MedtrumKit/Packets/Enums/ClearAlarmType.swift new file mode 100644 index 0000000..3f22408 --- /dev/null +++ b/MedtrumKit/Packets/Enums/ClearAlarmType.swift @@ -0,0 +1,13 @@ +// +// ClearAlarmType.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +enum ClearAlarmType : UInt8 { + // More might be possible, but unknown at this moment + + case hourlyMax = 4 + case dailyMax = 5 +} diff --git a/MedtrumKit/Packets/GetRecordPacket.swift b/MedtrumKit/Packets/GetRecordPacket.swift new file mode 100644 index 0000000..c057ec1 --- /dev/null +++ b/MedtrumKit/Packets/GetRecordPacket.swift @@ -0,0 +1,48 @@ +// +// GetRecordPacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +struct GetRecordPacketResponse { + let header: UInt8 + let unknown: UInt8 + let type: UInt8 + let unknown1: UInt8 + let serial: Data + let patchId: Data + let sequence: UInt16 +} + +class GetRecordPacket: MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = GetRecordPacketResponse + let commandType: UInt8 = CommandType.GET_RECORD + + let recordIndex: UInt16 + let patchId: Data + + init(recordIndex: UInt16, patchId: Data) { + self.recordIndex = recordIndex + self.patchId = patchId + } + + func getRequestBytes() -> Data { + return Data([ + UInt8(recordIndex & 0xFF), + UInt8(recordIndex >> 8) + ]) + patchId + } + + func parseResponse() -> GetRecordPacketResponse { + return GetRecordPacketResponse( + header: totalData[6], + unknown: totalData[7], + type: totalData[8], + unknown1: totalData[9], + serial: totalData.subdata(in: 10..<14), + patchId: totalData.subdata(in: 14..<16), + sequence: UInt16(totalData.subdata(in: 16..<18).toUInt64()) + ) + } +} diff --git a/MedtrumKit/Packets/GetTimePacket.swift b/MedtrumKit/Packets/GetTimePacket.swift index d743926..5e76ebe 100644 --- a/MedtrumKit/Packets/GetTimePacket.swift +++ b/MedtrumKit/Packets/GetTimePacket.swift @@ -19,7 +19,7 @@ class GetTimePacket : MedtrumBasePacket, MedtrumBasePacketProtocol { } func parseResponse() -> GetTimePacketResponse { - let secondsPassed = totalData[6..<10].toUInt64() + let secondsPassed = totalData.subdata(in: 6..<10).toUInt64() return GetTimePacketResponse( time: Date.fromMedtrumSeconds(secondsPassed) ) diff --git a/MedtrumKit/Packets/PollPatchPacket.swift b/MedtrumKit/Packets/PollPatchPacket.swift new file mode 100644 index 0000000..0f3861f --- /dev/null +++ b/MedtrumKit/Packets/PollPatchPacket.swift @@ -0,0 +1,22 @@ +// +// PollPatchPacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +struct PollPatchPacketResponse { } + +class PollPatchPacket: MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = PollPatchPacketResponse + + let commandType: UInt8 = CommandType.POLL_PATCH + + func getRequestBytes() -> Data { + return Data([]) + } + + func parseResponse() -> PollPatchPacketResponse { + return PollPatchPacketResponse() + } +} diff --git a/MedtrumKit/Packets/PrimePacket.swift b/MedtrumKit/Packets/PrimePacket.swift new file mode 100644 index 0000000..9606a96 --- /dev/null +++ b/MedtrumKit/Packets/PrimePacket.swift @@ -0,0 +1,22 @@ +// +// PrimePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +struct PrimePacketResponse { } + +class PrimePacket: MedtrumBasePacket, MedtrumBasePacketProtocol { + typealias T = PrimePacketResponse + + let commandType: UInt8 = CommandType.PRIME + + func getRequestBytes() -> Data { + return Data([]) + } + + func parseResponse() -> PrimePacketResponse { + return PrimePacketResponse() + } +} diff --git a/MedtrumKit/PumpManager/MedtrumPumpState.swift b/MedtrumKit/PumpManager/MedtrumPumpState.swift index ec57151..b4a4b9c 100644 --- a/MedtrumKit/PumpManager/MedtrumPumpState.swift +++ b/MedtrumKit/PumpManager/MedtrumPumpState.swift @@ -12,6 +12,7 @@ class MedtrumPumpState: RawRepresentable { required public init(rawValue: RawValue) { pumpSN = rawValue["pumpSN"] as? Data ?? Data() sessionToken = rawValue["sessionToken"] as? Data ?? Data() + patchId = rawValue["patchId"] as? Data ?? Data() deviceType = rawValue["deviceType"] as? UInt8 ?? 0 swVersion = rawValue["swVersion"] as? String ?? "0.0.0" pumpTime = rawValue["pumpTime"] as? Date ?? Date() @@ -24,11 +25,18 @@ class MedtrumPumpState: RawRepresentable { } else { pumpState = .none } + + if let rawBasalSchedule = rawValue["basalSchedule"] as? BasalSchedule.RawValue { + basalSchedule = BasalSchedule(rawValue: rawBasalSchedule) ?? BasalSchedule(entries: [LoopKit.RepeatingScheduleValue(startTime: 0, value: 0)]) + } else { + basalSchedule = BasalSchedule(entries: [LoopKit.RepeatingScheduleValue(startTime: 0, value: 0)]) + } } public init() { pumpSN = Data() sessionToken = Data() + patchId = Data() deviceType = 0 swVersion = "0.0.0" pumpTime = Date() @@ -37,6 +45,8 @@ class MedtrumPumpState: RawRepresentable { maxHourlyInsulin = 20 maxDailyInsulin = 100 + + basalSchedule = BasalSchedule(entries: [LoopKit.RepeatingScheduleValue(startTime: 0, value: 0)]) } public var rawValue: RawValue { @@ -44,6 +54,7 @@ class MedtrumPumpState: RawRepresentable { value["pumpSN"] = pumpSN value["sessionToken"] = sessionToken + value["patchId"] = patchId value["deviceType"] = deviceType value["swVersion"] = swVersion value["pumpTime"] = pumpTime @@ -51,12 +62,14 @@ class MedtrumPumpState: RawRepresentable { value["pumpState"] = pumpState.rawValue value["maxHourlyInsulin"] = maxHourlyInsulin value["maxDailyInsulin"] = maxDailyInsulin + value["basalSchedule"] = basalSchedule.rawValue return value } public var pumpSN: Data public var sessionToken: Data + public var patchId: Data public var deviceType: UInt8 public var swVersion: String @@ -69,4 +82,6 @@ class MedtrumPumpState: RawRepresentable { // Patch limits public var maxHourlyInsulin: Double public var maxDailyInsulin: Double + + public var basalSchedule: BasalSchedule } diff --git a/MedtrumKit/PumpManager/Models/BasalSchedule.swift b/MedtrumKit/PumpManager/Models/BasalSchedule.swift new file mode 100644 index 0000000..9441506 --- /dev/null +++ b/MedtrumKit/PumpManager/Models/BasalSchedule.swift @@ -0,0 +1,100 @@ +// +// BasalSchedule.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 04/03/2025. +// + +import LoopKit + +public struct BasalSchedule: RawRepresentable { + + public typealias RawValue = [String: Any] + + let entries: [BasalScheduleEntry] + + + public init(entries: [LoopKit.RepeatingScheduleValue]) { + self.entries = entries.map{ BasalScheduleEntry(rate: $0.value, startTime: $0.startTime) } + } + + public init?(rawValue: RawValue) { + guard let entries = rawValue["entries"] as? [BasalScheduleEntry.RawValue] else { + return nil + } + + self.entries = entries.compactMap { BasalScheduleEntry(rawValue: $0) } + } + + public var rawValue: RawValue { + let rawValue: RawValue = [ + "entries": entries.map { $0.rawValue } + ] + + return rawValue + } + + public func toData() -> Data { + var output = Data([UInt8(entries.count)]) + + zip(entries, entries.dropFirst()).forEach { (current, next) in + let rate = UInt16(round(current.rate / 0.05)) + let time = UInt16((next.startTime - current.startTime).minutes) + + if time > 0xFFF || rate > 0xFFF { + preconditionFailure("Rate or time is too big: \(rate), \(time)") + } + + let entries = Data([ + UInt8((rate >> 4) & 0xFF), + UInt8((rate << 4) & 0xF0 | (time >> 8) & 0x0F), + UInt8(time & 0xFF) + ]) + output.append(entries) + } + + if let lastEntry = entries.last { + let rate = UInt16(round(lastEntry.rate / 0.05)) + + let entries = Data([ + UInt8((rate >> 4) & 0xFF), + UInt8((rate << 4) & 0xF0), + 0 + ]) + } + + return output + } + +} + +public struct BasalScheduleEntry: RawRepresentable { + + public typealias RawValue = [String: Any] + + let rate: Double + let startTime: TimeInterval + + public init(rate: Double, startTime: TimeInterval) { + self.rate = rate + self.startTime = startTime + } + + public init?(rawValue: RawValue) { + guard let rate = rawValue["rate"] as? Double, let startTime = rawValue["startTime"] as? Double else { + return nil + } + + self.rate = rate + self.startTime = startTime + } + + public var rawValue: RawValue { + let rawValue: RawValue = [ + "rate": rate, + "startTime": startTime + ] + + return rawValue + } +} diff --git a/MedtrumKitTests/Packets/ActivatePacketTests.swift b/MedtrumKitTests/Packets/ActivatePacketTests.swift new file mode 100644 index 0000000..7815cbb --- /dev/null +++ b/MedtrumKitTests/Packets/ActivatePacketTests.swift @@ -0,0 +1,71 @@ +// +// ActivatePacket.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class ActivatePacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = ActivatePacket( + expirationTimer: 1, + alarmSetting: .BeepOnly, + hourlyMaxInsulin: 40, + dailyMaxInsulin: 180, + currentTDD: 0, + basalProfile: Data([3, 16, 14, 0, 0, 1, 2, 12, 12, 12]) + ) + + let expected1 = Data([29, 18, 0, 1, 0, 12, 1, 6, 0, 0, 30, 32, 3, 16, 14, 0, 0, 1, 3, 150]) + let expected2 = Data([29, 18, 0, 2, 16, 14, 0, 0, 1, 2, 12, 12, 12, 217, 9]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 2) + XCTAssertEqual(actual[0], expected1) + XCTAssertEqual(actual[1], expected2) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([26, 18, 19, 1, 0, 0, 41, 0, 0, 0, 152, 91, 28, 17, 1, 30, 0, 1, 0, 41, 0, 152, 91, 28, 17]) + var packet = ActivatePacket( + expirationTimer: 1, + alarmSetting: .BeepOnly, + hourlyMaxInsulin: 40, + dailyMaxInsulin: 180, + currentTDD: 0, + basalProfile: Data([3, 16, 14, 0, 0, 1, 2, 12, 12, 12]) + ) + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let actual = packet.parseResponse() + XCTAssertEqual(actual.patchId, Data([41, 0, 0, 0])) + XCTAssertEqual(actual.time, Date(timeIntervalSince1970: 1675605528)) + XCTAssertEqual(actual.basalType, BasalType.STANDARD) + XCTAssertEqual(actual.basalValue, 1.5) + XCTAssertEqual(actual.basalSequence, 1) + XCTAssertEqual(actual.basalPatchId, 41) + XCTAssertEqual(actual.basalStartTime, Date(timeIntervalSince1970: 1675605528)) + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([26, 18, 19, 1, 0, 0, 41, 0, 0, 0, 152, 91, 28, 17, 1, 30, 0, 1, 0, 41, 0, 152, 91, 28]) + var packet = ActivatePacket( + expirationTimer: 1, + alarmSetting: .BeepOnly, + hourlyMaxInsulin: 40, + dailyMaxInsulin: 180, + currentTDD: 0, + basalProfile: Data([3, 16, 14, 0, 0, 1, 2, 12, 12, 12]) + ) + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/AuthorizePacketTests.swift b/MedtrumKitTests/Packets/AuthorizePacketTests.swift new file mode 100644 index 0000000..543f8cd --- /dev/null +++ b/MedtrumKitTests/Packets/AuthorizePacketTests.swift @@ -0,0 +1,43 @@ +// +// AuthorizePacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class AuthorizePacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = AuthorizePacket(pumpSN: Data([217, 249, 118, 170]), sessionToken: Data([155, 2, 0, 0])) + + let expected = Data([14, 5, 0, 0, 2, 155, 2, 0, 0, 235, 57, 134, 200, 238, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([0, 5, 0, 0, 0, 0, 0, 80, 12, 1, 3, 103]) + var packet = AuthorizePacket(pumpSN: Data([217, 249, 118, 170]), sessionToken: Data([155, 2, 0, 0])) + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let actual = packet.parseResponse() + XCTAssertEqual(actual.deviceType, 80) + XCTAssertEqual(actual.swVersion, "12.1.3") + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([0, 5, 0, 0, 0, 0, 0, 80, 12, 1, 3]) + var packet = AuthorizePacket(pumpSN: Data([217, 249, 118, 170]), sessionToken: Data([155, 2, 0, 0])) + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/CancelBolusPacketTests.swift b/MedtrumKitTests/Packets/CancelBolusPacketTests.swift new file mode 100644 index 0000000..02cfc5a --- /dev/null +++ b/MedtrumKitTests/Packets/CancelBolusPacketTests.swift @@ -0,0 +1,23 @@ +// +// CancelBolusPacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class CancelBolusPacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = CancelBolusPacket() + + let expected = Data([6, 20, 0, 0, 1, 16, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } +} diff --git a/MedtrumKitTests/Packets/CancelTempBasalPacketTests.swift b/MedtrumKitTests/Packets/CancelTempBasalPacketTests.swift new file mode 100644 index 0000000..c636677 --- /dev/null +++ b/MedtrumKitTests/Packets/CancelTempBasalPacketTests.swift @@ -0,0 +1,46 @@ +// +// CancelTempBasalPacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class CancelTempBasalPacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = CancelTempBasalPacket() + + let expected = Data([5, 25, 0, 0, 167, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([18, 25, 16, 0, 0, 0, 1, 22, 0, 3, 0, 146, 0, 224, 238, 88, 17, 88]) + var packet = CancelTempBasalPacket() + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let actual = packet.parseResponse() + XCTAssertEqual(actual.basalType, BasalType.STANDARD) + XCTAssertEqual(actual.basalValue, 1.1) + XCTAssertEqual(actual.basalSequence, 3) + XCTAssertEqual(actual.basalPatchId, 146) + XCTAssertEqual(actual.basalStartTime, Date(timeIntervalSince1970: 1679575392)) + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([18, 25, 16, 0, 0, 0, 1, 22, 0, 3, 0, 146, 0, 224, 238, 88, 17]) + var packet = CancelTempBasalPacket() + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/ClearPumpAlarmPacketTests.swift b/MedtrumKitTests/Packets/ClearPumpAlarmPacketTests.swift new file mode 100644 index 0000000..42fa6da --- /dev/null +++ b/MedtrumKitTests/Packets/ClearPumpAlarmPacketTests.swift @@ -0,0 +1,23 @@ +// +// ClearPumpAlarmPacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class ClearPumpAlarmPacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = ClearPumpAlarmPacket(alarmType: .hourlyMax) + + let expected = Data([6, 115, 0, 0, 4, 10, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } +} diff --git a/MedtrumKitTests/Packets/GetDeviceTypePacketTests.swift b/MedtrumKitTests/Packets/GetDeviceTypePacketTests.swift new file mode 100644 index 0000000..65cd320 --- /dev/null +++ b/MedtrumKitTests/Packets/GetDeviceTypePacketTests.swift @@ -0,0 +1,43 @@ +// +// GetDeviceTypePacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class GetDeviceTypePacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = GetDeviceTypePacket() + + let expected = Data([5, 6, 0, 0, 65, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([0, 6, 0, 0, 0, 0, 80, 78, 97, 188, 0, 215]) + var packet = GetDeviceTypePacket() + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let actual = packet.parseResponse() + XCTAssertEqual(actual.deviceType, 80) + XCTAssertEqual(actual.deviceSN, Data([78, 97, 188, 0])) + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([0, 6, 0, 0, 0, 0, 80, 78, 97, 188, 0]) + var packet = GetDeviceTypePacket() + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/GetRecordPacketTests.swift b/MedtrumKitTests/Packets/GetRecordPacketTests.swift new file mode 100644 index 0000000..49e487f --- /dev/null +++ b/MedtrumKitTests/Packets/GetRecordPacketTests.swift @@ -0,0 +1,42 @@ +// +// GetRecordPacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class GetRecordPacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = GetRecordPacket(recordIndex: 4, patchId: Data([146, 0])) + + let expected = Data([9, 99, 0, 0, 4, 0, 146, 0, 246, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([35, 99, 9, 1, 0, 0, 170, 28, 2, 255, 251, 216, 229, 238, 14, 0, 192, 1, 165, 236, 174, 17, 165, 236, 174, 17, 1, 0, 26, 0, 0, 0, 154, 0, 208, 4]) + var packet = GetRecordPacket(recordIndex: 4, patchId: Data([146, 0])) + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let _ = packet.parseResponse() + // We dont test the response + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([35, 99, 9, 1, 0, 0, 170, 28, 2, 255, 251, 216, 229, 238, 14, 0, 192, 1, 165, 236, 174, 17, 165, 236, 174, 17, 1, 0, 26, 0, 0, 0, 154, 0, 208]) + var packet = GetRecordPacket(recordIndex: 4, patchId: Data([146, 0])) + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/GetTimePacketTest.swift b/MedtrumKitTests/Packets/GetTimePacketTest.swift new file mode 100644 index 0000000..2430579 --- /dev/null +++ b/MedtrumKitTests/Packets/GetTimePacketTest.swift @@ -0,0 +1,42 @@ +// +// GetTimePacketTest.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class GetTimePacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = GetTimePacket() + + let expected = Data([5, 11, 0, 0, 161, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } + + func testResponseGivenPacketWhenValuesSetThenReturnCorrectValues() throws { + let response = Data([0, 11, 0, 0, 0, 0, 224, 238, 88, 17, 22]) + var packet = GetTimePacket() + + packet.decode(response) + XCTAssertFalse(packet.failed) + + let actual = packet.parseResponse() + XCTAssertEqual(actual.time, Date(timeIntervalSince1970: 1679575392)) + } + + func testResponseGivenResponseWhenMessageTooShortThenResultFalse() throws { + let response = Data([0, 11, 0, 0, 0, 0, 224, 238, 88, 17]) + var packet = GetTimePacket() + + packet.decode(response) + XCTAssertTrue(packet.failed) + } +} diff --git a/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift b/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift index a259892..b0f90ec 100644 --- a/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift +++ b/MedtrumKitTests/Packets/MedtrumBasePacketTests.swift @@ -19,4 +19,27 @@ final class MedtrumBasePacketTests : XCTestCase { XCTAssertEqual(actual.count, 1) XCTAssertEqual(actual[0], expected) } + + func testWriteActivateCommandExpectThreePackets() throws { + let input = ActivatePacket( + expirationTimer: 0, + alarmSetting: .LightOnly, + hourlyMaxInsulin: 40, + dailyMaxInsulin: 180, + currentTDD: 0, + basalProfile: Data([7, 0, 160, 2, 240, 96, 2, 104, 33, 2, 224, 225, 1, 192, 3, 2, 236, 36, 2, 100, 133, 2]) + ) + + let expected1 = Data([41, 18, 0, 1, 0, 12, 0, 3, 0, 0, 30, 32, 3, 16, 14, 0, 0, 1, 7, 173]) + let expected2 = Data([41, 18, 0, 2, 0, 160, 2, 240, 96, 2, 104, 33, 2, 224, 225, 1, 192, 3, 2, 253]) + let expected3 = Data([41, 18, 0, 3, 236, 36, 2, 100, 133, 2, 144, 163]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 3) + XCTAssertEqual(actual[0], expected1) + XCTAssertEqual(actual[1], expected2) + XCTAssertEqual(actual[2], expected3) + } } diff --git a/MedtrumKitTests/Packets/PollPatchPacketTests.swift b/MedtrumKitTests/Packets/PollPatchPacketTests.swift new file mode 100644 index 0000000..cdd797c --- /dev/null +++ b/MedtrumKitTests/Packets/PollPatchPacketTests.swift @@ -0,0 +1,23 @@ +// +// PollPatchPacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class PollPatchPacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = PollPatchPacket() + + let expected = Data([5, 30, 0, 0, 166, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } +} diff --git a/MedtrumKitTests/Packets/PrimePacketTests.swift b/MedtrumKitTests/Packets/PrimePacketTests.swift new file mode 100644 index 0000000..cd21d3f --- /dev/null +++ b/MedtrumKitTests/Packets/PrimePacketTests.swift @@ -0,0 +1,23 @@ +// +// PrimePacketTests.swift +// MedtrumKit +// +// Created by Bastiaan Verhaar on 06/03/2025. +// + +@testable import MedtrumKit +import XCTest + +final class PrimePacketTests : XCTestCase { + func testRequestGivenPacketWhenValuesSetThenReturnCorrectByteArray() throws { + let input = PrimePacket() + + let expected = Data([5, 16, 0, 0, 164, 0]) + + let sequence: UInt8 = 0 + let actual = input.encode(sequenceNumber: sequence) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(actual[0], expected) + } +}