├── 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 | [](https://travis-ci.org/LoopKit/CGMBLEKit)
4 | [](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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------