├── CGMBLEKitUI ├── Assets.xcassets │ ├── Contents.json │ └── g6.imageset │ │ ├── g6.png │ │ └── Contents.json ├── UIColor.swift ├── IdentifiableClass.swift ├── CGMBLEKitUI.h ├── Info.plist ├── TransmitterManager+UI.swift ├── TransmitterSetupViewController.swift ├── TransmitterIDSetupViewController.swift └── Base.lproj │ └── TransmitterManagerSetup.storyboard ├── ResetTransmitter ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ ├── Icon-24@2x.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-44@2x.png │ │ ├── Icon-86@2x.png │ │ ├── Icon-98@2x.png │ │ ├── Icon-27.5@2x.png │ │ ├── ItunesArtwork@2x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── Contents.json ├── mul.lproj │ └── LaunchScreen.xcstrings ├── Views │ ├── TextField.swift │ ├── ParagraphView.swift │ └── Button.swift ├── CompletionViewController.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── AppDelegate.swift ├── ResetManager.swift └── ResetViewController.swift ├── CGMBLEKit Example ├── mul.lproj │ └── LaunchScreen.xcstrings ├── CommandQueue.swift ├── NSUserDefaults.swift ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── InfoPlist.xcstrings ├── AppDelegate.swift ├── ViewController.swift └── Localizable.xcstrings ├── CGMBLEKitG5Plugin ├── CGMBLEKitG5Plugin-Bridging-Header.h ├── CGMBLEKitG5Plugin.swift └── Info.plist ├── CGMBLEKitG6Plugin ├── CGMBLEKitG6Plugin-Bridging-Header.h ├── CGMBLEKitG6Plugin.swift └── Info.plist ├── CGMBLEKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── Shared-watchOS.xcscheme │ ├── ResetTransmitter.xcscheme │ ├── CGMBLEKit Example.xcscheme │ └── Shared.xcscheme ├── Pod └── CGMBLEKit.h ├── CGMBLEKit ├── Messages │ ├── GlucoseHistoryTxMessage.swift │ ├── FirmwareVersionTxMessage.swift │ ├── DisconnectTxMessage.swift │ ├── BatteryStatusTxMessage.swift │ ├── TransmitterVersionTxMessage.swift │ ├── BondRequestTxMessage.swift │ ├── GlucoseTxMessage.swift │ ├── KeepAliveTxMessage.swift │ ├── CalibrationDataTxMessage.swift │ ├── TransmitterTimeTxMessage.swift │ ├── AuthChallengeTxMessage.swift │ ├── SessionStopTxMessage.swift │ ├── CalibrateGlucoseRxMessage.swift │ ├── CalibrateGlucoseTxMessage.swift │ ├── ResetMessage.swift │ ├── CalibrationDataRxMessage.swift │ ├── TransmitterMessage.swift │ ├── AuthChallengeRxMessage.swift │ ├── AuthRequestRxMessage.swift │ ├── SessionStartTxMessage.swift │ ├── TransmitterVersionRxMessage.swift │ ├── AuthRequestTxMessage.swift │ ├── SessionStopRxMessage.swift │ ├── TransmitterTimeRxMessage.swift │ ├── SessionStartRxMessage.swift │ ├── GlucoseRxMessage.swift │ └── GlucoseBackfillMessage.swift ├── AESCrypt.h ├── CGMBLEKit.h ├── Calibration.swift ├── Info.plist ├── AESCrypt.m ├── NSData+CRC.swift ├── CBPeripheral.swift ├── PeripheralManagerError.swift ├── TransmitterStatus.swift ├── OSLog.swift ├── Opcode.swift ├── TransmitterManagerState.swift ├── Glucose+SensorDisplayable.swift ├── Command.swift ├── BluetoothServices.swift ├── CalibrationState.swift ├── Glucose.swift └── PeripheralManager+G5.swift ├── crowdin.yml ├── .travis.yml ├── Common ├── HKUnit.swift ├── LocalizedString.swift ├── Locked.swift ├── TimeInterval.swift └── Data.swift ├── CGMBLEKitTests ├── CalibrationDataRxMessageTests.swift ├── TransmitterIDTests.swift ├── TransmitterVersionRxMessageTests.swift ├── Info.plist ├── SessionStartRxMessageTests.swift ├── SessionStopRxMessageTests.swift ├── TransmitterTimeRxMessageTests.swift ├── GlucoseRxMessageTests.swift └── GlucoseTests.swift ├── .gitignore ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /CGMBLEKitUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CGMBLEKit Example/mul.lproj/LaunchScreen.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | 5 | }, 6 | "version" : "1.0" 7 | } -------------------------------------------------------------------------------- /CGMBLEKitUI/Assets.xcassets/g6.imageset/g6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/CGMBLEKitUI/Assets.xcassets/g6.imageset/g6.png -------------------------------------------------------------------------------- /ResetTransmitter/mul.lproj/LaunchScreen.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | 5 | }, 6 | "version" : "1.0" 7 | } -------------------------------------------------------------------------------- /CGMBLEKitG5Plugin/CGMBLEKitG5Plugin-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /CGMBLEKitG6Plugin/CGMBLEKitG6Plugin-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoopKit/CGMBLEKit/HEAD/ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pod/CGMBLEKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // CGMBLEKit.h 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 12/31/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | 10 | #import -------------------------------------------------------------------------------- /CGMBLEKitUI/Assets.xcassets/g6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "g6.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/GlucoseHistoryTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseHistoryTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct GlucoseHistoryTxMessage { 13 | let opcode: Opcode = .glucoseHistoryTx 14 | } 15 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/FirmwareVersionTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirmwareVersionTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct FirmwareVersionTxMessage { 13 | let opcode: Opcode = .firmwareVersionTx 14 | } 15 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/DisconnectTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisconnectTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct DisconnectTxMessage: TransmitterTxMessage { 13 | var data: Data { 14 | return Data(for: .disconnectTx) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/BatteryStatusTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatteryStatusTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct BatteryStatusTxMessage { 13 | let opcode: Opcode = .batteryStatusTx 14 | 15 | // Response: 23003c012f01cd021f247bae 16 | } 17 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/TransmitterVersionTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterVersionTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct TransmitterVersionTxMessage { 13 | typealias Response = TransmitterVersionRxMessage 14 | 15 | let opcode: Opcode = .transmitterVersionTx 16 | } 17 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/BondRequestTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondRequestTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Initiates a bond with the central 13 | struct BondRequestTxMessage: TransmitterTxMessage { 14 | var data: Data { 15 | return Data(for: .bondRequest) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CGMBLEKit/AESCrypt.h: -------------------------------------------------------------------------------- 1 | // 2 | // AESCrypt.h 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 6/17/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AESCrypt : NSObject 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | + (nullable NSData *)encryptData:(NSData *)data usingKey:(NSData *)key error:(NSError **)error; 16 | 17 | NS_ASSUME_NONNULL_END 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/GlucoseTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct GlucoseTxMessage: RespondableMessage { 13 | typealias Response = GlucoseRxMessage 14 | 15 | var data: Data { 16 | return Data(for: .glucoseTx).appendingCRC() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /CGMBLEKit/Localizable.xcstrings 3 | translation: /CGMBLEKit/Localizable.xcstrings 4 | multilingual: 1 5 | - source: /CGMBLEKitUI/Localizable.xcstrings 6 | translation: /CGMBLEKitUI/Localizable.xcstrings 7 | multilingual: 1 8 | - source: /CGMBLEKitUI/mul.lproj/TransmitterManagerSetup.xcstrings 9 | translation: /CGMBLEKit/CGMBLEKitUI/mul.lproj/TransmitterManagerSetup.xcstrings 10 | multilingual: 1 11 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/KeepAliveTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeepAliveTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct KeepAliveTxMessage: TransmitterTxMessage { 13 | let time: UInt8 14 | 15 | var data: Data { 16 | var data = Data(for: .keepAlive) 17 | data.append(time) 18 | return data 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/CalibrationDataTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrationDataTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Paul Dickens on 17/03/2018. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct CalibrationDataTxMessage: RespondableMessage { 13 | typealias Response = CalibrationDataRxMessage 14 | 15 | var data: Data { 16 | return Data(for: .calibrationDataTx).appendingCRC() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CGMBLEKit/CGMBLEKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // CGMBLEKit.h 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 12/30/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | //! Project version number for CGMBLEKIt. 13 | FOUNDATION_EXPORT double CGMBLEKitVersionNumber; 14 | 15 | //! Project version string for CGMBLEKit. 16 | FOUNDATION_EXPORT const unsigned char CGMBLEKitVersionString[]; 17 | 18 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/TransmitterTimeTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterTimeTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct TransmitterTimeTxMessage: RespondableMessage { 13 | typealias Response = TransmitterTimeRxMessage 14 | 15 | var data: Data { 16 | return Data(for: .transmitterTimeTx).appendingCRC() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/AuthChallengeTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthChallengeTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/22/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct AuthChallengeTxMessage: TransmitterTxMessage { 13 | let challengeHash: Data 14 | 15 | var data: Data { 16 | var data = Data(for: .authChallengeTx) 17 | data.append(challengeHash) 18 | return data 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CGMBLEKitUI/UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor.swift 3 | // LoopKitUI 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | extension UIColor { 12 | static let delete = UIColor.higRed() 13 | } 14 | 15 | 16 | // MARK: - HIG colors 17 | // See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/ 18 | extension UIColor { 19 | private static func higRed() -> UIColor { 20 | return UIColor(red: 1, green: 59 / 255, blue: 48 / 255, alpha: 1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode12.2 3 | 4 | before_script: 5 | - ./Scripts/carthage.sh bootstrap 6 | 7 | script: 8 | # Build frameworks and run tests 9 | - travis_wait 25 xcodebuild -project CGMBLEKit.xcodeproj -scheme Shared build -destination name="iPhone 8" test | xcpretty 10 | # Build apps 11 | - xcodebuild -project CGMBLEKit.xcodeproj -scheme "CGMBLEKit Example" build -destination name="iPhone 8" | xcpretty 12 | - xcodebuild -project CGMBLEKit.xcodeproj -scheme ResetTransmitter build -destination name="iPhone 8" 13 | 14 | -------------------------------------------------------------------------------- /Common/HKUnit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HKUnit.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 8/6/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: HKUnit.literUnit(with: .deci)) 15 | }() 16 | 17 | static let milligramsPerDeciliterPerMinute: HKUnit = { 18 | return HKUnit.milligramsPerDeciliter.unitDivided(by: .minute()) 19 | }() 20 | } 21 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/SessionStopTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStopTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct SessionStopTxMessage: RespondableMessage { 13 | typealias Response = SessionStopRxMessage 14 | 15 | let stopTime: UInt32 16 | 17 | var data: Data { 18 | var data = Data(for: .sessionStopTx) 19 | data.append(stopTime) 20 | return data.appendingCRC() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CGMBLEKitTests/CalibrationDataRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrationDataRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 9/18/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | 13 | class CalibrationDataRxMessageTests: XCTestCase { 14 | 15 | func testMessage() { 16 | let data = Data(hexadecimalString: "33002b290090012900ae00800050e929001225")! 17 | XCTAssertNotNil(CalibrationDataRxMessage(data: data)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /CGMBLEKitTests/TransmitterIDTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterIDTests.swift 3 | // xDripG5Tests 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | @testable import CGMBLEKit 10 | 11 | class TransmitterIDTests: XCTestCase { 12 | 13 | /// Sanity check the hash computation path 14 | func testComputeHash() { 15 | let id = TransmitterID(id: "123456") 16 | 17 | XCTAssertEqual("e60d4a7999b0fbb2", id.computeHash(of: Data(hexadecimalString: "0123456789abcdef")!)!.hexadecimalString) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /CGMBLEKitUI/IdentifiableClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifiableClass.swift 3 | // Naterade 4 | // 5 | // Created by Nathan Racklyeft on 5/22/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 | 23 | 24 | extension UITableViewCell: IdentifiableClass { } 25 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/CalibrateGlucoseRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrateGlucoseRxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Paul Dickens on 25/02/2018. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public struct CalibrateGlucoseRxMessage: TransmitterRxMessage { 13 | init?(data: Data) { 14 | guard data.count == 5 && data.isCRCValid else { 15 | return nil 16 | } 17 | 18 | guard data.starts(with: .calibrateGlucoseRx) else { 19 | return nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CGMBLEKitUI/CGMBLEKitUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // CGMBLEKitUI.h 3 | // CGMBLEKitUI 4 | // 5 | // Created by Nathan Racklyeft on 7/28/18. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CGMBLEKitUI. 12 | FOUNDATION_EXPORT double CGMBLEKitUIVersionNumber; 13 | 14 | //! Project version string for CGMBLEKitUI. 15 | FOUNDATION_EXPORT const unsigned char CGMBLEKitUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/CalibrateGlucoseTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrateGlucoseTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct CalibrateGlucoseTxMessage: RespondableMessage { 13 | typealias Response = CalibrateGlucoseRxMessage 14 | 15 | let time: UInt32 16 | let glucose: UInt16 17 | 18 | var data: Data { 19 | var data = Data(for: .calibrateGlucoseTx) 20 | data.append(glucose) 21 | data.append(time) 22 | return data.appendingCRC() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/ResetMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResetMessage.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct ResetTxMessage: RespondableMessage { 12 | typealias Response = ResetRxMessage 13 | 14 | var data: Data { 15 | return Data(for: .resetTx).appendingCRC() 16 | } 17 | } 18 | 19 | 20 | struct ResetRxMessage: TransmitterRxMessage { 21 | let status: UInt8 22 | 23 | init?(data: Data) { 24 | guard data.count >= 2, data.starts(with: .resetRx) else { 25 | return nil 26 | } 27 | 28 | status = data[1] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CGMBLEKitG5Plugin/CGMBLEKitG5Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGMBLEKitG5Plugin.swift 3 | // CGMBLEKitG5Plugin 4 | // 5 | // Created by Nathaniel Hamming on 2019-12-19. 6 | // Copyright © 2019 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import LoopKitUI 11 | import CGMBLEKit 12 | import CGMBLEKitUI 13 | 14 | class CGMBLEKitG5Plugin: NSObject, CGMManagerUIPlugin { 15 | private let log = OSLog(category: "CGMBLEKitG5Plugin") 16 | 17 | public var cgmManagerType: CGMManagerUI.Type? { 18 | return G5CGMManager.self 19 | } 20 | 21 | override init() { 22 | super.init() 23 | log.default("Instantiated") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CGMBLEKitG6Plugin/CGMBLEKitG6Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGMBLEKitG6Plugin.swift 3 | // CGMBLEKitG6Plugin 4 | // 5 | // Created by Nathaniel Hamming on 2019-12-13. 6 | // Copyright © 2019 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import LoopKitUI 11 | import CGMBLEKit 12 | import CGMBLEKitUI 13 | 14 | class CGMBLEKitG6Plugin: NSObject, CGMManagerUIPlugin { 15 | private let log = OSLog(category: "CGMBLEKitG6Plugin") 16 | 17 | public var cgmManagerType: CGMManagerUI.Type? { 18 | return G6CGMManager.self 19 | } 20 | 21 | override init() { 22 | super.init() 23 | log.default("Instantiated") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/CalibrationDataRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrationDataRxMessage.swift 3 | // Pods 4 | // 5 | // Created by Nate Racklyeft on 9/18/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct CalibrationDataRxMessage: TransmitterRxMessage { 13 | let glucose: UInt16 14 | let timestamp: UInt32 15 | 16 | init?(data: Data) { 17 | guard data.count == 19 && data.isCRCValid else { 18 | return nil 19 | } 20 | 21 | guard data.starts(with: .calibrationDataRx) else { 22 | return nil 23 | } 24 | 25 | glucose = data[11..<13].toInt() & 0xfff 26 | timestamp = data[13..<17].toInt() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CGMBLEKitTests/TransmitterVersionRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterVersionRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 9/29/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | class TransmitterVersionRxMessageTests: XCTestCase { 13 | 14 | func testRxMessage() { 15 | let data = Data(hexadecimalString: "4b0001000011df2900005100037000f00009b6")! 16 | let message = TransmitterVersionRxMessage(data: data)! 17 | 18 | XCTAssertEqual(0, message.status) 19 | XCTAssertEqual([1, 0, 0, 17], message.firmwareVersion) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/TransmitterMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterCommand.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/22/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// A data sequence written to the transmitter 13 | protocol TransmitterTxMessage { 14 | 15 | /// The data to write 16 | var data: Data { get } 17 | 18 | } 19 | 20 | 21 | protocol RespondableMessage: TransmitterTxMessage { 22 | associatedtype Response: TransmitterRxMessage 23 | } 24 | 25 | 26 | /// A data sequence received by the transmitter 27 | protocol TransmitterRxMessage { 28 | 29 | 30 | init?(data: Data) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/AuthChallengeRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthChallengeRxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/22/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct AuthChallengeRxMessage: TransmitterRxMessage { 13 | let isAuthenticated: Bool 14 | let isBonded: Bool 15 | 16 | init?(data: Data) { 17 | guard data.count >= 3 else { 18 | return nil 19 | } 20 | 21 | guard data.starts(with: .authChallengeRx) else { 22 | return nil 23 | } 24 | 25 | isAuthenticated = data[1] == 0x1 26 | isBonded = data[2] == 0x1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/AuthRequestRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthRequestRxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/22/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct AuthRequestRxMessage: TransmitterRxMessage { 13 | let tokenHash: Data 14 | let challenge: Data 15 | 16 | init?(data: Data) { 17 | guard data.count >= 17 else { 18 | return nil 19 | } 20 | 21 | guard data.starts(with: .authRequestRx) else { 22 | return nil 23 | } 24 | 25 | tokenHash = data.subdata(in: 1..<9) 26 | challenge = data.subdata(in: 9..<17) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ResetTransmitter/Views/TextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class TextField: UITextField { 11 | 12 | private let textInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) 13 | 14 | override func editingRect(forBounds bounds: CGRect) -> CGRect { 15 | return bounds.inset(by: textInset) 16 | } 17 | 18 | override func textRect(forBounds bounds: CGRect) -> CGRect { 19 | return bounds.inset(by: textInset) 20 | } 21 | 22 | override func placeholderRect(forBounds bounds: CGRect) -> CGRect { 23 | return bounds.inset(by: textInset) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/SessionStartTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStartTxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct SessionStartTxMessage: RespondableMessage { 13 | typealias Response = SessionStartRxMessage 14 | 15 | /// Time since activation in Dex seconds 16 | let startTime: UInt32 17 | 18 | /// Time in seconds since Unix Epoch 19 | let secondsSince1970: UInt32 20 | 21 | var data: Data { 22 | var data = Data(for: .sessionStartTx) 23 | data.append(startTime) 24 | data.append(secondsSince1970) 25 | return data.appendingCRC() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Common/LocalizedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedString.swift 3 | // LoopUI 4 | // 5 | // Created by Kathryn DiSimone on 8/15/18. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal 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 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/TransmitterVersionRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterVersionRxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 9/29/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct TransmitterVersionRxMessage: TransmitterRxMessage { 13 | let status: UInt8 14 | let firmwareVersion: [UInt8] 15 | 16 | init?(data: Data) { 17 | guard data.count == 19 && data.isCRCValid else { 18 | return nil 19 | } 20 | 21 | guard data.starts(with: .transmitterVersionRx) else { 22 | return nil 23 | } 24 | 25 | status = data[1] 26 | firmwareVersion = data[2..<6].map { $0 } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/AuthRequestTxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthRequestTxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/22/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct AuthRequestTxMessage: TransmitterTxMessage { 13 | let singleUseToken: Data 14 | let endByte: UInt8 = 0x2 15 | 16 | init() { 17 | let uuid = UUID().uuid 18 | 19 | singleUseToken = Data([uuid.0, uuid.1, uuid.2, uuid.3, 20 | uuid.4, uuid.5, uuid.6, uuid.7]) 21 | } 22 | 23 | var data: Data { 24 | var data = Data(for: .authRequestTx) 25 | data.append(singleUseToken) 26 | data.append(endByte) 27 | return data 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | 34 | Pods/ 35 | Carthage/ 36 | .gitmodules 37 | -------------------------------------------------------------------------------- /CGMBLEKit/Calibration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calibration.swift 3 | // xDripG5 4 | // 5 | // Created by Paul Dickens on 17/03/2018. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import HealthKit 11 | 12 | 13 | public struct Calibration { 14 | init?(calibrationMessage: CalibrationDataRxMessage, activationDate: Date) { 15 | guard calibrationMessage.glucose > 0 else { 16 | return nil 17 | } 18 | 19 | let unit = HKUnit.milligramsPerDeciliter 20 | 21 | glucose = HKQuantity(unit: unit, doubleValue: Double(calibrationMessage.glucose)) 22 | date = activationDate.addingTimeInterval(TimeInterval(calibrationMessage.timestamp)) 23 | } 24 | 25 | public let glucose: HKQuantity 26 | public let date: Date 27 | } 28 | -------------------------------------------------------------------------------- /ResetTransmitter/Views/ParagraphView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphView.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class ParagraphView: UITextView { 11 | 12 | override func awakeFromNib() { 13 | super.awakeFromNib() 14 | 15 | textContainer.lineFragmentPadding = 0 16 | 17 | let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle 18 | paragraphStyle.paragraphSpacing = 10 19 | 20 | attributedText = NSAttributedString( 21 | string: text, 22 | attributes: [ 23 | .paragraphStyle: paragraphStyle, 24 | .font: UIFont.preferredFont(forTextStyle: .body) 25 | ] 26 | ) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /CGMBLEKit Example/CommandQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandQueue.swift 3 | // CGMBLEKit Example 4 | // 5 | // Created by Paul Dickens on 25/03/2018. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CGMBLEKit 11 | 12 | 13 | class CommandQueue { 14 | private var list = Array() 15 | private var lock = os_unfair_lock() 16 | 17 | func enqueue(_ element: Command) { 18 | os_unfair_lock_lock(&lock) 19 | list.append(element) 20 | os_unfair_lock_unlock(&lock) 21 | } 22 | 23 | func dequeue() -> Command? { 24 | os_unfair_lock_lock(&lock) 25 | defer { 26 | os_unfair_lock_unlock(&lock) 27 | } 28 | if !list.isEmpty { 29 | return list.removeFirst() 30 | } else { 31 | return nil 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CGMBLEKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 3.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | 24 | 25 | -------------------------------------------------------------------------------- /CGMBLEKitUI/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 | FMWK 17 | CFBundleShortVersionString 18 | 3.2 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CGMBLEKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/SessionStopRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStopRxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 6/4/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct SessionStopRxMessage: TransmitterRxMessage { 13 | let status: UInt8 14 | let received: UInt8 15 | let sessionStopTime: UInt32 16 | let sessionStartTime: UInt32 17 | let transmitterTime: UInt32 18 | 19 | init?(data: Data) { 20 | guard data.count == 17 && data.isCRCValid else { 21 | return nil 22 | } 23 | 24 | guard data.starts(with: .sessionStopRx) else { 25 | return nil 26 | } 27 | 28 | status = data[1] 29 | received = data[2] 30 | sessionStopTime = data[3..<7].toInt() 31 | sessionStartTime = data[7..<11].toInt() 32 | transmitterTime = data[11..<15].toInt() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CGMBLEKit Example/NSUserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSUserDefaults.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 11/24/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension UserDefaults { 13 | var passiveModeEnabled: Bool { 14 | get { 15 | return bool(forKey: "passiveModeEnabled") 16 | } 17 | set { 18 | set(newValue, forKey: "passiveModeEnabled") 19 | } 20 | } 21 | 22 | var stayConnected: Bool { 23 | get { 24 | return object(forKey: "stayConnected") != nil ? bool(forKey: "stayConnected") : true 25 | } 26 | set { 27 | set(newValue, forKey: "stayConnected") 28 | } 29 | } 30 | 31 | var transmitterID: String { 32 | get { 33 | return string(forKey: "transmitterID") ?? "500000" 34 | } 35 | set { 36 | set(newValue, forKey: "transmitterID") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/TransmitterTimeRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterTimeRxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct TransmitterTimeRxMessage: TransmitterRxMessage { 13 | let status: UInt8 14 | let currentTime: UInt32 15 | let sessionStartTime: UInt32 16 | 17 | init?(data: Data) { 18 | guard data.count == 16 && data.isCRCValid else { 19 | return nil 20 | } 21 | 22 | guard data.starts(with: .transmitterTimeRx) else { 23 | return nil 24 | } 25 | 26 | status = data[1] 27 | currentTime = data[2..<6].toInt() 28 | sessionStartTime = data[6..<10].toInt() 29 | 30 | } 31 | } 32 | 33 | extension TransmitterTimeRxMessage: Equatable { } 34 | 35 | func ==(lhs: TransmitterTimeRxMessage, rhs: TransmitterTimeRxMessage) -> Bool { 36 | return lhs.currentTime == rhs.currentTime 37 | } 38 | -------------------------------------------------------------------------------- /Common/Locked.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Locked.swift 3 | // LoopKit 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import os.lock 9 | 10 | 11 | internal class Locked { 12 | private var lock = os_unfair_lock() 13 | private var _value: T 14 | 15 | init(_ value: T) { 16 | os_unfair_lock_lock(&lock) 17 | defer { os_unfair_lock_unlock(&lock) } 18 | _value = value 19 | } 20 | 21 | var value: T { 22 | get { 23 | os_unfair_lock_lock(&lock) 24 | defer { os_unfair_lock_unlock(&lock) } 25 | return _value 26 | } 27 | set { 28 | os_unfair_lock_lock(&lock) 29 | defer { os_unfair_lock_unlock(&lock) } 30 | _value = newValue 31 | } 32 | } 33 | 34 | func mutate(_ changes: (_ value: inout T) -> Void) -> T { 35 | os_unfair_lock_lock(&lock) 36 | defer { os_unfair_lock_unlock(&lock) } 37 | changes(&_value) 38 | return _value 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/SessionStartRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStartRxMessage.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 6/4/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct SessionStartRxMessage: TransmitterRxMessage { 13 | let status: UInt8 14 | let received: UInt8 15 | 16 | // I've only seen examples of these 2 values matching 17 | let requestedStartTime: UInt32 18 | let sessionStartTime: UInt32 19 | 20 | let transmitterTime: UInt32 21 | 22 | init?(data: Data) { 23 | guard data.count == 17 && data.isCRCValid else { 24 | return nil 25 | } 26 | 27 | guard data.starts(with: .sessionStartRx) else { 28 | return nil 29 | } 30 | 31 | status = data[1] 32 | received = data[2] 33 | requestedStartTime = data[3..<7].toInt() 34 | sessionStartTime = data[7..<11].toInt() 35 | transmitterTime = data[11..<15].toInt() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nathan Racklyeft 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 | 23 | -------------------------------------------------------------------------------- /CGMBLEKitG5Plugin/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSHumanReadableCopyright 22 | Copyright © 2019 LoopKit Authors. All rights reserved. 23 | NSPrincipalClass 24 | CGMBLEKitG5Plugin 25 | com.loopkit.Loop.CGMManagerDisplayName 26 | Dexcom G5 27 | com.loopkit.Loop.CGMManagerIdentifier 28 | DexG5Transmitter 29 | 30 | 31 | -------------------------------------------------------------------------------- /CGMBLEKitG6Plugin/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSHumanReadableCopyright 22 | Copyright © 2019 LoopKit Authors. All rights reserved. 23 | NSPrincipalClass 24 | CGMBLEKitG6Plugin 25 | com.loopkit.Loop.CGMManagerDisplayName 26 | Dexcom G6 / ONE 27 | com.loopkit.Loop.CGMManagerIdentifier 28 | DexG6Transmitter 29 | 30 | 31 | -------------------------------------------------------------------------------- /CGMBLEKit/AESCrypt.m: -------------------------------------------------------------------------------- 1 | // 2 | // AESCrypt.m 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 6/17/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | #import "AESCrypt.h" 10 | #import 11 | 12 | @implementation AESCrypt 13 | 14 | + (NSData *)encryptData:(NSData *)data usingKey:(NSData *)key error:(NSError * _Nullable __autoreleasing *)error 15 | { 16 | NSMutableData *dataOut = [NSMutableData dataWithLength: data.length + kCCBlockSizeAES128]; 17 | 18 | CCCryptorStatus status = CCCrypt(kCCEncrypt, 19 | kCCAlgorithmAES, 20 | kCCOptionECBMode, 21 | key.bytes, 22 | key.length, 23 | NULL, 24 | data.bytes, 25 | data.length, 26 | dataOut.mutableBytes, 27 | dataOut.length, 28 | NULL); 29 | 30 | return status == kCCSuccess ? dataOut : nil; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /ResetTransmitter/Views/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | class Button: UIButton { 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | super.init(coder: aDecoder) 15 | } 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | 20 | backgroundColor = tintColor 21 | layer.cornerRadius = 6 22 | 23 | titleLabel?.adjustsFontForContentSizeCategory = true 24 | contentEdgeInsets.top = 14 25 | contentEdgeInsets.bottom = 14 26 | setTitleColor(.white, for: .normal) 27 | } 28 | 29 | override func tintColorDidChange() { 30 | super.tintColorDidChange() 31 | 32 | backgroundColor = tintColor 33 | } 34 | 35 | override func prepareForInterfaceBuilder() { 36 | super.prepareForInterfaceBuilder() 37 | 38 | tintColor = .blue 39 | tintColorDidChange() 40 | } 41 | 42 | override var isHighlighted: Bool { 43 | didSet { 44 | alpha = isHighlighted ? 0.5 : 1 45 | } 46 | } 47 | 48 | override var isEnabled: Bool { 49 | didSet { 50 | tintAdjustmentMode = isEnabled ? .automatic : .dimmed 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CGMBLEKit/NSData+CRC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+CRC.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 4/7/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /** 13 | CRC-CCITT (XModem) 14 | 15 | [http://www.lammertbies.nl/comm/info/crc-calculation.html]() 16 | 17 | [http://web.mit.edu/6.115/www/amulet/xmodem.htm]() 18 | */ 19 | extension Collection where Element == UInt8 { 20 | private var crcCCITTXModem: UInt16 { 21 | var crc: UInt16 = 0 22 | 23 | for byte in self { 24 | crc ^= UInt16(byte) << 8 25 | 26 | for _ in 0..<8 { 27 | if crc & 0x8000 != 0 { 28 | crc = crc << 1 ^ 0x1021 29 | } else { 30 | crc = crc << 1 31 | } 32 | } 33 | } 34 | 35 | return crc 36 | } 37 | 38 | var crc16: UInt16 { 39 | return crcCCITTXModem 40 | } 41 | } 42 | 43 | 44 | extension UInt8 { 45 | var crc16: UInt16 { 46 | return [self].crc16 47 | } 48 | } 49 | 50 | 51 | extension Data { 52 | var isCRCValid: Bool { 53 | return dropLast(2).crc16 == suffix(2).toInt() 54 | } 55 | 56 | func appendingCRC() -> Data { 57 | var data = self 58 | data.append(crc16) 59 | return data 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ResetTransmitter/CompletionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompletionViewController.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import UserNotifications 10 | 11 | class CompletionViewController: UITableViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | if UIApplication.shared.applicationState == .background { 19 | let content = UNMutableNotificationContent() 20 | content.badge = 1 21 | content.title = NSLocalizedString("Transmitter Reset Complete", comment: "Notification title for background completion notification") 22 | content.body = textView.text 23 | content.sound = .default 24 | 25 | let request = UNNotificationRequest(identifier: "Completion", content: content, trigger: nil) 26 | 27 | UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) 28 | } 29 | } 30 | 31 | override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 32 | return false 33 | } 34 | 35 | override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 36 | return nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CGMBLEKit/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 | func itemWithUUIDString(_ uuidString: String) -> Element? { 37 | for attribute in self { 38 | if attribute.uuid.uuidString == uuidString { 39 | return attribute 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CGMBLEKit/PeripheralManagerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralManagerError.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import CoreBluetooth 9 | 10 | 11 | enum PeripheralManagerError: Error { 12 | case cbPeripheralError(Error) 13 | case notReady 14 | case invalidConfiguration 15 | case timeout 16 | case unknownCharacteristic 17 | } 18 | 19 | 20 | extension PeripheralManagerError: LocalizedError { 21 | var errorDescription: String? { 22 | switch self { 23 | case .cbPeripheralError(let error): 24 | return error.localizedDescription 25 | case .notReady: 26 | return LocalizedString("Peripheral isnʼt connected", comment: "Not ready error description") 27 | case .invalidConfiguration: 28 | return LocalizedString("Peripheral command was invalid", comment: "invlid config error description") 29 | case .timeout: 30 | return LocalizedString("Peripheral did not respond in time", comment: "Timeout error description") 31 | case .unknownCharacteristic: 32 | return LocalizedString("Unknown characteristic", comment: "Error description") 33 | } 34 | } 35 | 36 | var failureReason: String? { 37 | switch self { 38 | case .cbPeripheralError(let error as NSError): 39 | return error.localizedFailureReason 40 | default: 41 | return errorDescription 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Common/TimeInterval.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTimeInterval.swift 3 | // Naterade 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 | static func hours(_ hours: Double) -> TimeInterval { 14 | return self.init(hours: hours) 15 | } 16 | 17 | static func minutes(_ minutes: Int) -> TimeInterval { 18 | return self.init(minutes: Double(minutes)) 19 | } 20 | 21 | static func minutes(_ minutes: Double) -> TimeInterval { 22 | return self.init(minutes: minutes) 23 | } 24 | 25 | static func seconds(_ seconds: Double) -> TimeInterval { 26 | return self.init(seconds) 27 | } 28 | 29 | static func milliseconds(_ milliseconds: Double) -> TimeInterval { 30 | return self.init(milliseconds / 1000) 31 | } 32 | 33 | init(minutes: Double) { 34 | self.init(minutes * 60) 35 | } 36 | 37 | init(hours: Double) { 38 | self.init(minutes: hours * 60) 39 | } 40 | 41 | init(seconds: Double) { 42 | self.init(seconds) 43 | } 44 | 45 | init(milliseconds: Double) { 46 | self.init(milliseconds / 1000) 47 | } 48 | 49 | var milliseconds: Double { 50 | return self * 1000 51 | } 52 | 53 | var minutes: Double { 54 | return self / 60.0 55 | } 56 | 57 | var hours: Double { 58 | return minutes / 60.0 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /CGMBLEKit/TransmitterStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterStatus.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/26/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public enum TransmitterStatus { 13 | public typealias RawValue = UInt8 14 | 15 | case ok 16 | case lowBattery 17 | case unknown(RawValue) 18 | 19 | init(rawValue: RawValue) { 20 | switch rawValue { 21 | case 0: 22 | self = .ok 23 | case 0x81: 24 | self = .lowBattery 25 | default: 26 | self = .unknown(rawValue) 27 | } 28 | } 29 | } 30 | 31 | 32 | extension TransmitterStatus: Equatable { } 33 | 34 | public func ==(lhs: TransmitterStatus, rhs: TransmitterStatus) -> Bool { 35 | switch (lhs, rhs) { 36 | case (.ok, .ok), (.lowBattery, .lowBattery): 37 | return true 38 | case (.unknown(let left), .unknown(let right)) where left == right: 39 | return true 40 | default: 41 | return false 42 | } 43 | } 44 | 45 | 46 | extension TransmitterStatus { 47 | public var localizedDescription: String { 48 | switch self { 49 | case .ok: 50 | return LocalizedString("OK", comment: "Describes a functioning transmitter") 51 | case .lowBattery: 52 | return LocalizedString("Low Battery", comment: "Describes a low battery") 53 | case .unknown(let value): 54 | return "TransmitterStatus.unknown(\(value))" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CGMBLEKit/OSLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLog.swift 3 | // Loop 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import os.log 9 | 10 | 11 | extension OSLog { 12 | convenience init(category: String) { 13 | self.init(subsystem: "com.loopkit.CGMBLEKit", category: category) 14 | } 15 | 16 | func debug(_ message: StaticString, _ args: CVarArg...) { 17 | log(message, type: .debug, args) 18 | } 19 | 20 | func info(_ message: StaticString, _ args: CVarArg...) { 21 | log(message, type: .info, args) 22 | } 23 | 24 | func `default`(_ message: StaticString, _ args: CVarArg...) { 25 | log(message, type: .default, args) 26 | } 27 | 28 | func error(_ message: StaticString, _ args: CVarArg...) { 29 | log(message, type: .error, args) 30 | } 31 | 32 | private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { 33 | switch args.count { 34 | case 0: 35 | os_log(message, log: self, type: type) 36 | case 1: 37 | os_log(message, log: self, type: type, args[0]) 38 | case 2: 39 | os_log(message, log: self, type: type, args[0], args[1]) 40 | case 3: 41 | os_log(message, log: self, type: type, args[0], args[1], args[2]) 42 | case 4: 43 | os_log(message, log: self, type: type, args[0], args[1], args[2], args[3]) 44 | case 5: 45 | os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4]) 46 | default: 47 | os_log(message, log: self, type: type, args) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CGMBLEKitTests/SessionStartRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStartRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 6/4/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | /// Thanks to https://github.com/mthatcher for the fixtures! 13 | class SessionStartRxMessageTests: XCTestCase { 14 | 15 | func testSuccessfulStart() { 16 | var data = Data(hexadecimalString: "2700014bf871004bf87100e9f8710095d9")! 17 | var message = SessionStartRxMessage(data: data)! 18 | 19 | XCTAssertEqual(0, message.status) 20 | XCTAssertEqual(1, message.received) 21 | XCTAssertEqual(7469131, message.requestedStartTime) 22 | XCTAssertEqual(7469131, message.sessionStartTime) 23 | XCTAssertEqual(7469289, message.transmitterTime) 24 | 25 | data = Data(hexadecimalString: "2700012bfd71002bfd710096fd71000f6a")! 26 | message = SessionStartRxMessage(data: data)! 27 | 28 | XCTAssertEqual(0, message.status) 29 | XCTAssertEqual(1, message.received) 30 | XCTAssertEqual(7470379, message.requestedStartTime) 31 | XCTAssertEqual(7470379, message.sessionStartTime) 32 | XCTAssertEqual(7470486, message.transmitterTime) 33 | 34 | data = Data(hexadecimalString: "2700017cff71007cff7100eeff7100aeed")! 35 | message = SessionStartRxMessage(data: data)! 36 | 37 | XCTAssertEqual(0, message.status) 38 | XCTAssertEqual(1, message.received) 39 | XCTAssertEqual(7470972, message.requestedStartTime) 40 | XCTAssertEqual(7470972, message.sessionStartTime) 41 | XCTAssertEqual(7471086, message.transmitterTime) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /CGMBLEKitTests/SessionStopRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionStopRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 6/4/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | /// Thanks to https://github.com/mthatcher for the fixtures! 13 | class SessionStopRxMessageTests: XCTestCase { 14 | 15 | func testSuccessfulStop() { 16 | var data = Data(hexadecimalString: "29000128027200ffffffff47027200ba85")! 17 | var message = SessionStopRxMessage(data: data)! 18 | 19 | XCTAssertEqual(0, message.status) 20 | XCTAssertEqual(1, message.received) 21 | XCTAssertEqual(7471656, message.sessionStopTime) 22 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 23 | XCTAssertEqual(7471687, message.transmitterTime) 24 | 25 | data = Data(hexadecimalString: "2900013ffe7100ffffffffc2fe71008268")! 26 | message = SessionStopRxMessage(data: data)! 27 | 28 | XCTAssertEqual(0, message.status) 29 | XCTAssertEqual(1, message.received) 30 | XCTAssertEqual(7470655, message.sessionStopTime) 31 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 32 | XCTAssertEqual(7470786, message.transmitterTime) 33 | 34 | data = Data(hexadecimalString: "290001f5fb7100ffffffff6afc7100fa8a")! 35 | message = SessionStopRxMessage(data: data)! 36 | 37 | XCTAssertEqual(0, message.status) 38 | XCTAssertEqual(1, message.received) 39 | XCTAssertEqual(7470069, message.sessionStopTime) 40 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 41 | XCTAssertEqual(7470186, message.transmitterTime) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /CGMBLEKit Example/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 | APPL 17 | CFBundleShortVersionString 18 | 3.2 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIBackgroundModes 24 | 25 | bluetooth-central 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /CGMBLEKit Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CGMBLEKit/Opcode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Opcode.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Opcode: UInt8 { 11 | // Auth 12 | case authRequestTx = 0x01 13 | 14 | case authRequestRx = 0x03 15 | case authChallengeTx = 0x04 16 | case authChallengeRx = 0x05 17 | case keepAlive = 0x06 // auth; setAdvertisementParametersTx for control 18 | case bondRequest = 0x07 19 | 20 | // Control 21 | case disconnectTx = 0x09 22 | 23 | case setAdvertisementParametersRx = 0x1c 24 | 25 | case firmwareVersionTx = 0x20 26 | case firmwareVersionRx = 0x21 27 | case batteryStatusTx = 0x22 28 | case batteryStatusRx = 0x23 29 | case transmitterTimeTx = 0x24 30 | case transmitterTimeRx = 0x25 31 | case sessionStartTx = 0x26 32 | case sessionStartRx = 0x27 33 | case sessionStopTx = 0x28 34 | case sessionStopRx = 0x29 35 | 36 | case glucoseTx = 0x30 37 | case glucoseRx = 0x31 38 | case calibrationDataTx = 0x32 39 | case calibrationDataRx = 0x33 40 | case calibrateGlucoseTx = 0x34 41 | case calibrateGlucoseRx = 0x35 42 | 43 | case glucoseHistoryTx = 0x3e 44 | 45 | case resetTx = 0x42 46 | case resetRx = 0x43 47 | 48 | case transmitterVersionTx = 0x4a 49 | case transmitterVersionRx = 0x4b 50 | 51 | case glucoseG6Tx = 0x4e 52 | case glucoseG6Rx = 0x4f 53 | 54 | case glucoseBackfillTx = 0x50 55 | case glucoseBackfillRx = 0x51 56 | } 57 | 58 | 59 | extension Data { 60 | init(for opcode: Opcode) { 61 | self.init([opcode.rawValue]) 62 | } 63 | 64 | func starts(with opcode: Opcode) -> Bool { 65 | guard count > 0 else { 66 | return false 67 | } 68 | 69 | return self[startIndex] == opcode.rawValue 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ResetTransmitter/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /CGMBLEKit Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CGMBLEKitTests/TransmitterTimeRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterTimeRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 6/4/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | /// Thanks to https://github.com/mthatcher for the fixtures! 13 | class TransmitterTimeRxMessageTests: XCTestCase { 14 | 15 | func testNoSession() { 16 | var data = Data(hexadecimalString: "2500e8f87100ffffffff010000000a70")! 17 | var message = TransmitterTimeRxMessage(data: data)! 18 | 19 | XCTAssertEqual(0, message.status) 20 | XCTAssertEqual(7469288, message.currentTime) 21 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 22 | 23 | data = Data(hexadecimalString: "250096fd7100ffffffff01000000226d")! 24 | message = TransmitterTimeRxMessage(data: data)! 25 | 26 | XCTAssertEqual(0, message.status) 27 | XCTAssertEqual(7470486, message.currentTime) 28 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 29 | 30 | data = Data(hexadecimalString: "2500eeff7100ffffffff010000008952")! 31 | message = TransmitterTimeRxMessage(data: data)! 32 | 33 | XCTAssertEqual(0, message.status) 34 | XCTAssertEqual(7471086, message.currentTime) 35 | XCTAssertEqual(0xffffffff, message.sessionStartTime) 36 | } 37 | 38 | func testInSession() { 39 | var data = Data(hexadecimalString: "2500470272007cff710001000000fa1d")! 40 | var message = TransmitterTimeRxMessage(data: data)! 41 | 42 | XCTAssertEqual(0, message.status) 43 | XCTAssertEqual(7471687, message.currentTime) 44 | XCTAssertEqual(7470972, message.sessionStartTime) 45 | 46 | data = Data(hexadecimalString: "2500beb24d00f22d4d000100000083c0")! 47 | message = TransmitterTimeRxMessage(data: data)! 48 | 49 | XCTAssertEqual(0, message.status) 50 | XCTAssertEqual(5092030, message.currentTime) 51 | XCTAssertEqual(5058034, message.sessionStartTime) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ResetTransmitter/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Reset 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 3.2 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSBluetoothAlwaysUsageDescription 26 | Bluetooth is used to communicate with continuous glucose monitor devices 27 | NSBluetoothPeripheralUsageDescription 28 | Bluetooth is used to communicate with continuous glucose monitor devices 29 | UIBackgroundModes 30 | 31 | bluetooth-central 32 | 33 | UILaunchStoryboardName 34 | LaunchScreen 35 | UIMainStoryboardFile 36 | Main 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /CGMBLEKit/TransmitterManagerState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterManagerState.swift 3 | // CGMBLEKit 4 | // 5 | // Created by Pete Schwamb on 9/11/23. 6 | // Copyright © 2023 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoopKit 11 | 12 | public struct TransmitterManagerState: RawRepresentable, Equatable { 13 | public typealias RawValue = CGMManager.RawStateValue 14 | 15 | public static let version = 1 16 | 17 | public var transmitterID: String 18 | 19 | public var passiveModeEnabled: Bool = true 20 | 21 | public var transmitterStartDate: Date? 22 | 23 | public var sensorStartOffset: UInt32? 24 | 25 | public var shouldSyncToRemoteService: Bool 26 | 27 | public init( 28 | transmitterID: String, 29 | shouldSyncToRemoteService: Bool = false, 30 | transmitterStartDate: Date? = nil, 31 | sensorStartOffset: UInt32? = nil 32 | ) { 33 | self.transmitterID = transmitterID 34 | self.shouldSyncToRemoteService = shouldSyncToRemoteService 35 | self.transmitterStartDate = transmitterStartDate 36 | self.sensorStartOffset = sensorStartOffset 37 | } 38 | 39 | public init?(rawValue: RawValue) { 40 | guard let transmitterID = rawValue["transmitterID"] as? String 41 | else { 42 | return nil 43 | } 44 | 45 | let shouldSyncToRemoteService = rawValue["shouldSyncToRemoteService"] as? Bool ?? false 46 | 47 | let transmitterStartDate = rawValue["transmitterStartDate"] as? Date 48 | 49 | let sensorStartOffset = rawValue["sensorStartOffset"] as? UInt32 50 | 51 | self.init( 52 | transmitterID: transmitterID, 53 | shouldSyncToRemoteService: shouldSyncToRemoteService, 54 | transmitterStartDate: transmitterStartDate, 55 | sensorStartOffset: sensorStartOffset 56 | ) 57 | } 58 | 59 | public var rawValue: RawValue { 60 | var rval: RawValue = [ 61 | "transmitterID": transmitterID, 62 | "shouldSyncToRemoteService": shouldSyncToRemoteService 63 | ] 64 | 65 | rval["transmitterStartDate"] = transmitterStartDate 66 | rval["sensorStartOffset"] = sensorStartOffset 67 | 68 | return rval 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ResetTransmitter/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /CGMBLEKit/Glucose+SensorDisplayable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseRxMessage.swift 3 | // Loop 4 | // 5 | // Created by Nathan Racklyeft on 5/30/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoopKit 11 | 12 | 13 | extension Glucose: GlucoseDisplayable { 14 | public var isStateValid: Bool { 15 | return state == .known(.ok) && status == .ok 16 | } 17 | 18 | public var stateDescription: String { 19 | var messages = [String]() 20 | 21 | switch state { 22 | case .known(.ok): 23 | break // Suppress the "OK" message 24 | default: 25 | messages.append(state.localizedDescription) 26 | } 27 | 28 | switch self.status { 29 | case .ok: 30 | if messages.isEmpty { 31 | messages.append(status.localizedDescription) 32 | } else { 33 | break // Suppress the "OK" message 34 | } 35 | case .lowBattery, .unknown: 36 | messages.append(status.localizedDescription) 37 | } 38 | 39 | return messages.joined(separator: ". ") 40 | } 41 | 42 | public var trendType: GlucoseTrend? { 43 | guard trend < Int(Int8.max) else { 44 | return nil 45 | } 46 | 47 | switch trend { 48 | case let x where x <= -30: 49 | return .downDownDown 50 | case let x where x <= -20: 51 | return .downDown 52 | case let x where x <= -10: 53 | return .down 54 | case let x where x < 10: 55 | return .flat 56 | case let x where x < 20: 57 | return .up 58 | case let x where x < 30: 59 | return .upUp 60 | default: 61 | return .upUpUp 62 | } 63 | } 64 | 65 | public var isLocal: Bool { 66 | return true 67 | } 68 | 69 | // TODO Placeholders. This functionality will come with LOOP-1311 70 | public var glucoseRangeCategory: GlucoseRangeCategory? { 71 | return nil 72 | } 73 | } 74 | 75 | extension Glucose { 76 | public var condition: GlucoseCondition? { 77 | if glucoseMessage.glucose < GlucoseLimits.minimum { 78 | return .belowRange 79 | } else if glucoseMessage.glucose > GlucoseLimits.maximum { 80 | return .aboveRange 81 | } else { 82 | return nil 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CGMBLEKit/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // xDripG5 4 | // 5 | // Created by Paul Dickens on 22/03/2018. 6 | // Copyright © 2018 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import HealthKit 11 | 12 | 13 | public enum Command: RawRepresentable { 14 | public typealias RawValue = [String: Any] 15 | 16 | case startSensor(at: Date) 17 | case stopSensor(at: Date) 18 | case calibrateSensor(to: HKQuantity, at: Date) 19 | case resetTransmitter 20 | 21 | public init?(rawValue: RawValue) { 22 | guard let action = rawValue["action"] as? Action.RawValue else { 23 | return nil 24 | } 25 | 26 | let date = rawValue["date"] as? Date 27 | 28 | switch Action(rawValue: action) { 29 | case .startSensor?: 30 | guard let date = date else { 31 | return nil 32 | } 33 | self = .startSensor(at: date) 34 | case .stopSensor?: 35 | guard let date = date else { 36 | return nil 37 | } 38 | self = .stopSensor(at: date) 39 | case .calibrateSensor?: 40 | guard let date = date, let glucose = rawValue["glucose"] as? HKQuantity else { 41 | return nil 42 | } 43 | self = .calibrateSensor(to: glucose, at: date) 44 | case .resetTransmitter?: 45 | self = .resetTransmitter 46 | case .none: 47 | return nil 48 | } 49 | } 50 | 51 | private enum Action: Int { 52 | case startSensor, stopSensor, calibrateSensor, resetTransmitter 53 | } 54 | 55 | public var rawValue: RawValue { 56 | switch self { 57 | case .startSensor(let date): 58 | return [ 59 | "action": Action.startSensor.rawValue, 60 | "date": date 61 | ] 62 | case .stopSensor(let date): 63 | return [ 64 | "action": Action.stopSensor.rawValue, 65 | "date": date 66 | ] 67 | case .calibrateSensor(let glucose, let date): 68 | return [ 69 | "action": Action.calibrateSensor.rawValue, 70 | "date": date, 71 | "glucose": glucose 72 | ] 73 | case .resetTransmitter: 74 | return [ 75 | "action": Action.resetTransmitter.rawValue 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/GlucoseRxMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseRxMessage.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 11/23/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public struct GlucoseSubMessage: TransmitterRxMessage { 13 | static let size = 8 14 | 15 | public let timestamp: UInt32 16 | public let glucoseIsDisplayOnly: Bool 17 | public let glucose: UInt16 18 | public let state: UInt8 19 | public let trend: Int8 20 | 21 | init?(data: Data) { 22 | guard data.count >= GlucoseSubMessage.size else { 23 | return nil 24 | } 25 | 26 | var start = data.startIndex 27 | var end = start.advanced(by: 4) 28 | timestamp = data[start.. 0 34 | glucose = glucoseBytes & 0xfff 35 | 36 | start = end 37 | end = start.advanced(by: 1) 38 | state = data[start] 39 | 40 | start = end 41 | end = start.advanced(by: 1) 42 | trend = Int8(bitPattern: data[start]) 43 | } 44 | } 45 | 46 | 47 | public struct GlucoseRxMessage: TransmitterRxMessage { 48 | public let status: UInt8 49 | public let sequence: UInt32 50 | public let glucose: GlucoseSubMessage 51 | 52 | init?(data: Data) { 53 | guard data.count >= 16 && data.isCRCValid else { 54 | return nil 55 | } 56 | 57 | guard data.starts(with: .glucoseRx) || data.starts(with: .glucoseG6Rx) else { 58 | return nil 59 | } 60 | 61 | status = data[1] 62 | sequence = data[2..<6].toInt() 63 | 64 | guard let glucose = GlucoseSubMessage(data: data[6...]) else { 65 | return nil 66 | } 67 | self.glucose = glucose 68 | } 69 | } 70 | 71 | extension GlucoseSubMessage: Equatable { 72 | public static func ==(lhs: GlucoseSubMessage, rhs: GlucoseSubMessage) -> Bool { 73 | return lhs.timestamp == rhs.timestamp && 74 | lhs.glucoseIsDisplayOnly == rhs.glucoseIsDisplayOnly && 75 | lhs.glucose == rhs.glucose && 76 | lhs.state == rhs.state && 77 | lhs.trend == rhs.trend 78 | } 79 | } 80 | 81 | 82 | extension GlucoseRxMessage: Equatable { 83 | public static func ==(lhs: GlucoseRxMessage, rhs: GlucoseRxMessage) -> Bool { 84 | return lhs.status == rhs.status && 85 | lhs.sequence == rhs.sequence && 86 | lhs.glucose == rhs.glucose 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CGMBLEKit 2 | 3 | [![CI Status](http://img.shields.io/travis/LoopKit/CGMBLEKit.svg?style=flat)](https://travis-ci.org/LoopKit/CGMBLEKit) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | 6 | A iOS framework providing an interface for communicating with the G5 and G6 glucose transmitters over Bluetooth. 7 | 8 | *Please note this project is neither created nor backed by Dexcom, Inc. This software is not intended for use in therapy.* 9 | 10 | ## Requirements 11 | 12 | This framework connects to a G5 or G6 Mobile Transmitter via Bluetooth LE. It does not connect to the G4 Share Receiver or any earlier CGM products. 13 | 14 | ## Frameworks Installation 15 | 16 | ### Carthage 17 | 18 | CGMBLEKit is available through [Carthage](https://github.com/Carthage/Carthage). To install it, add the following line to your Cartfile: 19 | 20 | ```ruby 21 | github "LoopKit/CGMBLEKit" 22 | ``` 23 | 24 | Note that you'll need to confgure your target to link against `CommonCrypto.framework` in addition to `CGMBLEKit.framework` 25 | 26 | ## Usage 27 | 28 | If you plan to run your app alongside the G5 Mobile application, make sure to set `passiveModeEnabled` to true. 29 | 30 | ### Examples 31 | 32 | [glucose-badge](https://github.com/dennisgove/glucose-badge) – Display the latest glucose values as an app icon badge 33 | 34 | ## ResetTransmitter App Installation 35 | 36 | Download the CGMBLEKit code by clicking on the green `Clone or Download` button (scroll up on this page and you'll find it), then select `Download Zip` 37 | 38 | ![ResetTransmitter help](https://github.com/Kdisimone/images/blob/master/resetTransmitter-first.png) 39 | 40 | Then navigate to the `CGMBLEKit` folder that just downloaded to your computer. Double-click on the `CGMBLEKit.xcodeproj` file to open the project in Xcode. 41 | 42 | ![ResetTransmitter help](https://github.com/Kdisimone/images/blob/master/resetTransmitter-download.png) 43 | 44 | To install the ResetTransmitter App on your iPhone, simply make sure to sign the ResetTransmitter target and then select just the `ResetTransmitter` scheme in the build area. Make sure your iPhone is plugged into the computer, select your iPhone from the top of the `Devices` in the 4th circled area, screenshot below. Note: You do not have to change bundle IDs or anything beyond the steps listed. 45 | 46 | ![ResetTransmitter help](https://github.com/Kdisimone/images/blob/master/resetTransmitter.png) 47 | 48 | 49 | ## Code of Conduct 50 | 51 | Please note that this project is released with a [Contributor Code of Conduct](https://github.com/LoopKit/LoopKit/blob/master/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 52 | 53 | ## License 54 | 55 | CGMBLEKit is available under the MIT license. See the LICENSE file for more info. 56 | -------------------------------------------------------------------------------- /CGMBLEKit/BluetoothServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothServices.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 10/16/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import CoreBluetooth 10 | 11 | /* 12 | G5 BLE attributes, retrieved using LightBlue on 2015-10-01 13 | 14 | These are the G4 details, for reference: 15 | https://github.com/StephenBlackWasAlreadyTaken/xDrip/blob/af20e32652d19aa40becc1a39f6276cad187fdce/app/src/main/java/com/eveningoutpost/dexdrip/UtilityModels/DexShareAttributes.java 16 | */ 17 | 18 | protocol CBUUIDRawValue: RawRepresentable {} 19 | extension CBUUIDRawValue where RawValue == String { 20 | var cbUUID: CBUUID { 21 | return CBUUID(string: rawValue) 22 | } 23 | } 24 | 25 | 26 | enum TransmitterServiceUUID: String, CBUUIDRawValue { 27 | case deviceInfo = "180A" 28 | case advertisement = "FEBC" 29 | case cgmService = "F8083532-849E-531C-C594-30F1F86A4EA5" 30 | 31 | case serviceB = "F8084532-849E-531C-C594-30F1F86A4EA5" 32 | } 33 | 34 | 35 | enum DeviceInfoCharacteristicUUID: String, CBUUIDRawValue { 36 | // Read 37 | // "DexcomUN" 38 | case manufacturerNameString = "2A29" 39 | } 40 | 41 | 42 | enum CGMServiceCharacteristicUUID: String, CBUUIDRawValue { 43 | 44 | // Read/Notify 45 | case communication = "F8083533-849E-531C-C594-30F1F86A4EA5" 46 | 47 | // Write/Indicate 48 | case control = "F8083534-849E-531C-C594-30F1F86A4EA5" 49 | 50 | // Write/Indicate 51 | case authentication = "F8083535-849E-531C-C594-30F1F86A4EA5" 52 | 53 | // Read/Write/Notify 54 | case backfill = "F8083536-849E-531C-C594-30F1F86A4EA5" 55 | 56 | // // Unknown attribute present on older G6 transmitters 57 | // case unknown1 = "F8083537-849E-531C-C594-30F1F86A4EA5" 58 | // 59 | // // Updated G6 characteristic (read/notify) 60 | // case unknown2 = "F8083538-849E-531C-C594-30F1F86A4EA5" 61 | } 62 | 63 | 64 | enum ServiceBCharacteristicUUID: String, CBUUIDRawValue { 65 | // Write/Indicate 66 | case characteristicE = "F8084533-849E-531C-C594-30F1F86A4EA5" 67 | // Read/Write/Notify 68 | case characteristicF = "F8084534-849E-531C-C594-30F1F86A4EA5" 69 | } 70 | 71 | 72 | extension PeripheralManager.Configuration { 73 | static var dexcomG5: PeripheralManager.Configuration { 74 | return PeripheralManager.Configuration( 75 | serviceCharacteristics: [ 76 | TransmitterServiceUUID.cgmService.cbUUID: [ 77 | CGMServiceCharacteristicUUID.communication.cbUUID, 78 | CGMServiceCharacteristicUUID.authentication.cbUUID, 79 | CGMServiceCharacteristicUUID.control.cbUUID, 80 | CGMServiceCharacteristicUUID.backfill.cbUUID, 81 | ] 82 | ], 83 | notifyingCharacteristics: [:], 84 | valueUpdateMacros: [:] 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CGMBLEKit Example/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CFBundleName" : { 5 | "comment" : "Bundle name", 6 | "extractionState" : "manual", 7 | "localizations" : { 8 | "da" : { 9 | "stringUnit" : { 10 | "state" : "translated", 11 | "value" : "Eksempel på CGMBLEKit" 12 | } 13 | }, 14 | "de" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "CGMBLEKit Beispiel" 18 | } 19 | }, 20 | "es" : { 21 | "stringUnit" : { 22 | "state" : "translated", 23 | "value" : "Ejemplo de CGMBLEKit" 24 | } 25 | }, 26 | "fi" : { 27 | "stringUnit" : { 28 | "state" : "translated", 29 | "value" : "CGMBLEKit esimerkki" 30 | } 31 | }, 32 | "fr" : { 33 | "stringUnit" : { 34 | "state" : "translated", 35 | "value" : "Exemple CGMBLEKit" 36 | } 37 | }, 38 | "he" : { 39 | "stringUnit" : { 40 | "state" : "translated", 41 | "value" : "CGMBLEKit Example" 42 | } 43 | }, 44 | "it" : { 45 | "stringUnit" : { 46 | "state" : "translated", 47 | "value" : "esempio CGMBLE kit" 48 | } 49 | }, 50 | "nb" : { 51 | "stringUnit" : { 52 | "state" : "translated", 53 | "value" : "CGMBLEKit Eksempel" 54 | } 55 | }, 56 | "nl" : { 57 | "stringUnit" : { 58 | "state" : "translated", 59 | "value" : "CGMBLEKit Voorbeeld" 60 | } 61 | }, 62 | "pl" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "CGMBLEKit Example" 66 | } 67 | }, 68 | "ro" : { 69 | "stringUnit" : { 70 | "state" : "translated", 71 | "value" : "Exemplu CGMBLEKit" 72 | } 73 | }, 74 | "ru" : { 75 | "stringUnit" : { 76 | "state" : "translated", 77 | "value" : "Пример CGMBLEKit" 78 | } 79 | }, 80 | "sk" : { 81 | "stringUnit" : { 82 | "state" : "translated", 83 | "value" : "Príklad CGMBLEKit" 84 | } 85 | }, 86 | "sv" : { 87 | "stringUnit" : { 88 | "state" : "translated", 89 | "value" : "CGMBLEKit Example" 90 | } 91 | }, 92 | "tr" : { 93 | "stringUnit" : { 94 | "state" : "translated", 95 | "value" : "CGMBLEKit Örneği" 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "version" : "1.0" 102 | } -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /CGMBLEKitTests/GlucoseRxMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseRxMessageTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/5/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CGMBLEKit 11 | 12 | 13 | class GlucoseRxMessageTests: XCTestCase { 14 | 15 | func testMessageData() { 16 | let data = Data(hexadecimalString: "3100680a00008a715700cc0006ffc42a")! 17 | let message = GlucoseRxMessage(data: data)! 18 | 19 | XCTAssertEqual(0, message.status) 20 | XCTAssertEqual(2664, message.sequence) 21 | XCTAssertEqual(5730698, message.glucose.timestamp) 22 | XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) 23 | XCTAssertEqual(204, message.glucose.glucose) 24 | XCTAssertEqual(6, message.glucose.state) 25 | XCTAssertEqual(-1, message.glucose.trend) 26 | } 27 | 28 | func testNegativeTrend() { 29 | let data = Data(hexadecimalString: "31006f0a0000be7957007a0006e4818d")! 30 | let message = GlucoseRxMessage(data: data)! 31 | 32 | XCTAssertEqual(0, message.status) 33 | XCTAssertEqual(2671, message.sequence) 34 | XCTAssertEqual(5732798, message.glucose.timestamp) 35 | XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) 36 | XCTAssertEqual(122, message.glucose.glucose) 37 | XCTAssertEqual(6, message.glucose.state) 38 | XCTAssertEqual(-28, message.glucose.trend) 39 | } 40 | 41 | func testDisplayOnly() { 42 | let data = Data(hexadecimalString: "3100700a0000f17a5700584006e3cee9")! 43 | let message = GlucoseRxMessage(data: data)! 44 | 45 | XCTAssertEqual(0, message.status) 46 | XCTAssertEqual(2672, message.sequence) 47 | XCTAssertEqual(5733105, message.glucose.timestamp) 48 | XCTAssertTrue(message.glucose.glucoseIsDisplayOnly) 49 | XCTAssertEqual(88, message.glucose.glucose) 50 | XCTAssertEqual(6, message.glucose.state) 51 | XCTAssertEqual(-29, message.glucose.trend) 52 | } 53 | 54 | func testOldTransmitter() { 55 | let data = Data(hexadecimalString: "3100aa00000095a078008b00060a8b34")! 56 | let message = GlucoseRxMessage(data: data)! 57 | 58 | XCTAssertEqual(0, message.status) 59 | XCTAssertEqual(170, message.sequence) 60 | XCTAssertEqual(7905429, message.glucose.timestamp) // 90 days, status is still OK 61 | XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) 62 | XCTAssertEqual(139, message.glucose.glucose) 63 | XCTAssertEqual(6, message.glucose.state) 64 | XCTAssertEqual(10, message.glucose.trend) 65 | } 66 | 67 | func testZeroSequence() { 68 | let data = Data(hexadecimalString: "3100000000008eb14d00820006f6a038")! 69 | let message = GlucoseRxMessage(data: data)! 70 | 71 | XCTAssertEqual(0, message.status) 72 | XCTAssertEqual(0, message.sequence) 73 | XCTAssertEqual(5091726, message.glucose.timestamp) 74 | XCTAssertFalse(message.glucose.glucoseIsDisplayOnly) 75 | XCTAssertEqual(130, message.glucose.glucose) 76 | XCTAssertEqual(6, message.glucose.state) 77 | XCTAssertEqual(-10, message.glucose.trend) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CGMBLEKit/CalibrationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalibrationState.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 8/6/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public enum CalibrationState: RawRepresentable { 13 | public typealias RawValue = UInt8 14 | 15 | public enum State: RawValue { 16 | case stopped = 1 17 | case warmup = 2 18 | 19 | case needFirstInitialCalibration = 4 20 | case needSecondInitialCalibration = 5 21 | case ok = 6 22 | case needCalibration7 = 7 23 | case calibrationError8 = 8 24 | case calibrationError9 = 9 25 | case calibrationError10 = 10 26 | case sensorFailure11 = 11 27 | case sensorFailure12 = 12 28 | case calibrationError13 = 13 29 | case needCalibration14 = 14 30 | case sessionFailure15 = 15 31 | case sessionFailure16 = 16 32 | case sessionFailure17 = 17 33 | case questionMarks = 18 34 | } 35 | 36 | case known(State) 37 | case unknown(RawValue) 38 | 39 | public init(rawValue: RawValue) { 40 | guard let state = State(rawValue: rawValue) else { 41 | self = .unknown(rawValue) 42 | return 43 | } 44 | 45 | self = .known(state) 46 | } 47 | 48 | public var rawValue: RawValue { 49 | switch self { 50 | case .known(let state): 51 | return state.rawValue 52 | case .unknown(let rawValue): 53 | return rawValue 54 | } 55 | } 56 | 57 | public var hasReliableGlucose: Bool { 58 | guard case .known(let state) = self else { 59 | return false 60 | } 61 | 62 | switch state { 63 | case .stopped, 64 | .warmup, 65 | .needFirstInitialCalibration, 66 | .needSecondInitialCalibration, 67 | .calibrationError8, 68 | .calibrationError9, 69 | .calibrationError10, 70 | .sensorFailure11, 71 | .sensorFailure12, 72 | .calibrationError13, 73 | .sessionFailure15, 74 | .sessionFailure16, 75 | .sessionFailure17, 76 | .questionMarks: 77 | return false 78 | case .ok, .needCalibration7, .needCalibration14: 79 | return true 80 | } 81 | } 82 | } 83 | 84 | extension CalibrationState: Equatable { 85 | public static func ==(lhs: CalibrationState, rhs: CalibrationState) -> Bool { 86 | switch (lhs, rhs) { 87 | case (.known(let lhs), .known(let rhs)): 88 | return lhs == rhs 89 | case (.unknown(let lhs), .unknown(let rhs)): 90 | return lhs == rhs 91 | default: 92 | return false 93 | } 94 | } 95 | } 96 | 97 | extension CalibrationState: CustomStringConvertible { 98 | public var description: String { 99 | switch self { 100 | case .known(let state): 101 | return String(describing: state) 102 | case .unknown(let value): 103 | return ".unknown(\(value))" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | In the interest of fostering an open and welcoming environment, we as 4 | contributors and maintainers pledge to making participation in our project and 5 | our community a harassment-free experience for everyone, regardless of age, body 6 | size, disability, ethnicity, gender identity and expression, level of experience, 7 | nationality, personal appearance, race, religion, or sexual identity and 8 | orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment 13 | include: 14 | 15 | * Using welcoming and inclusive language 16 | * Being respectful of differing viewpoints and experiences 17 | * Gracefully accepting constructive criticism 18 | * Focusing on what is best for the community 19 | * Showing empathy towards other community members 20 | 21 | Examples of unacceptable behavior by participants include: 22 | 23 | * The use of sexualized language or imagery and unwelcome sexual attention or 24 | advances 25 | * Trolling, insulting/derogatory comments, and personal or political attacks 26 | * Public or private harassment 27 | * Publishing others' private information, such as a physical or electronic 28 | address, without explicit permission 29 | * Other conduct which could reasonably be considered inappropriate in a 30 | professional setting 31 | 32 | ## Our Responsibilities 33 | 34 | Project maintainers are responsible for clarifying the standards of acceptable 35 | behavior and are expected to take appropriate and fair corrective action in 36 | response to any instances of unacceptable behavior. 37 | 38 | Project maintainers have the right and responsibility to remove, edit, or 39 | reject comments, commits, code, wiki edits, issues, and other contributions 40 | that are not aligned to this Code of Conduct, or to ban temporarily or 41 | permanently any contributor for other behaviors that they deem inappropriate, 42 | threatening, offensive, or harmful. 43 | 44 | ## Scope 45 | 46 | This Code of Conduct applies both within project spaces and in public spaces 47 | when an individual is representing the project or its community. Examples of 48 | representing a project or community include using an official project e-mail 49 | address, posting via an official social media account, or acting as an appointed 50 | representative at an online or offline event. Representation of a project may be 51 | further defined and clarified by project maintainers. 52 | 53 | ## Enforcement 54 | 55 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 56 | reported by contacting the maintaner [via email](mailto:loudnate@gmail.com). All 57 | complaints will be reviewed and investigated and will result in a response that 58 | is deemed necessary and appropriate to the circumstances. The project team is 59 | obligated to maintain confidentiality with regard to the reporter of an incident. 60 | Further details of specific enforcement policies may be posted separately. 61 | 62 | Project maintainers who do not follow or enforce the Code of Conduct in good 63 | faith may face temporary or permanent repercussions as determined by other 64 | members of the project's leadership. 65 | 66 | ## Attribution 67 | 68 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 69 | available at [http://contributor-covenant.org/version/1/4][version] 70 | 71 | [homepage]: http://contributor-covenant.org 72 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CGMBLEKitUI/TransmitterManager+UI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterManager+UI.swift 3 | // Loop 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import LoopKit 10 | import LoopKitUI 11 | import HealthKit 12 | import CGMBLEKit 13 | 14 | 15 | extension G5CGMManager: CGMManagerUI { 16 | public static var onboardingImage: UIImage? { 17 | return nil 18 | } 19 | 20 | public static func setupViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, prefersToSkipUserInteraction: Bool = false) -> SetupUIResult { 21 | let setupVC = TransmitterSetupViewController.instantiateFromStoryboard() 22 | setupVC.cgmManagerType = self 23 | return .userInteractionRequired(setupVC) 24 | } 25 | 26 | public func settingsViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) ->CGMManagerViewController { 27 | let settings = TransmitterSettingsViewController(cgmManager: self, displayGlucosePreference: displayGlucosePreference) 28 | let nav = CGMManagerSettingsNavigationViewController(rootViewController: settings) 29 | return nav 30 | } 31 | 32 | public var smallImage: UIImage? { 33 | return nil 34 | } 35 | 36 | // TODO Placeholder. 37 | public var cgmStatusHighlight: DeviceStatusHighlight? { 38 | return nil 39 | } 40 | 41 | // TODO Placeholder. 42 | public var cgmStatusBadge: DeviceStatusBadge? { 43 | return nil 44 | } 45 | 46 | // TODO Placeholder. 47 | public var cgmLifecycleProgress: DeviceLifecycleProgress? { 48 | return nil 49 | } 50 | } 51 | 52 | 53 | extension G6CGMManager: CGMManagerUI { 54 | public static var onboardingImage: UIImage? { 55 | return nil 56 | } 57 | 58 | public static func setupViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, prefersToSkipUserInteraction: Bool = false) -> SetupUIResult { 59 | let setupVC = TransmitterSetupViewController.instantiateFromStoryboard() 60 | setupVC.cgmManagerType = self 61 | return .userInteractionRequired(setupVC) 62 | } 63 | 64 | public func settingsViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) ->CGMManagerViewController { 65 | let settings = TransmitterSettingsViewController(cgmManager: self, displayGlucosePreference: displayGlucosePreference) 66 | let nav = CGMManagerSettingsNavigationViewController(rootViewController: settings) 67 | return nav 68 | } 69 | 70 | public var smallImage: UIImage? { 71 | UIImage(named: "g6", in: Bundle(for: TransmitterSetupViewController.self), compatibleWith: nil)! 72 | } 73 | 74 | // TODO Placeholder. 75 | public var cgmStatusHighlight: DeviceStatusHighlight? { 76 | return nil 77 | } 78 | 79 | // TODO Placeholder. 80 | public var cgmStatusBadge: DeviceStatusBadge? { 81 | return nil 82 | } 83 | 84 | // TODO Placeholder. 85 | public var cgmLifecycleProgress: DeviceLifecycleProgress? { 86 | return nil 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/xcshareddata/xcschemes/ResetTransmitter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Common/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSData.swift 3 | // xDripG5 4 | // 5 | // Created by Nathan Racklyeft on 3/5/16. 6 | // Copyright © 2016 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 toInt() -> T { 28 | return to(T.self) 29 | } 30 | 31 | func toBigEndian(_ type: T.Type) -> T { 32 | return T(bigEndian: toDefaultEndian(type)) 33 | } 34 | 35 | mutating func append(_ newElement: T) { 36 | withUnsafePointer(to: newElement.littleEndian) { (ptr: UnsafePointer) in 37 | append(UnsafeBufferPointer(start: ptr, count: 1)) 38 | } 39 | } 40 | 41 | mutating func appendBigEndian(_ newElement: T) { 42 | withUnsafePointer(to: newElement.bigEndian) { (ptr: UnsafePointer) in 43 | append(UnsafeBufferPointer(start: ptr, count: 1)) 44 | } 45 | } 46 | 47 | init(_ value: T) { 48 | self = withUnsafePointer(to: value.littleEndian) { (ptr: UnsafePointer) -> Data in 49 | return Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) 50 | } 51 | } 52 | 53 | init(bigEndian value: T) { 54 | self = withUnsafePointer(to: value.bigEndian) { (ptr: UnsafePointer) -> Data in 55 | return Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) 56 | } 57 | } 58 | } 59 | 60 | 61 | // String conversion methods, adapted from https://stackoverflow.com/questions/40276322/hex-binary-string-conversion-in-swift/40278391#40278391 62 | extension Data { 63 | init?(hexadecimalString: String) { 64 | self.init(capacity: hexadecimalString.utf16.count / 2) 65 | 66 | // Convert 0 ... 9, a ... f, A ...F to their decimal value, 67 | // return nil for all other input characters 68 | func decodeNibble(u: UInt16) -> UInt8? { 69 | switch u { 70 | case 0x30 ... 0x39: // '0'-'9' 71 | return UInt8(u - 0x30) 72 | case 0x41 ... 0x46: // 'A'-'F' 73 | return UInt8(u - 0x41 + 10) // 10 since 'A' is 10, not 0 74 | case 0x61 ... 0x66: // 'a'-'f' 75 | return UInt8(u - 0x61 + 10) // 10 since 'a' is 10, not 0 76 | default: 77 | return nil 78 | } 79 | } 80 | 81 | var even = true 82 | var byte: UInt8 = 0 83 | for c in hexadecimalString.utf16 { 84 | guard let val = decodeNibble(u: c) else { return nil } 85 | if even { 86 | byte = val << 4 87 | } else { 88 | byte += val 89 | self.append(byte) 90 | } 91 | even = !even 92 | } 93 | guard even else { return nil } 94 | } 95 | 96 | var hexadecimalString: String { 97 | return map { String(format: "%02hhx", $0) }.joined() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /CGMBLEKit/Messages/GlucoseBackfillMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseBackfillMessage.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | // 50 05 02 00 b7ff5200 66045300 00000000 0000 7138 11 | 12 | struct GlucoseBackfillTxMessage: RespondableMessage { 13 | typealias Response = GlucoseBackfillRxMessage 14 | 15 | let byte1: UInt8 16 | let byte2: UInt8 17 | let identifier: UInt8 18 | 19 | let startTime: UInt32 20 | let endTime: UInt32 21 | 22 | let length: UInt32 = 0 23 | let backfillCRC: UInt16 = 0 24 | 25 | var data: Data { 26 | var data = Data(for: .glucoseBackfillTx) 27 | data.append(contentsOf: [byte1, byte2, identifier]) 28 | data.append(startTime) 29 | data.append(endTime) 30 | data.append(length) 31 | data.append(backfillCRC) 32 | 33 | return data.appendingCRC() 34 | } 35 | } 36 | 37 | // 51 00 01 00 b7ff5200 66045300 32000000 e6cb 9805 38 | 39 | struct GlucoseBackfillRxMessage: TransmitterRxMessage { 40 | let status: UInt8 41 | let backfillStatus: UInt8 42 | let identifier: UInt8 43 | let startTime: UInt32 44 | let endTime: UInt32 45 | let bufferLength: UInt32 46 | let bufferCRC: UInt16 47 | 48 | init?(data: Data) { 49 | guard data.count == 20, 50 | data.isCRCValid, 51 | data.starts(with: .glucoseBackfillRx) 52 | else { 53 | return nil 54 | } 55 | 56 | status = data[1] 57 | backfillStatus = data[2] 58 | identifier = data[3] 59 | startTime = data[4..<8].toInt() 60 | endTime = data[8..<12].toInt() 61 | bufferLength = data[12..<16].toInt() 62 | bufferCRC = data[16..<18].toInt() 63 | } 64 | } 65 | 66 | // 0100bc460000b7ff52008b0006eee30053008500 67 | // 020006eb0f025300800006ee3a0353007e0006f5 68 | // 030066045300790006f8 69 | 70 | struct GlucoseBackfillFrameBuffer { 71 | let identifier: UInt8 72 | private var frames: [Data] = [] 73 | 74 | init(identifier: UInt8) { 75 | self.identifier = identifier 76 | } 77 | 78 | mutating func append(_ frame: Data) { 79 | // Byte 0 is the frame index 80 | // Byte 1 is the identifier 81 | guard frame.count > 2, 82 | frame[0] == frames.count + 1, 83 | frame[1] == identifier else { 84 | return 85 | } 86 | 87 | frames.append(frame) 88 | } 89 | 90 | var count: Int { 91 | return frames.reduce(0, { $0 + $1.count }) 92 | } 93 | 94 | var crc16: UInt16 { 95 | return frames.reduce(into: Data(), { $0.append($1) }).crc16 96 | } 97 | 98 | var glucose: [GlucoseSubMessage] { 99 | // Drop the first 2 bytes from each frame 100 | let data = frames.reduce(into: Data(), { $0.append($1.dropFirst(2)) }) 101 | 102 | // Drop the first 4 bytes from the combined message 103 | // Byte 0: ?? 104 | // Byte 1: ?? 105 | // Byte 2: ?? (only seen 0 so far) 106 | // Byte 3: ?? (only seen 0 so far) 107 | let glucoseData = data.dropFirst(4) 108 | 109 | return stride( 110 | from: glucoseData.startIndex, 111 | to: glucoseData.endIndex, 112 | by: GlucoseSubMessage.size 113 | ).compactMap { 114 | let range = $0..<$0.advanced(by: GlucoseSubMessage.size) 115 | guard glucoseData.endIndex >= range.endIndex else { 116 | return nil 117 | } 118 | 119 | return GlucoseSubMessage(data: glucoseData[range]) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CGMBLEKitUI/TransmitterSetupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterSetupViewController.swift 3 | // CGMBLEKitUI 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import LoopKit 10 | import LoopKitUI 11 | import CGMBLEKit 12 | import ShareClient 13 | 14 | class TransmitterSetupViewController: UINavigationController, CGMManagerOnboarding, UINavigationControllerDelegate, CompletionNotifying { 15 | class func instantiateFromStoryboard() -> TransmitterSetupViewController { 16 | return UIStoryboard(name: "TransmitterManagerSetup", bundle: Bundle(for: TransmitterSetupViewController.self)).instantiateInitialViewController() as! TransmitterSetupViewController 17 | } 18 | 19 | weak var cgmManagerOnboardingDelegate: CGMManagerOnboardingDelegate? 20 | weak var completionDelegate: CompletionDelegate? 21 | 22 | var cgmManagerType: TransmitterManager.Type! 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | delegate = self 28 | view.backgroundColor = .systemGroupedBackground 29 | navigationBar.shadowImage = UIImage() 30 | } 31 | 32 | func completeSetup(state: TransmitterManagerState) { 33 | if let manager = cgmManagerType.init(state: state) as? CGMManagerUI { 34 | cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didCreateCGMManager: manager) 35 | cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didOnboardCGMManager: manager) 36 | completionDelegate?.completionNotifyingDidComplete(self) 37 | } 38 | } 39 | 40 | // MARK: - UINavigationControllerDelegate 41 | 42 | func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { 43 | // Read state values 44 | let viewControllers = navigationController.viewControllers 45 | let count = navigationController.viewControllers.count 46 | 47 | if count >= 2 { 48 | switch viewControllers[count - 2] { 49 | case _ as TransmitterIDSetupViewController: 50 | break 51 | default: 52 | break 53 | } 54 | } 55 | 56 | if let setupViewController = viewController as? SetupTableViewController { 57 | setupViewController.delegate = self 58 | } 59 | 60 | // Set state values 61 | switch viewController { 62 | case _ as TransmitterIDSetupViewController: 63 | break 64 | default: 65 | break 66 | } 67 | 68 | // Adjust the appearance for the main setup view controllers only 69 | if viewController is SetupTableViewController { 70 | navigationBar.isTranslucent = false 71 | navigationBar.shadowImage = UIImage() 72 | } else { 73 | navigationBar.isTranslucent = true 74 | navigationBar.shadowImage = nil 75 | viewController.navigationItem.largeTitleDisplayMode = .never 76 | } 77 | } 78 | 79 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 80 | 81 | // Adjust the appearance for the main setup view controllers only 82 | if viewController is SetupTableViewController { 83 | navigationBar.isTranslucent = false 84 | navigationBar.shadowImage = UIImage() 85 | } else { 86 | navigationBar.isTranslucent = true 87 | navigationBar.shadowImage = nil 88 | } 89 | } 90 | } 91 | 92 | extension TransmitterSetupViewController: SetupTableViewControllerDelegate { 93 | public func setupTableViewControllerCancelButtonPressed(_ viewController: SetupTableViewController) { 94 | completionDelegate?.completionNotifyingDidComplete(self) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CGMBLEKit/Glucose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Glucose.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 8/6/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import HealthKit 11 | 12 | enum GlucoseLimits { 13 | static var minimum: UInt16 = 40 14 | static var maximum: UInt16 = 400 15 | } 16 | 17 | public struct Glucose { 18 | let glucoseMessage: GlucoseSubMessage 19 | let timeMessage: TransmitterTimeRxMessage 20 | 21 | init( 22 | transmitterID: String, 23 | glucoseMessage: GlucoseRxMessage, 24 | timeMessage: TransmitterTimeRxMessage, 25 | calibrationMessage: CalibrationDataRxMessage? = nil, 26 | activationDate: Date 27 | ) { 28 | self.init( 29 | transmitterID: transmitterID, 30 | status: glucoseMessage.status, 31 | glucoseMessage: glucoseMessage.glucose, 32 | timeMessage: timeMessage, 33 | calibrationMessage: calibrationMessage, 34 | activationDate: activationDate 35 | ) 36 | } 37 | 38 | init( 39 | transmitterID: String, 40 | status: UInt8, 41 | glucoseMessage: GlucoseSubMessage, 42 | timeMessage: TransmitterTimeRxMessage, 43 | calibrationMessage: CalibrationDataRxMessage? = nil, 44 | activationDate: Date 45 | ) { 46 | self.transmitterID = transmitterID 47 | self.glucoseMessage = glucoseMessage 48 | self.timeMessage = timeMessage 49 | self.status = TransmitterStatus(rawValue: status) 50 | self.activationDate = activationDate 51 | 52 | sessionStartDate = activationDate.addingTimeInterval(TimeInterval(timeMessage.sessionStartTime)) 53 | sessionExpDate = activationDate.addingTimeInterval(TimeInterval(timeMessage.sessionStartTime) + (10*24*60*60)) 54 | readDate = activationDate.addingTimeInterval(TimeInterval(glucoseMessage.timestamp)) 55 | lastCalibration = calibrationMessage != nil ? Calibration(calibrationMessage: calibrationMessage!, activationDate: activationDate) : nil 56 | } 57 | 58 | // MARK: - Transmitter Info 59 | public let transmitterID: String 60 | public let status: TransmitterStatus 61 | public let activationDate: Date 62 | public let sessionStartDate: Date 63 | public let sessionExpDate: Date 64 | 65 | // MARK: - Glucose Info 66 | public let lastCalibration: Calibration? 67 | public let readDate: Date 68 | 69 | public var isDisplayOnly: Bool { 70 | return glucoseMessage.glucoseIsDisplayOnly 71 | } 72 | 73 | public var glucose: HKQuantity? { 74 | guard state.hasReliableGlucose && glucoseMessage.glucose >= 39 else { 75 | return nil 76 | } 77 | 78 | let unit = HKUnit.milligramsPerDeciliter 79 | 80 | return HKQuantity(unit: unit, doubleValue: Double(min(max(glucoseMessage.glucose, GlucoseLimits.minimum), GlucoseLimits.maximum))) 81 | } 82 | 83 | public var state: CalibrationState { 84 | return CalibrationState(rawValue: glucoseMessage.state) 85 | } 86 | 87 | public var trend: Int { 88 | return Int(glucoseMessage.trend) 89 | } 90 | 91 | public var trendRate: HKQuantity? { 92 | guard glucoseMessage.trend < Int8.max && glucoseMessage.trend > Int8.min else { 93 | return nil 94 | } 95 | 96 | let unit = HKUnit.milligramsPerDeciliterPerMinute 97 | return HKQuantity(unit: unit, doubleValue: Double(glucoseMessage.trend) / 10) 98 | } 99 | 100 | // An identifier for this reading thatʼs consistent between backfill/live data 101 | public var syncIdentifier: String { 102 | return "\(transmitterID) \(glucoseMessage.timestamp)" 103 | } 104 | } 105 | 106 | 107 | extension Glucose: Equatable { 108 | public static func ==(lhs: Glucose, rhs: Glucose) -> Bool { 109 | return lhs.glucoseMessage == rhs.glucoseMessage && lhs.syncIdentifier == rhs.syncIdentifier 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/xcshareddata/xcschemes/CGMBLEKit Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /CGMBLEKitTests/GlucoseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlucoseTests.swift 3 | // xDripG5 4 | // 5 | // Created by Nate Racklyeft on 8/6/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import HealthKit 11 | @testable import CGMBLEKit 12 | 13 | class GlucoseTests: XCTestCase { 14 | 15 | var timeMessage: TransmitterTimeRxMessage! 16 | var calendar = Calendar(identifier: .gregorian) 17 | var activationDate: Date! 18 | 19 | override func setUp() { 20 | super.setUp() 21 | 22 | let data = Data(hexadecimalString: "2500470272007cff710001000000fa1d")! 23 | timeMessage = TransmitterTimeRxMessage(data: data)! 24 | 25 | calendar.timeZone = TimeZone(identifier: "UTC")! 26 | 27 | activationDate = calendar.date(from: DateComponents(year: 2016, month: 10, day: 1))! 28 | } 29 | 30 | func testMessageData() { 31 | let data = Data(hexadecimalString: "3100680a00008a715700cc0006ffc42a")! 32 | let message = GlucoseRxMessage(data: data)! 33 | let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) 34 | 35 | XCTAssertEqual(TransmitterStatus.ok, glucose.status) 36 | XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 7, minute: 51, second: 38))!, glucose.readDate) 37 | XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 26, hour: 11, minute: 16, second: 12))!, glucose.sessionStartDate) 38 | XCTAssertFalse(glucose.isDisplayOnly) 39 | XCTAssertEqual(204, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) 40 | XCTAssertEqual(.known(.ok), glucose.state) 41 | XCTAssertEqual(-1, glucose.trend) 42 | } 43 | 44 | func testNegativeTrend() { 45 | let data = Data(hexadecimalString: "31006f0a0000be7957007a0006e4818d")! 46 | let message = GlucoseRxMessage(data: data)! 47 | let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) 48 | 49 | XCTAssertEqual(TransmitterStatus.ok, glucose.status) 50 | XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 8, minute: 26, second: 38))!, glucose.readDate) 51 | XCTAssertFalse(glucose.isDisplayOnly) 52 | XCTAssertEqual(122, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) 53 | XCTAssertEqual(.known(.ok), glucose.state) 54 | XCTAssertEqual(-28, glucose.trend) 55 | } 56 | 57 | func testDisplayOnly() { 58 | let data = Data(hexadecimalString: "3100700a0000f17a5700584006e3cee9")! 59 | let message = GlucoseRxMessage(data: data)! 60 | let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) 61 | 62 | XCTAssertEqual(TransmitterStatus.ok, glucose.status) 63 | XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 6, hour: 8, minute: 31, second: 45))!, glucose.readDate) 64 | XCTAssertTrue(glucose.isDisplayOnly) 65 | XCTAssertEqual(88, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) 66 | XCTAssertEqual(.known(.ok), glucose.state) 67 | XCTAssertEqual(-29, message.glucose.trend) 68 | } 69 | 70 | func testOldTransmitter() { 71 | let data = Data(hexadecimalString: "3100aa00000095a078008b00060a8b34")! 72 | let message = GlucoseRxMessage(data: data)! 73 | let glucose = Glucose(transmitterID: "123456", glucoseMessage: message, timeMessage: timeMessage, activationDate: activationDate) 74 | 75 | XCTAssertEqual(TransmitterStatus.ok, glucose.status) 76 | XCTAssertEqual(calendar.date(from: DateComponents(year: 2016, month: 12, day: 31, hour: 11, minute: 57, second: 09))!, glucose.readDate) // 90 days, status is still OK 77 | XCTAssertFalse(glucose.isDisplayOnly) 78 | XCTAssertEqual(139, glucose.glucose?.doubleValue(for: .milligramsPerDeciliter)) 79 | XCTAssertEqual(.known(.ok), glucose.state) 80 | XCTAssertEqual(10, message.glucose.trend) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /ResetTransmitter/ResetManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResetManager.swift 3 | // ResetTransmitter 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import CGMBLEKit 9 | import os.log 10 | 11 | 12 | class ResetManager { 13 | enum State { 14 | case initialized 15 | case resetting(transmitter: Transmitter) 16 | case completed 17 | } 18 | 19 | private(set) var state: State { 20 | get { 21 | return lockedState.value 22 | } 23 | set { 24 | let oldValue = state 25 | 26 | if case .resetting(let transmitter) = oldValue { 27 | transmitter.stayConnected = false 28 | transmitter.stopScanning() 29 | transmitter.delegate = nil 30 | transmitter.commandSource = nil 31 | } 32 | 33 | lockedState.value = newValue 34 | 35 | if case .resetting(let transmitter) = newValue { 36 | transmitter.delegate = self 37 | transmitter.commandSource = self 38 | transmitter.resumeScanning() 39 | } 40 | 41 | os_log("State changed: %{public}@ -> %{public}@", log: log, type: .debug, String(describing: oldValue), String(describing: newValue)) 42 | delegate?.resetManager(self, didChangeStateFrom: oldValue) 43 | } 44 | } 45 | private let lockedState = Locked(State.initialized) 46 | 47 | private let log = OSLog(subsystem: "com.loopkit.CGMBLEKit", category: "ResetManager") 48 | 49 | weak var delegate: ResetManagerDelegate? 50 | } 51 | 52 | 53 | protocol ResetManagerDelegate: class { 54 | func resetManager(_ manager: ResetManager, didError error: Error) 55 | 56 | func resetManager(_ manager: ResetManager, didChangeStateFrom oldState: ResetManager.State) 57 | } 58 | 59 | 60 | extension ResetManager { 61 | func cancel() { 62 | guard case .resetting = state else { 63 | return 64 | } 65 | 66 | state = .initialized 67 | } 68 | 69 | func resetTransmitter(withID id: String) { 70 | guard id.count == 6 else { 71 | return 72 | } 73 | 74 | switch state { 75 | case .initialized, .completed: 76 | break 77 | case .resetting(transmitter: let transmitter): 78 | guard transmitter.ID != id else { 79 | return 80 | } 81 | } 82 | 83 | state = .resetting(transmitter: Transmitter(id: id, passiveModeEnabled: false)) 84 | 85 | #if targetEnvironment(simulator) 86 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { 87 | self.delegate?.resetManager(self, didError: TransmitterError.controlError("Simulated Error")) 88 | 89 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { 90 | if case .resetting = self.state { 91 | self.state = .completed 92 | } 93 | } 94 | } 95 | #endif 96 | } 97 | } 98 | 99 | 100 | extension ResetManager: TransmitterDelegate { 101 | 102 | func transmitter(_ transmitter: Transmitter, didError error: Error) { 103 | os_log("Transmitter error: %{public}@", log: log, type: .error, String(describing: error)) 104 | delegate?.resetManager(self, didError: error) 105 | } 106 | 107 | func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) { 108 | // Not interested 109 | } 110 | 111 | func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) { 112 | // Not interested 113 | } 114 | 115 | func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) { 116 | // Not interested 117 | } 118 | 119 | func transmitterDidConnect(_ transmitter: Transmitter) { 120 | // Not interested 121 | } 122 | 123 | } 124 | 125 | 126 | extension ResetManager: TransmitterCommandSource { 127 | func dequeuePendingCommand(for transmitter: Transmitter) -> Command? { 128 | if case .resetting = state { 129 | return .resetTransmitter 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func transmitter(_ transmitter: Transmitter, didFail command: Command, with error: Error) { 136 | os_log("Command error: %{public}@", log: log, type: .error, String(describing: error)) 137 | delegate?.resetManager(self, didError: error) 138 | } 139 | 140 | func transmitter(_ transmitter: Transmitter, didComplete command: Command) { 141 | if case .resetTransmitter = command { 142 | state = .completed 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CGMBLEKitUI/TransmitterIDSetupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransmitterIDSetupViewController.swift 3 | // CGMBLEKitUI 4 | // 5 | // Copyright © 2018 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import LoopKit 10 | import LoopKitUI 11 | import CGMBLEKit 12 | import ShareClient 13 | 14 | class TransmitterIDSetupViewController: SetupTableViewController { 15 | 16 | lazy private(set) var shareManager = ShareClientManager() 17 | 18 | private func updateShareUsername() { 19 | shareUsernameLabel.text = shareManager.shareService.username ?? SettingsTableViewCell.TapToSetString 20 | } 21 | 22 | private(set) var transmitterID: String? { 23 | get { 24 | return transmitterIDTextField.text 25 | } 26 | set { 27 | transmitterIDTextField.text = newValue 28 | } 29 | } 30 | 31 | private func updateStateForSettings() { 32 | let isReadyToRead = transmitterID?.count == 6 33 | 34 | if isReadyToRead { 35 | continueState = .completed 36 | } else { 37 | continueState = .inputSettings 38 | } 39 | } 40 | 41 | private enum State { 42 | case loadingView 43 | case inputSettings 44 | case completed 45 | } 46 | 47 | private var continueState: State = .loadingView { 48 | didSet { 49 | switch continueState { 50 | case .loadingView: 51 | updateStateForSettings() 52 | case .inputSettings: 53 | footerView.primaryButton.isEnabled = false 54 | case .completed: 55 | footerView.primaryButton.isEnabled = true 56 | } 57 | } 58 | } 59 | 60 | override func continueButtonPressed(_ sender: Any) { 61 | if continueState == .completed, 62 | let setupViewController = navigationController as? TransmitterSetupViewController, 63 | let transmitterID = transmitterID 64 | { 65 | setupViewController.completeSetup(state: TransmitterManagerState(transmitterID: transmitterID)) 66 | } 67 | } 68 | 69 | override func cancelButtonPressed(_ sender: Any) { 70 | if transmitterIDTextField.isFirstResponder { 71 | transmitterIDTextField.resignFirstResponder() 72 | } else { 73 | super.cancelButtonPressed(sender) 74 | } 75 | } 76 | 77 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { 78 | return continueState == .completed 79 | } 80 | 81 | // MARK: - 82 | 83 | @IBOutlet private var shareUsernameLabel: UILabel! 84 | 85 | @IBOutlet private var transmitterIDTextField: UITextField! 86 | 87 | override func viewDidLoad() { 88 | super.viewDidLoad() 89 | 90 | updateShareUsername() 91 | 92 | continueState = .inputSettings 93 | } 94 | 95 | // MARK: - UITableViewDelegate 96 | 97 | private enum Section: Int { 98 | case transmitterID 99 | case share 100 | } 101 | 102 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 103 | switch Section(rawValue: indexPath.section)! { 104 | case .transmitterID: 105 | tableView.deselectRow(at: indexPath, animated: false) 106 | case .share: 107 | let authVC = AuthenticationViewController(authentication: shareManager.shareService) 108 | authVC.authenticationObserver = { [weak self] (service) in 109 | self?.shareManager.shareService = service 110 | self?.updateShareUsername() 111 | } 112 | 113 | show(authVC, sender: nil) 114 | } 115 | } 116 | } 117 | 118 | 119 | extension TransmitterIDSetupViewController: UITextFieldDelegate { 120 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 121 | guard let text = textField.text, let stringRange = Range(range, in: text) else { 122 | updateStateForSettings() 123 | return true 124 | } 125 | 126 | let newText = text.replacingCharacters(in: stringRange, with: string) 127 | 128 | if newText.count >= 6 { 129 | if newText.count == 6 { 130 | textField.text = newText 131 | textField.resignFirstResponder() 132 | } 133 | 134 | updateStateForSettings() 135 | return false 136 | } 137 | 138 | textField.text = newText 139 | updateStateForSettings() 140 | return false 141 | } 142 | 143 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 144 | return true 145 | } 146 | 147 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 148 | textField.resignFirstResponder() 149 | return true 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /ResetTransmitter/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@2x-1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@2x-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-40x40@1x.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@2x-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "ItunesArtwork@2x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "Icon-24@2x.png", 113 | "idiom" : "watch", 114 | "role" : "notificationCenter", 115 | "scale" : "2x", 116 | "size" : "24x24", 117 | "subtype" : "38mm" 118 | }, 119 | { 120 | "filename" : "Icon-27.5@2x.png", 121 | "idiom" : "watch", 122 | "role" : "notificationCenter", 123 | "scale" : "2x", 124 | "size" : "27.5x27.5", 125 | "subtype" : "42mm" 126 | }, 127 | { 128 | "filename" : "Icon-29@2x.png", 129 | "idiom" : "watch", 130 | "role" : "companionSettings", 131 | "scale" : "2x", 132 | "size" : "29x29" 133 | }, 134 | { 135 | "filename" : "Icon-29@3x.png", 136 | "idiom" : "watch", 137 | "role" : "companionSettings", 138 | "scale" : "3x", 139 | "size" : "29x29" 140 | }, 141 | { 142 | "filename" : "Icon-40@2x.png", 143 | "idiom" : "watch", 144 | "role" : "appLauncher", 145 | "scale" : "2x", 146 | "size" : "40x40", 147 | "subtype" : "38mm" 148 | }, 149 | { 150 | "filename" : "Icon-44@2x.png", 151 | "idiom" : "watch", 152 | "role" : "appLauncher", 153 | "scale" : "2x", 154 | "size" : "44x44", 155 | "subtype" : "40mm" 156 | }, 157 | { 158 | "idiom" : "watch", 159 | "role" : "appLauncher", 160 | "scale" : "2x", 161 | "size" : "50x50", 162 | "subtype" : "44mm" 163 | }, 164 | { 165 | "filename" : "Icon-86@2x.png", 166 | "idiom" : "watch", 167 | "role" : "quickLook", 168 | "scale" : "2x", 169 | "size" : "86x86", 170 | "subtype" : "38mm" 171 | }, 172 | { 173 | "filename" : "Icon-98@2x.png", 174 | "idiom" : "watch", 175 | "role" : "quickLook", 176 | "scale" : "2x", 177 | "size" : "98x98", 178 | "subtype" : "42mm" 179 | }, 180 | { 181 | "idiom" : "watch", 182 | "role" : "quickLook", 183 | "scale" : "2x", 184 | "size" : "108x108", 185 | "subtype" : "44mm" 186 | }, 187 | { 188 | "idiom" : "watch-marketing", 189 | "scale" : "1x", 190 | "size" : "1024x1024" 191 | } 192 | ], 193 | "info" : { 194 | "author" : "xcode", 195 | "version" : 1 196 | }, 197 | "properties" : { 198 | "pre-rendered" : true 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /CGMBLEKit/PeripheralManager+G5.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralManager+G5.swift 3 | // xDripG5 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import CoreBluetooth 9 | import os.log 10 | 11 | 12 | private let log = OSLog(category: "PeripheralManager+G5") 13 | 14 | 15 | extension PeripheralManager { 16 | private func getCharacteristicWithUUID(_ uuid: CGMServiceCharacteristicUUID) -> CBCharacteristic? { 17 | return peripheral.getCharacteristicWithUUID(uuid) 18 | } 19 | 20 | func setNotifyValue(_ enabled: Bool, 21 | for characteristicUUID: CGMServiceCharacteristicUUID, 22 | timeout: TimeInterval = 2) throws 23 | { 24 | guard let characteristic = getCharacteristicWithUUID(characteristicUUID) else { 25 | throw PeripheralManagerError.unknownCharacteristic 26 | } 27 | 28 | try setNotifyValue(enabled, for: characteristic, timeout: timeout) 29 | } 30 | 31 | func readMessage( 32 | for characteristicUUID: CGMServiceCharacteristicUUID, 33 | timeout: TimeInterval = 2 34 | ) throws -> R 35 | { 36 | guard let characteristic = getCharacteristicWithUUID(characteristicUUID) else { 37 | throw PeripheralManagerError.unknownCharacteristic 38 | } 39 | 40 | var capturedResponse: R? 41 | 42 | try runCommand(timeout: timeout) { 43 | addCondition(.valueUpdate(characteristic: characteristic, matching: { (data) -> Bool in 44 | guard let value = data else { 45 | return false 46 | } 47 | 48 | guard let response = R(data: value) else { 49 | // We don't recognize the contents. Keep listening. 50 | return false 51 | } 52 | 53 | capturedResponse = response 54 | return true 55 | })) 56 | 57 | peripheral.readValue(for: characteristic) 58 | } 59 | 60 | guard let response = capturedResponse else { 61 | // TODO: This is an "unknown value" issue, not a timeout 62 | if let value = characteristic.value { 63 | log.error("Unknown response data: %{public}@", value.hexadecimalString) 64 | } 65 | throw PeripheralManagerError.timeout 66 | } 67 | 68 | return response 69 | } 70 | 71 | /// - Throws: PeripheralManagerError 72 | func writeMessage(_ message: T, 73 | for characteristicUUID: CGMServiceCharacteristicUUID, 74 | type: CBCharacteristicWriteType = .withResponse, 75 | timeout: TimeInterval = 2 76 | ) throws -> T.Response 77 | { 78 | guard let characteristic = getCharacteristicWithUUID(characteristicUUID) else { 79 | throw PeripheralManagerError.unknownCharacteristic 80 | } 81 | 82 | var capturedResponse: T.Response? 83 | 84 | try runCommand(timeout: timeout) { 85 | if case .withResponse = type { 86 | addCondition(.write(characteristic: characteristic)) 87 | } 88 | 89 | if characteristic.isNotifying { 90 | addCondition(.valueUpdate(characteristic: characteristic, matching: { (data) -> Bool in 91 | guard let value = data else { 92 | return false 93 | } 94 | 95 | guard let response = T.Response(data: value) else { 96 | // We don't recognize the contents. Keep listening. 97 | return false 98 | } 99 | 100 | capturedResponse = response 101 | return true 102 | })) 103 | } 104 | 105 | peripheral.writeValue(message.data, for: characteristic, type: type) 106 | } 107 | 108 | guard let response = capturedResponse else { 109 | // TODO: This is an "unknown value" issue, not a timeout 110 | if let value = characteristic.value { 111 | log.error("Unknown response data: %{public}@", value.hexadecimalString) 112 | } 113 | throw PeripheralManagerError.timeout 114 | } 115 | 116 | return response 117 | } 118 | 119 | /// - Throws: PeripheralManagerError 120 | func writeMessage(_ message: TransmitterTxMessage, 121 | for characteristicUUID: CGMServiceCharacteristicUUID, 122 | type: CBCharacteristicWriteType = .withResponse, 123 | timeout: TimeInterval = 2) throws 124 | { 125 | guard let characteristic = getCharacteristicWithUUID(characteristicUUID) else { 126 | throw PeripheralManagerError.unknownCharacteristic 127 | } 128 | 129 | try writeValue(message.data, for: characteristic, type: type, timeout: timeout) 130 | } 131 | } 132 | 133 | 134 | fileprivate extension CBPeripheral { 135 | func getServiceWithUUID(_ uuid: TransmitterServiceUUID) -> CBService? { 136 | return services?.itemWithUUIDString(uuid.rawValue) 137 | } 138 | 139 | func getCharacteristicForServiceUUID(_ serviceUUID: TransmitterServiceUUID, withUUIDString UUIDString: String) -> CBCharacteristic? { 140 | guard let characteristics = getServiceWithUUID(serviceUUID)?.characteristics else { 141 | return nil 142 | } 143 | 144 | return characteristics.itemWithUUIDString(UUIDString) 145 | } 146 | 147 | func getCharacteristicWithUUID(_ uuid: CGMServiceCharacteristicUUID) -> CBCharacteristic? { 148 | return getCharacteristicForServiceUUID(.cgmService, withUUIDString: uuid.rawValue) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /CGMBLEKit Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 10/1/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CGMBLEKit 11 | import CoreBluetooth 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate, TransmitterDelegate, TransmitterCommandSource { 15 | 16 | var window: UIWindow? 17 | 18 | static var sharedDelegate: AppDelegate { 19 | return UIApplication.shared.delegate as! AppDelegate 20 | } 21 | 22 | var transmitterID: String? { 23 | didSet { 24 | if let id = transmitterID { 25 | transmitter = Transmitter( 26 | id: id, 27 | passiveModeEnabled: UserDefaults.standard.passiveModeEnabled 28 | ) 29 | transmitter?.stayConnected = UserDefaults.standard.stayConnected 30 | transmitter?.delegate = self 31 | transmitter?.commandSource = self 32 | 33 | UserDefaults.standard.transmitterID = id 34 | } 35 | glucose = nil 36 | } 37 | } 38 | 39 | var transmitter: Transmitter? 40 | 41 | let commandQueue = CommandQueue() 42 | 43 | var glucose: Glucose? 44 | 45 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 46 | 47 | transmitterID = UserDefaults.standard.transmitterID 48 | 49 | return true 50 | } 51 | 52 | func applicationWillResignActive(_ application: UIApplication) { 53 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 54 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 55 | } 56 | 57 | func applicationDidEnterBackground(_ application: UIApplication) { 58 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 59 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 60 | 61 | if let transmitter = transmitter, !transmitter.stayConnected { 62 | transmitter.stopScanning() 63 | } 64 | } 65 | 66 | func applicationWillEnterForeground(_ application: UIApplication) { 67 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 68 | } 69 | 70 | func applicationDidBecomeActive(_ application: UIApplication) { 71 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 72 | 73 | transmitter?.resumeScanning() 74 | } 75 | 76 | func applicationWillTerminate(_ application: UIApplication) { 77 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 78 | } 79 | 80 | // MARK: - TransmitterDelegate 81 | 82 | private let dateFormatter: DateFormatter = { 83 | let dateFormatter = DateFormatter() 84 | 85 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 86 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 87 | 88 | return dateFormatter 89 | }() 90 | 91 | func dequeuePendingCommand(for transmitter: Transmitter) -> Command? { 92 | return commandQueue.dequeue() 93 | } 94 | 95 | func transmitter(_ transmitter: Transmitter, didFail command: Command, with error: Error) { 96 | // TODO: implement 97 | } 98 | 99 | func transmitter(_ transmitter: Transmitter, didComplete command: Command) { 100 | // TODO: implement 101 | } 102 | 103 | func transmitter(_ transmitter: Transmitter, didError error: Error) { 104 | DispatchQueue.main.async { 105 | if let vc = self.window?.rootViewController as? TransmitterDelegate { 106 | vc.transmitter(transmitter, didError: error) 107 | } 108 | } 109 | } 110 | 111 | func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) { 112 | self.glucose = glucose 113 | DispatchQueue.main.async { 114 | if let vc = self.window?.rootViewController as? TransmitterDelegate { 115 | vc.transmitter(transmitter, didRead: glucose) 116 | } 117 | } 118 | } 119 | 120 | func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) { 121 | DispatchQueue.main.async { 122 | if let vc = self.window?.rootViewController as? TransmitterDelegate { 123 | vc.transmitter(transmitter, didReadUnknownData: data) 124 | } 125 | } 126 | } 127 | 128 | func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) { 129 | DispatchQueue.main.async { 130 | if let vc = self.window?.rootViewController as? TransmitterDelegate { 131 | vc.transmitter(transmitter, didReadBackfill: glucose) 132 | } 133 | } 134 | } 135 | 136 | func transmitterDidConnect(_ transmitter: Transmitter) { 137 | // Ignore 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /CGMBLEKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 79 | 80 | 81 | 82 | 84 | 90 | 91 | 92 | 93 | 94 | 104 | 105 | 111 | 112 | 113 | 114 | 120 | 121 | 127 | 128 | 129 | 130 | 132 | 133 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /CGMBLEKit Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // xDrip5 4 | // 5 | // Created by Nathan Racklyeft on 10/1/15. 6 | // Copyright © 2015 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import HealthKit 11 | import CGMBLEKit 12 | 13 | class ViewController: UIViewController, TransmitterDelegate, UITextFieldDelegate { 14 | 15 | @IBOutlet weak var titleLabel: UILabel! 16 | 17 | @IBOutlet weak var subtitleLabel: UILabel! 18 | 19 | @IBOutlet weak var passiveModeEnabledSwitch: UISwitch! 20 | 21 | @IBOutlet weak var stayConnectedSwitch: UISwitch! 22 | 23 | @IBOutlet weak var transmitterIDField: UITextField! 24 | 25 | @IBOutlet weak var scanningIndicatorView: UIActivityIndicatorView! 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | passiveModeEnabledSwitch.isOn = AppDelegate.sharedDelegate.transmitter?.passiveModeEnabled ?? false 31 | 32 | stayConnectedSwitch.isOn = AppDelegate.sharedDelegate.transmitter?.stayConnected ?? false 33 | 34 | transmitterIDField.text = AppDelegate.sharedDelegate.transmitter?.ID 35 | } 36 | 37 | override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | 40 | updateIndicatorViewDisplay() 41 | } 42 | 43 | // MARK: - Actions 44 | 45 | func updateIndicatorViewDisplay() { 46 | if let transmitter = AppDelegate.sharedDelegate.transmitter, transmitter.isScanning { 47 | scanningIndicatorView.startAnimating() 48 | } else { 49 | scanningIndicatorView.stopAnimating() 50 | } 51 | } 52 | 53 | @IBAction func toggleStayConnected(_ sender: UISwitch) { 54 | AppDelegate.sharedDelegate.transmitter?.stayConnected = sender.isOn 55 | UserDefaults.standard.stayConnected = sender.isOn 56 | 57 | updateIndicatorViewDisplay() 58 | } 59 | 60 | @IBAction func togglePassiveMode(_ sender: UISwitch) { 61 | AppDelegate.sharedDelegate.transmitter?.passiveModeEnabled = sender.isOn 62 | UserDefaults.standard.passiveModeEnabled = sender.isOn 63 | } 64 | 65 | @IBAction func start(_ sender: UIButton) { 66 | let dialog = UIAlertController(title: "Confirm", message: "Start sensor session.", preferredStyle: .alert) 67 | 68 | dialog.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!) in 69 | AppDelegate.sharedDelegate.commandQueue.enqueue(.startSensor(at: Date())) 70 | })) 71 | 72 | dialog.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 73 | 74 | present(dialog, animated: true, completion: nil) 75 | } 76 | 77 | @IBAction func calibrate(_ sender: UIButton) { 78 | let dialog = UIAlertController(title: "Enter BG", message: "Calibrate sensor.", preferredStyle: .alert) 79 | 80 | let unit = HKUnit.milligramsPerDeciliter 81 | 82 | dialog.addTextField { (textField : UITextField!) in 83 | textField.placeholder = unit.unitString 84 | textField.keyboardType = .numberPad 85 | } 86 | 87 | dialog.addAction(UIAlertAction(title: "Calibrate", style: .default, handler: { (action: UIAlertAction!) in 88 | let textField = dialog.textFields![0] as UITextField 89 | let minGlucose = HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: 40) 90 | let maxGlucose = HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: 400) 91 | 92 | if let text = textField.text, let entry = Double(text) { 93 | guard entry >= minGlucose.doubleValue(for: unit) && entry <= maxGlucose.doubleValue(for: unit) else { 94 | // TODO: notify the user if the glucose is not in range 95 | return 96 | } 97 | let glucose = HKQuantity(unit: unit, doubleValue: Double(entry)) 98 | AppDelegate.sharedDelegate.commandQueue.enqueue(.calibrateSensor(to: glucose, at: Date())) 99 | } 100 | })) 101 | 102 | dialog.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 103 | 104 | present(dialog, animated: true, completion: nil) 105 | } 106 | 107 | @IBAction func stop(_ sender: UIButton) { 108 | let dialog = UIAlertController(title: "Confirm", message: "Stop sensor session.", preferredStyle: .alert) 109 | 110 | dialog.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!) in 111 | AppDelegate.sharedDelegate.commandQueue.enqueue(.stopSensor(at: Date())) 112 | })) 113 | 114 | dialog.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 115 | 116 | present(dialog, animated: true, completion: nil) 117 | } 118 | 119 | // MARK: - UITextFieldDelegate 120 | 121 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 122 | if let text = textField.text { 123 | let newString = text.replacingCharacters(in: range.rangeOfString(text), with: string) 124 | 125 | if newString.count > 6 { 126 | return false 127 | } else if newString.count == 6 { 128 | AppDelegate.sharedDelegate.transmitterID = newString 129 | textField.text = newString 130 | 131 | textField.resignFirstResponder() 132 | 133 | return false 134 | } 135 | } 136 | 137 | return true 138 | } 139 | 140 | func textFieldDidEndEditing(_ textField: UITextField) { 141 | if textField.text?.count != 6 { 142 | textField.text = UserDefaults.standard.transmitterID 143 | } 144 | } 145 | 146 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 147 | return true 148 | } 149 | 150 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 151 | return true 152 | } 153 | 154 | // MARK: - TransmitterDelegate 155 | 156 | func transmitter(_ transmitter: Transmitter, didError error: Error) { 157 | print("Transmitter Error: \(error)") 158 | titleLabel.text = NSLocalizedString("Error", comment: "Title displayed during error response") 159 | 160 | subtitleLabel.text = "\(error)" 161 | } 162 | 163 | func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) { 164 | let unit = HKUnit.milligramsPerDeciliter 165 | if let value = glucose.glucose?.doubleValue(for: unit) { 166 | titleLabel.text = "\(value) \(unit.unitString)" 167 | } else { 168 | titleLabel.text = String(describing: glucose.state) 169 | } 170 | 171 | 172 | let date = glucose.readDate 173 | subtitleLabel.text = DateFormatter.localizedString(from: date, dateStyle: .none, timeStyle: .long) 174 | } 175 | 176 | func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) { 177 | titleLabel.text = NSLocalizedString("Unknown Data", comment: "Title displayed during unknown data response") 178 | subtitleLabel.text = data.hexadecimalString 179 | } 180 | 181 | func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) { 182 | titleLabel.text = NSLocalizedString("Backfill", comment: "Title displayed during backfill response") 183 | subtitleLabel.text = String(describing: glucose.map { $0.glucose }) 184 | } 185 | 186 | func transmitterDidConnect(_ transmitter: Transmitter) { 187 | // Ignore 188 | } 189 | 190 | } 191 | 192 | 193 | private extension NSRange { 194 | func rangeOfString(_ string: String) -> Range { 195 | let startIndex = string.index(string.startIndex, offsetBy: location) 196 | let endIndex = string.index(startIndex, offsetBy: length) 197 | return startIndex.. CGFloat { 99 | // Update the constraint once to fit the height of the screen 100 | if indexPath.section == tableView.numberOfSections - 1 && needsButtonTopSpaceUpdate { 101 | needsButtonTopSpaceUpdate = false 102 | let currentValue = buttonTopSpace.constant 103 | let suggestedValue = max(0, tableView.bounds.size.height - tableView.contentSize.height - tableView.safeAreaInsets.bottom - tableView.safeAreaInsets.top) 104 | 105 | if abs(currentValue - suggestedValue) > .ulpOfOne { 106 | buttonTopSpace.constant = suggestedValue 107 | } 108 | } 109 | 110 | return UITableView.automaticDimension 111 | } 112 | 113 | override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 114 | return false 115 | } 116 | 117 | override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 118 | return nil 119 | } 120 | 121 | // MARK: - Actions 122 | 123 | @IBAction func performAction(_ sender: Any) { 124 | switch state { 125 | case .empty, .needsConfiguration: 126 | // Actions are not allowed 127 | break 128 | case .configured: 129 | // Begin reset 130 | resetTransmitter(withID: transmitterIDField.text ?? "") 131 | case .resetting: 132 | // Cancel pending reset 133 | resetManager.cancel() 134 | case .completed: 135 | // Ignore actions here 136 | break 137 | } 138 | } 139 | 140 | private func resetTransmitter(withID id: String) { 141 | let controller = UIAlertController( 142 | title: NSLocalizedString("Are you sure you want to reset this transmitter?", comment: "Title of the reset confirmation sheet"), 143 | message: NSLocalizedString("It will take up to 10 minutes to complete.", comment: "Message of the reset confirmation sheet"), preferredStyle: .actionSheet 144 | ) 145 | 146 | controller.addAction(UIAlertAction( 147 | title: NSLocalizedString("Reset", comment: "Reset button title"), 148 | style: .destructive, 149 | handler: { (action) in 150 | self.resetManager.resetTransmitter(withID: id) 151 | } 152 | )) 153 | 154 | controller.addAction(UIAlertAction( 155 | title: NSLocalizedString("Cancel", comment: "Title of button to cancel reset"), 156 | style: .cancel, 157 | handler: nil 158 | )) 159 | 160 | present(controller, animated: true, completion: nil) 161 | } 162 | } 163 | 164 | 165 | // MARK: - UI state management 166 | extension ResetViewController { 167 | private func updateButtonState() { 168 | switch state { 169 | case .empty, .needsConfiguration: 170 | resetButton.isEnabled = false 171 | case .configured, .resetting, .completed: 172 | resetButton.isEnabled = true 173 | } 174 | 175 | switch state { 176 | case .empty, .needsConfiguration, .configured: 177 | resetButton.setTitle(NSLocalizedString("Reset", comment: "Title of button to begin reset"), for: .normal) 178 | resetButton.tintColor = .red 179 | case .resetting, .completed: 180 | resetButton.setTitle(NSLocalizedString("Cancel", comment: "Title of button to cancel reset"), for: .normal) 181 | resetButton.tintColor = .darkGray 182 | } 183 | } 184 | 185 | private func updateTransmitterIDFieldState() { 186 | switch state { 187 | case .empty, .needsConfiguration: 188 | transmitterIDField.text = "" 189 | transmitterIDField.isEnabled = true 190 | case .configured: 191 | transmitterIDField.isEnabled = true 192 | case .resetting, .completed: 193 | transmitterIDField.isEnabled = false 194 | } 195 | } 196 | 197 | private func updateStatusIndicatorState() { 198 | switch self.state { 199 | case .empty, .needsConfiguration, .configured, .completed: 200 | self.spinner.stopAnimating() 201 | self.errorLabel.superview?.isHidden = true 202 | case .resetting: 203 | self.spinner.startAnimating() 204 | if let error = lastError { 205 | self.errorLabel.text = String(describing: error) 206 | } 207 | self.errorLabel.superview?.isHidden = 208 | (self.lastError == nil) 209 | } 210 | } 211 | } 212 | 213 | 214 | extension ResetViewController: ResetManagerDelegate { 215 | func resetManager(_ manager: ResetManager, didError error: Error) { 216 | DispatchQueue.main.async { 217 | self.lastError = error 218 | self.updateStatusIndicatorState() 219 | } 220 | } 221 | 222 | func resetManager(_ manager: ResetManager, didChangeStateFrom oldState: ResetManager.State) { 223 | DispatchQueue.main.async { 224 | switch manager.state { 225 | case .initialized: 226 | self.state = .configured 227 | case .resetting: 228 | self.state = .resetting 229 | case .completed: 230 | self.state = .completed 231 | } 232 | } 233 | } 234 | } 235 | 236 | extension ResetViewController: UINavigationControllerDelegate { 237 | func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask { 238 | return .portrait 239 | } 240 | } 241 | 242 | extension ResetViewController: UITextFieldDelegate { 243 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 244 | textField.resignFirstResponder() 245 | return false 246 | } 247 | 248 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 249 | guard let text = textField.text, let stringRange = Range(range, in: text) else { 250 | state = .needsConfiguration 251 | return true 252 | } 253 | 254 | let newText = text.replacingCharacters(in: stringRange, with: string) 255 | 256 | if newText.count >= 6 { 257 | if newText.count == 6 { 258 | textField.text = newText 259 | textField.resignFirstResponder() 260 | } 261 | 262 | state = .configured 263 | return false 264 | } 265 | 266 | state = .needsConfiguration 267 | return true 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /CGMBLEKit Example/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Backfill" : { 5 | "comment" : "Title displayed during backfill response", 6 | "extractionState" : "manual", 7 | "localizations" : { 8 | "da" : { 9 | "stringUnit" : { 10 | "state" : "translated", 11 | "value" : "Backfill" 12 | } 13 | }, 14 | "de" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "Auffüllen" 18 | } 19 | }, 20 | "en" : { 21 | "stringUnit" : { 22 | "state" : "translated", 23 | "value" : "Backfill" 24 | } 25 | }, 26 | "es" : { 27 | "stringUnit" : { 28 | "state" : "translated", 29 | "value" : "Rellenar" 30 | } 31 | }, 32 | "fi" : { 33 | "stringUnit" : { 34 | "state" : "translated", 35 | "value" : "Täyttö" 36 | } 37 | }, 38 | "fr" : { 39 | "stringUnit" : { 40 | "state" : "translated", 41 | "value" : "Récupération des données antérieures" 42 | } 43 | }, 44 | "he" : { 45 | "stringUnit" : { 46 | "state" : "translated", 47 | "value" : "מילוי לאחור" 48 | } 49 | }, 50 | "it" : { 51 | "stringUnit" : { 52 | "state" : "translated", 53 | "value" : "Riempimento" 54 | } 55 | }, 56 | "ja" : { 57 | "stringUnit" : { 58 | "state" : "translated", 59 | "value" : "埋め戻し" 60 | } 61 | }, 62 | "nb" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "Hent historikk" 66 | } 67 | }, 68 | "nl" : { 69 | "stringUnit" : { 70 | "state" : "translated", 71 | "value" : "Aanvullen" 72 | } 73 | }, 74 | "pl" : { 75 | "stringUnit" : { 76 | "state" : "translated", 77 | "value" : "Backfill" 78 | } 79 | }, 80 | "pt-BR" : { 81 | "stringUnit" : { 82 | "state" : "translated", 83 | "value" : "Preenchimento" 84 | } 85 | }, 86 | "ro" : { 87 | "stringUnit" : { 88 | "state" : "translated", 89 | "value" : "Completare retroactivă" 90 | } 91 | }, 92 | "ru" : { 93 | "stringUnit" : { 94 | "state" : "translated", 95 | "value" : "Заполнение данными" 96 | } 97 | }, 98 | "sv" : { 99 | "stringUnit" : { 100 | "state" : "translated", 101 | "value" : "Fyller i historisk data" 102 | } 103 | }, 104 | "tr" : { 105 | "stringUnit" : { 106 | "state" : "translated", 107 | "value" : "Dolgu" 108 | } 109 | }, 110 | "vi" : { 111 | "stringUnit" : { 112 | "state" : "translated", 113 | "value" : "Lấp đầy" 114 | } 115 | }, 116 | "zh-Hans" : { 117 | "stringUnit" : { 118 | "state" : "translated", 119 | "value" : "数据回填" 120 | } 121 | } 122 | } 123 | }, 124 | "Error" : { 125 | "comment" : "Title displayed during error response", 126 | "extractionState" : "manual", 127 | "localizations" : { 128 | "da" : { 129 | "stringUnit" : { 130 | "state" : "translated", 131 | "value" : "Fejl" 132 | } 133 | }, 134 | "de" : { 135 | "stringUnit" : { 136 | "state" : "translated", 137 | "value" : "Fehler" 138 | } 139 | }, 140 | "en" : { 141 | "stringUnit" : { 142 | "state" : "translated", 143 | "value" : "Error" 144 | } 145 | }, 146 | "es" : { 147 | "stringUnit" : { 148 | "state" : "translated", 149 | "value" : "Error" 150 | } 151 | }, 152 | "fi" : { 153 | "stringUnit" : { 154 | "state" : "translated", 155 | "value" : "Virhe" 156 | } 157 | }, 158 | "fr" : { 159 | "stringUnit" : { 160 | "state" : "translated", 161 | "value" : "Erreur" 162 | } 163 | }, 164 | "he" : { 165 | "stringUnit" : { 166 | "state" : "translated", 167 | "value" : "שגיאה" 168 | } 169 | }, 170 | "it" : { 171 | "stringUnit" : { 172 | "state" : "translated", 173 | "value" : "Errore" 174 | } 175 | }, 176 | "ja" : { 177 | "stringUnit" : { 178 | "state" : "translated", 179 | "value" : "エラー" 180 | } 181 | }, 182 | "nb" : { 183 | "stringUnit" : { 184 | "state" : "translated", 185 | "value" : "Feil" 186 | } 187 | }, 188 | "nl" : { 189 | "stringUnit" : { 190 | "state" : "translated", 191 | "value" : "Fout" 192 | } 193 | }, 194 | "pl" : { 195 | "stringUnit" : { 196 | "state" : "translated", 197 | "value" : "Błąd" 198 | } 199 | }, 200 | "pt-BR" : { 201 | "stringUnit" : { 202 | "state" : "translated", 203 | "value" : "Erro" 204 | } 205 | }, 206 | "ro" : { 207 | "stringUnit" : { 208 | "state" : "translated", 209 | "value" : "Eroare" 210 | } 211 | }, 212 | "ru" : { 213 | "stringUnit" : { 214 | "state" : "translated", 215 | "value" : "Ошибка" 216 | } 217 | }, 218 | "sk" : { 219 | "stringUnit" : { 220 | "state" : "translated", 221 | "value" : "Chyba" 222 | } 223 | }, 224 | "sv" : { 225 | "stringUnit" : { 226 | "state" : "translated", 227 | "value" : "Fel" 228 | } 229 | }, 230 | "tr" : { 231 | "stringUnit" : { 232 | "state" : "translated", 233 | "value" : "Hata" 234 | } 235 | }, 236 | "vi" : { 237 | "stringUnit" : { 238 | "state" : "translated", 239 | "value" : "Lỗi" 240 | } 241 | }, 242 | "zh-Hans" : { 243 | "stringUnit" : { 244 | "state" : "translated", 245 | "value" : "错误" 246 | } 247 | } 248 | } 249 | }, 250 | "Unknown Data" : { 251 | "comment" : "Title displayed during unknown data response", 252 | "extractionState" : "manual", 253 | "localizations" : { 254 | "da" : { 255 | "stringUnit" : { 256 | "state" : "translated", 257 | "value" : "Ukendte data" 258 | } 259 | }, 260 | "de" : { 261 | "stringUnit" : { 262 | "state" : "translated", 263 | "value" : "Unbekannte Daten" 264 | } 265 | }, 266 | "en" : { 267 | "stringUnit" : { 268 | "state" : "translated", 269 | "value" : "Unknown Data" 270 | } 271 | }, 272 | "es" : { 273 | "stringUnit" : { 274 | "state" : "translated", 275 | "value" : "Datos desconocidos" 276 | } 277 | }, 278 | "fi" : { 279 | "stringUnit" : { 280 | "state" : "translated", 281 | "value" : "Tuntematon tieto" 282 | } 283 | }, 284 | "fr" : { 285 | "stringUnit" : { 286 | "state" : "translated", 287 | "value" : "Donnée inconnue" 288 | } 289 | }, 290 | "he" : { 291 | "stringUnit" : { 292 | "state" : "translated", 293 | "value" : "מידע לא ידוע" 294 | } 295 | }, 296 | "it" : { 297 | "stringUnit" : { 298 | "state" : "translated", 299 | "value" : "Dati sconosciuti" 300 | } 301 | }, 302 | "ja" : { 303 | "stringUnit" : { 304 | "state" : "translated", 305 | "value" : "不明なデータ" 306 | } 307 | }, 308 | "nb" : { 309 | "stringUnit" : { 310 | "state" : "translated", 311 | "value" : "Ukjent data" 312 | } 313 | }, 314 | "nl" : { 315 | "stringUnit" : { 316 | "state" : "translated", 317 | "value" : "Onbekende Gegevens" 318 | } 319 | }, 320 | "pl" : { 321 | "stringUnit" : { 322 | "state" : "translated", 323 | "value" : "Unknown Data" 324 | } 325 | }, 326 | "pt-BR" : { 327 | "stringUnit" : { 328 | "state" : "translated", 329 | "value" : "Dados Desconhecidos" 330 | } 331 | }, 332 | "ro" : { 333 | "stringUnit" : { 334 | "state" : "translated", 335 | "value" : "Date necunoscute" 336 | } 337 | }, 338 | "ru" : { 339 | "stringUnit" : { 340 | "state" : "translated", 341 | "value" : "Неизвестные данные" 342 | } 343 | }, 344 | "sk" : { 345 | "stringUnit" : { 346 | "state" : "translated", 347 | "value" : "Neznáme dáta" 348 | } 349 | }, 350 | "sv" : { 351 | "stringUnit" : { 352 | "state" : "translated", 353 | "value" : "Okänd data" 354 | } 355 | }, 356 | "tr" : { 357 | "stringUnit" : { 358 | "state" : "translated", 359 | "value" : "Bilinmeyen Veri" 360 | } 361 | }, 362 | "vi" : { 363 | "stringUnit" : { 364 | "state" : "translated", 365 | "value" : "Không nhận biết dữ liệu" 366 | } 367 | }, 368 | "zh-Hans" : { 369 | "stringUnit" : { 370 | "state" : "translated", 371 | "value" : "未知数据" 372 | } 373 | } 374 | } 375 | } 376 | }, 377 | "version" : "1.0" 378 | } -------------------------------------------------------------------------------- /CGMBLEKitUI/Base.lproj/TransmitterManagerSetup.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | The transmitter ID can be found printed on the back of the device, on the side of the box it came in, and from within the settings menus of the receiver and mobile app. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | --------------------------------------------------------------------------------