├── OmniBLE
├── PumpManagerUI
│ ├── OmnipodUI.xcassets
│ │ ├── Contents.json
│ │ ├── Pod.imageset
│ │ │ ├── pod1x.png
│ │ │ ├── pod2x.png
│ │ │ ├── pod3x.png
│ │ │ └── Contents.json
│ │ ├── PodBottom.imageset
│ │ │ ├── PodBottom1x.png
│ │ │ ├── PodBottom2x.png
│ │ │ ├── PodBottom3x.png
│ │ │ └── Contents.json
│ │ └── PodLarge.imageset
│ │ │ ├── PodLarge@1x.png
│ │ │ ├── PodLarge@2x.png
│ │ │ ├── PodLarge@3x.png
│ │ │ └── Contents.json
│ ├── Views
│ │ ├── HUDAssets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── pod_life
│ │ │ │ ├── Contents.json
│ │ │ │ └── pod_life.imageset
│ │ │ │ │ ├── Pod Life.pdf
│ │ │ │ │ └── Contents.json
│ │ │ └── reservoir
│ │ │ │ ├── Contents.json
│ │ │ │ ├── pod_reservoir.imageset
│ │ │ │ ├── reservoir.pdf
│ │ │ │ └── Contents.json
│ │ │ │ └── pod_reservoir_mask.imageset
│ │ │ │ ├── reservoir_mask.pdf
│ │ │ │ └── Contents.json
│ │ ├── ExpirationReminderDateTableViewCell.swift
│ │ ├── OmnipodReservoirView.swift
│ │ └── PodLifeHUDView.swift
│ ├── ViewControllers
│ │ ├── PodReplacementNavigationController.swift
│ │ ├── PodSetupCompleteViewController.swift
│ │ └── OmnipodPumpManagerSetupViewController.swift
│ └── OmnipodPumpManager+UI.swift
├── Bluetooth
│ ├── Session
│ │ ├── Session.swift
│ │ ├── SessionKeys.swift
│ │ ├── EapSqn.swift
│ │ ├── EapMessage.swift
│ │ └── Milenage.swift
│ ├── Pair
│ │ ├── PairResult.swift
│ │ ├── PairMessage.swift
│ │ ├── KeyExchange.swift
│ │ └── LTKExchanger.swift
│ ├── PeripheralManagerError.swift
│ ├── BluetoothErrors.swift
│ ├── Util
│ │ ├── OmniRandomByteGenerator.swift
│ │ └── X25519KeyGenerator.swift
│ ├── EnDecrypt
│ │ ├── Nonce.swift
│ │ └── EnDecrypt.swift
│ ├── Ids.swift
│ ├── CBPeripheral.swift
│ ├── Packet
│ │ ├── PayloadJoiner.swift
│ │ ├── PayloadSplitter.swift
│ │ └── BLEPacket.swift
│ ├── Id.swift
│ ├── StringLengthPrefixEncoding.swift
│ ├── BluetoothServices.swift
│ └── MessagePacket.swift
├── OmniBLE.h
├── OmnipodCommon
│ ├── BasalSchedule+LoopKit.swift
│ ├── MessageBlocks
│ │ ├── PlaceholderMessageBlock.swift
│ │ ├── DeactivatePodCommand.swift
│ │ ├── AssignAddressCommand.swift
│ │ ├── AcknowledgeAlertCommand.swift
│ │ ├── PodInfoResponse.swift
│ │ ├── GetStatusCommand.swift
│ │ ├── FaultConfigCommand.swift
│ │ ├── PodInfoActivationTime.swift
│ │ ├── PodInfo.swift
│ │ ├── ErrorResponse.swift
│ │ ├── PodInfoConfiguredAlerts.swift
│ │ ├── PodInfoPulseLogPlus.swift
│ │ ├── MessageBlock.swift
│ │ ├── CancelDeliveryCommand.swift
│ │ ├── StatusResponse.swift
│ │ ├── BeepConfigCommand.swift
│ │ ├── SetupPodCommand.swift
│ │ ├── BolusExtraCommand.swift
│ │ ├── TempBasalExtraCommand.swift
│ │ ├── PodInfoPulseLog.swift
│ │ ├── BasalScheduleExtraCommand.swift
│ │ └── ConfigureAlertsCommand.swift
│ ├── BeepType.swift
│ ├── PodInsulinMeasurements.swift
│ ├── PodDoseProgressEstimator.swift
│ ├── CRC16.swift
│ ├── PodProgressStatus.swift
│ ├── BasalSchedule.swift
│ ├── Message.swift
│ └── Pod.swift
├── OmnipodPlugin.swift
└── Info.plist
├── Cartfile
├── OmniBLE.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ └── OmniBLETests.xcscheme
├── Cartfile.resolved
├── Common
├── NibLoadable.swift
├── IdentifiableClass.swift
├── NumberFormatter.swift
├── LocalizedString.swift
├── TimeZone.swift
├── HKUnit.swift
├── OSLog.swift
├── TimeInterval.swift
├── UIColor.swift
└── Data.swift
├── OmniBLETests
├── HexConversionTests.swift
├── TestUtilities.swift
├── Info.plist
├── Driver
│ └── Comm
│ │ ├── message
│ │ ├── PayloadSplitJoinTests.swift
│ │ ├── StringLengthPrefixEncodingTests.swift
│ │ ├── PayloadJoinerTest.swift
│ │ ├── PayloadSplitterTest.swift
│ │ └── MessagePacketTests.swift
│ │ ├── pair
│ │ └── KeyExchangeTests.swift
│ │ └── endecrypt
│ │ └── EnDecryptTests.swift
└── OmnipodTests.swift
├── LICENSE
├── carthage.sh
├── README.md
└── .gitignore
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/pod_life/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | github "LoopKit/LoopKit" "loop-release/v2.2.5"
2 | github "LoopKit/MKRingProgressView" "appex-safe"
3 | github "ps2/rileylink_ios" "loop-release/v2.2.5"
4 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod1x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod2x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/pod3x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom1x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom2x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/PodBottom3x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@1x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@2x.png
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/PodLarge@3x.png
--------------------------------------------------------------------------------
/OmniBLE.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/pod_life/pod_life.imageset/Pod Life.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/pod_life/pod_life.imageset/Pod Life.pdf
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "LoopKit/LoopKit" "3a67f4ac7a1f2484f8527304eb14f21e30f6ca95"
2 | github "LoopKit/MKRingProgressView" "f548a5c64832be2d37d7c91b5800e284887a2a0a"
3 | github "ps2/rileylink_ios" "15d19970f589d1678a486a9b7cfa2430111ee3ea"
4 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Session/Session.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Session.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/8/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir.imageset/reservoir.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir.imageset/reservoir.pdf
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/reservoir_mask.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randallknutson/OmniBLE/HEAD/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/reservoir_mask.pdf
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Pair/PairResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PairResult.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/4/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct PairResult {
11 | var ltk: Data
12 | var address: UInt32
13 | var msgSeq: UInt8
14 | }
15 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "reservoir_mask.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/reservoir/pod_reservoir.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "reservoir.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/HUDAssets.xcassets/pod_life/pod_life.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Pod Life.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/Common/NibLoadable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NibLoadable.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nate Racklyeft on 7/2/16.
6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | protocol NibLoadable: IdentifiableClass {
13 | static func nib() -> UINib
14 | }
15 |
16 |
17 | extension NibLoadable {
18 | static func nib() -> UINib {
19 | return UINib(nibName: className, bundle: Bundle(for: self))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Session/SessionKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SessionKeys.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/8/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct SessionKeys {
12 | var ck: Data
13 | var nonce: Nonce
14 | var msgSequenceNumber: Int
15 | }
16 |
17 | struct SessionNegotiationResynchronization {
18 | let synchronizedEapSqn: EapSqn
19 | let msgSequenceNumber: UInt8
20 | }
21 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/PeripheralManagerError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PeripheralManagerErrors.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/18/21.
6 | //
7 |
8 | import Foundation
9 |
10 | enum PeripheralManagerError: Error {
11 | case cbPeripheralError(Error)
12 | case notReady
13 | case incorrectResponse
14 | case timeout([PeripheralManager.CommandCondition])
15 | case emptyValue
16 | case unknownCharacteristic
17 | case serviceNotFound
18 | case nack
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/Common/IdentifiableClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IdentifiableClass.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nathan Racklyeft on 2/9/16.
6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | protocol IdentifiableClass: AnyObject {
13 | static var className: String { get }
14 | }
15 |
16 |
17 | extension IdentifiableClass {
18 | static var className: String {
19 | return NSStringFromClass(self).components(separatedBy: ".").last!
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/OmniBLE/OmniBLE.h:
--------------------------------------------------------------------------------
1 | //
2 | // OmniBLE.h
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 9/11/21.
6 | //
7 |
8 | #import
9 |
10 | //! Project version number for OmniBLE.
11 | FOUNDATION_EXPORT double OmniBLEVersionNumber;
12 |
13 | //! Project version string for OmniBLE.
14 | FOUNDATION_EXPORT const unsigned char OmniBLEVersionString[];
15 |
16 | // In this header, you should import all the public headers of your framework using statements like #import
17 |
18 |
19 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/Pod.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "pod1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "pod2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "pod3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodLarge.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "PodLarge@1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "PodLarge@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "PodLarge@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodUI.xcassets/PodBottom.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "PodBottom1x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "PodBottom2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "PodBottom3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/BasalSchedule+LoopKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BasalSchedule+LoopKit.swift
3 | // (formerly PodCommsSession+LoopKit.swift)
4 | // OmnipodKit
5 | //
6 | // Created by Pete Schwamb on 9/25/18.
7 | // Copyright © 2018 Pete Schwamb. All rights reserved.
8 | //
9 |
10 | import Foundation
11 | import LoopKit
12 |
13 | extension BasalSchedule {
14 | public init(repeatingScheduleValues: [LoopKit.RepeatingScheduleValue]) {
15 | self.init(entries: repeatingScheduleValues.map { BasalScheduleEntry(rate: $0.value, startTime: $0.startTime) })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/OmniBLETests/HexConversionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HexConversionTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class HexConversionTests: XCTestCase {
13 |
14 | func testConversion(){
15 | let hexString = "00,01,54,57,10,23,03,00,00,c0,ff,ff,ff,fe,08,20,2e,a8,50,30".replacingOccurrences(of: ",", with: "")
16 | let f1 = Data(hexadecimalString: hexString)!
17 | assert(f1.hexadecimalString == hexString)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/BluetoothErrors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BluetoothErrors.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/3/21.
6 | //
7 |
8 | import Foundation
9 | import CoreBluetooth
10 |
11 | enum BluetoothErrors: Error {
12 | case DiscoveredInvalidPodException(_ message: String, _ data: [CBUUID])
13 | case InvalidLTKKey(_ message: String)
14 | case PairingException(_ message: String)
15 | case MessageIOException(_ message: String)
16 | case CouldNotParseMessageException(_ message: String)
17 | case IncorrectPacketException(_ payload: Data, _ location: Int)
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/OmniBLETests/TestUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestUtilities.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | extension String {
10 | //From start to, but not including, toIndex
11 | func substring(startIndex _startIndexInt: Int, toIndex _toIndexInt: Int) -> String? {
12 | assert(_startIndexInt < _toIndexInt)
13 | let startIndex = index(self.startIndex, offsetBy: _startIndexInt)
14 | let endIndex = index(self.startIndex, offsetBy: _toIndexInt - 1)
15 | return String(self[startIndex...endIndex])
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Common/NumberFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberFormatter.swift
3 | // OmnipodKit
4 | //
5 | // Copyright © 2017 Pete Schwamb. All rights reserved.
6 | //
7 |
8 | import Foundation
9 |
10 | extension NumberFormatter {
11 | func decibleString(from decibles: Int?) -> String? {
12 | if let decibles = decibles, let formatted = string(from: NSNumber(value: decibles)) {
13 | return String(format: LocalizedString("%@ dB", comment: "Unit format string for an RSSI value in decibles"), formatted)
14 | } else {
15 | return nil
16 | }
17 | }
18 |
19 | func string(from number: Double) -> String? {
20 | return string(from: NSNumber(value: number))
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Util/OmniRandomByteGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RandomByteGenerator.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/8/21.
6 | //
7 |
8 | import Foundation
9 | class OmniRandomByteGenerator: RandomByteGenerator {
10 | func nextBytes(length: Int) -> Data {
11 | var bytes = [Int8](repeating: 0, count: length)
12 | let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
13 | if status == errSecSuccess { // Always test the status.
14 | return Data(bytes: bytes, count: bytes.count)
15 | }
16 | return Data()
17 | }
18 | }
19 |
20 | protocol RandomByteGenerator {
21 | func nextBytes(length: Int) -> Data
22 | }
23 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodPlugin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OmniBLEPlugin.swift
3 | // OmniBLEPlugin
4 | //
5 | // Created by Randall Knutson on 09/11/21.
6 | //
7 |
8 | import Foundation
9 | import LoopKitUI
10 | import OmniKit
11 | import OmniKitUI
12 | import os.log
13 |
14 | class OmnipodPlugin: NSObject, LoopUIPlugin {
15 | private let log = OSLog(category: "OmnipodPlugin")
16 |
17 | public var pumpManagerType: PumpManagerUI.Type? {
18 | return OmnipodPumpManager.self
19 | }
20 |
21 | public var cgmManagerType: CGMManagerUI.Type? {
22 | return nil
23 | }
24 |
25 | override init() {
26 | super.init()
27 | log.default("OmnipodPlugin Instantiated")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/EnDecrypt/Nonce.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Nonce.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/4/21.
6 | //
7 |
8 | import Foundation
9 |
10 | class Nonce {
11 | let prefix: Data
12 |
13 | init (prefix: Data) {
14 | guard prefix.count == 8 else { fatalError("Nonce prefix should be 8 bytes long") }
15 | self.prefix = prefix
16 | }
17 |
18 | func toData(sqn: Int, podReceiving: Bool) -> Data {
19 | var ret = Data(bigEndian: sqn)
20 | .subdata(in: 3..<8)
21 | if (podReceiving) {
22 | ret[0] = UInt8(ret[0] & 127)
23 | } else {
24 | ret[0] = UInt8(ret[0] | 128)
25 | }
26 | return prefix + ret
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Common/LocalizedString.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalizedString.swift
3 | // OmnipodKit
4 | //
5 | // Created by Kathryn DiSimone on 8/15/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private class FrameworkBundle {
12 | static let main = Bundle(for: FrameworkBundle.self)
13 | }
14 |
15 | func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String {
16 | if let value = value {
17 | return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment)
18 | } else {
19 | return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Ids.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Ids.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/5/21.
6 | //
7 |
8 | import Foundation
9 |
10 | let CONTROLLER_ID: Int = 4242
11 | let POD_ID_NOT_ACTIVATED = Data(hexadecimalString: "FFFFFFFE")!
12 |
13 | public class Ids {
14 | static func notActivated() -> Id {
15 | return Id(POD_ID_NOT_ACTIVATED)
16 | }
17 | static func controllerId() -> Id {
18 | return Id.fromInt(CONTROLLER_ID)
19 | }
20 | let myId: Id
21 | let podId: Id
22 |
23 | init(podState: PodState?) {
24 | myId = Id.fromInt(CONTROLLER_ID)
25 | let uniqueId = podState != nil ? Id.fromLong(podState!.address) : myId
26 | podId = uniqueId.increment()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/OmniBLETests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Session/EapSqn.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EapSqn.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/17/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum EapSqnError: Error {
12 | case InvalidSize
13 | }
14 |
15 | class EapSqn {
16 | private let SIZE = 6
17 | let data: Data
18 |
19 | init(data: Data) throws {
20 | guard data.count == SIZE else { throw EapSqnError.InvalidSize }
21 | self.data = data
22 | }
23 |
24 | init(int: Int) {
25 | self.data = withUnsafeBytes(of: int.bigEndian) { Data($0) }
26 | }
27 |
28 | func toInt() -> Int {
29 | return (Data([0x00, 0x00]) + data).withUnsafeBytes {
30 | $0.load(as: Int.self).bigEndian
31 | }
32 | }
33 |
34 | func increment() -> EapSqn {
35 | return EapSqn(int: toInt() + 1)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PlaceholderMessageBlock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaceholderMessageBlock.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 10/24/17.
6 | // Copyright © 2017 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct PlaceholderMessageBlock: MessageBlock {
12 | public let blockType: MessageBlockType
13 | public let length: UInt8
14 |
15 | public let data: Data
16 |
17 | public init(encodedData: Data) throws {
18 | if encodedData.count < 2 {
19 | throw MessageBlockError.notEnoughData
20 | }
21 | guard let blockType = MessageBlockType(rawValue: encodedData[0]) else {
22 | throw MessageBlockError.unknownBlockType(rawVal: encodedData[0])
23 | }
24 | self.blockType = blockType
25 | length = encodedData[1]
26 | data = encodedData.prefix(upTo: Int(length))
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/message/PayloadSplitJoinTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayloadSplitJoinTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class PayloadSplitJoinTests: XCTestCase {
13 |
14 | func testSplitAndJoinBack() {
15 | for _ in 0...250 {
16 | let payload = Data(Int.random(in: 1..<100))
17 | let splitter = PayloadSplitter(payload: payload)
18 | let packets = splitter.splitInPackets()
19 | let joiner = try! PayloadJoiner(firstPacket: packets[0].toData())
20 | for p in packets[1...] {
21 | try! joiner.accumulate(packet: p.toData())
22 | }
23 | let got = try! joiner.finalize()
24 | assert(got.hexadecimalString == payload.hexadecimalString)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Common/TimeZone.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeZone.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nate Racklyeft on 10/2/16.
6 | // Copyright © 2016 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension TimeZone {
12 | static var currentFixed: TimeZone {
13 | return TimeZone(secondsFromGMT: TimeZone.current.secondsFromGMT())!
14 | }
15 |
16 | var fixed: TimeZone {
17 | return TimeZone(secondsFromGMT: secondsFromGMT())!
18 | }
19 |
20 | /// This only works for fixed utc offset timezones
21 | func scheduleOffset(forDate date: Date) -> TimeInterval {
22 | var calendar = Calendar.current
23 | calendar.timeZone = self
24 | let components = calendar.dateComponents([.day , .month, .year], from: date)
25 | guard let startOfSchedule = calendar.date(from: components) else {
26 | fatalError("invalid date")
27 | }
28 | return date.timeIntervalSince(startOfSchedule)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/message/StringLengthPrefixEncodingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringLengthPrefixEncodingTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class StringLengthPrefixEncodingTests: XCTestCase {
13 |
14 | let p0Payload = Data(hexadecimalString: "50,30,3d,00,01,a5".replacingOccurrences(of: ",", with: ""))!
15 | let p0Content = Data(hexadecimalString:"a5")!
16 |
17 | func testFormatKeysP0() {
18 | let payload = StringLengthPrefixEncoding.formatKeys(keys: ["P0="], payloads: [p0Content])
19 | assert(p0Payload.hexadecimalString == payload.hexadecimalString)
20 | }
21 |
22 | func testParseKeysP0() {
23 | let parsed = try! StringLengthPrefixEncoding.parseKeys(["P0="], p0Payload)
24 | assert(parsed.count == 1)
25 | assert(parsed[0].toHexString() == p0Content.toHexString())
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/message/PayloadJoinerTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayloadJoinerTest.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class PayloadJoinerTest: XCTestCase {
13 |
14 | func testJoiner() {
15 |
16 | let f1 = Data(hexadecimalString: "00,01,54,57,10,23,03,00,00,c0,ff,ff,ff,fe,08,20,2e,a8,50,30".replacingOccurrences(of: ",", with: ""))!
17 | let f2 = Data(hexadecimalString: "01,04,bc,20,1f,f6,3d,00,01,a5,ff,ff,ff,fe,08,20,2e,a8,50,30".replacingOccurrences(of: ",", with: ""))!
18 |
19 | let payload = "54,57,10,23,03,00,00,c0,ff,ff,ff,fe,08,20,2e,a8,50,30,3d,00,01,a5".replacingOccurrences(of: ",", with: "")
20 | let joiner = try! PayloadJoiner(firstPacket: f1)
21 | try! joiner.accumulate(packet: f2)
22 | let actual = try! joiner.finalize()
23 | assert(payload == actual.hexadecimalString)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/OmniBLETests/OmnipodTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OmnipodTests.swift
3 | // OmnipodTests
4 | //
5 | // Created by Randall Knutson on 9/11/21.
6 | //
7 |
8 | import XCTest
9 | @testable import OmniBLE
10 |
11 | class OmnipodTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Common/HKUnit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HKUnit.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nathan Racklyeft on 1/17/16.
6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | extension HKUnit {
13 | static let milligramsPerDeciliter: HKUnit = {
14 | return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
15 | }()
16 |
17 | static let millimolesPerLiter: HKUnit = {
18 | return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
19 | }()
20 |
21 | var foundationUnit: Unit? {
22 | if self == HKUnit.milligramsPerDeciliter {
23 | return UnitConcentrationMass.milligramsPerDeciliter
24 | }
25 |
26 | if self == HKUnit.millimolesPerLiter {
27 | return UnitConcentrationMass.millimolesPerLiter(withGramsPerMole: HKUnitMolarMassBloodGlucose)
28 | }
29 |
30 | if self == HKUnit.gram() {
31 | return UnitMass.grams
32 | }
33 |
34 | return nil
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/OmniBLE/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | com.loopkit.Loop.PumpManagerDisplayName
22 | Omnipod Dash
23 | com.loopkit.Loop.PumpManagerIdentifier
24 | Omnipod-Dash
25 | NSPrincipalClass
26 | OmniBLE.OmnipodPlugin
27 |
28 |
29 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Pair/PairMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PairMessage.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/4/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct PairMessage {
11 | public let sequenceNumber: UInt8
12 | public let source: Id
13 | public let destination: Id
14 | private let keys: [String]
15 | private let payloads: [Data]
16 | public let message: MessagePacket
17 |
18 | init(sequenceNumber: UInt8, source: Id, destination: Id, keys: [String], payloads: [Data]) {
19 | self.sequenceNumber = sequenceNumber
20 | self.source = source
21 | self.destination = destination
22 | self.keys = keys
23 | self.payloads = payloads
24 | message = MessagePacket(
25 | type: MessageType.PAIRING,
26 | destination: destination.toUInt32(),
27 | payload: StringLengthPrefixEncoding.formatKeys(
28 | keys: keys,
29 | payloads: payloads
30 | ),
31 | sequenceNumber :sequenceNumber,
32 | sas: true
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/CBPeripheral.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CBPeripheral.swift
3 | // xDripG5
4 | //
5 | // Copyright © 2017 LoopKit Authors. All rights reserved.
6 | //
7 |
8 | import CoreBluetooth
9 |
10 |
11 | // MARK: - Discovery helpers.
12 | extension CBPeripheral {
13 | func servicesToDiscover(from serviceUUIDs: [CBUUID]) -> [CBUUID] {
14 | let knownServiceUUIDs = services?.compactMap({ $0.uuid }) ?? []
15 | return serviceUUIDs.filter({ !knownServiceUUIDs.contains($0) })
16 | }
17 |
18 | func characteristicsToDiscover(from characteristicUUIDs: [CBUUID], for service: CBService) -> [CBUUID] {
19 | let knownCharacteristicUUIDs = service.characteristics?.compactMap({ $0.uuid }) ?? []
20 | return characteristicUUIDs.filter({ !knownCharacteristicUUIDs.contains($0) })
21 | }
22 | }
23 |
24 |
25 | extension Collection where Element: CBAttribute {
26 | func itemWithUUID(_ uuid: CBUUID) -> Element? {
27 | for attribute in self {
28 | if attribute.uuid == uuid {
29 | return attribute
30 | }
31 | }
32 |
33 | return nil
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Randall Knutson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeactivatePodCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 2/24/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct DeactivatePodCommand : NonceResyncableMessageBlock {
12 |
13 | // ID1:1f00ee84 PTYPE:PDM SEQ:09 ID2:1f00ee84 B9:34 BLEN:6 MTYPE:1c04 BODY:0f7dc4058344 CRC:f1
14 |
15 | public let blockType: MessageBlockType = .deactivatePod
16 |
17 | public var nonce: UInt32
18 |
19 | // e1f78752 07 8196
20 | public var data: Data {
21 | var data = Data([
22 | blockType.rawValue,
23 | 4,
24 | ])
25 | data.appendBigEndian(nonce)
26 | return data
27 | }
28 |
29 | public init(encodedData: Data) throws {
30 | if encodedData.count < 6 {
31 | throw MessageBlockError.notEnoughData
32 | }
33 | self.nonce = encodedData[2...].toBigEndian(UInt32.self)
34 | }
35 |
36 | public init(nonce: UInt32) {
37 | self.nonce = nonce
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/message/PayloadSplitterTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayloadSplitterTest.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class PayloadSplitterTest: XCTestCase {
13 | func testSplitter() {
14 | let f1 = "00,01,54,57,10,23,03,00,00,c0,ff,ff,ff,fe,08,20,2e,a8,50,30".replacingOccurrences(of:",", with:"")
15 | let f2 = "01,04,bc,20,1f,f6,3d,00,01,a5,ff,ff,ff,fe,08,20,2e,a8,50,30".replacingOccurrences(of:",", with:"")
16 | let payload = Data(hexadecimalString: "54,57,10,23,03,00,00,c0,ff,ff,ff,fe,08,20,2e,a8,50,30,3d,00,01,a5".replacingOccurrences(of:",", with:""))!
17 |
18 | let splitter = PayloadSplitter(payload: payload)
19 | let packets = splitter.splitInPackets()
20 |
21 | assert(packets.count == 2)
22 | assert(f1 == packets[0].toData().hexadecimalString)
23 | let p2 = packets[1].toData().hexadecimalString
24 | assert(p2.count >= 10)
25 |
26 | assert(f2.substring(startIndex:0, toIndex:20) == p2.substring(startIndex: 0, toIndex: 20))
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/carthage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
6 | trap 'rm -f "$xcconfig"' INT TERM HUP EXIT
7 |
8 | # For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise
9 | # the build will fail on lipo due to duplicate architectures.
10 |
11 | CURRENT_XCODE_VERSION="$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2 | cut -d'.' -f1)00"
12 | CURRENT_XCODE_BUILD=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3)
13 |
14 | echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_${CURRENT_XCODE_VERSION}__BUILD_${CURRENT_XCODE_BUILD} = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig
15 |
16 | echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_'${CURRENT_XCODE_VERSION}' = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_$(XCODE_VERSION_MAJOR)__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig
17 | echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig
18 |
19 | export XCODE_XCCONFIG_FILE="$xcconfig"
20 | carthage "$@"
21 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/AssignAddressCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AssignAddressCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 2/12/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct AssignAddressCommand : MessageBlock {
12 |
13 | public let blockType: MessageBlockType = .assignAddress
14 | public let length: Int = 6
15 |
16 | let address: UInt32
17 |
18 | public var data: Data {
19 | var data = Data([
20 | blockType.rawValue,
21 | 4
22 | ])
23 | data.appendBigEndian(self.address)
24 | return data
25 | }
26 |
27 | public init(encodedData: Data) throws {
28 | if encodedData.count < length {
29 | throw MessageBlockError.notEnoughData
30 | }
31 |
32 | self.address = encodedData[2...].toBigEndian(UInt32.self)
33 | }
34 |
35 | public init(address: UInt32) {
36 | self.address = address
37 | }
38 | }
39 |
40 | extension AssignAddressCommand: CustomDebugStringConvertible {
41 | public var debugDescription: String {
42 | return "AssignAddressCommand(address:\(Data(bigEndian: address).hexadecimalString))"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Util/X25519KeyGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // X25519KeyGenerator.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/8/21.
6 | //
7 | import CryptoKit
8 | import Foundation
9 |
10 | struct X25519KeyGenerator: PrivateKeyGenerator {
11 | func generatePrivateKey() -> Data {
12 | let key = Curve25519.KeyAgreement.PrivateKey()
13 | return key.rawRepresentation
14 | }
15 | func publicFromPrivate(_ privateKey: Data) throws -> Data{
16 | let key = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKey)
17 | return key.publicKey.rawRepresentation
18 | }
19 | func computeSharedSecret(_ privateKey: Data, _ publicKey: Data) throws -> Data {
20 | let priv = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKey)
21 | let pub = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: publicKey)
22 | let secret = try priv.sharedSecretFromKeyAgreement(with: pub)
23 | return secret.withUnsafeBytes({ return Data($0)})
24 | }
25 | }
26 |
27 | protocol PrivateKeyGenerator {
28 | func generatePrivateKey() -> Data
29 | func publicFromPrivate(_ privateKey: Data) throws -> Data
30 | func computeSharedSecret(_ privateKey: Data, _ publicKey: Data) throws -> Data
31 | }
32 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/AcknowledgeAlertCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AcknowledgeAlertCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Eelke Jager on 18/09/2018.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct AcknowledgeAlertCommand : NonceResyncableMessageBlock {
12 | // OFF 1 2 3 4 5 6
13 | // 11 05 NNNNNNNN MM
14 |
15 | public let blockType: MessageBlockType = .acknowledgeAlert
16 | public let length: UInt8 = 5
17 | public var nonce: UInt32
18 | public let alerts: AlertSet
19 |
20 | public init(nonce: UInt32, alerts: AlertSet) {
21 | self.nonce = nonce
22 | self.alerts = alerts
23 | }
24 |
25 | public init(encodedData: Data) throws {
26 | if encodedData.count < 7 {
27 | throw MessageBlockError.notEnoughData
28 | }
29 | self.nonce = encodedData[2...].toBigEndian(UInt32.self)
30 | self.alerts = AlertSet(rawValue: encodedData[6])
31 | }
32 |
33 | public var data: Data {
34 | var data = Data([
35 | blockType.rawValue,
36 | length
37 | ])
38 | data.appendBigEndian(nonce)
39 | data.append(alerts.rawValue)
40 | return data
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInfoResponse.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 2/23/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct PodInfoResponse : MessageBlock {
12 |
13 | public let blockType : MessageBlockType = .podInfoResponse
14 | public let podInfoResponseSubType : PodInfoResponseSubType
15 | public let podInfo : PodInfo
16 | public let data : Data
17 |
18 | public init(encodedData: Data) throws {
19 | guard let subType = PodInfoResponseSubType(rawValue: encodedData[2]) else {
20 | throw MessageError.unknownValue(value: encodedData[2], typeDescription: "PodInfoResponseSubType")
21 | }
22 | self.podInfoResponseSubType = subType
23 | let len = encodedData.count
24 | podInfo = try podInfoResponseSubType.podInfoType.init(encodedData: encodedData.subdata(in: 2..= 16 else {
25 | throw MessageBlockError.notEnoughData
26 | }
27 | self.faultEventCode = FaultEventCode(rawValue: encodedData[1])
28 | self.timeActivation = TimeInterval(minutes: Double((Int(encodedData[2] & 0b1) << 8) + Int(encodedData[3])))
29 | self.dateTime = DateComponents(encodedDateTime: encodedData.subdata(in: 12..<17))
30 | self.data = Data(encodedData)
31 | }
32 | }
33 |
34 | extension DateComponents {
35 | init(encodedDateTime: Data) {
36 | self.init()
37 |
38 | year = Int(encodedDateTime[2]) + 2000
39 | month = Int(encodedDateTime[0])
40 | day = Int(encodedDateTime[1])
41 | hour = Int(encodedDateTime[3])
42 | minute = Int(encodedDateTime[4])
43 |
44 | calendar = Calendar(identifier: .gregorian)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInfoResponseSubType.swift
3 | // OmniKit
4 | //
5 | // Created by Eelke Jager on 15/09/2018.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol PodInfo {
12 | init(encodedData: Data) throws
13 | var podInfoType: PodInfoResponseSubType { get }
14 | var data: Data { get }
15 |
16 | }
17 |
18 | public enum PodInfoResponseSubType: UInt8, Equatable {
19 | case normal = 0x00
20 | case configuredAlerts = 0x01 // Returns information on configured alerts
21 | case detailedStatus = 0x02 // Returned on any pod fault
22 | case pulseLogPlus = 0x03 // Returns up to the last 60 pulse log entries plus additional info
23 | case activationTime = 0x05 // Returns activation date, elapsed time, and fault code
24 | case pulseLogRecent = 0x50 // Returns the last 50 pulse log entries
25 | case pulseLogPrevious = 0x51 // Like 0x50, but returns up to the previous 50 entries before the last 50
26 |
27 | public var podInfoType: PodInfo.Type {
28 | switch self {
29 | case .normal:
30 | return StatusResponse.self as! PodInfo.Type
31 | case .configuredAlerts:
32 | return PodInfoConfiguredAlerts.self
33 | case .detailedStatus:
34 | return DetailedStatus.self
35 | case .pulseLogPlus:
36 | return PodInfoPulseLogPlus.self
37 | case .activationTime:
38 | return PodInfoActivationTime.self
39 | case .pulseLogRecent:
40 | return PodInfoPulseLogRecent.self
41 | case .pulseLogPrevious:
42 | return PodInfoPulseLogPrevious.self
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Common/TimeInterval.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSTimeInterval.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nathan Racklyeft on 1/9/16.
6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension TimeInterval {
13 |
14 | static func days(_ days: Double) -> TimeInterval {
15 | return self.init(days: days)
16 | }
17 |
18 | static func hours(_ hours: Double) -> TimeInterval {
19 | return self.init(hours: hours)
20 | }
21 |
22 | static func minutes(_ minutes: Int) -> TimeInterval {
23 | return self.init(minutes: Double(minutes))
24 | }
25 |
26 | static func minutes(_ minutes: Double) -> TimeInterval {
27 | return self.init(minutes: minutes)
28 | }
29 |
30 | static func seconds(_ seconds: Double) -> TimeInterval {
31 | return self.init(seconds)
32 | }
33 |
34 | static func milliseconds(_ milliseconds: Double) -> TimeInterval {
35 | return self.init(milliseconds / 1000)
36 | }
37 |
38 | init(days: Double) {
39 | self.init(hours: days * 24)
40 | }
41 |
42 | init(hours: Double) {
43 | self.init(minutes: hours * 60)
44 | }
45 |
46 | init(minutes: Double) {
47 | self.init(minutes * 60)
48 | }
49 |
50 | init(seconds: Double) {
51 | self.init(seconds)
52 | }
53 |
54 | init(milliseconds: Double) {
55 | self.init(milliseconds / 1000)
56 | }
57 |
58 | var milliseconds: Double {
59 | return self * 1000
60 | }
61 |
62 | init(hundredthsOfMilliseconds: Double) {
63 | self.init(hundredthsOfMilliseconds / 100000)
64 | }
65 |
66 | var hundredthsOfMilliseconds: Double {
67 | return self * 100000
68 | }
69 |
70 | var minutes: Double {
71 | return self / 60.0
72 | }
73 |
74 | var hours: Double {
75 | return minutes / 60.0
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/PodInsulinMeasurements.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInsulinMeasurements.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 9/5/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct PodInsulinMeasurements: RawRepresentable, Equatable {
12 | public typealias RawValue = [String: Any]
13 |
14 | public let validTime: Date
15 | public let delivered: Double
16 | public let reservoirLevel: Double?
17 |
18 | public init(insulinDelivered: Double, reservoirLevel: Double?, setupUnitsDelivered: Double?, validTime: Date) {
19 | self.validTime = validTime
20 | self.reservoirLevel = reservoirLevel
21 | if let setupUnitsDelivered = setupUnitsDelivered {
22 | self.delivered = insulinDelivered - setupUnitsDelivered
23 | } else {
24 | // subtract off the fixed setup command values as we don't have an actual value (yet)
25 | self.delivered = max(insulinDelivered - Pod.primeUnits - Pod.cannulaInsertionUnits, 0)
26 | }
27 | }
28 |
29 | // RawRepresentable
30 | public init?(rawValue: RawValue) {
31 | guard
32 | let validTime = rawValue["validTime"] as? Date,
33 | let delivered = rawValue["delivered"] as? Double
34 | else {
35 | return nil
36 | }
37 | self.validTime = validTime
38 | self.delivered = delivered
39 | self.reservoirLevel = rawValue["reservoirLevel"] as? Double
40 | }
41 |
42 | public var rawValue: RawValue {
43 | var rawValue: RawValue = [
44 | "validTime": validTime,
45 | "delivered": delivered
46 | ]
47 |
48 | if let reservoirLevel = reservoirLevel {
49 | rawValue["reservoirLevel"] = reservoirLevel
50 | }
51 |
52 | return rawValue
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/PodDoseProgressEstimator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodDoseProgressEstimator.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 3/12/19.
6 | // Copyright © 2019 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import LoopKit
11 |
12 | class PodDoseProgressEstimator: DoseProgressTimerEstimator {
13 |
14 | let dose: DoseEntry
15 |
16 | weak var pumpManager: PumpManager?
17 |
18 | override var progress: DoseProgress {
19 | let elapsed = -dose.startDate.timeIntervalSinceNow
20 | let duration = dose.endDate.timeIntervalSince(dose.startDate)
21 | let percentComplete = min(elapsed / duration, 1)
22 | let delivered = pumpManager?.roundToSupportedBolusVolume(units: percentComplete * dose.programmedUnits) ?? dose.programmedUnits
23 | return DoseProgress(deliveredUnits: delivered, percentComplete: percentComplete)
24 | }
25 |
26 | init(dose: DoseEntry, pumpManager: PumpManager, reportingQueue: DispatchQueue) {
27 | self.dose = dose
28 | self.pumpManager = pumpManager
29 | super.init(reportingQueue: reportingQueue)
30 | }
31 |
32 | override func timerParameters() -> (delay: TimeInterval, repeating: TimeInterval) {
33 | let timeSinceStart = dose.startDate.timeIntervalSinceNow
34 | let timeBetweenPulses: TimeInterval
35 | switch dose.type {
36 | case .bolus:
37 | timeBetweenPulses = Pod.pulseSize / Pod.bolusDeliveryRate
38 | case .basal, .tempBasal:
39 | timeBetweenPulses = Pod.pulseSize / (dose.unitsPerHour / TimeInterval(hours: 1))
40 | default:
41 | fatalError("Can only estimate progress on basal rates or boluses.")
42 | }
43 | let delayUntilNextPulse = timeBetweenPulses - timeSinceStart.remainder(dividingBy: timeBetweenPulses)
44 |
45 | return (delay: delayUntilNextPulse, repeating: timeBetweenPulses)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/ErrorResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorResponse.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 2/25/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | fileprivate let errorResponseCode_badNonce: UInt8 = 0x14
12 |
13 | public enum ErrorResponseType {
14 | case badNonce(nonceResyncKey: UInt16)
15 | case nonretryableError(code: UInt8, faultEventCode: FaultEventCode, podProgress: PodProgressStatus)
16 | }
17 |
18 | // 06 14 WWWW, WWWW is the encoded nonce resync key
19 | // 06 EE FF0P, EE != 0x14, FF = fault code (if any), 0P = pod progress status (1..15)
20 |
21 | public struct ErrorResponse : MessageBlock {
22 | public let blockType: MessageBlockType = .errorResponse
23 | public let errorResponseType: ErrorResponseType
24 | public let data: Data
25 |
26 | public init(encodedData: Data) throws {
27 | let errorCode = encodedData[2]
28 | switch (errorCode) {
29 | case errorResponseCode_badNonce:
30 | // For this error code only the 2 next bytes are the encoded nonce resync key.
31 | let nonceResyncKey: UInt16 = encodedData[3...].toBigEndian(UInt16.self)
32 | errorResponseType = .badNonce(nonceResyncKey: nonceResyncKey)
33 | break
34 | default:
35 | // All other error codes are some non-retryable command error. In this case,
36 | // the next 2 bytes are any saved fault code (typically 0) and the pod progress value.
37 | let faultEventCode = FaultEventCode(rawValue: encodedData[3])
38 | guard let podProgress = PodProgressStatus(rawValue: encodedData[4]) else {
39 | throw MessageError.unknownValue(value: encodedData[4], typeDescription: "ErrorResponse PodProgressStatus")
40 | }
41 | errorResponseType = .nonretryableError(code: errorCode, faultEventCode: faultEventCode, podProgress: podProgress)
42 | break
43 | }
44 | self.data = encodedData
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInfoConfiguredAlerts.swift
3 | // OmniKit
4 | //
5 | // Created by Eelke Jager on 16/09/2018.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Type 1 Pod Info returns information about the currently configured alerts
12 | public struct PodInfoConfiguredAlerts : PodInfo {
13 | // CMD 1 2 3 4 5 6 7 8 910 1112 1314 1516 1718 1920
14 | // DATA 0 1 2 3 4 5 6 7 8 910 1112 1314 1516 1718
15 | // 02 13 01 XXXX VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV
16 |
17 | public let podInfoType : PodInfoResponseSubType = .configuredAlerts
18 | public let word_278 : Data
19 | public let alertsActivations : [AlertActivation]
20 | public let data : Data
21 |
22 | public struct AlertActivation {
23 | let beepType: BeepType
24 | let unitsLeft: Double
25 | let timeFromPodStart: UInt8
26 |
27 | public init(beepType: BeepType, timeFromPodStart: UInt8, unitsLeft: Double) {
28 | self.beepType = beepType
29 | self.timeFromPodStart = timeFromPodStart
30 | self.unitsLeft = unitsLeft
31 | }
32 | }
33 |
34 | public init(encodedData: Data) throws {
35 | guard encodedData.count >= 11 else {
36 | throw MessageBlockError.notEnoughData
37 | }
38 |
39 | self.word_278 = encodedData[1...2]
40 |
41 | let numAlertTypes = 8
42 | let beepType = BeepType.self
43 |
44 | var activations = [AlertActivation]()
45 |
46 | for alarmType in (0.. = Array()
16 |
17 | init(firstPacket: Data) throws {
18 | let firstPacket = try FirstBlePacket.parse(payload: firstPacket)
19 | fragments.append(firstPacket)
20 | fullFragments = firstPacket.fullFragments
21 | crc = firstPacket.crc32
22 | oneExtraPacket = firstPacket.oneExtraPacket
23 | }
24 |
25 | func accumulate(packet: Data) throws {
26 | if (packet.count < 3) { // idx, size, at least 1 byte of payload
27 | throw BluetoothErrors.IncorrectPacketException(packet, (expectedIndex + 1))
28 | }
29 | let idx = Int(packet[0])
30 | if (idx != expectedIndex + 1) {
31 | throw BluetoothErrors.IncorrectPacketException(packet, (expectedIndex + 1))
32 | }
33 | expectedIndex += 1
34 | switch idx{
35 | case let index where index < fullFragments:
36 | fragments.append(try MiddleBlePacket.parse(payload: packet))
37 | case let index where index == fullFragments:
38 | let lastPacket = try LastBlePacket.parse(payload: packet)
39 | fragments.append(lastPacket)
40 | crc = lastPacket.crc32
41 | oneExtraPacket = lastPacket.oneExtraPacket
42 | case let index where index == fullFragments + 1 && oneExtraPacket:
43 | fragments.append(try LastOptionalPlusOneBlePacket.parse(payload: packet))
44 | case let index where index > fullFragments:
45 | throw BluetoothErrors.IncorrectPacketException(packet, idx)
46 | default:
47 | throw BluetoothErrors.IncorrectPacketException(packet, idx)
48 | }
49 | }
50 |
51 | func finalize() throws -> Data {
52 | let payloads = fragments.map { x in x.payload }
53 | let bb = payloads.reduce(Data(), { acc, elem in acc + elem })
54 | if (bb.crc32() != crc) {
55 | print("uh oh")
56 | // throw CrcMismatchException(bb.crc32(), crc, bb)
57 | }
58 | return bb
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/message/MessagePacketTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagePacketTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class MessagePacketTests: XCTestCase {
13 |
14 | let payloadString = "54,57,11,01,07,00,03,40,08,20,2e,a8,08,20,2e,a9,ab,35,d8,31,60,9b,b8,fe,3a,3b,de,5b,18,37,24,9a,16,db,f8,e4,d3,05,e9,75,dc,81,7c,37,07,cc,41,5f,af,8a".replacingOccurrences(of: ",", with: "")
15 |
16 | func testParseMessagePacket() {
17 | let msg = try! MessagePacket.parse(payload: Data(hexadecimalString: payloadString)!)
18 | assert(msg.type == MessageType.ENCRYPTED)
19 | assert(msg.source == Id.fromLong(136326824))
20 | assert(msg.destination == Id.fromLong(136326825))
21 | assert(msg.sequenceNumber == 7)
22 | assert(msg.ackNumber == 0)
23 | assert(msg.eqos == 1)
24 | assert(msg.priority == false)
25 | assert(msg.lastMessage == false)
26 | assert(msg.gateway == false)
27 | assert(msg.sas == true)
28 | assert(msg.tfs == false)
29 | assert(msg.version == 0)
30 | let index1 = payloadString.index(payloadString.startIndex, offsetBy: 32)
31 | let toCheck = payloadString[index1...]
32 | assert(msg.payload.hexadecimalString == String(toCheck))
33 | }
34 |
35 |
36 | func testSerializeMessagePacket() {
37 | let payload = Data(hexadecimalString: payloadString)!
38 | let msg = MessagePacket(type: .ENCRYPTED,
39 | source: Id.fromLong(136326824).toUInt32(),
40 | destination: Id.fromLong(136326825).toUInt32(),
41 | payload: payload,
42 | sequenceNumber: 0,
43 | ack: false, ackNumber: 0,
44 | eqos: 1,
45 | priority: false,
46 | lastMessage: false,
47 | gateway: false,
48 | sas: true,
49 | tfs: false,
50 | version: 0)
51 |
52 | assert(msg.payload.bytes.toHexString() == payloadString)
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 | .DS_Store
5 |
6 | ## User settings
7 | xcuserdata/
8 |
9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
10 | *.xcscmblueprint
11 | *.xccheckout
12 |
13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
14 | build/
15 | DerivedData/
16 | *.moved-aside
17 | *.pbxuser
18 | !default.pbxuser
19 | *.mode1v3
20 | !default.mode1v3
21 | *.mode2v3
22 | !default.mode2v3
23 | *.perspectivev3
24 | !default.perspectivev3
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 |
29 | ## App packaging
30 | *.ipa
31 | *.dSYM.zip
32 | *.dSYM
33 |
34 | ## Playgrounds
35 | timeline.xctimeline
36 | playground.xcworkspace
37 |
38 | # Swift Package Manager
39 | #
40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41 | # Packages/
42 | # Package.pins
43 | # Package.resolved
44 | # *.xcodeproj
45 | #
46 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
47 | # hence it is not needed unless you have added a package configuration file to your project
48 | # .swiftpm
49 |
50 | .build/
51 |
52 | # CocoaPods
53 | #
54 | # We recommend against adding the Pods directory to your .gitignore. However
55 | # you should judge for yourself, the pros and cons are mentioned at:
56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
57 | #
58 | # Pods/
59 | #
60 | # Add this line if you want to avoid checking in source code from the Xcode workspace
61 | # *.xcworkspace
62 |
63 | # Carthage
64 | #
65 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
66 | # Carthage/Checkouts
67 |
68 | Carthage/Build/
69 |
70 | # Accio dependency management
71 | Dependencies/
72 | .accio/
73 |
74 | # fastlane
75 | #
76 | # It is recommended to not store the screenshots in the git repo.
77 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
78 | # For more information about the recommended setup visit:
79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
80 |
81 | fastlane/report.xml
82 | fastlane/Preview.html
83 | fastlane/screenshots/**/*.png
84 | fastlane/test_output
85 |
86 | # Code Injection
87 | #
88 | # After new code Injection tools there's a generated folder /iOSInjectionProject
89 | # https://github.com/johnno1962/injectionforxcode
90 |
91 | iOSInjectionProject/
92 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/ExpirationReminderDateTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExpirationReminderDateTableViewCell.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 4/11/19.
6 | // Copyright © 2019 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import LoopKitUI
11 |
12 | public class ExpirationReminderDateTableViewCell: DatePickerTableViewCell {
13 |
14 | public weak var delegate: DatePickerTableViewCellDelegate?
15 |
16 | @IBOutlet public weak var titleLabel: UILabel! {
17 | didSet {
18 | // Setting this color in code because the nib isn't being applied correctly
19 | if #available(iOSApplicationExtension 13.0, *) {
20 | titleLabel?.textColor = .label
21 | }
22 | }
23 | }
24 |
25 | @IBOutlet public weak var dateLabel: UILabel! {
26 | didSet {
27 | // Setting this color in code because the nib isn't being applied correctly
28 | if #available(iOSApplicationExtension 13.0, *) {
29 | dateLabel?.textColor = .secondaryLabel
30 | }
31 |
32 | switch effectiveUserInterfaceLayoutDirection {
33 | case .leftToRight:
34 | dateLabel?.textAlignment = .right
35 | case .rightToLeft:
36 | dateLabel?.textAlignment = .left
37 | @unknown default:
38 | dateLabel?.textAlignment = .right
39 | }
40 | }
41 | }
42 |
43 | var maximumDate: Date? {
44 | set {
45 | datePicker.maximumDate = newValue
46 | }
47 | get {
48 | return datePicker.maximumDate
49 | }
50 | }
51 |
52 | var minimumDate: Date? {
53 | set {
54 | datePicker.minimumDate = newValue
55 | }
56 | get {
57 | return datePicker.minimumDate
58 | }
59 | }
60 |
61 | private lazy var formatter: DateFormatter = {
62 | let formatter = DateFormatter()
63 | formatter.timeStyle = .short
64 | formatter.dateStyle = .medium
65 | formatter.doesRelativeDateFormatting = true
66 |
67 | return formatter
68 | }()
69 |
70 | public override func updateDateLabel() {
71 | dateLabel.text = formatter.string(from: date)
72 | }
73 |
74 | public override func dateChanged(_ sender: UIDatePicker) {
75 | super.dateChanged(sender)
76 |
77 | delegate?.datePickerTableViewCellDidUpdateDate(self)
78 | }
79 | }
80 |
81 | extension ExpirationReminderDateTableViewCell: NibLoadable { }
82 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Id.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Id.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/5/21.
6 | //
7 |
8 | import Foundation
9 |
10 | class Id: Equatable {
11 |
12 | static private let PERIPHERAL_NODE_INDEX: UInt8 = 1
13 |
14 | static func fromInt(_ v: Int) -> Id {
15 | return Id(Data(bigEndian: v).subdata(in: 4..<8))
16 | }
17 |
18 | static func fromLong(_ v: UInt32) -> Id {
19 | return Id(Data(bigEndian: v))
20 | }
21 |
22 |
23 | let address: Data
24 |
25 | init(_ address: Data) {
26 | guard address.count == 4 else {
27 | // TODO: Should probably throw an error here.
28 | // require(address.size == 4)
29 | self.address = Data([0x00, 0x00, 0x00, 0x00])
30 | return
31 | }
32 | self.address = address
33 | }
34 |
35 | /**
36 | * Used to obtain podId from controllerId
37 | * The original PDM seems to rotate over 3 Ids:
38 | * controllerID+1, controllerID+2 and controllerID+3
39 | */
40 | func increment() -> Id {
41 | var nodeId = address
42 |
43 | //Zero out last 2 bits on right which would round down in sequence of {4, 8, 12, 16, 20, ...}
44 | nodeId[3] = UInt8(Int(nodeId[3]) & -4)
45 |
46 | //Increment by adding 1
47 | nodeId[3] = nodeId[3] | Id.PERIPHERAL_NODE_INDEX
48 | return Id(nodeId)
49 | }
50 |
51 | /*
52 | TODO: the above implementation is ported from AndroidAPS while we implemented the version below.
53 | It is not clear if skipping every 4 numbers above is intentional or a bug. It would be preferred to use the
54 | AndroidAPS version though until we can successfully pair so we can send identical data payloads.
55 | */
56 | /*
57 | func increment() -> Id {
58 | var val = address.toBigEndian(Int.self)
59 | val += 1
60 | if (val >= 4246) {
61 | val = 4243
62 | }
63 | return Id.fromInt(val)
64 | }
65 | */
66 |
67 | // TODO:
68 | // override func toString(): String {
69 | // val asInt = ByteBuffer.wrap(address).int
70 | // return "$asInt/${address.toHex()}"
71 | // }
72 |
73 | func toInt64() -> Int64 {
74 | return address.toBigEndian(Int64.self)
75 | }
76 |
77 |
78 | func toUInt32() -> UInt32 {
79 | return address.toBigEndian(UInt32.self)
80 | }
81 |
82 |
83 | //MARK: Comparable
84 |
85 | static func == (lhs: Id, rhs: Id) -> Bool {
86 | return lhs.address == rhs.address
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInfoPulseLogPlus.swift
3 | // OmniKit
4 | //
5 | // Created by Eelke Jager on 22/09/2018.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Type 3 Pod Info returns up to the last 60 pulse log entries pulse some additional info
12 | public struct PodInfoPulseLogPlus : PodInfo {
13 | // CMD 1 2 3 4 5 6 7 8 9 10
14 | // DATA 0 1 2 3 4 5 6 7 8
15 | // 02 LL 03 PP QQQQ SSSS 04 3c XXXXXXXX ...
16 |
17 | public let podInfoType : PodInfoResponseSubType = .pulseLogPlus
18 | public let faultEventCode: FaultEventCode // fault code
19 | public let timeFaultEvent: TimeInterval // fault time since activation
20 | public let timeActivation: TimeInterval // current time since activation
21 | public let entrySize : Int // always 4
22 | public let maxEntries : Int // always 60
23 | public let nEntries : Int // how many 32-bit pulse log entries returned (calculated)
24 | public let pulseLog : [UInt32]
25 | public let data : Data
26 |
27 | public init(encodedData: Data) throws {
28 | guard encodedData[6] == MemoryLayout.size else {
29 | throw MessageError.unknownValue(value: encodedData[6], typeDescription: "pulseLog entry size")
30 | }
31 | let entrySize = Int(encodedData[6])
32 | let logStartByteOffset = 8 // starting byte offset of the pulse log in DATA
33 | let nLogBytesReturned = encodedData.count - logStartByteOffset
34 | let nEntries = nLogBytesReturned / entrySize
35 | let maxEntries = Int(encodedData[7])
36 | guard encodedData.count >= logStartByteOffset && (nLogBytesReturned & 0x3) == 0 else {
37 | throw MessageBlockError.notEnoughData // not enough data to start log or a non-integral # of pulse log entries
38 | }
39 | guard maxEntries >= nEntries else {
40 | throw MessageBlockError.parseError
41 | }
42 | self.entrySize = entrySize
43 | self.nEntries = nEntries
44 | self.maxEntries = maxEntries
45 | self.faultEventCode = FaultEventCode(rawValue: encodedData[1])
46 | self.timeFaultEvent = TimeInterval(minutes: Double((Int(encodedData[2] & 0b1) << 8) + Int(encodedData[3])))
47 | self.timeActivation = TimeInterval(minutes: Double((Int(encodedData[4] & 0b1) << 8) + Int(encodedData[5])))
48 | self.pulseLog = createPulseLog(encodedData: encodedData, logStartByteOffset: logStartByteOffset, nEntries: self.nEntries)
49 | self.data = encodedData
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Common/UIColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nathan Racklyeft on 1/23/16.
6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | extension UIColor {
13 | @nonobjc static var tintColor: UIColor? = nil
14 |
15 | @nonobjc static let secondaryLabelColor = UIColor(red: 142 / 255, green: 142 / 255, blue: 147 / 255, alpha: 1)
16 |
17 | @nonobjc static let gridColor = UIColor(white: 193 / 255, alpha: 1)
18 |
19 | @nonobjc static let glucoseTintColor = UIColor.HIGTealBlueColor()
20 |
21 | @nonobjc static let IOBTintColor = UIColor.HIGOrangeColor()
22 |
23 | @nonobjc static let COBTintColor = UIColor.HIGYellowColor()
24 |
25 | @nonobjc static let doseTintColor = UIColor.HIGGreenColor()
26 |
27 | @nonobjc static let freshColor = UIColor.HIGGreenColor()
28 |
29 | @nonobjc static let agingColor = UIColor.HIGYellowColor()
30 |
31 | @nonobjc static let staleColor = UIColor.HIGRedColor()
32 |
33 | @nonobjc static let unknownColor = UIColor.HIGGrayColor().withAlphaComponent(0.5)
34 |
35 | @nonobjc static let deleteColor = UIColor.HIGRedColor()
36 |
37 | // MARK: - HIG colors
38 | // See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/
39 |
40 | private static func HIGTealBlueColor() -> UIColor {
41 | return UIColor(red: 90 / 255, green: 200 / 255, blue: 250 / 255, alpha: 1)
42 | }
43 |
44 | private static func HIGYellowColor() -> UIColor {
45 | return UIColor(red: 1, green: 204 / 255, blue: 0 / 255, alpha: 1)
46 | }
47 |
48 | private static func HIGOrangeColor() -> UIColor {
49 | return UIColor(red: 1, green: 149 / 255, blue: 0 / 255, alpha: 1)
50 | }
51 |
52 | private static func HIGPinkColor() -> UIColor {
53 | return UIColor(red: 1, green: 45 / 255, blue: 85 / 255, alpha: 1)
54 | }
55 |
56 | private static func HIGBlueColor() -> UIColor {
57 | return UIColor(red: 0, green: 122 / 255, blue: 1, alpha: 1)
58 | }
59 |
60 | private static func HIGGreenColor() -> UIColor {
61 | return UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1)
62 | }
63 |
64 | private static func HIGRedColor() -> UIColor {
65 | return UIColor(red: 1, green: 59 / 255, blue: 48 / 255, alpha: 1)
66 | }
67 |
68 | private static func HIGPurpleColor() -> UIColor {
69 | return UIColor(red: 88 / 255, green: 86 / 255, blue: 214 / 255, alpha: 1)
70 | }
71 |
72 | private static func HIGGrayColor() -> UIColor {
73 | return UIColor(red: 142 / 255, green: 143 / 255, blue: 147 / 255, alpha: 1)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/pair/KeyExchangeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyExchangeTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class KeyExchangeTest: XCTestCase {
13 |
14 | func testKeyExchange() {
15 |
16 | let pdmNonce = Data(hexadecimalString:"edfdacb242c7f4e1d2bc4d93ca3c5706")!
17 |
18 | let privateKey = Data(hexadecimalString: "27ec94b71a201c5e92698d668806ae5ba00594c307cf5566e60c1fc53a6f6bb6")!
19 | let privateKeyGenerator = MockFixedPrivateKeyGenerator(fixedPrivateKey: privateKey, generator: X25519KeyGenerator())
20 | let randomByteGenerator = MockRandomByteGenerator(fixedData: pdmNonce)
21 | let ke = try! KeyExchange(
22 | privateKeyGenerator,
23 | randomByteGenerator
24 | )
25 | let podPublicKey = Data(hexadecimalString:"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74")!
26 | let podNonce = Data(hexadecimalString: "00000000000000000000000000000000")!
27 | try! ke.updatePodPublicData(podPublicKey + podNonce)
28 | assert(ke.pdmPublic.hexadecimalString == "f2b6940243aba536a66e19fb9a39e37f1e76a1cd50ab59b3e05313b4fc93975e")
29 | assert(ke.pdmConf.hexadecimalString == "5fc3b4da865e838ceaf1e9e8bb85d1ac")
30 | try! ke.validatePodConf(Data(hexadecimalString: "af4f10db5f96e5d9cd6cfc1f54f4a92f")!)
31 | assert(ke.ltk.hexadecimalString == "341e16d13f1cbf73b19d1c2964fee02b")
32 | }
33 | }
34 |
35 |
36 |
37 | struct MockRandomByteGenerator: RandomByteGenerator {
38 |
39 | let fixedData: Data
40 |
41 | func nextBytes(length: Int) -> Data {
42 | return fixedData
43 | }
44 | }
45 |
46 | struct MockFixedPrivateKeyGenerator: PrivateKeyGenerator {
47 |
48 | let fixedPrivateKey: Data
49 | private let realGenerator: PrivateKeyGenerator
50 |
51 | init(fixedPrivateKey: Data, generator: PrivateKeyGenerator){
52 | self.fixedPrivateKey = fixedPrivateKey
53 | self.realGenerator = generator
54 | }
55 |
56 | func generatePrivateKey() -> Data {
57 | return fixedPrivateKey
58 | }
59 |
60 | func publicFromPrivate(_ privateKey: Data) throws -> Data {
61 | assert(privateKey == self.fixedPrivateKey)
62 | return try realGenerator.publicFromPrivate(privateKey)
63 | }
64 |
65 | func computeSharedSecret(_ privateKey: Data, _ publicKey: Data) throws -> Data {
66 | assert(privateKey == self.fixedPrivateKey)
67 | return try realGenerator.computeSharedSecret(privateKey, publicKey)
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Session/EapMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EapMessage.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/8/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum EapCode: UInt8 {
12 | case REQUEST = 0x01
13 | case RESPONSE = 0x02
14 | case SUCCESS = 0x03
15 | case FAILURE = 0x04
16 | }
17 |
18 | struct EapMessage {
19 | var code: EapCode
20 | var identifier: UInt8
21 | var subType: UInt8 = 0
22 | var attributes: [EapAkaAttribute]
23 |
24 | func toData() -> Data {
25 |
26 | var joinedAttributes = Data()
27 | for attribute in attributes {
28 | joinedAttributes.append(attribute.toData())
29 | }
30 |
31 | let attrSize = joinedAttributes.count
32 | if (attrSize == 0) {
33 | return Data([code.rawValue, identifier, 0x00, 0x04])
34 | }
35 | let totalSize = EapMessage.HEADER_SIZE + attrSize
36 |
37 | var bb = Data()
38 | bb.append(code.rawValue)
39 | bb.append(identifier)
40 | bb.append(UInt8((totalSize >> 8) & 0xFF))
41 | bb.append(UInt8(totalSize & 0xFF))
42 | bb.append(UInt8(EapMessage.AKA_PACKET_TYPE))
43 | bb.append(UInt8(EapMessage.SUBTYPE_AKA_CHALLENGE))
44 | bb.append(Data([0x00, 0x00]))
45 | bb.append(joinedAttributes)
46 |
47 | return bb
48 | }
49 |
50 | private static let HEADER_SIZE = 8
51 | private static let SUBTYPE_AKA_CHALLENGE = 0x01
52 | static let SUBTYPE_SYNCRONIZATION_FAILURE = 0x04
53 |
54 | private static let AKA_PACKET_TYPE = 0x17
55 |
56 | static func parse(payload: Data) throws -> EapMessage {
57 | guard payload.count > 4 else { throw MessageError.notEnoughData }
58 |
59 | let totalSize = (Int(payload[2]) << 8) | Int(payload[3])
60 | guard payload.count == totalSize else { throw MessageError.notEnoughData }
61 |
62 |
63 | if (payload.count == 4) { // SUCCESS/FAILURE
64 | return EapMessage(
65 | code: EapCode(rawValue: payload[0])!,
66 | identifier: payload[1],
67 | attributes: []
68 | )
69 | }
70 | if (totalSize > 0 && payload[4] != AKA_PACKET_TYPE) {
71 | throw MessageError.validationFailed(description: "Invalid eap payload.")
72 | }
73 | let attributesPayload = payload.subdata(in: 8.. UInt16 {
48 |
49 | var acc: UInt16 = 0
50 | for byte in self {
51 | let idx = (acc ^ UInt16(byte)) & 0xff
52 | acc = (acc >> 8) ^ crc16Table[Int(idx)]
53 | }
54 | return acc
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/MessageBlock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageBlock.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 10/14/17.
6 | // Copyright © 2017 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum MessageBlockError: Error {
12 | case notEnoughData
13 | case unknownBlockType(rawVal: UInt8)
14 | case parseError
15 | }
16 |
17 | // See https://github.com/openaps/openomni/wiki/Message-Types
18 | public enum MessageBlockType: UInt8 {
19 | case versionResponse = 0x01
20 | case podInfoResponse = 0x02
21 | case setupPod = 0x03
22 | case errorResponse = 0x06
23 | case assignAddress = 0x07
24 | case faultConfig = 0x08
25 | case getStatus = 0x0e
26 | case acknowledgeAlert = 0x11
27 | case basalScheduleExtra = 0x13
28 | case tempBasalExtra = 0x16
29 | case bolusExtra = 0x17
30 | case configureAlerts = 0x19
31 | case setInsulinSchedule = 0x1a
32 | case deactivatePod = 0x1c
33 | case statusResponse = 0x1d
34 | case beepConfig = 0x1e
35 | case cancelDelivery = 0x1f
36 |
37 | public var blockType: MessageBlock.Type {
38 | switch self {
39 | case .versionResponse:
40 | return VersionResponse.self
41 | case .acknowledgeAlert:
42 | return AcknowledgeAlertCommand.self
43 | case .podInfoResponse:
44 | return PodInfoResponse.self
45 | case .setupPod:
46 | return SetupPodCommand.self
47 | case .errorResponse:
48 | return ErrorResponse.self
49 | case .assignAddress:
50 | return AssignAddressCommand.self
51 | case .getStatus:
52 | return GetStatusCommand.self
53 | case .basalScheduleExtra:
54 | return BasalScheduleExtraCommand.self
55 | case .bolusExtra:
56 | return BolusExtraCommand.self
57 | case .configureAlerts:
58 | return ConfigureAlertsCommand.self
59 | case .setInsulinSchedule:
60 | return SetInsulinScheduleCommand.self
61 | case .deactivatePod:
62 | return DeactivatePodCommand.self
63 | case .statusResponse:
64 | return StatusResponse.self
65 | case .tempBasalExtra:
66 | return TempBasalExtraCommand.self
67 | case .beepConfig:
68 | return BeepConfigCommand.self
69 | case .cancelDelivery:
70 | return CancelDeliveryCommand.self
71 | case .faultConfig:
72 | return FaultConfigCommand.self
73 | }
74 | }
75 | }
76 |
77 | public protocol MessageBlock {
78 | init(encodedData: Data) throws
79 |
80 | var blockType: MessageBlockType { get }
81 | var data: Data { get }
82 | }
83 |
84 | public protocol NonceResyncableMessageBlock : MessageBlock {
85 | var nonce: UInt32 { get set }
86 | }
87 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/StringLengthPrefixEncoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringLengthPrefixEncoding.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/5/21.
6 | //
7 |
8 | import Foundation
9 |
10 | final class StringLengthPrefixEncoding {
11 |
12 | static private let LENGTH_BYTES = 2
13 |
14 | static func parseKeys(_ keys: Array, _ payload: Data) throws -> [Data] {
15 | var ret = Array(repeating: Data(capacity: 0), count: keys.count)
16 | var remaining = payload
17 | for (index, key) in keys.enumerated() {
18 | guard remaining.count >= key.count else {
19 | throw BluetoothErrors.MessageIOException("Payload too short: \(payload)")
20 | }
21 | if (String(decoding: remaining.subdata(in: 0..= (key.count + LENGTH_BYTES) else {
29 | throw BluetoothErrors.MessageIOException("Payload too short: \(payload)")
30 | }
31 | remaining = remaining.subdata(in: key.count..= length else {
39 | throw BluetoothErrors.MessageIOException("Payload too short: \(payload)")
40 | }
41 | ret[index] = remaining.subdata(in: LENGTH_BYTES.., payloads: Array) -> Data {
48 | let payloadTotalSize = payloads.reduce(0, { acc, i in acc + i.count })
49 | let keyTotalSize = keys.reduce(0, { acc, i in acc + i.count })
50 | let zeros = payloads.reduce(0, { acc, i in acc + (i.count == 0 ? 1 : 0) })
51 |
52 | var bb = Data(capacity: 2 * (keys.count - zeros) + keyTotalSize + payloadTotalSize)
53 | for (idx, key) in keys.enumerated() {
54 | let payload = payloads[idx]
55 | bb.append(key.data(using: .utf8)!)
56 | if (payload.count != 0) {
57 | bb.append(withUnsafeBytes(of: Int16(payload.count).bigEndian) { Data($0) })
58 | bb.append(payload)
59 | }
60 | }
61 |
62 | return bb
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/EnDecrypt/EnDecrypt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnDecrypt.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/4/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CryptoSwift
11 | import os.log
12 |
13 | class EnDecrypt {
14 | private let MAC_SIZE = 8
15 | private let log = OSLog(category: "EnDecrypt")
16 | private let nonce: Nonce
17 | private let ck: Data
18 |
19 | init(nonce: Nonce, ck: Data) {
20 | self.nonce = nonce
21 | self.ck = ck
22 | }
23 |
24 | func decrypt(_ msg: MessagePacket, _ nonceSeq: Int) throws -> MessagePacket {
25 | let payload = msg.payload
26 | let header = msg.asData(forEncryption: false).subdata(in: 0..<16)
27 |
28 | let n = nonce.toData(sqn: nonceSeq, podReceiving: false)
29 | log.debug("Decrypt ck %@", ck.hexadecimalString)
30 | log.debug("Decrypt header %@", header.hexadecimalString)
31 | log.debug("Decrypt payload: %@", payload.hexadecimalString)
32 | log.debug("Decrypt Nonce %@", n.hexadecimalString)
33 | log.debug("Decrypt Tag: %@", Data(payload).subdata(in: (payload.count - MAC_SIZE).. MessagePacket {
45 | let payload = headerMessage.payload
46 | let header = headerMessage.asData(forEncryption: true).subdata(in: 0..<16)
47 |
48 | let n = nonce.toData(sqn: nonceSeq, podReceiving: true)
49 | log.debug("Encrypt Ck %@", ck.hexadecimalString)
50 | log.debug("Encrypt Header %@", header.hexadecimalString)
51 | log.debug("Encrypt Payload: %@", payload.hexadecimalString)
52 | log.debug("Encrypt Nonce %@", n.hexadecimalString)
53 | let ccm = CCM(iv: n.bytes, tagLength: MAC_SIZE, messageLength: payload.count, additionalAuthenticatedData: header.bytes)
54 | let aes = try AES(key: ck.bytes, blockMode: ccm, padding: .noPadding)
55 | let encryptedPayload = try aes.encrypt(payload.bytes)
56 | log.debug("Encrypted payload: %@", Data(encryptedPayload).subdata(in: 0..<(encryptedPayload.count - MAC_SIZE)).hexadecimalString)
57 | log.debug("Encrypt Tag: %@", Data(encryptedPayload).subdata(in: (encryptedPayload.count - MAC_SIZE)..> 4)!
76 | }
77 |
78 | public init(nonce: UInt32, deliveryType: DeliveryType, beepType: BeepType) {
79 | self.nonce = nonce
80 | self.deliveryType = deliveryType
81 | self.beepType = beepType
82 | }
83 | }
84 |
85 | extension CancelDeliveryCommand: CustomDebugStringConvertible {
86 | public var debugDescription: String {
87 | return "CancelDeliveryCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), deliveryType:\(deliveryType), beepType:\(beepType))"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/OmniBLETests/Driver/Comm/endecrypt/EnDecryptTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnDecryptTests.swift
3 | // OmniBLE
4 | //
5 | // Created by Bill Gestrich on 12/11/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import OmniBLE
11 |
12 | class EnDecryptTest: XCTestCase {
13 |
14 | func testDecrypt() {
15 | let received = "54,57,11,a1,0c,16,03,00,08,20,2e,a9,08,20,2e,a8,34,7c,b9,7b,38,5d,45,a3,c4,0e,40,4c,55,71,5e,f3,c3,86,50,17,36,7e,62,3c,7d,0b,46,9e,81,cd,fd,9a".replacingOccurrences(of:",", with:"")
16 | let decryptedPayload =
17 | "30,2e,30,3d,00,12,08,20,2e,a9,1c,0a,1d,05,00,16,b0,00,00,00,0b,ff,01,fe".replacingOccurrences(of:",", with:"")
18 |
19 | let enDecrypt = EnDecrypt(
20 | nonce: Nonce(prefix:
21 | Data(hexadecimalString:"6c,ff,5d,18,b7,61,6c,ae".replacingOccurrences(of:",", with:""))!
22 | ),
23 | ck: Data(hexadecimalString: "55,79,9f,d2,66,64,cb,f6,e4,76,52,5e,2d,ee,52,c6".replacingOccurrences(of:",", with:""))!
24 | )
25 | let encryptedMessage = Data(hexadecimalString: received)!
26 | let decrypted = Data(hexadecimalString: decryptedPayload)!
27 | do {
28 | let msg = try MessagePacket.parse(payload: encryptedMessage)
29 | //AndroidAPS provides the nonceSequence in the nonce initialer, and increments it when encrypt/decrypt are called
30 | //This implementation increments it in MessageTransport.incrementNonceSeq, before encrypt/decrypt are called.
31 | let decryptedMsg = try enDecrypt.decrypt(msg, 23)
32 |
33 | assert(decrypted.hexadecimalString == decryptedMsg.payload.hexadecimalString)
34 | } catch {
35 | print(error)
36 | }
37 |
38 | }
39 |
40 |
41 | func testEncrypt() {
42 | let enDecrypt = EnDecrypt(
43 | nonce:Nonce(prefix:
44 | Data(hexadecimalString: "dda23c090a0a0a0a")!
45 | ),
46 | ck: Data(hexadecimalString: "ba1283744b6de9fab6d9b77d95a71d6e")!
47 | )
48 | let expectedEncryptedData = Data(hexadecimalString:
49 | "54571101070003400242000002420001" + "e09158bcb0285a81bf30635f3a17ee73f0afbb3286bc524a8a66" + "fb1bc5b001e56543")!
50 | let command = Data(hexadecimalString:"53302e303d000effffffff00060704ffffffff82b22c47302e30")!
51 | var msg = try! MessagePacket.parse(payload: expectedEncryptedData)//.copy(payload = command) // copy for the headersE
52 | msg.payload = command
53 | //AndroidAPS provides the nonceSequence in the nonce initialer, and increments it when encrypt/decrypt are called
54 | //This implementation increments it in MessageTransport.incrementNonceSeq, before encrypt/decrypt are called.
55 | let encryptedData = try! enDecrypt.encrypt(msg, 1)
56 |
57 | print("Original Encrypted: \(expectedEncryptedData.hexadecimalString)")
58 | print("Test Encrypted: \(encryptedData.asData().hexadecimalString)")
59 | assert(expectedEncryptedData.hexadecimalString == encryptedData.asData().hexadecimalString)
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/OmniBLE.xcodeproj/xcshareddata/xcschemes/OmniBLETests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Common/Data.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSData.swift
3 | // OmnipodKit
4 | //
5 | // Created by Nathan Racklyeft on 9/2/15.
6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension Data {
13 | private func toDefaultEndian(_: T.Type) -> T {
14 | return self.withUnsafeBytes({ (rawBufferPointer: UnsafeRawBufferPointer) -> T in
15 | let bufferPointer = rawBufferPointer.bindMemory(to: T.self)
16 | guard let pointer = bufferPointer.baseAddress else {
17 | return 0
18 | }
19 | return T(pointer.pointee)
20 | })
21 | }
22 |
23 | func to(_ type: T.Type) -> T {
24 | return T(littleEndian: toDefaultEndian(type))
25 | }
26 |
27 | func toBigEndian(_ type: T.Type) -> T {
28 | return T(bigEndian: toDefaultEndian(type))
29 | }
30 |
31 | mutating func append(_ newElement: T) {
32 | var element = newElement.littleEndian
33 | append(Data(bytes: &element, count: element.bitWidth / 8))
34 | }
35 |
36 | mutating func appendBigEndian(_ newElement: T) {
37 | var element = newElement.bigEndian
38 | append(Data(bytes: &element, count: element.bitWidth / 8))
39 | }
40 |
41 | init(_ value: T) {
42 | var value = value.littleEndian
43 | self.init(bytes: &value, count: value.bitWidth / 8)
44 | }
45 |
46 | init(bigEndian value: T) {
47 | var value = value.bigEndian
48 | self.init(bytes: &value, count: value.bitWidth / 8)
49 | }
50 | }
51 |
52 | // String conversion methods, adapted from https://stackoverflow.com/questions/40276322/hex-binary-string-conversion-in-swift/40278391#40278391
53 | extension Data {
54 | init?(hexadecimalString: String) {
55 | self.init(capacity: hexadecimalString.utf16.count / 2)
56 |
57 | // Convert 0 ... 9, a ... f, A ...F to their decimal value,
58 | // return nil for all other input characters
59 | func decodeNibble(u: UInt16) -> UInt8? {
60 | switch u {
61 | case 0x30 ... 0x39: // '0'-'9'
62 | return UInt8(u - 0x30)
63 | case 0x41 ... 0x46: // 'A'-'F'
64 | return UInt8(u - 0x41 + 10) // 10 since 'A' is 10, not 0
65 | case 0x61 ... 0x66: // 'a'-'f'
66 | return UInt8(u - 0x61 + 10) // 10 since 'a' is 10, not 0
67 | default:
68 | return nil
69 | }
70 | }
71 |
72 | var even = true
73 | var byte: UInt8 = 0
74 | for c in hexadecimalString.utf16 {
75 | guard let val = decodeNibble(u: c) else { return nil }
76 | if even {
77 | byte = val << 4
78 | } else {
79 | byte += val
80 | self.append(byte)
81 | }
82 | even = !even
83 | }
84 | guard even else { return nil }
85 | }
86 |
87 | var hexadecimalString: String {
88 | return map { String(format: "%02hhx", $0) }.joined()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/StatusResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusResponse.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 10/23/17.
6 | // Copyright © 2017 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct StatusResponse : MessageBlock {
12 | public let blockType: MessageBlockType = .statusResponse
13 | public let length: UInt8 = 10
14 | public let deliveryStatus: DeliveryStatus
15 | public let podProgressStatus: PodProgressStatus
16 | public let timeActive: TimeInterval
17 | public let reservoirLevel: Double?
18 | public let insulin: Double
19 | public let bolusNotDelivered: Double
20 | public let lastProgrammingMessageSeqNum: UInt8 // updated by pod for 03, 08, $11, $19, $1A, $1C, $1E & $1F command messages
21 | public let alerts: AlertSet
22 |
23 |
24 | public let data: Data
25 |
26 | public init(encodedData: Data) throws {
27 | if encodedData.count < length {
28 | throw MessageBlockError.notEnoughData
29 | }
30 |
31 | data = encodedData.prefix(upTo: Int(length))
32 |
33 | guard let deliveryStatus = DeliveryStatus(rawValue: encodedData[1] >> 4) else {
34 | throw MessageError.unknownValue(value: encodedData[1] >> 4, typeDescription: "DeliveryStatus")
35 | }
36 | self.deliveryStatus = deliveryStatus
37 |
38 | guard let podProgressStatus = PodProgressStatus(rawValue: encodedData[1] & 0xf) else {
39 | throw MessageError.unknownValue(value: encodedData[1] & 0xf, typeDescription: "PodProgressStatus")
40 | }
41 | self.podProgressStatus = podProgressStatus
42 |
43 | let minutes = ((Int(encodedData[7]) & 0x7f) << 6) + (Int(encodedData[8]) >> 2)
44 | self.timeActive = TimeInterval(minutes: Double(minutes))
45 |
46 | let highInsulinBits = Int(encodedData[2] & 0xf) << 9
47 | let midInsulinBits = Int(encodedData[3]) << 1
48 | let lowInsulinBits = Int(encodedData[4] >> 7)
49 | self.insulin = Double(highInsulinBits | midInsulinBits | lowInsulinBits) / Pod.pulsesPerUnit
50 |
51 | self.lastProgrammingMessageSeqNum = (encodedData[4] >> 3) & 0xf
52 |
53 | self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit
54 |
55 | self.alerts = AlertSet(rawValue: ((encodedData[6] & 0x7f) << 1) | (encodedData[7] >> 7))
56 |
57 | let reservoirValue = Double((Int(encodedData[8] & 0x3) << 8) + Int(encodedData[9])) / Pod.pulsesPerUnit
58 | if reservoirValue <= Pod.maximumReservoirReading {
59 | self.reservoirLevel = reservoirValue
60 | } else {
61 | self.reservoirLevel = nil
62 | }
63 | }
64 | }
65 |
66 | extension StatusResponse: CustomDebugStringConvertible {
67 | public var debugDescription: String {
68 | return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/BeepConfigCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BeepConfigCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Joseph Moran on 5/12/19.
6 | // Copyright © 2019 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct BeepConfigCommand : MessageBlock {
12 | // OFF 1 2 3 4 5
13 | // 1e 04 AABBCCDD
14 |
15 | public let blockType: MessageBlockType = .beepConfig
16 | public let beepConfigType: BeepConfigType
17 | public let basalCompletionBeep: Bool
18 | public let basalIntervalBeep: TimeInterval
19 | public let tempBasalCompletionBeep: Bool
20 | public let tempBasalIntervalBeep: TimeInterval
21 | public let bolusCompletionBeep: Bool
22 | public let bolusIntervalBeep: TimeInterval
23 |
24 | public init(beepConfigType: BeepConfigType, basalCompletionBeep: Bool = false, basalIntervalBeep: TimeInterval = 0, tempBasalCompletionBeep: Bool = false, tempBasalIntervalBeep: TimeInterval = 0, bolusCompletionBeep: Bool = false, bolusIntervalBeep: TimeInterval = 0) {
25 | self.beepConfigType = beepConfigType
26 | self.basalCompletionBeep = basalCompletionBeep
27 | self.basalIntervalBeep = basalIntervalBeep
28 | self.tempBasalCompletionBeep = tempBasalCompletionBeep
29 | self.tempBasalIntervalBeep = tempBasalIntervalBeep
30 | self.bolusCompletionBeep = bolusCompletionBeep
31 | self.bolusIntervalBeep = bolusIntervalBeep
32 | }
33 |
34 | public init(encodedData: Data) throws {
35 | if encodedData.count < 6 {
36 | throw MessageBlockError.notEnoughData
37 | }
38 | if let beepConfigType = BeepConfigType.init(rawValue: encodedData[2]) {
39 | self.beepConfigType = beepConfigType
40 | } else {
41 | throw MessageBlockError.parseError
42 | }
43 | self.basalCompletionBeep = encodedData[3] & (1<<6) != 0
44 | self.basalIntervalBeep = TimeInterval(minutes: Double(encodedData[3] & 0x3f))
45 | self.tempBasalCompletionBeep = encodedData[4] & (1<<6) != 0
46 | self.tempBasalIntervalBeep = TimeInterval(minutes: Double(encodedData[4] & 0x3f))
47 | self.bolusCompletionBeep = encodedData[5] & (1<<6) != 0
48 | self.bolusIntervalBeep = TimeInterval(minutes: Double(encodedData[5] & 0x3f))
49 | }
50 |
51 | public var data: Data {
52 | var data = Data([
53 | blockType.rawValue,
54 | 4,
55 | ])
56 | data.append(beepConfigType.rawValue)
57 | data.append((basalCompletionBeep ? (1<<6) : 0) + (UInt8(basalIntervalBeep.minutes) & 0x3f))
58 | data.append((tempBasalCompletionBeep ? (1<<6) : 0) + (UInt8(tempBasalIntervalBeep.minutes) & 0x3f))
59 | data.append((bolusCompletionBeep ? (1<<6) : 0) + (UInt8(bolusIntervalBeep.minutes) & 0x3f))
60 | return data
61 | }
62 | }
63 |
64 | extension BeepConfigCommand: CustomDebugStringConvertible {
65 | public var debugDescription: String {
66 | return "BeepConfigCommand(beepConfigType:\(beepConfigType), basalIntervalBeep:\(basalIntervalBeep), tempBasalCompletionBeep:\(tempBasalCompletionBeep), tempBasalIntervalBeep:\(tempBasalIntervalBeep), bolusCompletionBeep:\(bolusCompletionBeep), , bolusIntervalBeep:\(bolusIntervalBeep))"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/ViewControllers/PodReplacementNavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodReplacementNavigationController.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 11/28/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OmniKit
11 | import LoopKitUI
12 |
13 | class PodReplacementNavigationController: UINavigationController, UINavigationControllerDelegate, CompletionNotifying {
14 |
15 | weak var completionDelegate: CompletionDelegate?
16 |
17 | class func instantiatePodReplacementFlow(_ pumpManager: OmnipodPumpManager) -> PodReplacementNavigationController {
18 | let vc = UIStoryboard(name: "OmnipodPumpManager", bundle: Bundle(for: PodReplacementNavigationController.self)).instantiateViewController(withIdentifier: "PodReplacementFlow") as! PodReplacementNavigationController
19 | vc.pumpManager = pumpManager
20 | return vc
21 | }
22 |
23 | class func instantiateNewPodFlow(_ pumpManager: OmnipodPumpManager) -> PodReplacementNavigationController {
24 | let vc = UIStoryboard(name: "OmnipodPumpManager", bundle: Bundle(for: PodReplacementNavigationController.self)).instantiateViewController(withIdentifier: "NewPodFlow") as! PodReplacementNavigationController
25 | vc.pumpManager = pumpManager
26 | return vc
27 | }
28 |
29 | class func instantiateInsertCannulaFlow(_ pumpManager: OmnipodPumpManager) -> PodReplacementNavigationController {
30 | let vc = UIStoryboard(name: "OmnipodPumpManager", bundle: Bundle(for: PodReplacementNavigationController.self)).instantiateViewController(withIdentifier: "InsertCannulaFlow") as! PodReplacementNavigationController
31 | vc.pumpManager = pumpManager
32 | return vc
33 | }
34 |
35 | private(set) var pumpManager: OmnipodPumpManager!
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | if #available(iOSApplicationExtension 13.0, *) {
41 | // Prevent interactive dismissal
42 | isModalInPresentation = true
43 | }
44 |
45 | delegate = self
46 | }
47 |
48 | func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
49 |
50 | if let setupViewController = viewController as? SetupTableViewController {
51 | setupViewController.delegate = self
52 | }
53 |
54 | switch viewController {
55 | case let vc as ReplacePodViewController:
56 | vc.pumpManager = pumpManager
57 | case let vc as PairPodSetupViewController:
58 | vc.pumpManager = pumpManager
59 | case let vc as InsertCannulaSetupViewController:
60 | vc.pumpManager = pumpManager
61 | case let vc as PodSetupCompleteViewController:
62 | vc.pumpManager = pumpManager
63 | default:
64 | break
65 | }
66 |
67 | }
68 |
69 | func completeSetup() {
70 | completionDelegate?.completionNotifyingDidComplete(self)
71 | }
72 | }
73 |
74 | extension PodReplacementNavigationController: SetupTableViewControllerDelegate {
75 | func setupTableViewControllerCancelButtonPressed(_ viewController: SetupTableViewController) {
76 | completionDelegate?.completionNotifyingDidComplete(self)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/PodProgressStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodProgressStatus.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 9/28/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum PodProgressStatus: UInt8, CustomStringConvertible, Equatable {
12 | case initialized = 0
13 | case memoryInitialized = 1
14 | case reminderInitialized = 2
15 | case pairingCompleted = 3
16 | case priming = 4
17 | case primingCompleted = 5
18 | case basalInitialized = 6
19 | case insertingCannula = 7
20 | case aboveFiftyUnits = 8
21 | case fiftyOrLessUnits = 9
22 | case oneNotUsed = 10
23 | case twoNotUsed = 11
24 | case threeNotUsed = 12
25 | case faultEventOccurred = 13 // fault event occurred (a "screamer")
26 | case activationTimeExceeded = 14 // took > 2 hrs from progress 2 to 3 OR > 1 hr from 3 to 8
27 | case inactive = 15 // pod deactivated or a fatal packet state error
28 |
29 | public var readyForDelivery: Bool {
30 | return self == .fiftyOrLessUnits || self == .aboveFiftyUnits
31 | }
32 |
33 | public var description: String {
34 | switch self {
35 | case .initialized:
36 | return LocalizedString("Initialized", comment: "Pod initialized")
37 | case .memoryInitialized:
38 | return LocalizedString("Memory initialized", comment: "Pod memory initialized")
39 | case .reminderInitialized:
40 | return LocalizedString("Reminder initialized", comment: "Pod pairing reminder initialized")
41 | case .pairingCompleted:
42 | return LocalizedString("Pairing completed", comment: "Pod status when pairing completed")
43 | case .priming:
44 | return LocalizedString("Priming", comment: "Pod status when priming")
45 | case .primingCompleted:
46 | return LocalizedString("Priming completed", comment: "Pod state when priming completed")
47 | case .basalInitialized:
48 | return LocalizedString("Basal initialized", comment: "Pod state when basal initialized")
49 | case .insertingCannula:
50 | return LocalizedString("Inserting cannula", comment: "Pod state when inserting cannula")
51 | case .aboveFiftyUnits:
52 | return LocalizedString("Normal", comment: "Pod state when running above fifty units")
53 | case .fiftyOrLessUnits:
54 | return LocalizedString("Low reservoir", comment: "Pod state when running with fifty or less units")
55 | case .oneNotUsed:
56 | return LocalizedString("oneNotUsed", comment: "Pod state oneNotUsed")
57 | case .twoNotUsed:
58 | return LocalizedString("twoNotUsed", comment: "Pod state twoNotUsed")
59 | case .threeNotUsed:
60 | return LocalizedString("threeNotUsed", comment: "Pod state threeNotUsed")
61 | case .faultEventOccurred:
62 | return LocalizedString("Fault event occurred", comment: "Pod state when fault event has occurred")
63 | case .activationTimeExceeded:
64 | return LocalizedString("Activation time exceeded", comment: "Pod state when activation not completed in the time allowed")
65 | case .inactive:
66 | return LocalizedString("Deactivated", comment: "Pod state when deactivated")
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/SetupPodCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Setup PodCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 2/17/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct SetupPodCommand : MessageBlock {
12 |
13 | public let blockType: MessageBlockType = .setupPod
14 |
15 | let address: UInt32
16 | let lot: UInt32
17 | let tid: UInt32
18 | let dateComponents: DateComponents
19 | let packetTimeoutLimit: UInt8
20 |
21 | public static func dateComponents(date: Date, timeZone: TimeZone) -> DateComponents {
22 | var cal = Calendar(identifier: .gregorian)
23 | cal.timeZone = timeZone
24 | return cal.dateComponents([.day, .month, .year, .hour, .minute], from: date)
25 | }
26 |
27 | public static func date(from components: DateComponents, timeZone: TimeZone) -> Date? {
28 | var cal = Calendar(identifier: .gregorian)
29 | cal.timeZone = timeZone
30 | return cal.date(from: components)
31 | }
32 |
33 | // 03 13 1f08ced2 14 04 09 0b 11 0b 08 0000a640 00097c27 83e4
34 | public var data: Data {
35 | var data = Data([
36 | blockType.rawValue,
37 | 19,
38 | ])
39 | data.appendBigEndian(self.address)
40 |
41 | let year = UInt8((dateComponents.year ?? 2000) - 2000)
42 | let month = UInt8(dateComponents.month ?? 0)
43 | let day = UInt8(dateComponents.day ?? 0)
44 | let hour = UInt8(dateComponents.hour ?? 0)
45 | let minute = UInt8(dateComponents.minute ?? 0)
46 |
47 | let data2: Data = Data([
48 | UInt8(0x14), // Unknown
49 | packetTimeoutLimit,
50 | month,
51 | day,
52 | year,
53 | hour,
54 | minute
55 | ])
56 | data.append(data2)
57 | data.appendBigEndian(self.lot)
58 | data.appendBigEndian(self.tid)
59 | return data
60 | }
61 |
62 | public init(encodedData: Data) throws {
63 | if encodedData.count < 21 {
64 | throw MessageBlockError.notEnoughData
65 | }
66 | self.address = encodedData[2...].toBigEndian(UInt32.self)
67 | packetTimeoutLimit = encodedData[7]
68 | var components = DateComponents()
69 | components.month = Int(encodedData[8])
70 | components.day = Int(encodedData[9])
71 | components.year = Int(encodedData[10]) + 2000
72 | components.hour = Int(encodedData[11])
73 | components.minute = Int(encodedData[12])
74 | self.dateComponents = components
75 | self.lot = encodedData[13...].toBigEndian(UInt32.self)
76 | self.tid = encodedData[17...].toBigEndian(UInt32.self)
77 | }
78 |
79 | public init(address: UInt32, dateComponents: DateComponents, lot: UInt32, tid: UInt32, packetTimeoutLimit: UInt8 = 4) {
80 | self.address = address
81 | self.dateComponents = dateComponents
82 | self.lot = lot
83 | self.tid = tid
84 | self.packetTimeoutLimit = packetTimeoutLimit
85 | }
86 | }
87 |
88 | extension SetupPodCommand: CustomDebugStringConvertible {
89 | public var debugDescription: String {
90 | return "SetupPodCommand(address:\(Data(bigEndian: address).hexadecimalString), dateComponents:\(dateComponents), lot:\(lot), tid:\(tid), packetTimeoutLimit:\(packetTimeoutLimit))"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/ViewControllers/PodSetupCompleteViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodSetupCompleteViewController.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 9/18/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import LoopKitUI
11 | import OmniKit
12 |
13 | class PodSetupCompleteViewController: SetupTableViewController {
14 |
15 | @IBOutlet weak var expirationReminderDateCell: ExpirationReminderDateTableViewCell!
16 |
17 | var pumpManager: OmnipodPumpManager! {
18 | didSet {
19 | if let expirationReminderDate = pumpManager.expirationReminderDate, let podState = pumpManager.state.podState {
20 | expirationReminderDateCell.date = expirationReminderDate
21 | expirationReminderDateCell.datePicker.maximumDate = podState.expiresAt?.addingTimeInterval(-Pod.expirationReminderAlertMinTimeBeforeExpiration)
22 | expirationReminderDateCell.datePicker.minimumDate = podState.expiresAt?.addingTimeInterval(-Pod.expirationReminderAlertMaxTimeBeforeExpiration)
23 | }
24 | }
25 | }
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 |
30 | self.padFooterToBottom = false
31 |
32 | self.navigationItem.hidesBackButton = true
33 | self.navigationItem.rightBarButtonItem = nil
34 |
35 | expirationReminderDateCell.datePicker.datePickerMode = .dateAndTime
36 | #if swift(>=5.2)
37 | if #available(iOS 14.0, *) {
38 | expirationReminderDateCell.datePicker.preferredDatePickerStyle = .wheels
39 | }
40 | #endif
41 | expirationReminderDateCell.titleLabel.text = LocalizedString("Expiration Reminder", comment: "The title of the cell showing the pod expiration reminder date")
42 | expirationReminderDateCell.datePicker.minuteInterval = 1
43 | expirationReminderDateCell.delegate = self
44 | }
45 |
46 | override func continueButtonPressed(_ sender: Any) {
47 | if let setupVC = navigationController as? OmnipodPumpManagerSetupViewController {
48 | setupVC.finishedSetup()
49 | }
50 | if let replaceVC = navigationController as? PodReplacementNavigationController {
51 | replaceVC.completeSetup()
52 | }
53 | if pumpManager.confirmationBeeps {
54 | pumpManager.setConfirmationBeeps(enabled: true, completion: { (error) in
55 | if let error = error {
56 | DispatchQueue.main.async {
57 | let title = LocalizedString("Error emitting completion confirmation beep", comment: "The alert title for emitting completion beep error")
58 | self.present(UIAlertController(with: error, title: title), animated: true)
59 | }
60 | }
61 | })
62 | }
63 | }
64 |
65 | override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
66 | tableView.beginUpdates()
67 | return indexPath
68 | }
69 |
70 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
71 | tableView.deselectRow(at: indexPath, animated: true)
72 | tableView.endUpdates()
73 | }
74 |
75 | }
76 |
77 | extension PodSetupCompleteViewController: DatePickerTableViewCellDelegate {
78 | func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
79 | pumpManager.expirationReminderDate = cell.date
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Packet/PayloadSplitter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayloadSplitter.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/11/21.
6 | //
7 |
8 | import Foundation
9 | import CryptoSwift
10 |
11 | class PayloadSplitter {
12 | private let payload: Data
13 |
14 | init(payload: Data) {
15 | self.payload = payload
16 | }
17 |
18 | func splitInPackets() -> Array {
19 | if (payload.count <= FirstBlePacket.CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET) {
20 | return splitInOnePacket()
21 | }
22 | var ret = Array()
23 | let crc32 = payload.crc32()
24 | let middleFragments = (payload.count - FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS) / MiddleBlePacket.CAPACITY
25 | let rest = UInt8((payload.count - middleFragments * MiddleBlePacket.CAPACITY) - FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS)
26 | ret.append(
27 | FirstBlePacket(
28 | fullFragments: middleFragments + 1,
29 | payload: payload.subdata(in: 0.. 0) {
33 | for i in 1...middleFragments {
34 | let p = payload.subdata(in: (FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + (i - 1) * MiddleBlePacket.CAPACITY)..<(FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + i * MiddleBlePacket.CAPACITY))
35 | ret.append(
36 | MiddleBlePacket(
37 | index: UInt8(i),
38 | payload: p
39 | )
40 | )
41 | }
42 | }
43 | let end = min(LastBlePacket.CAPACITY, Int(rest))
44 | ret.append(
45 | LastBlePacket(
46 | index: UInt8(middleFragments + 1),
47 | size: rest,
48 | payload: payload.subdata(in: middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS.. LastBlePacket.CAPACITY) {
53 | ret.append(
54 | LastOptionalPlusOneBlePacket(
55 | index: UInt8(middleFragments + 2),
56 | payload: payload.subdata(in: middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + LastBlePacket.CAPACITY.. Array {
65 | var ret = Array()
66 | let crc32 = payload.crc32()
67 | let end = min(FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS, payload.count)
68 | ret.append(
69 | FirstBlePacket(
70 | fullFragments: 0,
71 | payload: payload.subdata(in: 0.. FirstBlePacket.CAPACITY_WITHOUT_MIDDLE_PACKETS) {
77 | ret.append(
78 | LastOptionalPlusOneBlePacket(
79 | index: 1,
80 | payload: payload.subdata(in: end.. 0 ? squareWaveDuration / Double(pulseCountX10) : 0
41 | data.appendBigEndian(UInt32(timeBetweenExtendedPulses.hundredthsOfMilliseconds))
42 | return data
43 | }
44 |
45 | public init(encodedData: Data) throws {
46 | if encodedData.count < 15 {
47 | throw MessageBlockError.notEnoughData
48 | }
49 |
50 | acknowledgementBeep = encodedData[2] & (1<<7) != 0
51 | completionBeep = encodedData[2] & (1<<6) != 0
52 | programReminderInterval = TimeInterval(minutes: Double(encodedData[2] & 0x3f))
53 |
54 | units = Double(encodedData[3...].toBigEndian(UInt16.self)) / 200
55 |
56 | let delayCounts = encodedData[5...].toBigEndian(UInt32.self)
57 | timeBetweenPulses = TimeInterval(hundredthsOfMilliseconds: Double(delayCounts))
58 |
59 | let pulseCountX10 = encodedData[9...].toBigEndian(UInt16.self)
60 | squareWaveUnits = Double(pulseCountX10) / 200
61 |
62 | let intervalCounts = encodedData[5...].toBigEndian(UInt32.self)
63 | let timeBetweenExtendedPulses = TimeInterval(hundredthsOfMilliseconds: Double(intervalCounts))
64 | squareWaveDuration = timeBetweenExtendedPulses * Double(pulseCountX10) / 10
65 | }
66 |
67 | public init(units: Double, timeBetweenPulses: TimeInterval = Pod.secondsPerBolusPulse, squareWaveUnits: Double = 0.0, squareWaveDuration: TimeInterval = 0, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) {
68 | self.acknowledgementBeep = acknowledgementBeep
69 | self.completionBeep = completionBeep
70 | self.programReminderInterval = programReminderInterval
71 | self.units = units
72 | self.timeBetweenPulses = timeBetweenPulses
73 | self.squareWaveUnits = squareWaveUnits
74 | self.squareWaveDuration = squareWaveDuration
75 | }
76 | }
77 |
78 |
79 | extension BolusExtraCommand: CustomDebugStringConvertible {
80 | public var debugDescription: String {
81 | return "BolusExtraCommand(units:\(units), timeBetweenPulses:\(timeBetweenPulses), squareWaveUnits:\(squareWaveUnits), squareWaveDuration:\(squareWaveDuration), acknowledgementBeep:\(acknowledgementBeep), completionBeep:\(completionBeep), programReminderInterval:\(programReminderInterval))"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/OmnipodPumpManager+UI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OmnipodPumpManager+UI.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 8/4/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import UIKit
12 | import LoopKit
13 | import LoopKitUI
14 | import OmniKit
15 |
16 | extension OmnipodPumpManager: PumpManagerUI {
17 |
18 | static public func setupViewController() -> (UIViewController & PumpManagerSetupViewController & CompletionNotifying) {
19 | return OmnipodPumpManagerSetupViewController.instantiateFromStoryboard()
20 | }
21 |
22 | public func settingsViewController() -> (UIViewController & CompletionNotifying) {
23 | let settings = OmnipodSettingsViewController(pumpManager: self)
24 | let nav = SettingsNavigationViewController(rootViewController: settings)
25 | return nav
26 | }
27 |
28 | public var smallImage: UIImage? {
29 | return UIImage(named: "Pod", in: Bundle(for: OmnipodSettingsViewController.self), compatibleWith: nil)!
30 | }
31 |
32 | public func hudProvider() -> HUDProvider? {
33 | return OmnipodHUDProvider(pumpManager: self)
34 | }
35 |
36 | public static func createHUDViews(rawValue: HUDProvider.HUDViewsRawState) -> [BaseHUDView] {
37 | return OmnipodHUDProvider.createHUDViews(rawValue: rawValue)
38 | }
39 |
40 | }
41 |
42 | // MARK: - DeliveryLimitSettingsTableViewControllerSyncSource
43 | extension OmnipodPumpManager {
44 | public func syncDeliveryLimitSettings(for viewController: DeliveryLimitSettingsTableViewController, completion: @escaping (DeliveryLimitSettingsResult) -> Void) {
45 | guard let maxBasalRate = viewController.maximumBasalRatePerHour,
46 | let maxBolus = viewController.maximumBolus else
47 | {
48 | completion(.failure(PodCommsError.invalidData))
49 | return
50 | }
51 |
52 | completion(.success(maximumBasalRatePerHour: maxBasalRate, maximumBolus: maxBolus))
53 | }
54 |
55 | public func syncButtonTitle(for viewController: DeliveryLimitSettingsTableViewController) -> String {
56 | return LocalizedString("Save", comment: "Title of button to save delivery limit settings") }
57 |
58 | public func syncButtonDetailText(for viewController: DeliveryLimitSettingsTableViewController) -> String? {
59 | return nil
60 | }
61 |
62 | public func deliveryLimitSettingsTableViewControllerIsReadOnly(_ viewController: DeliveryLimitSettingsTableViewController) -> Bool {
63 | return false
64 | }
65 | }
66 |
67 | // MARK: - BasalScheduleTableViewControllerSyncSource
68 | extension OmnipodPumpManager {
69 |
70 | public func syncScheduleValues(for viewController: BasalScheduleTableViewController, completion: @escaping (SyncBasalScheduleResult) -> Void) {
71 | let newSchedule = BasalSchedule(repeatingScheduleValues: viewController.scheduleItems)
72 | setBasalSchedule(newSchedule) { (error) in
73 | if let error = error {
74 | completion(.failure(error))
75 | } else {
76 | completion(.success(scheduleItems: viewController.scheduleItems, timeZone: self.state.timeZone))
77 | }
78 | }
79 | }
80 |
81 | public func syncButtonTitle(for viewController: BasalScheduleTableViewController) -> String {
82 | if self.hasActivePod {
83 | return LocalizedString("Sync With Pod", comment: "Title of button to sync basal profile from pod")
84 | } else {
85 | return LocalizedString("Save", comment: "Title of button to sync basal profile when no pod paired")
86 | }
87 | }
88 |
89 | public func syncButtonDetailText(for viewController: BasalScheduleTableViewController) -> String? {
90 | return nil
91 | }
92 |
93 | public func basalScheduleTableViewControllerIsReadOnly(_ viewController: BasalScheduleTableViewController) -> Bool {
94 | return false
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Pair/KeyExchange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyExchange.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/3/21.
6 | //
7 |
8 | import Foundation
9 | import CryptoSwift
10 |
11 | class KeyExchange {
12 | static let CMAC_SIZE = 16
13 |
14 | static let PUBLIC_KEY_SIZE = 32
15 | static let NONCE_SIZE = 16
16 |
17 | private let INTERMEDIARY_KEY_MAGIC_STRING = "TWIt".data(using: .utf8)
18 | private let PDM_CONF_MAGIC_PREFIX = "KC_2_U".data(using: .utf8)
19 | private let POD_CONF_MAGIC_PREFIX = "KC_2_V".data(using: .utf8)
20 |
21 | let pdmNonce: Data
22 | let pdmPrivate: Data
23 | let pdmPublic: Data
24 | var podPublic: Data
25 | var podNonce: Data
26 | var podConf: Data
27 | var pdmConf: Data
28 | var ltk: Data
29 |
30 | private let keyGenerator: PrivateKeyGenerator
31 | let randomByteGenerator: RandomByteGenerator
32 |
33 | init(_ keyGenerator: PrivateKeyGenerator, _ randomByteGenerator: RandomByteGenerator) throws {
34 | self.keyGenerator = keyGenerator
35 | self.randomByteGenerator = randomByteGenerator
36 |
37 | pdmNonce = randomByteGenerator.nextBytes(length: KeyExchange.NONCE_SIZE)
38 | pdmPrivate = keyGenerator.generatePrivateKey()
39 | pdmPublic = try keyGenerator.publicFromPrivate(pdmPrivate)
40 |
41 | podPublic = Data(capacity: KeyExchange.PUBLIC_KEY_SIZE)
42 | podNonce = Data(capacity: KeyExchange.NONCE_SIZE)
43 |
44 | podConf = Data(capacity: KeyExchange.CMAC_SIZE)
45 | pdmConf = Data(capacity: KeyExchange.CMAC_SIZE)
46 |
47 | ltk = Data(capacity: KeyExchange.CMAC_SIZE)
48 | }
49 |
50 | func updatePodPublicData(_ payload: Data) throws {
51 | if (payload.count != KeyExchange.PUBLIC_KEY_SIZE + KeyExchange.NONCE_SIZE) {
52 | throw BluetoothErrors.MessageIOException("Invalid payload size")
53 | }
54 | podPublic = payload.subdata(in: 0.. Data {
102 | let mac = try CMAC(key: key.bytes)
103 | return try Data(mac.authenticate(data.bytes))
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/BasalSchedule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BasalSchedule.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 4/4/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct BasalScheduleEntry: RawRepresentable, Equatable {
12 |
13 | public typealias RawValue = [String: Any]
14 |
15 | let rate: Double
16 | let startTime: TimeInterval
17 |
18 | public init(rate: Double, startTime: TimeInterval) {
19 | self.rate = rate
20 | self.startTime = startTime
21 | }
22 |
23 | // MARK: - RawRepresentable
24 | public init?(rawValue: RawValue) {
25 |
26 | guard
27 | let rate = rawValue["rate"] as? Double,
28 | let startTime = rawValue["startTime"] as? Double
29 | else {
30 | return nil
31 | }
32 |
33 | self.rate = rate
34 | self.startTime = startTime
35 | }
36 |
37 | public var rawValue: RawValue {
38 | let rawValue: RawValue = [
39 | "rate": rate,
40 | "startTime": startTime
41 | ]
42 |
43 | return rawValue
44 | }
45 | }
46 |
47 | // A basal schedule starts at midnight and should contain 24 hours worth of entries
48 | public struct BasalSchedule: RawRepresentable, Equatable {
49 |
50 | public typealias RawValue = [String: Any]
51 |
52 | let entries: [BasalScheduleEntry]
53 |
54 | public func rateAt(offset: TimeInterval) -> Double {
55 | let (_, entry, _) = lookup(offset: offset)
56 | return entry.rate
57 | }
58 |
59 | // Returns index, entry, and time remaining
60 | func lookup(offset: TimeInterval) -> (Int, BasalScheduleEntry, TimeInterval) {
61 | guard offset >= 0 && offset < .hours(24) else {
62 | fatalError("Schedule offset out of bounds")
63 | }
64 |
65 | var last: TimeInterval = .hours(24)
66 | for (index, entry) in entries.reversed().enumerated() {
67 | if entry.startTime <= offset {
68 | return (entries.count - (index + 1), entry, last - entry.startTime)
69 | }
70 | last = entry.startTime
71 | }
72 | fatalError("Schedule incomplete")
73 | }
74 |
75 | public init(entries: [BasalScheduleEntry]) {
76 | self.entries = entries
77 | }
78 |
79 | public func durations() -> [(rate: Double, duration: TimeInterval, startTime: TimeInterval)] {
80 | var last: TimeInterval = .hours(24)
81 | let durations = entries.reversed().map { (entry) -> (rate: Double, duration: TimeInterval, startTime: TimeInterval) in
82 | let duration = (rate: entry.rate, duration: last - entry.startTime, startTime: entry.startTime)
83 | last = entry.startTime
84 | return duration
85 | }
86 | return durations.reversed()
87 | }
88 |
89 | // MARK: - RawRepresentable
90 | public init?(rawValue: RawValue) {
91 |
92 | guard
93 | let entries = rawValue["entries"] as? [BasalScheduleEntry.RawValue]
94 | else {
95 | return nil
96 | }
97 |
98 | self.entries = entries.compactMap { BasalScheduleEntry(rawValue: $0) }
99 | }
100 |
101 | public var rawValue: RawValue {
102 | let rawValue: RawValue = [
103 | "entries": entries.map { $0.rawValue }
104 | ]
105 |
106 | return rawValue
107 | }
108 | }
109 |
110 | public extension Sequence where Element == BasalScheduleEntry {
111 | func adjacentEqualRatesMerged() -> [BasalScheduleEntry] {
112 | var output = [BasalScheduleEntry]()
113 | let _ = self.reduce(nil) { (lastRate, entry) -> TimeInterval? in
114 | if entry.rate != lastRate {
115 | output.append(entry)
116 | }
117 | return entry.rate
118 | }
119 | return output
120 | }
121 | }
122 |
123 |
124 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TempBasalExtraCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 6/6/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct TempBasalExtraCommand : MessageBlock {
12 |
13 | public let acknowledgementBeep: Bool
14 | public let completionBeep: Bool
15 | public let programReminderInterval: TimeInterval
16 | public let remainingPulses: Double
17 | public let delayUntilFirstTenthOfPulse: TimeInterval
18 | public let rateEntries: [RateEntry]
19 |
20 | public let blockType: MessageBlockType = .tempBasalExtra
21 |
22 | public var data: Data {
23 | let beepOptions = (UInt8(programReminderInterval.minutes) & 0x3f) + (completionBeep ? (1<<6) : 0) + (acknowledgementBeep ? (1<<7) : 0)
24 | var data = Data([
25 | blockType.rawValue,
26 | UInt8(8 + rateEntries.count * 6),
27 | beepOptions,
28 | 0
29 | ])
30 | data.appendBigEndian(UInt16(round(remainingPulses * 2) * 5))
31 | if remainingPulses == 0 {
32 | data.appendBigEndian(UInt32(delayUntilFirstTenthOfPulse.hundredthsOfMilliseconds) * 10)
33 | } else {
34 | data.appendBigEndian(UInt32(delayUntilFirstTenthOfPulse.hundredthsOfMilliseconds))
35 | }
36 | for entry in rateEntries {
37 | data.append(entry.data)
38 | }
39 | return data
40 | }
41 |
42 | public init(encodedData: Data) throws {
43 |
44 | let length = encodedData[1]
45 | let numEntries = (length - 8) / 6
46 |
47 | acknowledgementBeep = encodedData[2] & (1<<7) != 0
48 | completionBeep = encodedData[2] & (1<<6) != 0
49 | programReminderInterval = TimeInterval(minutes: Double(encodedData[2] & 0x3f))
50 |
51 | remainingPulses = Double(encodedData[4...].toBigEndian(UInt16.self)) / 10.0
52 | let timerCounter = encodedData[6...].toBigEndian(UInt32.self)
53 | if remainingPulses == 0 {
54 | delayUntilFirstTenthOfPulse = TimeInterval(hundredthsOfMilliseconds: Double(timerCounter) / 10)
55 | } else {
56 | delayUntilFirstTenthOfPulse = TimeInterval(hundredthsOfMilliseconds: Double(timerCounter))
57 | }
58 | var entries = [RateEntry]()
59 | for entryIndex in (0.. OmnipodPumpManagerSetupViewController {
29 | return UIStoryboard(name: "OmnipodPumpManager", bundle: Bundle(for: OmnipodPumpManagerSetupViewController.self)).instantiateInitialViewController() as! OmnipodPumpManagerSetupViewController
30 | }
31 |
32 | override public func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | if #available(iOSApplicationExtension 13.0, *) {
36 | // Prevent interactive dismissal
37 | isModalInPresentation = true
38 | view.backgroundColor = .systemBackground
39 | } else {
40 | view.backgroundColor = .white
41 | }
42 | navigationBar.shadowImage = UIImage()
43 |
44 | delegate = self
45 | }
46 |
47 | private(set) var pumpManager: OmnipodPumpManager?
48 |
49 | /*
50 | 1. Basal Rates & Delivery Limits
51 |
52 | 2. Pod Pairing/Priming
53 |
54 | 3. Cannula Insertion
55 |
56 | 4. Pod Setup Complete
57 | */
58 |
59 | public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
60 | // Read state values
61 | let viewControllers = navigationController.viewControllers
62 | let count = navigationController.viewControllers.count
63 |
64 | if count >= 2 {
65 | switch viewControllers[count - 2] {
66 | case let vc as PairPodSetupViewController:
67 | pumpManager = vc.pumpManager
68 | default:
69 | break
70 | }
71 | }
72 |
73 | if let setupViewController = viewController as? SetupTableViewController {
74 | setupViewController.delegate = self
75 | }
76 |
77 |
78 | // Set state values
79 | switch viewController {
80 | case let vc as PairPodSetupViewController:
81 | if let basalSchedule = basalSchedule {
82 | let schedule = BasalSchedule(repeatingScheduleValues: basalSchedule.items)
83 | let pumpManagerState = OmnipodPumpManagerState(podState: nil, timeZone: .currentFixed, basalSchedule: schedule)
84 | let pumpManager = OmnipodPumpManager(state: pumpManagerState)
85 | vc.pumpManager = pumpManager
86 | setupDelegate?.pumpManagerSetupViewController(self, didSetUpPumpManager: pumpManager)
87 | }
88 | case let vc as InsertCannulaSetupViewController:
89 | vc.pumpManager = pumpManager
90 | case let vc as PodSetupCompleteViewController:
91 | vc.pumpManager = pumpManager
92 | default:
93 | break
94 | }
95 | }
96 |
97 | open func finishedSetup() {
98 | if let pumpManager = pumpManager {
99 | let settings = OmnipodSettingsViewController(pumpManager: pumpManager)
100 | setViewControllers([settings], animated: true)
101 | }
102 | }
103 |
104 | public func finishedSettingsDisplay() {
105 | completionDelegate?.completionNotifyingDidComplete(self)
106 | }
107 | }
108 |
109 | extension OmnipodPumpManagerSetupViewController: SetupTableViewControllerDelegate {
110 | public func setupTableViewControllerCancelButtonPressed(_ viewController: SetupTableViewController) {
111 | completionDelegate?.completionNotifyingDidComplete(self)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PodInfoPulseLog.swift
3 | // OmniKit
4 | //
5 | // Created by Eelke Jager on 26/09/2018.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Type $50 Pod Info returns (up to) the most recent 50 32-bit pulse log entries
12 | public struct PodInfoPulseLogRecent : PodInfo {
13 | // CMD 1 2 3 4 5 6 7 8
14 | // DATA 0 1 2 3 4 5 6
15 | // 02 LL 50 IIII XXXXXXXX ...
16 |
17 | public let podInfoType : PodInfoResponseSubType = .pulseLogRecent
18 | public let indexLastEntry: Int // the pulse # for last pulse log entry
19 | public let nEntries : Int // how many 32-bit pulse entries returned (calculated)
20 | public let pulseLog : [UInt32]
21 | public let data : Data
22 |
23 | public init(encodedData: Data) throws {
24 | let logStartByteOffset = 3 // starting byte offset of the pulse log in DATA
25 | let nLogBytesReturned = encodedData.count - logStartByteOffset
26 | guard encodedData.count >= logStartByteOffset && (nLogBytesReturned & 0x3) == 0 else {
27 | throw MessageBlockError.notEnoughData // not enough data to start log or a non-integral # of pulse log entries
28 | }
29 | self.nEntries = nLogBytesReturned / MemoryLayout.size
30 | self.indexLastEntry = Int((UInt16(encodedData[1]) << 8) | UInt16(encodedData[2]))
31 | self.pulseLog = createPulseLog(encodedData: encodedData, logStartByteOffset: logStartByteOffset, nEntries: self.nEntries)
32 | self.data = encodedData
33 | }
34 | }
35 |
36 | // Type $51 Pod info returns (up to) the most previous 50 32-bit pulse log entries
37 | public struct PodInfoPulseLogPrevious : PodInfo {
38 | // CMD 1 2 3 4 5 6 7 8
39 | // DATA 0 1 2 3 4 5 6
40 | // 02 LL 51 NNNN XXXXXXXX ...
41 |
42 | public let podInfoType : PodInfoResponseSubType = .pulseLogPrevious
43 | public let nEntries : Int // how many 32-bit pulse log entries returned
44 | public let pulseLog : [UInt32]
45 | public let data : Data
46 |
47 | public init(encodedData: Data) throws {
48 | let logStartByteOffset = 3 // starting byte offset of the pulse log in DATA
49 | let nLogBytesReturned = encodedData.count - logStartByteOffset
50 | guard encodedData.count >= logStartByteOffset && (nLogBytesReturned & 0x3) == 0 else {
51 | throw MessageBlockError.notEnoughData // first 3 bytes missing or non-integral # of pulse log entries
52 | }
53 | let nEntriesCalculated = nLogBytesReturned / MemoryLayout.size
54 | self.nEntries = Int((UInt16(encodedData[1]) << 8) | UInt16(encodedData[2]))
55 | // verify we actually got all the reported entries
56 | if self.nEntries > nEntriesCalculated {
57 | throw MessageBlockError.notEnoughData // some pulse log entry count mismatch issue
58 | }
59 | self.pulseLog = createPulseLog(encodedData: encodedData, logStartByteOffset: logStartByteOffset, nEntries: self.nEntries)
60 | self.data = encodedData
61 | }
62 | }
63 |
64 | func createPulseLog(encodedData: Data, logStartByteOffset: Int, nEntries: Int) -> [UInt32] {
65 | var pulseLog: [UInt32] = Array(repeating: 0, count: nEntries)
66 | var index = 0
67 | while index < nEntries {
68 | pulseLog[index] = encodedData[(logStartByteOffset+(index*4))...].toBigEndian(UInt32.self)
69 | index += 1
70 | }
71 | return pulseLog
72 | }
73 |
74 | extension BinaryInteger {
75 | var binaryDescription: String {
76 | var binaryString = ""
77 | var internalNumber = self
78 | var counter = 0
79 |
80 | for _ in (1...self.bitWidth) {
81 | binaryString.insert(contentsOf: "\(internalNumber & 1)", at: binaryString.startIndex)
82 | internalNumber >>= 1
83 | counter += 1
84 | if counter % 8 == 0 {
85 | binaryString.insert(contentsOf: " ", at: binaryString.startIndex)
86 | }
87 | }
88 | return binaryString
89 | }
90 | }
91 |
92 | func pulseLogString(pulseLogEntries: [UInt32], lastPulseNumber: Int) -> String {
93 | var str: String = "Pulse eeeeee0a pppliiib cccccccc dfgggggg\n"
94 | var index = pulseLogEntries.count - 1
95 | var pulseNumber = lastPulseNumber
96 | while index >= 0 {
97 | str += String(format: "%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription + "\n"
98 | index -= 1
99 | pulseNumber -= 1
100 | }
101 | return str + "\n"
102 | }
103 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/MessageBlocks/BasalScheduleExtraCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BasalScheduleExtraCommand.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 3/30/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct BasalScheduleExtraCommand : MessageBlock {
12 |
13 | public let blockType: MessageBlockType = .basalScheduleExtra
14 |
15 | public let acknowledgementBeep: Bool
16 | public let completionBeep: Bool
17 | public let programReminderInterval: TimeInterval
18 | public let currentEntryIndex: UInt8
19 | public let remainingPulses: Double
20 | public let delayUntilNextTenthOfPulse: TimeInterval
21 | public let rateEntries: [RateEntry]
22 |
23 | public var data: Data {
24 | let beepOptions = (UInt8(programReminderInterval.minutes) & 0x3f) + (completionBeep ? (1<<6) : 0) + (acknowledgementBeep ? (1<<7) : 0)
25 | var data = Data([
26 | blockType.rawValue,
27 | UInt8(8 + rateEntries.count * 6),
28 | beepOptions,
29 | currentEntryIndex
30 | ])
31 | data.appendBigEndian(UInt16(round(remainingPulses * 10)))
32 | data.appendBigEndian(UInt32(round(delayUntilNextTenthOfPulse.milliseconds * 1000)))
33 | for entry in rateEntries {
34 | data.append(entry.data)
35 | }
36 | return data
37 | }
38 |
39 | public init(encodedData: Data) throws {
40 | if encodedData.count < 14 {
41 | throw MessageBlockError.notEnoughData
42 | }
43 | let length = encodedData[1]
44 | let numEntries = (length - 8) / 6
45 |
46 | acknowledgementBeep = encodedData[2] & (1<<7) != 0
47 | completionBeep = encodedData[2] & (1<<6) != 0
48 | programReminderInterval = TimeInterval(minutes: Double(encodedData[2] & 0x3f))
49 |
50 | currentEntryIndex = encodedData[3]
51 | remainingPulses = Double(encodedData[4...].toBigEndian(UInt16.self)) / 10.0
52 | let timerCounter = encodedData[6...].toBigEndian(UInt32.self)
53 | delayUntilNextTenthOfPulse = TimeInterval(hundredthsOfMilliseconds: Double(timerCounter))
54 | var entries = [RateEntry]()
55 | for entryIndex in (0..> 4
63 | guard let alertType = AlertSlot(rawValue: alertTypeBits) else {
64 | throw MessageError.unknownValue(value: alertTypeBits, typeDescription: "AlertType")
65 | }
66 | self.slot = alertType
67 |
68 | self.active = encodedData[0] & 0b1000 != 0
69 |
70 | self.autoOffModifier = encodedData[0] & 0b10 != 0
71 |
72 | self.duration = TimeInterval(minutes: Double((Int(encodedData[0] & 0b1) << 8) + Int(encodedData[1])))
73 |
74 | let yyyy = (Int(encodedData[2]) << 8) + (Int(encodedData[3])) & 0x3fff
75 |
76 | if encodedData[0] & 0b100 != 0 {
77 | let volume = Double(yyyy * 2) * Pod.pulseSize
78 | self.trigger = .unitsRemaining(volume)
79 | } else {
80 | self.trigger = .timeUntilAlert(TimeInterval(minutes: Double(yyyy)))
81 | }
82 |
83 | let beepRepeatBits = encodedData[4]
84 | guard let beepRepeat = BeepRepeat(rawValue: beepRepeatBits) else {
85 | throw MessageError.unknownValue(value: beepRepeatBits, typeDescription: "BeepRepeat")
86 | }
87 | self.beepRepeat = beepRepeat
88 |
89 | let beepTypeBits = encodedData[5]
90 | guard let beepType = BeepType(rawValue: beepTypeBits) else {
91 | throw MessageError.unknownValue(value: beepTypeBits, typeDescription: "BeepType")
92 | }
93 | self.beepType = beepType
94 |
95 | }
96 |
97 | public var data: Data {
98 | var firstByte = slot.rawValue << 4
99 | firstByte += active ? (1 << 3) : 0
100 |
101 | if case .unitsRemaining = trigger {
102 | firstByte += 1 << 2
103 | }
104 | if autoOffModifier {
105 | firstByte += 1 << 1
106 | }
107 | // High bit of duration
108 | firstByte += UInt8((Int(duration.minutes) >> 8) & 0x1)
109 |
110 | var data = Data([
111 | firstByte,
112 | UInt8(Int(duration.minutes) & 0xff)
113 | ])
114 |
115 | switch trigger {
116 | case .unitsRemaining(let volume):
117 | let ticks = UInt16(volume / Pod.pulseSize / 2)
118 | data.appendBigEndian(ticks)
119 | case .timeUntilAlert(let secondsUntilAlert):
120 | // round the time to alert to the nearest minute
121 | let minutes = UInt16((secondsUntilAlert + 30).minutes)
122 | data.appendBigEndian(minutes)
123 | }
124 | data.append(beepRepeat.rawValue)
125 | data.append(beepType.rawValue)
126 |
127 | return data
128 | }
129 | }
130 |
131 | extension ConfigureAlertsCommand: CustomDebugStringConvertible {
132 | public var debugDescription: String {
133 | return "ConfigureAlertsCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), configurations:\(configurations))"
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/OmniBLE/PumpManagerUI/Views/OmnipodReservoirView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OmnipodReservoirView.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 10/22/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import LoopKitUI
11 | import OmniKit
12 |
13 | public final class OmnipodReservoirView: LevelHUDView, NibLoadable {
14 |
15 | override public var orderPriority: HUDViewOrderPriority {
16 | return 11
17 | }
18 |
19 | @IBOutlet private weak var volumeLabel: UILabel!
20 |
21 | @IBOutlet private weak var alertLabel: UILabel! {
22 | didSet {
23 | alertLabel.alpha = 0
24 | alertLabel.textColor = UIColor.white
25 | alertLabel.layer.cornerRadius = 9
26 | alertLabel.clipsToBounds = true
27 | }
28 | }
29 |
30 | public class func instantiate() -> OmnipodReservoirView {
31 | return nib().instantiate(withOwner: nil, options: nil)[0] as! OmnipodReservoirView
32 | }
33 |
34 | override public func awakeFromNib() {
35 | super.awakeFromNib()
36 |
37 | volumeLabel.isHidden = true
38 | }
39 |
40 | private var reservoirLevel: Double? {
41 | didSet {
42 | level = reservoirLevel
43 |
44 | switch reservoirLevel {
45 | case .none:
46 | volumeLabel.isHidden = true
47 | case let x? where x > 0.25:
48 | volumeLabel.isHidden = true
49 | case let x? where x > 0.10:
50 | volumeLabel.textColor = tintColor
51 | volumeLabel.isHidden = false
52 | default:
53 | volumeLabel.textColor = tintColor
54 | volumeLabel.isHidden = false
55 | }
56 | }
57 | }
58 |
59 | override public func tintColorDidChange() {
60 | super.tintColorDidChange()
61 |
62 | volumeLabel.textColor = tintColor
63 | }
64 |
65 |
66 | private func updateColor() {
67 | switch reservoirAlertState {
68 | case .lowReservoir, .empty:
69 | alertLabel.backgroundColor = stateColors?.warning
70 | case .ok:
71 | alertLabel.backgroundColor = stateColors?.normal
72 | }
73 | }
74 |
75 | override public func stateColorsDidUpdate() {
76 | super.stateColorsDidUpdate()
77 | updateColor()
78 | }
79 |
80 |
81 | private var reservoirAlertState = ReservoirAlertState.ok {
82 | didSet {
83 | var alertLabelAlpha: CGFloat = 1
84 |
85 | switch reservoirAlertState {
86 | case .ok:
87 | alertLabelAlpha = 0
88 | case .lowReservoir, .empty:
89 | alertLabel.text = "!"
90 | }
91 |
92 | updateColor()
93 |
94 | if self.superview == nil {
95 | self.alertLabel.alpha = alertLabelAlpha
96 | } else {
97 | UIView.animate(withDuration: 0.25, animations: {
98 | self.alertLabel.alpha = alertLabelAlpha
99 | })
100 | }
101 | }
102 | }
103 |
104 | private lazy var timeFormatter: DateFormatter = {
105 | let formatter = DateFormatter()
106 | formatter.dateStyle = .none
107 | formatter.timeStyle = .short
108 |
109 | return formatter
110 | }()
111 |
112 | private lazy var numberFormatter: NumberFormatter = {
113 | let formatter = NumberFormatter()
114 | formatter.numberStyle = .decimal
115 | formatter.maximumFractionDigits = 0
116 |
117 | return formatter
118 | }()
119 |
120 | private let insulinFormatter: NumberFormatter = {
121 | let formatter = NumberFormatter()
122 | formatter.numberStyle = .decimal
123 | formatter.maximumFractionDigits = 3
124 | return formatter
125 | }()
126 |
127 |
128 | public func update(volume: Double?, at date: Date, level: Double?, reservoirAlertState: ReservoirAlertState) {
129 | self.reservoirLevel = level
130 |
131 | let time = timeFormatter.string(from: date)
132 | caption?.text = time
133 |
134 | if let volume = volume {
135 | if let units = numberFormatter.string(from: volume) {
136 | volumeLabel.text = String(format: LocalizedString("%@U", comment: "Format string for reservoir volume. (1: The localized volume)"), units)
137 |
138 | accessibilityValue = String(format: LocalizedString("%1$@ units remaining at %2$@", comment: "Accessibility format string for (1: localized volume)(2: time)"), units, time)
139 | }
140 | } else if let maxReservoirReading = insulinFormatter.string(from: Pod.maximumReservoirReading) {
141 | accessibilityValue = String(format: LocalizedString("Greater than %1$@ units remaining at %2$@", comment: "Accessibility format string for (1: localized volume)(2: time)"), maxReservoirReading, time)
142 | }
143 | self.reservoirAlertState = reservoirAlertState
144 | }
145 | }
146 |
147 |
148 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/Message.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Message.swift
3 | // OmniKit
4 | //
5 | // Created by Pete Schwamb on 10/14/17.
6 | // Copyright © 2017 Pete Schwamb. All rights reserved.
7 | //
8 | import Foundation
9 |
10 | public enum MessageError: Error {
11 | case notEnoughData
12 | case invalidCrc
13 | case invalidSequence
14 | case invalidAddress(address: UInt32)
15 | case parsingError(offset: Int, data: Data, error: Error)
16 | case unknownValue(value: UInt8, typeDescription: String)
17 | case validationFailed(description: String)
18 | }
19 |
20 | struct Message {
21 | let address: UInt32
22 | let messageBlocks: [MessageBlock]
23 | let sequenceNum: Int
24 | let expectFollowOnMessage: Bool
25 |
26 | init(address: UInt32, messageBlocks: [MessageBlock], sequenceNum: Int, expectFollowOnMessage: Bool = false) {
27 | self.address = address
28 | self.messageBlocks = messageBlocks
29 | self.sequenceNum = sequenceNum
30 | self.expectFollowOnMessage = expectFollowOnMessage
31 | }
32 |
33 | init(encodedData: Data) throws {
34 | guard encodedData.count >= 10 else {
35 | throw MessageError.notEnoughData
36 | }
37 | self.address = encodedData[0...].toBigEndian(UInt32.self)
38 | let b9 = encodedData[4]
39 | let bodyLen = encodedData[5]
40 |
41 | if bodyLen > encodedData.count - 8 {
42 | throw MessageError.notEnoughData
43 | }
44 |
45 | self.expectFollowOnMessage = (b9 & 0b10000000) != 0
46 | self.sequenceNum = Int((b9 >> 2) & 0b1111)
47 | let crc = (UInt16(encodedData[encodedData.count-2]) << 8) + UInt16(encodedData[encodedData.count-1])
48 | let msgWithoutCrc = encodedData.prefix(encodedData.count - 2)
49 | let computedCrc = UInt16(msgWithoutCrc.crc16())
50 |
51 | let acceptZeroCRC16 = true // needed for pod simulator which doesn't compute a CRC16
52 | let ignoreDecodeCRCErrors = true // temp hack to continue running
53 | if computedCrc != crc && !(acceptZeroCRC16 && crc == 0) {
54 | if (ignoreDecodeCRCErrors) {
55 | self.messageBlocks = try Message.decodeBlocks(data: Data(msgWithoutCrc.suffix(from: 6)))
56 | return // set breakpoint on this line to catch a CRC mismatch
57 | }
58 | throw MessageError.invalidCrc
59 | }
60 | self.messageBlocks = try Message.decodeBlocks(data: Data(msgWithoutCrc.suffix(from: 6)))
61 | }
62 |
63 | static private func decodeBlocks(data: Data) throws -> [MessageBlock] {
64 | var blocks = [MessageBlock]()
65 | var idx = 0
66 | repeat {
67 | guard let blockType = MessageBlockType(rawValue: data[idx]) else {
68 | throw MessageBlockError.unknownBlockType(rawVal: data[idx])
69 | }
70 | do {
71 | let block = try blockType.blockType.init(encodedData: Data(data.suffix(from: idx)))
72 | blocks.append(block)
73 | idx += Int(block.data.count)
74 | } catch (let error) {
75 | throw MessageError.parsingError(offset: idx, data: data.suffix(from: idx), error: error)
76 | }
77 | } while idx < data.count
78 | return blocks
79 | }
80 |
81 | func encoded() -> Data {
82 | var bytes = Data(bigEndian: address)
83 |
84 | var cmdData = Data()
85 | for cmd in messageBlocks {
86 | cmdData.append(cmd.data)
87 | }
88 |
89 | let b9: UInt8 = ((expectFollowOnMessage ? 1 : 0) << 7) + (UInt8(sequenceNum & 0b11111) << 2) + UInt8((cmdData.count >> 8) & 0b11)
90 | bytes.append(b9)
91 | bytes.append(UInt8(cmdData.count & 0xff))
92 |
93 | var data = Data(bytes) + cmdData
94 | let crc: UInt16 = data.crc16()
95 | data.appendBigEndian(crc)
96 | return data
97 | }
98 |
99 | var fault: DetailedStatus? {
100 | if messageBlocks.count > 0 && messageBlocks[0].blockType == .podInfoResponse,
101 | let infoResponse = messageBlocks[0] as? PodInfoResponse,
102 | infoResponse.podInfoResponseSubType == .detailedStatus,
103 | let detailedStatus = infoResponse.podInfo as? DetailedStatus,
104 | detailedStatus.isFaulted
105 | {
106 | return detailedStatus
107 | } else {
108 | return nil
109 | }
110 | }
111 |
112 | // returns the encoded length of a message
113 | static func messageLength(message: [MessageBlock]) -> Int {
114 | let message = Message(address: 0, messageBlocks: message, sequenceNum: 0)
115 | let encodedData = message.encoded()
116 | return encodedData.count
117 | }
118 | }
119 |
120 | extension Message: CustomDebugStringConvertible {
121 | var debugDescription: String {
122 | let sequenceNumStr = String(format: "%02d", sequenceNum)
123 | return "Message(\(Data(bigEndian: address).hexadecimalString) seq:\(sequenceNumStr) \(messageBlocks))"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/OmniBLE/OmnipodCommon/Pod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Pod.swift
3 | // OmnipodKit
4 | //
5 | // Created by Pete Schwamb on 4/4/18.
6 | // Copyright © 2018 Pete Schwamb. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Pod {
12 | // Volume of U100 insulin in one motor pulse
13 | // Must agree with value returned by pod during the pairing process.
14 | public static let pulseSize: Double = 0.05
15 |
16 | // Number of pulses required to deliver one unit of U100 insulin
17 | public static let pulsesPerUnit: Double = 1 / Pod.pulseSize
18 |
19 | // Seconds per pulse for boluses
20 | // Checked to verify it agrees with value returned by pod during the pairing process.
21 | public static let secondsPerBolusPulse: Double = 2
22 |
23 | // Units per second for boluses
24 | public static let bolusDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerBolusPulse
25 |
26 | // Seconds per pulse for priming/cannula insertion
27 | // Checked to verify it agrees with value returned by pod during the pairing process.
28 | public static let secondsPerPrimePulse: Double = 1
29 |
30 | // Units per second for priming/cannula insertion
31 | public static let primeDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerPrimePulse
32 |
33 | // User configured time before expiration advisory (PDM allows 1-24 hours)
34 | public static let expirationAlertWindow = TimeInterval(hours: 2)
35 |
36 | // Expiration advisory window: time after expiration alert, and end of service imminent alarm
37 | public static let expirationAdvisoryWindow = TimeInterval(hours: 7)
38 |
39 | // End of service imminent window, relative to pod end of service
40 | public static let endOfServiceImminentWindow = TimeInterval(hours: 1)
41 |
42 | // Total pod service time. A fault is triggered if this time is reached before pod deactivation.
43 | // Checked to verify it agrees with value returned by pod during the pairing process.
44 | public static let serviceDuration = TimeInterval(hours: 80)
45 |
46 | // Nomimal pod life (72 hours)
47 | public static let nominalPodLife = Pod.serviceDuration - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow
48 |
49 | // Maximum reservoir level reading
50 | public static let maximumReservoirReading: Double = 50
51 |
52 | // Reservoir Capacity
53 | public static let reservoirCapacity: Double = 200
54 |
55 | // Supported basal rates
56 | public static let supportedBasalRates: [Double] = (1...600).map { Double($0) / Double(pulsesPerUnit) }
57 |
58 | // Maximum number of basal schedule entries supported
59 | public static let maximumBasalScheduleEntryCount: Int = 24
60 |
61 | // Minimum duration of a single basal schedule entry
62 | public static let minimumBasalScheduleEntryDuration = TimeInterval.minutes(30)
63 |
64 | // Default amount for priming bolus using secondsPerPrimePulse timing.
65 | // Checked to verify it agrees with value returned by pod during the pairing process.
66 | public static let primeUnits = 2.6
67 |
68 | // Default amount for cannula insertion bolus using secondsPerPrimePulse timing.
69 | // Checked to verify it agrees with value returned by pod during the pairing process.
70 | public static let cannulaInsertionUnits = 0.5
71 |
72 | public static let cannulaInsertionUnitsExtra = 0.0 // edit to add a fixed additional amount of insulin during cannula insertion
73 |
74 | // Default and limits for expiration reminder alerts
75 | public static let expirationReminderAlertDefaultTimeBeforeExpiration = TimeInterval.hours(2)
76 | public static let expirationReminderAlertMinTimeBeforeExpiration = TimeInterval.hours(1)
77 | public static let expirationReminderAlertMaxTimeBeforeExpiration = TimeInterval.hours(24)
78 | }
79 |
80 | // DeliveryStatus used in StatusResponse and DetailedStatus
81 | public enum DeliveryStatus: UInt8, CustomStringConvertible {
82 | case suspended = 0
83 | case scheduledBasal = 1
84 | case tempBasalRunning = 2
85 | case priming = 4
86 | case bolusInProgress = 5
87 | case bolusAndTempBasal = 6
88 |
89 | public var bolusing: Bool {
90 | return self == .bolusInProgress || self == .bolusAndTempBasal
91 | }
92 |
93 | public var tempBasalRunning: Bool {
94 | return self == .tempBasalRunning || self == .bolusAndTempBasal
95 | }
96 |
97 | public var description: String {
98 | switch self {
99 | case .suspended:
100 | return LocalizedString("Suspended", comment: "Delivery status when insulin delivery is suspended")
101 | case .scheduledBasal:
102 | return LocalizedString("Scheduled basal", comment: "Delivery status when scheduled basal is running")
103 | case .tempBasalRunning:
104 | return LocalizedString("Temp basal running", comment: "Delivery status when temp basal is running")
105 | case .priming:
106 | return LocalizedString("Priming", comment: "Delivery status when pod is priming")
107 | case .bolusInProgress:
108 | return LocalizedString("Bolusing", comment: "Delivery status when bolusing")
109 | case .bolusAndTempBasal:
110 | return LocalizedString("Bolusing with temp basal", comment: "Delivery status when bolusing and temp basal is running")
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Session/Milenage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Milenage.swift
3 | // OmniBLE
4 | //
5 | // Created by Randall Knutson on 11/8/21.
6 | // Copyright © 2021 Randall Knutson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 | import CryptoSwift
12 |
13 | enum MilenageError: Error {
14 | case Error(String)
15 | }
16 |
17 | class Milenage {
18 | static let RESYNC_AMF = Data(hex: "0000")
19 | static let MILENAGE_OP = Data(hex: "cdc202d5123e20f62b6d676ac72cb318")
20 | static let MILENAGE_AMF = Data(hex: "b9b9")
21 | static let KEY_SIZE = 16
22 | static let AUTS_SIZE = 14
23 | static private let SQN = 6
24 |
25 | private let log = OSLog(subsystem: "Milenage", category: "Milenage")
26 | private let k: Data
27 | let sqn: Data
28 | let auts: Data
29 | let amf: Data
30 | var ck: Data
31 | var autn: Data
32 | var rand: Data
33 | var synchronizationSqn: Data
34 | var res: Data
35 | var ak: Data
36 | var macS: Data
37 | var receivedMacS: Data
38 |
39 | init(k: Data, sqn: Data, randParam: Data? = nil, auts: Data = Data(repeating: 0x00, count: 14), amf: Data = Milenage.MILENAGE_AMF) throws {
40 | guard (k.count == Milenage.KEY_SIZE) else { throw MilenageError.Error("Milenage key has to be \(Milenage.KEY_SIZE) bytes long. Received: \(k.hexadecimalString)") }
41 | guard (sqn.count == Milenage.SQN) else { throw MilenageError.Error("Milenage SQN has to be \(Milenage.SQN) long. Received: \(sqn.hexadecimalString)") }
42 | guard (auts.count == Milenage.AUTS_SIZE) else { throw MilenageError.Error("Milenage AUTS has to be \(Milenage.AUTS_SIZE) long. Received: \(auts.hexadecimalString)") }
43 | guard (amf.count == Milenage.MILENAGE_AMF.count) else {
44 | throw MilenageError.Error("Milenage AMF has to be ${MILENAGE_AMF.count} long." +
45 | "Received: ${amf.toHex()}")
46 | }
47 | self.k = k
48 | self.sqn = sqn
49 | self.auts = auts
50 | self.amf = amf
51 |
52 | let cipher = try AES(key: k.bytes, blockMode: ECB(), padding: .noPadding)
53 |
54 | let random = OmniRandomByteGenerator()
55 | rand = randParam ?? random.nextBytes(length: Milenage.KEY_SIZE)
56 |
57 | let opc = Data(try cipher.encrypt(Milenage.MILENAGE_OP.bytes)) ^ Milenage.MILENAGE_OP
58 | let randOpcEncrypted = Data(try cipher.encrypt((rand ^ opc).bytes))
59 | let randOpcEncryptedxorOpc = randOpcEncrypted ^ opc
60 | var resAkInput = randOpcEncryptedxorOpc.subdata(in: 0..(repeating: 0x00, count: Milenage.KEY_SIZE)
70 |
71 | for i in 0...15 {
72 | ckInput[(i + 12) % 16] = randOpcEncryptedxorOpc[i]
73 | }
74 | ckInput[15] = UInt8((Int(ckInput[15]) ^ 2))
75 |
76 | ck = Data(try cipher.encrypt(ckInput)) ^ opc
77 |
78 | let sqnAmf = sqn + amf + sqn + amf
79 | let sqnAmfxorOpc = sqnAmf ^ opc
80 | var macAInput = Array(repeating: 0x00, count: Milenage.KEY_SIZE)
81 |
82 | for i in 0...15 {
83 | macAInput[(i + 8) % 16] = sqnAmfxorOpc[i]
84 | }
85 |
86 | let macAFull = Data(try cipher.encrypt((Data(macAInput) ^ randOpcEncrypted).bytes)) ^ opc
87 | let macA = macAFull.subdata(in: 0..<8)
88 | macS = macAFull.subdata(in: 8..<16)
89 |
90 | autn = (ak ^ sqn) + amf + macA
91 |
92 | // Used for re-synchronisation AUTS = SQN^AK || MAC-S
93 | var akStarInput = Array(repeating: 0x00, count: Milenage.KEY_SIZE)
94 |
95 | for i in 0...15 {
96 | akStarInput[(i + 4) % 16] = randOpcEncryptedxorOpc[i]
97 | }
98 | akStarInput[15] = UInt8((Int(akStarInput[15]) ^ 8))
99 |
100 | let akStarFull = Data(try cipher.encrypt(akStarInput)) ^ opc
101 | let akStar = akStarFull.subdata(in: 0..<6)
102 |
103 | let seqxorAkStar = auts.subdata(in: 0..<6)
104 | synchronizationSqn = akStar ^ seqxorAkStar
105 | receivedMacS = auts.subdata(in: 6..<14)
106 |
107 | print("Milenage K: \(k.hexadecimalString)")
108 | print("Milenage RAND: \(rand.hexadecimalString)")
109 | print("Milenage SQN: \(sqn.hexadecimalString)")
110 | print("Milenage CK: \(ck.hexadecimalString)")
111 | print("Milenage AUTN: \(autn.hexadecimalString)")
112 | print("Milenage RES: \(res.hexadecimalString)")
113 | print("Milenage AK: \(ak.hexadecimalString)")
114 | print("Milenage AK STAR: \(akStar.hexadecimalString)")
115 | print("Milenage OPC: \(opc.hexadecimalString)")
116 | print("Milenage FullMAC: \(macAFull.hexadecimalString)")
117 | print("Milenage MacA: \(macA.hexadecimalString)")
118 | print("Milenage MacS: \(macS.hexadecimalString)")
119 | print("Milenage AUTS: \(auts.hexadecimalString)")
120 | print("Milenage synchronizationSqn: \(synchronizationSqn.hexadecimalString)")
121 | print("Milenage receivedMacS: \(receivedMacS.hexadecimalString)")
122 | }
123 |
124 | }
125 |
126 | extension Data {
127 | static func ^ (left: Data, right: Data) -> Data {
128 | var out = Array(repeating: 0x00, count: left.count)
129 | for i in 0.. MessagePacket {
18 | guard payload.count >= HEADER_SIZE else {
19 | throw BluetoothErrors.CouldNotParseMessageException("Incorrect header size")
20 | }
21 |
22 | guard (String(data: payload.subdata(in: 0..<2), encoding: .utf8) == MAGIC_PATTERN) else {
23 | throw BluetoothErrors.CouldNotParseMessageException("Magic pattern mismatch")
24 | }
25 | let payloadData = payload
26 |
27 | let f1 = Flag(payloadData[2])
28 | let sas = f1.get(3) != 0
29 | let tfs = f1.get(4) != 0
30 | let version = Int16(((f1.get(0) << 2) | (f1.get(1) << 1) | (f1.get(2) << 0)))
31 | let eqos = Int16((f1.get(7) | (f1.get(6) << 1) | (f1.get(5) << 2)))
32 |
33 | let f2 = Flag(payloadData[3])
34 | let ack = f2.get(0) != 0
35 | let priority = f2.get(1) != 0
36 | let lastMessage = f2.get(2) != 0
37 | let gateway = f2.get(3) != 0
38 | let type: MessageType = MessageType(rawValue: UInt8(f2.get(7) | (f2.get(6) << 1) | (f2.get(5) << 2) | (f2.get(4) << 3))) ?? .CLEAR
39 | if (version != 0) {
40 | throw BluetoothErrors.CouldNotParseMessageException("Wrong version")
41 | }
42 | let sequenceNumber = payloadData[4]
43 | let ackNumber = payloadData[5]
44 | let size = (UInt16(payloadData[6]) << 3) | (UInt16(payloadData[7]) >> 5)
45 | guard payload.count >= (Int(size) + MessagePacket.HEADER_SIZE) else {
46 | throw BluetoothErrors.CouldNotParseMessageException("Wrong payload size")
47 | }
48 |
49 | let payloadEnd = Int(16 + size + (type == MessageType.ENCRYPTED ? 8 : 0))
50 |
51 | return MessagePacket(
52 | type: type,
53 | source: Id(payload.subdata(in: 8..<12)).toUInt32(),
54 | destination: Id(payload.subdata(in: 12..<16)).toUInt32(),
55 | payload: payload.subdata(in: 16.. Data {
101 | var bb = Data(capacity: 16 + payload.count)
102 | bb.append(MessagePacket.MAGIC_PATTERN.data(using: .utf8)!)
103 |
104 | let f1 = Flag()
105 | f1.set(0, self.version & 4 != 0)
106 | f1.set(1, self.version & 2 != 0)
107 | f1.set(2, self.version & 1 != 0)
108 | f1.set(3, self.sas)
109 | f1.set(4, self.tfs)
110 | f1.set(5, self.eqos & 4 != 0)
111 | f1.set(6, self.eqos & 2 != 0)
112 | f1.set(7, self.eqos & 1 != 0)
113 |
114 | let f2 = Flag()
115 | f2.set(0, self.ack)
116 | f2.set(1, self.priority)
117 | f2.set(2, self.lastMessage)
118 | f2.set(3, self.gateway)
119 | f2.set(4, self.type.rawValue & 8 != 0)
120 | f2.set(5, self.type.rawValue & 4 != 0)
121 | f2.set(6, self.type.rawValue & 2 != 0)
122 | f2.set(7, self.type.rawValue & 1 != 0)
123 |
124 | bb.append(f1.value)
125 | bb.append(f2.value)
126 | bb.append(self.sequenceNumber)
127 | bb.append(self.ackNumber)
128 | let size = payload.count - ((type == MessageType.ENCRYPTED && !forEncryption) ? 8 : 0)
129 | bb.append(UInt8(size >> 3))
130 | bb.append(UInt8((size << 5) & 0xff))
131 | bb.append(self.source.address)
132 | bb.append(self.destination.address)
133 |
134 | bb.append(self.payload)
135 |
136 | return bb
137 | }
138 | }
139 |
140 | private class Flag {
141 | var value: UInt8
142 |
143 | init(_ value: UInt8 = 0) {
144 | self.value = value
145 | }
146 |
147 | func set(_ idx: UInt8, _ set: Bool) {
148 | let mask: UInt8 = 1 << (7 - idx)
149 | if (!set) {
150 | return
151 | }
152 | value = value | mask
153 | }
154 |
155 | func get(_ idx: UInt8) -> UInt8 {
156 | let mask: UInt8 = 1 << (7 - idx)
157 | if (value & mask == 0) {
158 | return 0
159 | }
160 | return 1
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Pair/LTKExchanger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LTKExchanger.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/3/21.
6 | //
7 | import Foundation
8 | import os.log
9 |
10 | class LTKExchanger {
11 | static let GET_POD_STATUS_HEX_COMMAND: Data = Data(hex: "ffc32dbd08030e0100008a")
12 | // This is the binary representation of "GetPodStatus command"
13 |
14 | static private let SP1 = "SP1="
15 | static private let SP2 = ",SP2="
16 | static private let SPS1 = "SPS1="
17 | static private let SPS2 = "SPS2="
18 | static private let SP0GP0 = "SP0,GP0"
19 | static private let P0 = "P0="
20 | static private let UNKNOWN_P0_PAYLOAD = Data([0xa5])
21 |
22 | private let manager: PeripheralManager
23 | private let ids: Ids
24 | private let podAddress = Ids.notActivated()
25 | private let keyExchange = try! KeyExchange(X25519KeyGenerator(), OmniRandomByteGenerator())
26 | private var seq: UInt8 = 1
27 |
28 | private let log = OSLog(category: "LTKExchanger")
29 |
30 | init(manager: PeripheralManager, ids: Ids) {
31 | self.manager = manager
32 | self.ids = ids
33 | }
34 |
35 | func negotiateLTK() throws -> PairResult {
36 | log.debug("Sending sp1sp2")
37 | let sp1sp2 = PairMessage(
38 | sequenceNumber: seq,
39 | source: ids.myId,
40 | destination: podAddress,
41 | keys: [LTKExchanger.SP1, LTKExchanger.SP2],
42 | payloads: [ids.podId.address, sp2()]
43 | )
44 | try throwOnSendError(sp1sp2.message, LTKExchanger.SP1 + LTKExchanger.SP2)
45 |
46 | seq += 1
47 | log.debug("Sending sps1")
48 | let sps1 = PairMessage(
49 | sequenceNumber: seq,
50 | source: ids.myId,
51 | destination: podAddress,
52 | keys: [LTKExchanger.SPS1],
53 | payloads: [keyExchange.pdmPublic + keyExchange.pdmNonce]
54 | )
55 | try throwOnSendError(sps1.message, LTKExchanger.SPS1)
56 |
57 | log.debug("Reading sps1")
58 | let podSps1 = try manager.readMessage(true)
59 | guard let _ = podSps1 else {
60 | throw BluetoothErrors.PairingException("Could not read SPS1")
61 | }
62 | try processSps1FromPod(podSps1!)
63 | // now we have all the data to generate: confPod, confPdm, ltk and noncePrefix
64 |
65 | log.debug("Sending sps2")
66 | seq += 1
67 | let sps2 = PairMessage(
68 | sequenceNumber: seq,
69 | source: ids.myId,
70 | destination: podAddress,
71 | keys: [LTKExchanger.SPS2],
72 | payloads: [keyExchange.pdmConf]
73 | )
74 | try throwOnSendError(sps2.message, LTKExchanger.SPS2)
75 |
76 | let podSps2 = try manager.readMessage()
77 | guard let _ = podSps2 else {
78 | throw BluetoothErrors.PairingException("Could not read SPS2")
79 | }
80 | try validatePodSps2(podSps2!)
81 | // No exception throwing after this point. It is possible that the pod saved the LTK
82 |
83 | seq += 1
84 | // send SP0GP0
85 | let sp0gp0 = PairMessage(
86 | sequenceNumber: seq,
87 | source: ids.myId,
88 | destination: podAddress,
89 | keys: [LTKExchanger.SP0GP0],
90 | payloads: [Data()]
91 | )
92 | let result = try manager.sendMessage(sp0gp0.message)
93 | guard ((result as? MessageSendSuccess) != nil) else {
94 | throw BluetoothErrors.PairingException("Error sending SP0GP0: \(result)")
95 | }
96 |
97 | let p0 = try manager.readMessage()
98 | guard let _ = p0 else {
99 | throw BluetoothErrors.PairingException("Could not read P0")
100 | }
101 | try validateP0(p0!)
102 |
103 | guard keyExchange.ltk.count == 16 else {
104 | throw BluetoothErrors.InvalidLTKKey("Invalid Key, got \(String(data: keyExchange.ltk, encoding: .utf8) ?? "")")
105 | }
106 |
107 | return PairResult(
108 | ltk: keyExchange.ltk,
109 | address: ids.podId.toUInt32(),
110 | msgSeq: seq
111 | )
112 | }
113 |
114 | private func throwOnSendError(_ msg: MessagePacket, _ msgType: String) throws {
115 | let result = try manager.sendMessage(msg)
116 | guard ((result as? MessageSendSuccess) != nil) else {
117 | throw BluetoothErrors.PairingException("Could not send or confirm $msgType: \(result)")
118 | }
119 | }
120 |
121 | private func processSps1FromPod(_ msg: MessagePacket) throws {
122 | log.debug("Received SPS1 from pod: %@", msg.payload.hexadecimalString)
123 |
124 | let payload = try StringLengthPrefixEncoding.parseKeys([LTKExchanger.SPS1], msg.payload)[0]
125 | log.debug("SPS1 payload from pod: %@", payload.hexadecimalString)
126 | try keyExchange.updatePodPublicData(payload)
127 | }
128 |
129 | private func validatePodSps2(_ msg: MessagePacket) throws {
130 | log.debug("Received SPS2 from pod: %@", msg.payload.hexadecimalString)
131 |
132 | let payload = try StringLengthPrefixEncoding.parseKeys([LTKExchanger.SPS2], msg.payload)[0]
133 | log.debug("SPS2 payload from pod: %@", payload.hexadecimalString)
134 |
135 | if (payload.count != KeyExchange.CMAC_SIZE) {
136 | throw BluetoothErrors.MessageIOException("Invalid payload size")
137 | }
138 | try keyExchange.validatePodConf(payload)
139 | }
140 |
141 | private func sp2() -> Data {
142 | // This is GetPodStatus command, with page 0 parameter.
143 | // We could replace that in the future with the serialized GetPodStatus()
144 | return LTKExchanger.GET_POD_STATUS_HEX_COMMAND
145 | }
146 |
147 | private func validateP0(_ msg: MessagePacket) throws {
148 | log.debug("Received P0 from pod: %@", msg.payload.hexadecimalString)
149 |
150 | let payload = try StringLengthPrefixEncoding.parseKeys([LTKExchanger.P0], msg.payload)[0]
151 | log.debug("P0 payload from pod: %@", payload.hexadecimalString)
152 | if (payload != LTKExchanger.UNKNOWN_P0_PAYLOAD) {
153 | throw BluetoothErrors.PairingException("Reveived invalid P0 payload: \(payload)")
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/OmniBLE/Bluetooth/Packet/BLEPacket.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BLEPacket.swift
3 | // OmnipodKit
4 | //
5 | // Created by Randall Knutson on 8/11/21.
6 | //
7 |
8 | import Foundation
9 | let MAX_SIZE = 20
10 |
11 | protocol BlePacket {
12 | var payload: Data { get }
13 |
14 | func toData() -> Data
15 | }
16 |
17 | struct FirstBlePacket: BlePacket {
18 | private static let HEADER_SIZE_WITHOUT_MIDDLE_PACKETS = 7 // we are using all fields
19 | private static let HEADER_SIZE_WITH_MIDDLE_PACKETS = 2
20 |
21 | internal static let CAPACITY_WITHOUT_MIDDLE_PACKETS =
22 | MAX_SIZE - HEADER_SIZE_WITHOUT_MIDDLE_PACKETS // we are using all fields
23 | internal static let CAPACITY_WITH_MIDDLE_PACKETS =
24 | MAX_SIZE - HEADER_SIZE_WITH_MIDDLE_PACKETS // we are not using crc32 or size
25 | internal static let CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET = 18
26 |
27 | private static let MAX_FRAGMENTS = 15 // 15*20=300 bytes
28 |
29 | let fullFragments: Int
30 | let payload: Data
31 | var size: UInt8?
32 | var crc32: Data?
33 | var oneExtraPacket: Bool = false
34 |
35 | func toData() -> Data {
36 | var bb = Data(capacity: MAX_SIZE)
37 | bb.append(UInt8(0)) // index
38 | bb.append(UInt8(fullFragments)) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket
39 |
40 | if let crc32 = crc32 {
41 | bb.append(crc32)
42 | }
43 | if let size = size {
44 | bb.append(UInt8(size))
45 | }
46 | bb.append(payload)
47 |
48 | return bb;
49 | }
50 |
51 | static func parse(payload: Data) throws -> FirstBlePacket {
52 | guard payload.count >= HEADER_SIZE_WITH_MIDDLE_PACKETS else {
53 | throw BluetoothErrors.MessageIOException("Wrong packet size")
54 | }
55 |
56 | if (Int(payload[0]) != 0) {
57 | // most likely we lost the first packet.
58 | throw BluetoothErrors.IncorrectPacketException(payload, 0)
59 | }
60 | let fullFragments = Int(payload[1])
61 | guard (fullFragments < MAX_FRAGMENTS) else { throw BluetoothErrors.MessageIOException("Received more than $MAX_FRAGMENTS fragments") }
62 |
63 | guard payload.count >= HEADER_SIZE_WITHOUT_MIDDLE_PACKETS else {
64 | throw BluetoothErrors.MessageIOException("Wrong packet size")
65 | }
66 |
67 | if (fullFragments == 0) {
68 | let rest = payload[6]
69 | let end = min(Int(rest) + HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, payload.count)
70 | guard payload.count >= end else {
71 | throw BluetoothErrors.MessageIOException("Wrong packet size")
72 | }
73 |
74 | return FirstBlePacket(
75 | fullFragments: fullFragments,
76 | payload: payload.subdata(in: HEADER_SIZE_WITHOUT_MIDDLE_PACKETS.. end
80 | )
81 | }
82 | else if (payload.count < MAX_SIZE) {
83 | throw BluetoothErrors.IncorrectPacketException(payload, 0)
84 | }
85 | else {
86 | return FirstBlePacket(
87 | fullFragments: fullFragments,
88 | payload: payload.subdata(in: HEADER_SIZE_WITH_MIDDLE_PACKETS.. Data {
100 | return Data([index]) + payload
101 | }
102 |
103 | static func parse(payload: Data) throws -> MiddleBlePacket {
104 | guard payload.count >= MAX_SIZE else { throw BluetoothErrors.MessageIOException("Wrong packet size") }
105 | return MiddleBlePacket(
106 | index: payload[0],
107 | payload: payload.subdata(in: 1.. Data {
123 | var bb = Data(capacity: MAX_SIZE)
124 | bb.append(index)
125 | bb.append(size)
126 | bb.append(crc32)
127 | bb.append(payload)
128 | bb.append(Data(count: MAX_SIZE - payload.count - LastBlePacket.HEADER_SIZE))
129 | return bb
130 | }
131 |
132 | static func parse(payload: Data) throws -> LastBlePacket {
133 | guard payload.count >= LastBlePacket.HEADER_SIZE else { throw BluetoothErrors.MessageIOException("Wrong packet size") }
134 |
135 | let rest = payload[1]
136 | let end = min(Int(rest) + LastBlePacket.HEADER_SIZE, payload.count)
137 |
138 | guard payload.count >= end else { throw BluetoothErrors.MessageIOException("Wrong packet size") }
139 |
140 | return LastBlePacket(
141 | index: payload[0],
142 | size: rest,
143 | payload: payload.subdata(in: LastBlePacket.HEADER_SIZE.. end
146 | )
147 | }
148 | }
149 |
150 | struct LastOptionalPlusOneBlePacket: BlePacket {
151 | static let HEADER_SIZE = 2
152 | let index: UInt8
153 | let payload: Data
154 | let size: UInt8
155 |
156 | func toData() -> Data {
157 | return Data([index, size]) + payload + Data(count: MAX_SIZE - payload.count - 2)
158 | }
159 |
160 | static func parse(payload: Data) throws -> LastOptionalPlusOneBlePacket {
161 | guard payload.count >= 2 else { throw BluetoothErrors.MessageIOException("Wrong packet size") }
162 | let size = payload[1]
163 | guard payload.count >= HEADER_SIZE + Int(size) else { throw BluetoothErrors.MessageIOException("Wrong packet size") }
164 |
165 | return LastOptionalPlusOneBlePacket(
166 | index: payload[0],
167 | payload: payload.subdata(in: HEADER_SIZE.. PodLifeHUDView {
64 | return nib().instantiate(withOwner: nil, options: nil)[0] as! PodLifeHUDView
65 | }
66 |
67 | public func setPodLifeCycle(startTime: Date, lifetime: TimeInterval) {
68 | self.startTime = startTime
69 | self.lifetime = lifetime
70 | updateProgressCircle()
71 |
72 | if timer == nil {
73 | self.timer = Timer.scheduledTimer(withTimeInterval: .seconds(10), repeats: true) { [weak self] _ in
74 | self?.updateProgressCircle()
75 | }
76 | }
77 | }
78 |
79 | override open func stateColorsDidUpdate() {
80 | super.stateColorsDidUpdate()
81 | updateProgressCircle()
82 | updateAlertStateLabel()
83 | }
84 |
85 | private var endColor: UIColor? {
86 | didSet {
87 | let primaryColor = endColor ?? UIColor(red: 198 / 255, green: 199 / 255, blue: 201 / 255, alpha: 1)
88 | self.progressRing.endColor = primaryColor
89 | self.progressRing.startColor = primaryColor
90 | }
91 | }
92 |
93 | private lazy var timeFormatter: DateComponentsFormatter = {
94 | let formatter = DateComponentsFormatter()
95 |
96 | formatter.allowedUnits = [.hour, .minute]
97 | formatter.maximumUnitCount = 1
98 | formatter.unitsStyle = .abbreviated
99 |
100 | return formatter
101 | }()
102 |
103 | private func updateAlertStateLabel() {
104 | var alertLabelAlpha: CGFloat = 1
105 |
106 | if alertState == .fault {
107 | timer = nil
108 | }
109 |
110 | switch alertState {
111 | case .fault:
112 | alertLabel.text = "!"
113 | alertLabel.backgroundColor = stateColors?.error
114 | case .warning:
115 | alertLabel.text = "!"
116 | alertLabel.backgroundColor = stateColors?.warning
117 | case .none:
118 | alertLabelAlpha = 0
119 | }
120 | alertLabel.alpha = alertLabelAlpha
121 | UIView.animate(withDuration: 0.25, animations: {
122 | self.alertLabel.alpha = alertLabelAlpha
123 | })
124 | }
125 |
126 | private func updateProgressCircle() {
127 |
128 | if let startTime = startTime, let lifetime = lifetime {
129 | let age = -startTime.timeIntervalSinceNow
130 | let progress = Double(age / lifetime)
131 | progressRing.progress = progress
132 |
133 | if progress < 0.75 {
134 | self.endColor = stateColors?.normal
135 | progressRing.shadowOpacity = 0
136 | } else if progress < 1.0 {
137 | self.endColor = stateColors?.warning
138 | progressRing.shadowOpacity = 0.5
139 | } else {
140 | self.endColor = stateColors?.error
141 | progressRing.shadowOpacity = 0.8
142 | }
143 |
144 | let remaining = (lifetime - age)
145 |
146 | // Update time label and caption
147 | if alertState == .fault {
148 | timeLabel.isHidden = true
149 | caption.text = LocalizedString("Fault", comment: "Pod life HUD view label")
150 | } else if remaining > .hours(24) {
151 | timeLabel.isHidden = true
152 | caption.text = LocalizedString("Pod Age", comment: "Label describing pod age view")
153 | } else if remaining > 0 {
154 | let remainingFlooredToHour = remaining > .hours(1) ? remaining - remaining.truncatingRemainder(dividingBy: .hours(1)) : remaining
155 | if let timeString = timeFormatter.string(from: remainingFlooredToHour) {
156 | timeLabel.isHidden = false
157 | timeLabel.text = timeString
158 | }
159 | caption.text = LocalizedString("Remaining", comment: "Label describing time remaining view")
160 | } else {
161 | timeLabel.isHidden = true
162 | caption.text = LocalizedString("Replace Pod", comment: "Label indicating pod replacement necessary")
163 | }
164 | }
165 | }
166 |
167 | func pauseUpdates() {
168 | timer?.invalidate()
169 | timer = nil
170 | }
171 |
172 | override public func awakeFromNib() {
173 | super.awakeFromNib()
174 | }
175 | }
176 |
--------------------------------------------------------------------------------