├── iOS Example
├── iOS Example
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── UI
│ │ ├── Devices
│ │ │ ├── Device.swift
│ │ │ ├── DevicesViewModel.swift
│ │ │ ├── DeviceListView.swift
│ │ │ └── DevicesView.swift
│ │ └── DeviceDetails
│ │ │ ├── ConnectionStatus.swift
│ │ │ ├── DeviceDetailsViewModel.swift
│ │ │ └── DeviceDetailsView.swift
│ ├── Util
│ │ └── Cancellable.swift
│ ├── Bluetooth
│ │ ├── Connection
│ │ │ ├── CBConnectionState.swift
│ │ │ ├── CBConnectionFactory.swift
│ │ │ ├── CBConnectionStateListener.swift
│ │ │ ├── CBConnectedDevice.swift
│ │ │ └── CBConnection.swift
│ │ ├── Discovery
│ │ │ ├── CBScanResult.swift
│ │ │ └── CBScanner.swift
│ │ └── RowerData
│ │ │ └── ConnectedRowerDataDevice.swift
│ ├── iOS Example.entitlements
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── SceneDelegate.swift
│ └── Info.plist
├── iOS Example.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
└── iOS Example.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
└── WaterRowerData-BLE
│ ├── FTMS_v1.0.pdf
│ ├── Internal
│ ├── GattSpecification
│ │ ├── Requirement.swift
│ │ ├── Field.swift
│ │ ├── BitRequirement.swift
│ │ ├── Format.swift
│ │ └── Data+ReadFormat.swift
│ └── RowerDataSpecification
│ │ ├── RowerDataFlagsField.swift
│ │ ├── RowerDataHeartRateField.swift
│ │ ├── RowerDataStrokeRateField.swift
│ │ ├── RowerDataAveragePaceField.swift
│ │ ├── RowerDataElapsedTimeField.swift
│ │ ├── RowerDataStrokeCountField.swift
│ │ ├── RowerDataTotalEnergyField.swift
│ │ ├── RowerDataAveragePowerField.swift
│ │ ├── RowerDataEnergyPerHourField.swift
│ │ ├── RowerDataRemainingTimeField.swift
│ │ ├── RowerDataTotalDistanceField.swift
│ │ ├── RowerDataEnergyPerMinuteField.swift
│ │ ├── RowerDataResistanceLevelField.swift
│ │ ├── RowerDataAverageStrokeRateField.swift
│ │ ├── RowerDataInstantaneousPaceField.swift
│ │ ├── RowerDataInstantaneousPowerField.swift
│ │ └── RowerDataMetabolicEquivalentField.swift
│ ├── FitnessMachineService.swift
│ ├── Info.plist
│ ├── README.md
│ ├── RowerData.swift
│ └── RowerDataCharacteristic.swift
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── WaterRowerData-iOS.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── WaterRowerData_BLE_Info.plist
├── WaterRowerData_iOS_Info.plist
├── WaterRowerData_BLETests_Info.plist
├── WaterRowerData_iOSTests_Info.plist
├── xcshareddata
│ └── xcschemes
│ │ └── WaterRowerData-iOS-Package.xcscheme
└── project.pbxproj
├── .gitignore
├── .github
└── workflows
│ └── test.yml
├── WaterRowerData-BLE.podspec
├── Package.swift
├── .swiftlint.yml
├── RELEASING.md
├── Tests
└── WaterRowerData-BLETests
│ ├── Info.plist
│ ├── CharacteristicData.swift
│ ├── CharacteristicFlags.swift
│ ├── RowerDataCharacteristicFlags.swift
│ └── RowerDataCharacteristicTest.swift
├── README.md
└── LICENSE
/iOS Example/iOS Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "author": "xcode",
4 | "version": 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/FTMS_v1.0.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamma/WaterRowerData-iOS/master/Sources/WaterRowerData-BLE/FTMS_v1.0.pdf
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "author": "xcode",
4 | "version": 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/Devices/Device.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Device: Identifiable {
4 |
5 | let id: UUID
6 | let name: String
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/GattSpecification/Requirement.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol Requirement {
4 |
5 | func check(in data: Data) -> Bool
6 | }
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Util/Cancellable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol Cancellable {
4 |
5 | func cancel()
6 | }
7 |
8 | class Cancelled: Cancellable {
9 |
10 | func cancel() {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/GattSpecification/Field.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol Field {
4 |
5 | var name: String { get }
6 | var format: Format { get }
7 |
8 | func isPresent(in data: Data) -> Bool
9 | }
10 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/DeviceDetails/ConnectionStatus.swift:
--------------------------------------------------------------------------------
1 | enum ConnectionStatus: String {
2 |
3 | case disconnected = "Disconnected"
4 | case connecting = "Connecting"
5 | case connected = "Connected"
6 | case failed = "Failed"
7 | }
8 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Connection/CBConnectionState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum CBConnectionState {
4 |
5 | case disconnected
6 | case connecting
7 | case connected(device: CBConnectedDevice)
8 | case failed
9 | }
10 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Discovery/CBScanResult.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreBluetooth
3 |
4 | struct CBScanResult {
5 |
6 | let peripheral: CBPeripheral
7 | let advertisementData: [String: Any]
8 | let rssi: NSNumber
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # User settings
2 | xcuserdata/
3 |
4 | # Build files
5 | build/
6 | .build/
7 |
8 | # Pod files
9 | Pods/
10 |
11 | # IDEA files
12 | .idea/*
13 | !.idea/codeStyles
14 | !.idea/inspectionProfiles
15 |
16 | # MacOS files
17 | .DS_Store
18 |
19 | # Misc
20 | *.swp
21 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/FitnessMachineService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | Describes the Fitness Machine Service.
5 | */
6 | public struct FitnessMachineService {
7 |
8 | /**
9 | The UUID value that identifies this service.
10 | */
11 | public static let uuid = UUID(uuidString: "00001826-0000-1000-8000-00805F9B34FB")!
12 | }
13 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataFlagsField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataFlagsField: Field = RowerDataFlagsField()
4 |
5 | private struct RowerDataFlagsField: Field {
6 |
7 | var name = "Flags"
8 | var format: Format = .UInt16
9 |
10 | func isPresent(in data: Data) -> Bool {
11 | return true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/Devices/DevicesViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | class DevicesViewModel: ObservableObject {
4 |
5 | @Published var devices = [Device]()
6 |
7 | func append(_ device: Device) {
8 | if !devices.contains(where: { d in
9 | d.id == device.id
10 | }) {
11 | devices.append(device)
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/GattSpecification/BitRequirement.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct BitRequirement: Requirement {
4 |
5 | let bitIndex: Int
6 | let bitValue: Int
7 |
8 | func check(in data: Data) -> Bool {
9 | let flagsValue = data.readIntValue(format: .UInt16, offset: 0)
10 | return flagsValue & (1 << bitIndex) == (bitValue << bitIndex)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/iOS Example.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.device.bluetooth
6 |
7 | com.apple.security.app-sandbox
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/Devices/DeviceListView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DeviceListView: View {
4 |
5 | @Binding var devices: [Device]
6 |
7 | var body: some View {
8 | List(devices) { device in
9 | NavigationLink(
10 | destination: DeviceDetailsView(viewModel: DeviceDetailsViewModel(device))
11 | ) {
12 | Text(device.name)
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | test:
7 | runs-on: macOS-latest
8 |
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | - name: Swift Test
13 | run: swift test
14 | - name: Pod lib lint
15 | run: pod lib lint --verbose
16 | - name: XCodeBuild
17 | run: xcodebuild -workspace iOS\ Example/iOS\ Example.xcworkspace -scheme "iOS Example" build CODE_SIGNING_REQUIRED=NO
18 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataHeartRateField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataHeartRateField: Field = RowerDataHeartRateField()
4 |
5 | private struct RowerDataHeartRateField: Field {
6 |
7 | var name = "Heart Rate"
8 | var format: Format = .UInt8
9 |
10 | private let requirement = BitRequirement(bitIndex: 9, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataStrokeRateField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataStrokeRateField: Field = RowerDataStrokeRateField()
4 |
5 | private struct RowerDataStrokeRateField: Field {
6 |
7 | var name = "Stroke Rate"
8 | var format: Format = .UInt8
9 |
10 | private let requirement = BitRequirement(bitIndex: 0, bitValue: 0)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataAveragePaceField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataAveragePaceField: Field = RowerDataAveragePaceField()
4 |
5 | private struct RowerDataAveragePaceField: Field {
6 |
7 | var name = "Average Pace"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 4, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataElapsedTimeField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataElapsedTimeField: Field = RowerDataElapsedTimeField()
4 |
5 | private struct RowerDataElapsedTimeField: Field {
6 |
7 | var name = "Elapsed Time"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 11, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataStrokeCountField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataStrokeCountField: Field = RowerDataStrokeCountField()
4 |
5 | private struct RowerDataStrokeCountField: Field {
6 |
7 | var name = "Stroke Count"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 0, bitValue: 0)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataTotalEnergyField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataTotalEnergyField: Field = RowerDataTotalEnergyField()
4 |
5 | private struct RowerDataTotalEnergyField: Field {
6 |
7 | var name = "Total Energy"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 8, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataAveragePowerField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataAveragePowerField: Field = RowerDataAveragePowerField()
4 |
5 | private struct RowerDataAveragePowerField: Field {
6 |
7 | var name = "Average Power"
8 | var format: Format = .SInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 6, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataEnergyPerHourField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataEnergyPerHourField: Field = RowerDataEnergyPerHourField()
4 |
5 | private struct RowerDataEnergyPerHourField: Field {
6 |
7 | var name = "Energy Per Hour"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 8, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataRemainingTimeField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataRemainingTimeField: Field = RowerDataRemainingTimeField()
4 |
5 | private struct RowerDataRemainingTimeField: Field {
6 |
7 | var name = "Remaining Time"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 12, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataTotalDistanceField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataTotalDistanceField: Field = RowerDataTotalDistanceField()
4 |
5 | private struct RowerDataTotalDistanceField: Field {
6 |
7 | var name = "Total Distance"
8 | var format: Format = .UInt24
9 |
10 | private let requirement = BitRequirement(bitIndex: 2, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/Devices/DevicesView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DevicesView: View {
4 |
5 | @ObservedObject var viewModel: DevicesViewModel
6 |
7 | var body: some View {
8 | DeviceListView(devices: $viewModel.devices)
9 | .navigationBarTitle("Available devices")
10 | }
11 | }
12 |
13 | struct DevicesView_Previews: PreviewProvider {
14 | static var previews: some View {
15 | DevicesView(
16 | viewModel: DevicesViewModel()
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataEnergyPerMinuteField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataEnergyPerMinuteField: Field = RowerDataEnergyPerMinuteField()
4 |
5 | private struct RowerDataEnergyPerMinuteField: Field {
6 |
7 | var name = "Energy Per Minute"
8 | var format: Format = .UInt8
9 |
10 | private let requirement = BitRequirement(bitIndex: 8, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataResistanceLevelField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataResistanceLevelField: Field = RowerDataResistanceLevelField()
4 |
5 | private struct RowerDataResistanceLevelField: Field {
6 |
7 | var name = "Resistance Level"
8 | var format: Format = .SInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 7, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/GattSpecification/Format.swift:
--------------------------------------------------------------------------------
1 | enum Format {
2 |
3 | case UInt8
4 | case UInt16
5 | case UInt24
6 | case SInt16
7 | }
8 |
9 | extension Format {
10 |
11 | func numberOfBytes() -> Int {
12 | switch self {
13 | case .UInt8:
14 | return 1
15 | case .UInt16:
16 | return 2
17 | case .UInt24:
18 | return 3
19 | case .SInt16:
20 | return 2
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataAverageStrokeRateField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataAverageStrokeRateField: Field = RowerDataAverageStrokeRateField()
4 |
5 | private struct RowerDataAverageStrokeRateField: Field {
6 |
7 | var name = "Average Stroke Rate"
8 | var format: Format = .UInt8
9 |
10 | private let requirement = BitRequirement(bitIndex: 1, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataInstantaneousPaceField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataInstantaneousPaceField: Field = RowerDataInstantaneousPaceField()
4 |
5 | private struct RowerDataInstantaneousPaceField: Field {
6 |
7 | var name = "Instantaneous Pace"
8 | var format: Format = .UInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 3, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataInstantaneousPowerField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataInstantaneousPowerField: Field = RowerDataInstantaneousPowerField()
4 |
5 | private struct RowerDataInstantaneousPowerField: Field {
6 |
7 | var name = "Instantaneous Power"
8 | var format: Format = .SInt16
9 |
10 | private let requirement = BitRequirement(bitIndex: 5, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/RowerDataSpecification/RowerDataMetabolicEquivalentField.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | let rowerDataMetabolicEquivalentField: Field = RowerDataMetabolicEquivalentField()
4 |
5 | private struct RowerDataMetabolicEquivalentField: Field {
6 |
7 | var name = "Metabolic Equivalent"
8 | var format: Format = .UInt8
9 |
10 | private let requirement = BitRequirement(bitIndex: 10, bitValue: 1)
11 |
12 | func isPresent(in data: Data) -> Bool {
13 | return requirement.check(in: data)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WaterRowerData-BLE.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'WaterRowerData-BLE'
3 | s.version = '0.1.1'
4 | s.summary = 'A library for reading data from a BLE-connected WaterRower device.'
5 |
6 | s.homepage = 'https://github.com/WaterRower-UK/WaterRowerData-iOS'
7 | s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
8 | s.author = { 'Niek Haarman' => 'niek@305.nl' }
9 |
10 | s.source = { :git => 'https://github.com/WaterRower-UK/WaterRowerData-iOS.git', :tag => s.version.to_s }
11 | s.swift_version = '5.0'
12 |
13 | s.ios.deployment_target = '11.0'
14 |
15 | s.source_files = 'Sources/WaterRowerData-BLE/**/*.swift'
16 | end
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "WaterRowerData-iOS",
7 | platforms: [
8 | .iOS(.v11)
9 | ],
10 | products: [
11 | .library(
12 | name: "WaterRowerData-BLE",
13 | targets: ["WaterRowerData-BLE"]
14 | )
15 | ],
16 | dependencies: [
17 | ],
18 | targets: [
19 | .target(
20 | name: "WaterRowerData-BLE",
21 | dependencies: []
22 | ),
23 | .testTarget(
24 | name: "WaterRowerData-BLETests",
25 | dependencies: ["WaterRowerData-BLE"]
26 | )
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | excluded:
2 | - Pods
3 | - vendor
4 |
5 | disabled_rules:
6 | - identifier_name # Only allows names with 3-40 characters
7 | - switch_case_alignment # Clashes with .idea formatting
8 | - file_length # Test files are allowed to grow large
9 | - type_body_length # Again, test files.
10 | - function_body_length # And again, test files.
11 | - cyclomatic_complexity # Will be handled during code review
12 | - type_name # Necessary for SwiftUI previews
13 | - nesting # Nesting is perfect for grouping
14 | - todo # We can't implement everything at once
15 | - multiple_closures_with_trailing_closure # This is recommended with SwiftUI
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/RowerData/ConnectedRowerDataDevice.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreBluetooth
3 | import WaterRowerData_BLE
4 |
5 | class ConnectedRowerDataDevice {
6 |
7 | private let connectedBleDevice: CBConnectedDevice
8 |
9 | init(from device: CBConnectedDevice) {
10 | self.connectedBleDevice = device
11 | }
12 |
13 | func rowerData(callback: @escaping (RowerData) -> Void) -> Cancellable {
14 | return connectedBleDevice.listen(
15 | serviceUUID: FitnessMachineService.uuid,
16 | characteristicUUID: RowerDataCharacteristic.uuid
17 | ) { data in
18 | callback(RowerDataCharacteristic.decode(data: data))
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/WaterRowerData_BLE_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/WaterRowerData_iOS_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/WaterRowerData_BLETests_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | BNDL
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/WaterRowerData_iOSTests_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | BNDL
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Connection/CBConnectionFactory.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreBluetooth
3 |
4 | /**
5 | A factory class for creating CBConnection instances.
6 |
7 | This class is not thread safe.
8 | */
9 | class CBConnectionFactory {
10 |
11 | static let instance = CBConnectionFactory()
12 |
13 | private init() {}
14 |
15 | private var connections = [UUID: CBConnection]()
16 |
17 | func create(from peripheral: CBPeripheral) -> CBConnection {
18 | return create(from: peripheral.identifier)
19 | }
20 |
21 | func create(from identifier: UUID) -> CBConnection {
22 | if let connection = connections[identifier] {
23 | return connection
24 | }
25 |
26 | let connection = CBConnection(identifier: identifier)
27 | connections[identifier] = connection
28 | return connection
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # RELEASING
2 |
3 | This library is available via Swift Package Manager and CocoaPods.
4 | Once you have [configured your setup](#setup), execute the following to
5 | make a release:
6 |
7 | - Update the podspec's `s.version` value in `WaterRowerData-BLE.podspec`
8 | - Commit the changes
9 | - Tag the commit as a release: `git tag x.x.x`
10 | - Push the tags: `git push --tags`
11 | - Execute `pod lib lint` and ensure there are no errors or warnings
12 | - Execute `pod trunk push WaterRowerData-BLE.podspec`
13 |
14 | ## Setup
15 |
16 | You'll need to be authorized to push the library to the CocoaPods trunk.
17 |
18 | - Execute `pod trunk register my@email.com 'My name'`
19 | - Click on the verification link you've received in your mailbox
20 | - Request to be added as a maintainer to the project, see https://guides.cocoapods.org/making/getting-setup-with-trunk#adding-other-people-as-contributors
21 |
--------------------------------------------------------------------------------
/Tests/WaterRowerData-BLETests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Connection/CBConnectionStateListener.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | protocol CBConnectionStateListener: NSObjectProtocol {
4 |
5 | /** Invoked when the connection state has changed. */
6 | func onConnectionStateChanged(_ connectionState: CBConnectionState)
7 | }
8 |
9 | /**
10 | A convenience function to construct CBConnectionStateListeners using a closure.
11 | */
12 | func connectionStateListener(_ closure: @escaping (CBConnectionState) -> Void) -> CBConnectionStateListener {
13 | return ClosureCBBleConnectionStateListener(closure)
14 | }
15 |
16 | private class ClosureCBBleConnectionStateListener: NSObject, CBConnectionStateListener {
17 |
18 | private let closure: (CBConnectionState) -> Void
19 |
20 | init(_ closure: @escaping (CBConnectionState) -> Void) {
21 | self.closure = closure
22 | }
23 |
24 | func onConnectionStateChanged(_ connectionState: CBConnectionState) {
25 | closure(connectionState)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import os
2 | import UIKit
3 |
4 | @UIApplicationMain
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | func application(
8 | _ application: UIApplication,
9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
10 | ) -> Bool {
11 | os_log("application didFinishLaunching", type: .debug)
12 | return true
13 | }
14 |
15 | func application(
16 | _ application: UIApplication,
17 | configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions
18 | ) -> UISceneConfiguration {
19 | os_log("application connectingSceneSession", type: .debug)
20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | }
22 |
23 | func application(
24 | _ application: UIApplication,
25 | didDiscardSceneSessions sceneSessions: Set
26 | ) {
27 | os_log("application didDiscardSceneSessions", type: .debug)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/README.md:
--------------------------------------------------------------------------------
1 | # WaterRowerData-BLE
2 |
3 | This target contains the sources for reading data from a
4 | BLE connected WaterRower device, such as an S5 monitor.
5 |
6 |
7 | The BLE enabled WaterRower modules use the FTMS GATT service
8 | specification with the RowerData GATT characteristic.
9 | This target provides classes that decode raw data received
10 | from a GATT characteristic into useable rower data.
11 |
12 | ## Usage
13 |
14 | Once connected to a WaterRower monitor and receiving data
15 | (see ['Receiving data'](#receiving-data)), you'll receive the data as a [`Data`
16 | byte buffer](https://developer.apple.com/documentation/foundation/data).
17 | You can pass this `Data` instance into the static
18 | `RowerDataCharacteristic.decode(data: Data)` function to decode the bytes
19 | into a `RowerData` struct:
20 |
21 | ```swift
22 | import WaterRowerData_BLE
23 |
24 | /* ... */
25 |
26 | func peripheral(
27 | _ peripheral: CBPeripheral,
28 | didUpdateValueFor characteristic: CBCharacteristic,
29 | error: Error?
30 | ) {
31 | guard let data = characteristic.value else {
32 | return
33 | }
34 |
35 | let rowerData = RowerDataCharacteristic.decode(data)
36 | print(rowerData)
37 | }
38 | ```
39 |
40 | This `RowerData` struct will contain the rower data that
41 | was encoded in the `Data` byte buffer.
42 |
43 | > :warning: **Note**: A single `Data` byte buffer instance does not always
44 | contain _all_ data values.
45 | Due to restrictions in buffer size some of the `RowerData` properties will be absent,
46 | which is represented as a `nil` value.
47 |
48 |
49 | ## Receiving data
50 |
51 | TODO
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WaterRowerData for iOS
2 |
3 | A swift package for reading data from a BLE-connected WaterRower device.
4 |
5 | ## WaterRowerData-BLE
6 |
7 | The WaterRowerData-BLE target contains the sources for reading data from
8 | a BLE connected WaterRower device, such as an S5 monitor.
9 | See [WaterRowerData-BLE/Readme](Sources/WaterRowerData-BLE/README.md) for
10 | usage instructions.
11 |
12 | ## Setup
13 |
14 | To include the package in your project, follow one of the following methods:
15 |
16 | ### XCode
17 |
18 | See [Adding Package Dependencies to Your App](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app):
19 |
20 | - Select File > Swift Packages > Add Package Dependency
21 | - Enter `https://github.com/WaterRowerData-UK/WaterRowerData-iOS`
22 | - Select the version, or leave as is
23 | - Choose package products and targets
24 |
25 | ### XCodeGen
26 |
27 | In your `project.yml` file include the following, replacing `x.x.x` with the
28 | latest release version:
29 |
30 | ```diff
31 | + packages:
32 | + SwiftPM:
33 | + url: https://github.com/WaterRowerData-UK/WaterRowerData-iOS
34 | + version: x.x.x
35 | targets:
36 | App
37 | + dependencies:
38 | + - package: SwiftPM
39 | + - product: WaterRowerData-BLE
40 | ```
41 |
42 | Run `xcodegen generate`.
43 |
44 | ## Development
45 |
46 | Swift Package Manager is used to test and build this library:
47 |
48 | - `swift build` builds the sources into binaries
49 | - `swift test` builds and runs the tests
50 |
51 | Open `WaterRowerData-iOS.xcodeproj` to start editing the source files.
52 | Open `iOS Example/iOS Example.xcworkspace` to view and run the sample
53 | application.
54 |
55 | ## Releasing
56 |
57 | See [RELEASING.md](RELEASING.md)
58 |
--------------------------------------------------------------------------------
/iOS Example/iOS 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 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/Internal/GattSpecification/Data+ReadFormat.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Data {
4 |
5 | func readIntValue(format: Format, offset: Int) -> Int {
6 | switch format {
7 | case .UInt8:
8 | return unsignedByteToInt(self[offset])
9 | case .UInt16:
10 | return unsignedBytesToInt(self[offset], self[offset + 1])
11 | case .UInt24:
12 | return unsignedBytesToInt(self[offset], self[offset + 1], self[offset + 2])
13 | case .SInt16:
14 | return unsignedToSigned(unsignedBytesToInt(self[offset], self[offset + 1]), size: 16)
15 | }
16 | }
17 |
18 | private func unsignedByteToInt(_ b: UInt8) -> Int {
19 | return Int(b) & 0xFF
20 | }
21 |
22 | private func unsignedBytesToInt(_ b0: UInt8, _ b1: UInt8) -> Int {
23 | return unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)
24 | }
25 |
26 | private func unsignedBytesToInt(_ b0: UInt8, _ b1: UInt8, _ b2: UInt8) -> Int {
27 | return unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8) + (unsignedByteToInt(b2) << 16)
28 | }
29 |
30 | private func unsignedBytesToInt(_ b0: UInt8, _ b1: UInt8, _ b2: UInt8, _ b3: UInt8) -> Int {
31 | return unsignedByteToInt(b0) +
32 | (unsignedByteToInt(b1) << 8) +
33 | (unsignedByteToInt(b2) << 16) +
34 | (unsignedByteToInt(b3) << 24)
35 | }
36 |
37 | /**
38 | Convert an unsigned integer value to a two's-complement encoded
39 | signed value.
40 | */
41 | private func unsignedToSigned(_ unsigned: Int, size: Int) -> Int {
42 | if unsigned & (1 << (size - 1)) == 0 {
43 | return unsigned
44 | }
45 |
46 | let a = 1 << size - 1
47 | return -1 * (a - (unsigned & (a - 1)))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "iphone",
5 | "scale": "2x",
6 | "size": "20x20"
7 | },
8 | {
9 | "idiom": "iphone",
10 | "scale": "3x",
11 | "size": "20x20"
12 | },
13 | {
14 | "idiom": "iphone",
15 | "scale": "2x",
16 | "size": "29x29"
17 | },
18 | {
19 | "idiom": "iphone",
20 | "scale": "3x",
21 | "size": "29x29"
22 | },
23 | {
24 | "idiom": "iphone",
25 | "scale": "2x",
26 | "size": "40x40"
27 | },
28 | {
29 | "idiom": "iphone",
30 | "scale": "3x",
31 | "size": "40x40"
32 | },
33 | {
34 | "idiom": "iphone",
35 | "scale": "2x",
36 | "size": "60x60"
37 | },
38 | {
39 | "idiom": "iphone",
40 | "scale": "3x",
41 | "size": "60x60"
42 | },
43 | {
44 | "idiom": "ipad",
45 | "scale": "1x",
46 | "size": "20x20"
47 | },
48 | {
49 | "idiom": "ipad",
50 | "scale": "2x",
51 | "size": "20x20"
52 | },
53 | {
54 | "idiom": "ipad",
55 | "scale": "1x",
56 | "size": "29x29"
57 | },
58 | {
59 | "idiom": "ipad",
60 | "scale": "2x",
61 | "size": "29x29"
62 | },
63 | {
64 | "idiom": "ipad",
65 | "scale": "1x",
66 | "size": "40x40"
67 | },
68 | {
69 | "idiom": "ipad",
70 | "scale": "2x",
71 | "size": "40x40"
72 | },
73 | {
74 | "idiom": "ipad",
75 | "scale": "1x",
76 | "size": "76x76"
77 | },
78 | {
79 | "idiom": "ipad",
80 | "scale": "2x",
81 | "size": "76x76"
82 | },
83 | {
84 | "idiom": "ipad",
85 | "scale": "2x",
86 | "size": "83.5x83.5"
87 | },
88 | {
89 | "idiom": "ios-marketing",
90 | "scale": "1x",
91 | "size": "1024x1024"
92 | }
93 | ],
94 | "info": {
95 | "author": "xcode",
96 | "version": 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/DeviceDetails/DeviceDetailsViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 | import WaterRowerData_BLE
4 |
5 | class DeviceDetailsViewModel: ObservableObject {
6 |
7 | private let device: Device
8 | private let connection: CBConnection
9 |
10 | var deviceName: String {
11 | return device.name
12 | }
13 |
14 | @Published var connectionStatus: ConnectionStatus = .disconnected
15 | @Published var rowerData: RowerData?
16 |
17 | init(_ device: Device) {
18 | self.device = device
19 | self.connection = CBConnectionFactory.instance.create(from: device.id)
20 | }
21 |
22 | private var connectionStatusCancellable: Cancellable?
23 |
24 | func viewDidAppear() {
25 | connectionStatusCancellable = connection.addConnectionStateListener(connectionStateListener { state in
26 | self.handle(connectionUpdate: state)
27 | })
28 | }
29 |
30 | private var rowerDataCancellable: Cancellable?
31 |
32 | private func handle(connectionUpdate state: CBConnectionState) {
33 | switch state {
34 | case .disconnected:
35 | self.connectionStatus = .disconnected
36 | case .connecting:
37 | self.connectionStatus = .connecting
38 | case .connected:
39 | self.connectionStatus = .connected
40 | case .failed:
41 | self.connectionStatus = .failed
42 | }
43 |
44 | if case .connected(device: let device) = state {
45 | rowerDataCancellable = ConnectedRowerDataDevice(from: device).rowerData { rowerData in
46 | self.rowerData = rowerData
47 | }
48 | } else {
49 | rowerDataCancellable = nil
50 | rowerData = nil
51 | }
52 | }
53 |
54 | func viewDidDisappear() {
55 | connectionStatusCancellable = nil
56 | rowerDataCancellable = nil
57 | disconnect()
58 | }
59 |
60 | func connect() {
61 | connection.connect()
62 | }
63 |
64 | func disconnect() {
65 | connection.disconnect()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import os
2 | import CoreBluetooth
3 | import UIKit
4 | import SwiftUI
5 | import WaterRowerData_BLE
6 |
7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
8 |
9 | var window: UIWindow?
10 |
11 | private let scanner = CBScanner()
12 | private var cancellable: Cancellable?
13 |
14 | func scene(
15 | _ scene: UIScene,
16 | willConnectTo session: UISceneSession,
17 | options connectionOptions: UIScene.ConnectionOptions
18 | ) {
19 | os_log("scene willConnectToSession", type: .debug)
20 |
21 | let devicesViewModel = DevicesViewModel()
22 | let devicesView = DevicesView(viewModel: devicesViewModel)
23 |
24 | cancellable = scanner.startScan(
25 | // withServices: [CBUUID(nsuuid: FitnessMachineService.uuid)]
26 | withServices: nil
27 | ) { result in
28 | if let name = result.peripheral.name {
29 | devicesViewModel.append(
30 | Device(
31 | id: result.peripheral.identifier,
32 | name: name
33 | )
34 | )
35 | }
36 | }
37 |
38 | let rootView = NavigationView { devicesView }
39 |
40 | if let windowScene = scene as? UIWindowScene {
41 | let window = UIWindow(windowScene: windowScene)
42 | window.rootViewController = UIHostingController(rootView: rootView)
43 | self.window = window
44 | window.makeKeyAndVisible()
45 | }
46 | }
47 |
48 | func sceneDidDisconnect(_ scene: UIScene) {
49 | os_log("sceneDidDisconnect", type: .debug)
50 | }
51 |
52 | func sceneDidBecomeActive(_ scene: UIScene) {
53 | os_log("sceneDidBecomeActive", type: .debug)
54 | }
55 |
56 | func sceneWillResignActive(_ scene: UIScene) {
57 | os_log("sceneWillResignActive", type: .debug)
58 | }
59 |
60 | func sceneWillEnterForeground(_ scene: UIScene) {
61 | os_log("sceneWillEnterForeground", type: .debug)
62 | }
63 |
64 | func sceneDidEnterBackground(_ scene: UIScene) {
65 | os_log("sceneDidEnterBackground", type: .debug)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/WaterRowerData-BLETests/CharacteristicData.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | class CharacteristicData {
5 |
6 | static func create(
7 | flags: Data,
8 | values: UInt8...
9 | ) -> Data {
10 | var result = Data(count: flags.count + values.count)
11 | var i = 0
12 |
13 | flags.forEach { (value) in
14 | result[i] = value
15 | i += 1
16 | }
17 |
18 | values.forEach { (value) in
19 | result[i] = value
20 | i += 1
21 | }
22 |
23 | return result
24 | }
25 | }
26 |
27 | class CharacteristicDataTest: XCTestCase {
28 |
29 | func test_create_without_flags_or_values() {
30 | /* Given */
31 | let flags = CharacteristicFlags.createFlags(flags: [:])
32 |
33 | /* When */
34 | let result = CharacteristicData.create(flags: flags)
35 |
36 | /* Then */
37 | XCTAssertEqual(result.count, 2)
38 | XCTAssertEqual(result[0], UInt8("00000000", radix: 2))
39 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
40 | }
41 |
42 | func test_create_with_flags_without_values() {
43 | /* Given */
44 | let flags = CharacteristicFlags.createFlags(flags: [1: true])
45 |
46 | /* When */
47 | let result = CharacteristicData.create(flags: flags)
48 |
49 | /* Then */
50 | XCTAssertEqual(result.count, 2)
51 | XCTAssertEqual(result[0], UInt8("00000010", radix: 2))
52 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
53 | }
54 |
55 | func test_create_with_flags_with_single_value() {
56 | /* Given */
57 | let flags = CharacteristicFlags.createFlags(flags: [1: true])
58 |
59 | /* When */
60 | let result = CharacteristicData.create(flags: flags, values: 3)
61 |
62 | /* Then */
63 | XCTAssertEqual(result.count, 3)
64 | XCTAssertEqual(result[0], UInt8("00000010", radix: 2))
65 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
66 | XCTAssertEqual(result[2], 3)
67 | }
68 |
69 | func test_create_with_flags_with_multiple_values() {
70 | /* Given */
71 | let flags = CharacteristicFlags.createFlags(flags: [1: true])
72 |
73 | /* When */
74 | let result = CharacteristicData.create(flags: flags, values: 3, 5)
75 |
76 | /* Then */
77 | XCTAssertEqual(result.count, 4)
78 | XCTAssertEqual(result[0], UInt8("00000010", radix: 2))
79 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
80 | XCTAssertEqual(result[2], 3)
81 | XCTAssertEqual(result[3], 5)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/RowerData.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | A struct that contains decoded Rower Data.
5 | */
6 | public struct RowerData {
7 |
8 | /**
9 | The instantaneous stroke rate in strokes/minute.
10 | */
11 | public let strokeRate: Double?
12 |
13 | /**
14 | The total number of strokes since the beginning of the training session.
15 | */
16 | public let strokeCount: Int?
17 |
18 | /**
19 | The average stroke rate since the beginning of the training session, in
20 | strokes/minute.
21 | */
22 | public let averageStrokeRate: Double?
23 |
24 | /**
25 | The total distance since the beginning of the training session, in meters.
26 | */
27 | public let totalDistanceMeters: Int?
28 |
29 | /**
30 | The value of the pace (time per 500 meters) of the user while exercising,
31 | in seconds.
32 | */
33 | public let instantaneousPaceSeconds: Int?
34 |
35 | /**
36 | The value of the average pace (time per 500 meters) since the beginning of
37 | the training session, in seconds.
38 | */
39 | public let averagePaceSeconds: Int?
40 |
41 | /**
42 | The value of the instantaneous power in Watts.
43 | */
44 | public let instantaneousPowerWatts: Int?
45 |
46 | /**
47 | The value of the average power since the beginning of the training session,
48 | in Watts.
49 | */
50 | public let averagePowerWatts: Int?
51 |
52 | /**
53 | The current value of the resistance level.
54 | */
55 | public let resistanceLevel: Int?
56 |
57 | /**
58 | The total expended energy of a user since the training session has started,
59 | in Kilocalories.
60 | */
61 | public let totalEnergyKiloCalories: Int?
62 |
63 | /**
64 | The average expended energy of a user during a period of one hour, in
65 | Kilocalories.
66 | */
67 | public let energyPerHourKiloCalories: Int?
68 |
69 | /**
70 | The average expended energy of a user during a period of one minute, in
71 | Kilocalories.
72 | */
73 | public let energyPerMinuteKiloCalories: Int?
74 |
75 | /**
76 | The current heart rate value of the user, in beats per minute.
77 | */
78 | public let heartRate: Int?
79 |
80 | /**
81 | The metabolic equivalent of the user.
82 | */
83 | public let metabolicEquivalent: Double?
84 |
85 | /**
86 | The elapsed time of a training session since the training session has
87 | started, in seconds.
88 | */
89 | public let elapsedTimeSeconds: Int?
90 |
91 | /**
92 | The remaining time of a selected training session, in seconds.
93 | */
94 | public let remainingTimeSeconds: Int?
95 | }
96 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSBluetoothAlwaysUsageDescription
6 | Connect to FTMS devices
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 | UISceneConfigurations
30 |
31 | UIWindowSceneSessionRoleApplication
32 |
33 |
34 | UISceneConfigurationName
35 | Default Configuration
36 | UISceneDelegateClassName
37 | $(PRODUCT_MODULE_NAME).SceneDelegate
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationPortraitUpsideDown
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/xcshareddata/xcschemes/WaterRowerData-iOS-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/Tests/WaterRowerData-BLETests/CharacteristicFlags.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | /**
5 | A helper class for tests to construct flag bytes.
6 | */
7 | class CharacteristicFlags {
8 |
9 | /**
10 | Creates a 16 bit data buffer, by putting a `1` on each indicated index:
11 |
12 | [0: true] becomes 0000 0000 0000 0001
13 | [7: true] becomes 0000 0000 1000 0000
14 | */
15 | static func createFlags(
16 | flags: [Int: Bool]
17 | ) -> Data {
18 | var result = Data(count: 2)
19 |
20 | let flagsValue = flags.enumerated()
21 | .reduce(UInt16(0)) { (accumulation, item) -> UInt16 in
22 | if item.element.value {
23 | return accumulation + UInt16(1 << item.element.key)
24 | }
25 |
26 | return accumulation
27 | }
28 |
29 | result[0] = UInt8(clamping: flagsValue & 0xFF)
30 | result[1] = UInt8(clamping: (flagsValue >> 8) & 0xFF)
31 |
32 | return result
33 | }
34 | }
35 |
36 | class CharacteristicFlagsTest: XCTestCase {
37 |
38 | func test_no_active_flags() {
39 | /* When */
40 | let result = CharacteristicFlags.createFlags(flags: [:])
41 |
42 | /* Then */
43 | XCTAssertEqual(result[0], UInt8("00000000", radix: 2))
44 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
45 | }
46 |
47 | func test_active_flag_on_0th_index() {
48 | /* When */
49 | let result = CharacteristicFlags.createFlags(flags: [0: true])
50 |
51 | /* Then */
52 | XCTAssertEqual(result[0], UInt8("00000001", radix: 2))
53 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
54 | }
55 |
56 | func test_active_flag_on_7th_index() {
57 | /* When */
58 | let result = CharacteristicFlags.createFlags(flags: [7: true])
59 |
60 | /* Then */
61 | XCTAssertEqual(result[0], UInt8("10000000", radix: 2))
62 | XCTAssertEqual(result[1], UInt8("00000000", radix: 2))
63 | }
64 |
65 | func test_active_flag_on_8th_index() {
66 | /* When */
67 | let result = CharacteristicFlags.createFlags(flags: [8: true])
68 |
69 | /* Then */
70 | XCTAssertEqual(result[0], UInt8("00000000", radix: 2))
71 | XCTAssertEqual(result[1], UInt8("00000001", radix: 2))
72 | }
73 |
74 | func test_active_flag_on_15th_index() {
75 | /* When */
76 | let result = CharacteristicFlags.createFlags(flags: [15: true])
77 |
78 | /* Then */
79 | XCTAssertEqual(result[0], UInt8("00000000", radix: 2))
80 | XCTAssertEqual(result[1], UInt8("10000000", radix: 2))
81 | }
82 |
83 | func test_all_active_flags() {
84 | /* Given */
85 | let flags = [
86 | 0: true,
87 | 1: true,
88 | 2: true,
89 | 3: true,
90 | 4: true,
91 | 5: true,
92 | 6: true,
93 | 7: true,
94 | 8: true,
95 | 9: true,
96 | 10: true,
97 | 11: true,
98 | 12: true,
99 | 13: true,
100 | 14: true,
101 | 15: true
102 | ]
103 |
104 | /* When */
105 | let result = CharacteristicFlags.createFlags(flags: flags)
106 |
107 | /* Then */
108 | XCTAssertEqual(result[0], UInt8("11111111", radix: 2))
109 | XCTAssertEqual(result[1], UInt8("11111111", radix: 2))
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/UI/DeviceDetails/DeviceDetailsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import WaterRowerData_BLE
3 |
4 | struct DeviceDetailsView: View {
5 |
6 | @ObservedObject var viewModel: DeviceDetailsViewModel
7 |
8 | var body: some View {
9 | List {
10 | connectionRow()
11 |
12 | if viewModel.connectionStatus == .connected {
13 | Group {
14 | dataRow(title: "Stroke rate") { rowerData in rowerData?.strokeRate }
15 | dataRow(title: "Stroke count") { rowerData in rowerData?.strokeCount }
16 | dataRow(title: "Average stroke rate") { rowerData in rowerData?.averageStrokeRate }
17 | dataRow(title: "Total distance") { rowerData in rowerData?.totalDistanceMeters }
18 | dataRow(title: "Instantaneous pace") { rowerData in rowerData?.instantaneousPaceSeconds }
19 | dataRow(title: "Average pace") { rowerData in rowerData?.averagePaceSeconds }
20 | dataRow(title: "Instantaneous power") { rowerData in rowerData?.instantaneousPowerWatts }
21 | dataRow(title: "Average power") { rowerData in rowerData?.averagePowerWatts }
22 | dataRow(title: "Resistance level") { rowerData in rowerData?.resistanceLevel }
23 | dataRow(title: "Total energy") { rowerData in rowerData?.totalEnergyKiloCalories }
24 | }
25 |
26 | Group {
27 | dataRow(title: "Energy per hour") { rowerData in rowerData?.energyPerHourKiloCalories }
28 | dataRow(title: "Energy per minute") { rowerData in rowerData?.energyPerMinuteKiloCalories }
29 | dataRow(title: "Heart rate") { rowerData in rowerData?.heartRate }
30 | dataRow(title: "Metabolic equivalent") { rowerData in rowerData?.metabolicEquivalent }
31 | dataRow(title: "Elapsed time") { rowerData in rowerData?.elapsedTimeSeconds }
32 | dataRow(title: "Remaining time") { rowerData in rowerData?.remainingTimeSeconds }
33 | }
34 | }
35 | }.navigationBarTitle(viewModel.deviceName)
36 | .onAppear { self.viewModel.viewDidAppear() }
37 | .onDisappear { self.viewModel.viewDidDisappear() }
38 | }
39 |
40 | private func connectionRow() -> some View {
41 | HStack {
42 | connectionStatusText()
43 | Spacer()
44 | connectionButton().buttonStyle(BorderlessButtonStyle())
45 | }
46 | }
47 |
48 | private func connectionStatusText() -> Text {
49 | return Text(viewModel.connectionStatus.rawValue)
50 | }
51 |
52 | private func connectionButton() -> some View {
53 | switch viewModel.connectionStatus {
54 | case .disconnected, .connecting, .failed:
55 | return AnyView(connectButton(connectionStatus: viewModel.connectionStatus))
56 | case .connected:
57 | return AnyView(disconnectButton())
58 | }
59 | }
60 |
61 | private func connectButton(connectionStatus: ConnectionStatus) -> some View {
62 | return Button(action: {
63 | self.viewModel.connect()
64 | }) {
65 | Text("Connect")
66 | }.disabled(connectionStatus != .disconnected)
67 | }
68 |
69 | private func disconnectButton() -> some View {
70 | return Button(action: {
71 | self.viewModel.disconnect()
72 | }) {
73 | Text("Disconnect")
74 | }.disabled(false)
75 | }
76 |
77 | private func dataRow(
78 | title: String,
79 | value: (RowerData?) -> Any?
80 | ) -> some View {
81 | return HStack {
82 | Text(title)
83 | Spacer()
84 | valueText(value: value(viewModel.rowerData))
85 | }
86 | }
87 |
88 | private func valueText(value: Any?) -> Text {
89 | if let value = value {
90 | return Text(String(describing: value))
91 | } else {
92 | return Text("-")
93 | }
94 | }
95 | }
96 |
97 | struct DeviceDetailsView_Previews: PreviewProvider {
98 | static var previews: some View {
99 | DeviceDetailsView(
100 | viewModel: DeviceDetailsViewModel(
101 | Device(id: UUID(), name: "My device")
102 | )
103 | )
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Connection/CBConnectedDevice.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreBluetooth
3 | import os
4 |
5 | private let log = OSLog(subsystem: "uk.co.waterrower.bluetooth.plist", category: "CBConnectedBleDevice")
6 |
7 | /**
8 | Represents a connected BLE device.
9 |
10 | Use `CBConnection` to obtain an instance of this class.
11 | */
12 | class CBConnectedDevice: NSObject, CBPeripheralDelegate {
13 |
14 | private let peripheral: CBPeripheral
15 |
16 | /**
17 | - Parameter from: A `CBPeripheral` that is connected.
18 | */
19 | init(
20 | from peripheral: CBPeripheral
21 | ) {
22 | self.peripheral = peripheral
23 | super.init()
24 | peripheral.delegate = self
25 | }
26 |
27 | private var callbacks: [CBUUID: [ListenCallback]] = [:]
28 |
29 | /**
30 | Enables characteristic notifications for given `serviceUUID` and
31 | `characteristicUUID` and invokes `callback` when notifications arrive.
32 |
33 | For simplicity for this sample app, this function does not disable the
34 | characteristic notifications when invoking the resulting cancellable.
35 |
36 | - Returns: A `Cancellable` that can be used to stop receiving notifications.
37 | A strong reference must be held to this instance,
38 | disposing of the reference cancels the listener.
39 | */
40 | func listen(
41 | serviceUUID: UUID,
42 | characteristicUUID: UUID,
43 | callback: @escaping (Data) -> Void
44 | ) -> Cancellable {
45 | guard let services: [CBService] = peripheral.services else {
46 | fatalError("Peripheral has no services")
47 | }
48 |
49 | guard let service = services.first(where: { service in
50 | service.uuid == CBUUID(nsuuid: serviceUUID)
51 | }) else {
52 | fatalError("Service doesn't exist for UUID: \(serviceUUID)")
53 | }
54 |
55 | guard let characteristic = service.characteristics?.first(where: { characteristic in
56 | characteristic.uuid == CBUUID(nsuuid: characteristicUUID)
57 | }) else {
58 | fatalError("Characteristic doesn't exist for UUID: \(characteristicUUID)")
59 | }
60 |
61 | let characteristicCBUUID = CBUUID(nsuuid: characteristicUUID)
62 |
63 | var callbackList: [ListenCallback]? = callbacks[characteristicCBUUID]
64 | if callbackList == nil {
65 | callbackList = []
66 | }
67 | let callback = ListenCallback(callback)
68 | callbackList!.append(callback)
69 | callbacks[characteristicCBUUID] = callbackList
70 |
71 | os_log("Set notify value true for %@", log: log, type: .debug, characteristic)
72 | peripheral.setNotifyValue(true, for: characteristic)
73 | peripheral.writeValue(
74 | Data(),
75 | for: characteristic,
76 | type: CBCharacteristicWriteType.withResponse
77 | )
78 |
79 | return CancelListening(
80 | self,
81 | characteristicCBUUID,
82 | callback
83 | )
84 | }
85 |
86 | private class ListenCallback {
87 |
88 | let closure: (Data) -> Void
89 |
90 | init(_ closure: @escaping (Data) -> Void) {
91 | self.closure = closure
92 | }
93 | }
94 |
95 | private class CancelListening: Cancellable {
96 |
97 | private weak var owner: CBConnectedDevice?
98 | private let uuid: CBUUID
99 | private let callback: ListenCallback
100 |
101 | init(
102 | _ owner: CBConnectedDevice,
103 | _ uuid: CBUUID,
104 | _ callback: ListenCallback
105 | ) {
106 | self.owner = owner
107 | self.uuid = uuid
108 | self.callback = callback
109 | }
110 |
111 | func cancel() {
112 | os_log("Removing callback for %@", log: log, type: .debug, uuid)
113 | owner?.callbacks[uuid]?.removeAll(where: { c -> Bool in
114 | c === callback
115 | })
116 | }
117 |
118 | deinit {
119 | cancel()
120 | }
121 | }
122 |
123 | func peripheral(
124 | _ peripheral: CBPeripheral,
125 | didUpdateValueFor characteristic: CBCharacteristic,
126 | error: Error?
127 | ) {
128 | callbacks[characteristic.uuid]?.forEach { callback in
129 | if let data = characteristic.value {
130 | callback.closure(data)
131 | }
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example/Bluetooth/Discovery/CBScanner.swift:
--------------------------------------------------------------------------------
1 | import os
2 | import CoreBluetooth
3 | import Foundation
4 |
5 | private let log = OSLog(subsystem: "uk.co.waterrower.bluetooth.plist", category: "CBBleScanner")
6 |
7 | /**
8 | A class that helps with scanning for BLE devices.
9 |
10 | This class instantiates a new `CBCentralManager` for each scan request,
11 | allowing the implementation to keep the scan results separate.
12 | */
13 | class CBScanner {
14 |
15 | /** Strong references to the active managers */
16 | private var managers: [CBCentralManager] = []
17 |
18 | /** Strong references to the active delegates */
19 | private var delegates: [CBCentralManagerDelegate] = []
20 |
21 | /**
22 | Starts a scan for peripherals that are advertising.
23 |
24 | - Parameter withServices: An array of CBUUID objects that the app is
25 | interested in. Each CBUUID object represents the UUID of a service
26 | that a peripheral advertises.
27 |
28 | - Parameter allowDuplicates: If `true`, filtering is disabled and a
29 | discovery event is generated each time an advertising packet is
30 | received from the peripheral. If `false`, multiple discoveries of
31 | the same peripheral are grouped in a single event.
32 |
33 | - Returns: A `Cancellable` that can be used to cancel the scan.
34 | A strong reference must be held to this instance, disposing of
35 | the reference cancels the scan.
36 | */
37 | func startScan(
38 | withServices scanServicesUUIDs: [CBUUID]?,
39 | allowDuplicates: Bool = false,
40 | _ callback: @escaping (CBScanResult) -> Void
41 | ) -> Cancellable {
42 | let delegate = BLEScanDelegate(withServices: scanServicesUUIDs, allowDuplicates: allowDuplicates, callback)
43 | let manager = CBCentralManager(delegate: delegate, queue: nil)
44 |
45 | managers.append(manager)
46 | delegates.append(delegate)
47 |
48 | return BLEScanCanceller(self, manager, delegate)
49 | }
50 |
51 | private class BLEScanDelegate: NSObject, CBCentralManagerDelegate {
52 |
53 | private let scanServiceUUIDs: [CBUUID]?
54 | private let allowDuplicates: Bool
55 | private let callback: (CBScanResult) -> Void
56 |
57 | init(
58 | withServices scanServiceUUIDs: [CBUUID]?,
59 | allowDuplicates: Bool,
60 | _ callback: @escaping (CBScanResult) -> Void
61 | ) {
62 | self.scanServiceUUIDs = scanServiceUUIDs
63 | self.allowDuplicates = allowDuplicates
64 | self.callback = callback
65 | }
66 |
67 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
68 | os_log("centralManagerDidUpdateState %d", log: log, type: .debug, central.state.rawValue)
69 |
70 | guard central.state == .poweredOn else {
71 | os_log("Invalid state, not scanning: %d", log: log, type: .error, central.state.rawValue)
72 | return
73 | }
74 |
75 | if scanServiceUUIDs == nil {
76 | os_log("Start scanning for peripherals without filters", log: log, type: .debug)
77 | } else {
78 | os_log(
79 | "Start scanning for peripherals: %@",
80 | log: log,
81 | type: .debug,
82 | scanServiceUUIDs!
83 | )
84 | }
85 |
86 | let options = [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
87 | central.scanForPeripherals(
88 | withServices: scanServiceUUIDs,
89 | options: options
90 | )
91 | }
92 |
93 | func centralManager(
94 | _ central: CBCentralManager,
95 | didDiscover peripheral: CBPeripheral,
96 | advertisementData: [String: Any],
97 | rssi RSSI: NSNumber
98 | ) {
99 | // os_log("centralManagerDidDiscover %@", log: log, type: .debug, "\(peripheral)")
100 |
101 | callback(
102 | CBScanResult(
103 | peripheral: peripheral,
104 | advertisementData: advertisementData,
105 | rssi: RSSI
106 | )
107 | )
108 | }
109 | }
110 |
111 | private class BLEScanCanceller: Cancellable {
112 |
113 | private unowned let scanner: CBScanner
114 |
115 | // swiftlint:disable weak_delegate
116 | private let delegate: CBCentralManagerDelegate
117 | private let manager: CBCentralManager
118 |
119 | init(
120 | _ scanner: CBScanner,
121 | _ manager: CBCentralManager,
122 | _ delegate: CBCentralManagerDelegate
123 |
124 | ) {
125 | self.scanner = scanner
126 | self.manager = manager
127 | self.delegate = delegate
128 | }
129 |
130 | func cancel() {
131 | os_log("Cancelling scan", log: log, type: .debug)
132 | scanner.managers.removeAll { m in m === manager }
133 | scanner.delegates.removeAll { d in d === delegate }
134 |
135 | if manager.state == .poweredOn {
136 | manager.stopScan()
137 | }
138 | }
139 |
140 | deinit {
141 | cancel()
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Tests/WaterRowerData-BLETests/RowerDataCharacteristicFlags.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 |
4 | class RowerDataCharacteristicFlags {
5 |
6 | static func create(
7 | moreDataPresent: Bool = false,
8 | averageStrokeRatePresent: Bool = false,
9 | totalDistancePresent: Bool = false,
10 | instantaneousPacePresent: Bool = false,
11 | averagePacePresent: Bool = false,
12 | instantaneousPowerPresent: Bool = false,
13 | averagePowerPresent: Bool = false,
14 | resistanceLevelPresent: Bool = false,
15 | expendedEnergyPresent: Bool = false,
16 | heartRatePresent: Bool = false,
17 | metabolicEquivalentPresent: Bool = false,
18 | elapsedTimePresent: Bool = false,
19 | remainingTimePresent: Bool = false
20 | ) -> Data {
21 | var flags: [Int: Bool] = [:]
22 |
23 | if !moreDataPresent {
24 | flags[0] = true
25 | }
26 |
27 | if averageStrokeRatePresent {
28 | flags[1] = true
29 | }
30 |
31 | if totalDistancePresent {
32 | flags[2] = true
33 | }
34 |
35 | if instantaneousPacePresent {
36 | flags[3] = true
37 | }
38 |
39 | if averagePacePresent {
40 | flags[4] = true
41 | }
42 |
43 | if instantaneousPowerPresent {
44 | flags[5] = true
45 | }
46 |
47 | if averagePowerPresent {
48 | flags[6] = true
49 | }
50 |
51 | if resistanceLevelPresent {
52 | flags[7] = true
53 | }
54 |
55 | if expendedEnergyPresent {
56 | flags[8] = true
57 | }
58 |
59 | if heartRatePresent {
60 | flags[9] = true
61 | }
62 |
63 | if metabolicEquivalentPresent {
64 | flags[10] = true
65 | }
66 |
67 | if elapsedTimePresent {
68 | flags[11] = true
69 | }
70 |
71 | if remainingTimePresent {
72 | flags[12] = true
73 | }
74 |
75 | return CharacteristicFlags.createFlags(flags: flags)
76 | }
77 | }
78 |
79 | class RowerDataCharacteristicFlagsTest: XCTestCase {
80 |
81 | func test_moreData_present() {
82 | /* When */
83 | let result = RowerDataCharacteristicFlags.create(
84 | moreDataPresent: true
85 | )
86 |
87 | /* Then */
88 | XCTAssertEqual(result[0], UInt8("00000000", radix: 2))
89 | }
90 |
91 | func test_moreData_notPresent() {
92 | /* When */
93 | let result = RowerDataCharacteristicFlags.create(
94 | moreDataPresent: false
95 | )
96 |
97 | /* Then */
98 | XCTAssertEqual(result[0], UInt8("00000001", radix: 2))
99 | }
100 |
101 | func test_averageStrokeRate_present() {
102 | /* When */
103 | let result = RowerDataCharacteristicFlags.create(
104 | moreDataPresent: true,
105 | averageStrokeRatePresent: true
106 | )
107 |
108 | /* Then */
109 | XCTAssertEqual(result[0], UInt8("00000010", radix: 2))
110 | }
111 |
112 | func test_totalDistance_present() {
113 | /* When */
114 | let result = RowerDataCharacteristicFlags.create(
115 | moreDataPresent: true,
116 | totalDistancePresent: true
117 | )
118 |
119 | /* Then */
120 | XCTAssertEqual(result[0], UInt8("00000100", radix: 2))
121 | }
122 |
123 | func test_instantaneousPace_present() {
124 | /* When */
125 | let result = RowerDataCharacteristicFlags.create(
126 | moreDataPresent: true,
127 | instantaneousPacePresent: true
128 | )
129 |
130 | /* Then */
131 | XCTAssertEqual(result[0], UInt8("00001000", radix: 2))
132 | }
133 |
134 | func test_averagePace_present() {
135 | /* When */
136 | let result = RowerDataCharacteristicFlags.create(
137 | moreDataPresent: true,
138 | averagePacePresent: true
139 | )
140 |
141 | /* Then */
142 | XCTAssertEqual(result[0], UInt8("00010000", radix: 2))
143 | }
144 |
145 | func test_instantaneousPower_present() {
146 | /* When */
147 | let result = RowerDataCharacteristicFlags.create(
148 | moreDataPresent: true,
149 | instantaneousPowerPresent: true
150 | )
151 |
152 | /* Then */
153 | XCTAssertEqual(result[0], UInt8("00100000", radix: 2))
154 | }
155 |
156 | func test_averagePower_present() {
157 | /* When */
158 | let result = RowerDataCharacteristicFlags.create(
159 | moreDataPresent: true,
160 | averagePowerPresent: true
161 | )
162 |
163 | /* Then */
164 | XCTAssertEqual(result[0], UInt8("01000000", radix: 2))
165 | }
166 |
167 | func test_resistanceLevel_present() {
168 | /* When */
169 | let result = RowerDataCharacteristicFlags.create(
170 | moreDataPresent: true,
171 | resistanceLevelPresent: true
172 | )
173 |
174 | /* Then */
175 | XCTAssertEqual(result[0], UInt8("10000000", radix: 2))
176 | }
177 |
178 | func test_expendedEnergy_present() {
179 | /* When */
180 | let result = RowerDataCharacteristicFlags.create(
181 | expendedEnergyPresent: true
182 | )
183 |
184 | /* Then */
185 | XCTAssertEqual(result[1], UInt8("00000001", radix: 2))
186 | }
187 |
188 | func test_heartRate_present() {
189 | /* When */
190 | let result = RowerDataCharacteristicFlags.create(
191 | heartRatePresent: true
192 | )
193 |
194 | /* Then */
195 | XCTAssertEqual(result[1], UInt8("00000010", radix: 2))
196 | }
197 |
198 | func test_metabolicEquivalent_present() {
199 | /* When */
200 | let result = RowerDataCharacteristicFlags.create(
201 | metabolicEquivalentPresent: true
202 | )
203 |
204 | /* Then */
205 | XCTAssertEqual(result[1], UInt8("00000100", radix: 2))
206 | }
207 |
208 | func test_elapsedTime_present() {
209 | /* When */
210 | let result = RowerDataCharacteristicFlags.create(
211 | elapsedTimePresent: true
212 | )
213 |
214 | /* Then */
215 | XCTAssertEqual(result[1], UInt8("00001000", radix: 2))
216 | }
217 |
218 | func test_remainingTime_present() {
219 | /* When */
220 | let result = RowerDataCharacteristicFlags.create(
221 | remainingTimePresent: true
222 | )
223 |
224 | /* Then */
225 | XCTAssertEqual(result[1], UInt8("00010000", radix: 2))
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/Sources/WaterRowerData-BLE/RowerDataCharacteristic.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | A class that can decode raw bytes into `RowerData` instances.
5 |
6 | This class follows the Rower Data characteristic specification
7 | as described in section 4.8 "Rower Data" of the
8 | Fitness Machine Service (FTMS) Bluetooth Service specification,
9 | revision v1.0.
10 |
11 | A copy of this specification can be found on
12 | https://www.bluetooth.com/specifications/gatt/
13 | */
14 | public class RowerDataCharacteristic {
15 |
16 | /**
17 | The UUID value that identifies this characteristic.
18 | */
19 | public static let uuid = UUID(uuidString: "00002AD1-0000-1000-8000-00805F9B34FB")!
20 |
21 | /**
22 | Decodes given `data` into a `RowerData` instance.
23 |
24 | Due to restrictions in the byte buffer size some of the `RowerData`
25 | properties will be absent, which is represented as a `nil` value.
26 |
27 | - Parameter data: A `Data` instance that contains the encoded data
28 | as described in the Rower Data characteristic
29 | specification.
30 |
31 | - Returns: A `RowerData` instance with the decoded properties.
32 | Properties will be `nil` if not present in the encoded
33 | data.
34 | */
35 | public static func decode(data: Data) -> RowerData {
36 | return RowerData(
37 | strokeRate: strokeRate(from: data),
38 | strokeCount: strokeCount(from: data),
39 | averageStrokeRate: averageStrokeRate(from: data),
40 | totalDistanceMeters: totalDistanceMeters(from: data),
41 | instantaneousPaceSeconds: instantaneousPaceSeconds(from: data),
42 | averagePaceSeconds: averagePaceSeconds(from: data),
43 | instantaneousPowerWatts: instantaneousPower(from: data),
44 | averagePowerWatts: averagePower(from: data),
45 | resistanceLevel: resistanceLevel(from: data),
46 | totalEnergyKiloCalories: totalEnergy(from: data),
47 | energyPerHourKiloCalories: energyPerHour(from: data),
48 | energyPerMinuteKiloCalories: energyPerMinute(from: data),
49 | heartRate: heartRate(from: data),
50 | metabolicEquivalent: metabolicEquivalent(from: data),
51 | elapsedTimeSeconds: elapsedTime(from: data),
52 | remainingTimeSeconds: remainingTime(from: data)
53 | )
54 | }
55 |
56 | private static let fields: [Field] = [
57 | rowerDataFlagsField,
58 | rowerDataStrokeRateField,
59 | rowerDataStrokeCountField,
60 | rowerDataAverageStrokeRateField,
61 | rowerDataTotalDistanceField,
62 | rowerDataInstantaneousPaceField,
63 | rowerDataAveragePaceField,
64 | rowerDataInstantaneousPowerField,
65 | rowerDataAveragePowerField,
66 | rowerDataResistanceLevelField,
67 | rowerDataTotalEnergyField,
68 | rowerDataEnergyPerHourField,
69 | rowerDataEnergyPerMinuteField,
70 | rowerDataHeartRateField,
71 | rowerDataMetabolicEquivalentField,
72 | rowerDataElapsedTimeField,
73 | rowerDataRemainingTimeField
74 | ]
75 |
76 | private static func strokeRate(from data: Data) -> Double? {
77 | guard let intValue = readIntValue(from: data, for: rowerDataStrokeRateField) else {
78 | return nil
79 | }
80 |
81 | return Double(intValue) / 2.0
82 | }
83 |
84 | private static func strokeCount(from data: Data) -> Int? {
85 | return readIntValue(from: data, for: rowerDataStrokeCountField)
86 | }
87 |
88 | private static func averageStrokeRate(from data: Data) -> Double? {
89 | guard let intValue = readIntValue(from: data, for: rowerDataAverageStrokeRateField) else {
90 | return nil
91 | }
92 |
93 | return Double(intValue) / 2.0
94 | }
95 |
96 | private static func totalDistanceMeters(from data: Data) -> Int? {
97 | return readIntValue(from: data, for: rowerDataTotalDistanceField)
98 | }
99 |
100 | private static func instantaneousPaceSeconds(from data: Data) -> Int? {
101 | return readIntValue(from: data, for: rowerDataInstantaneousPaceField)
102 | }
103 |
104 | private static func averagePaceSeconds(from data: Data) -> Int? {
105 | return readIntValue(from: data, for: rowerDataAveragePaceField)
106 | }
107 |
108 | private static func instantaneousPower(from data: Data) -> Int? {
109 | return readIntValue(from: data, for: rowerDataInstantaneousPowerField)
110 | }
111 |
112 | private static func averagePower(from data: Data) -> Int? {
113 | return readIntValue(from: data, for: rowerDataAveragePowerField)
114 | }
115 |
116 | private static func resistanceLevel(from data: Data) -> Int? {
117 | return readIntValue(from: data, for: rowerDataResistanceLevelField)
118 | }
119 |
120 | private static func totalEnergy(from data: Data) -> Int? {
121 | let result = readIntValue(from: data, for: rowerDataTotalEnergyField)
122 | if result == 0xFFFF {
123 | return nil
124 | }
125 |
126 | return result
127 | }
128 |
129 | private static func energyPerHour(from data: Data) -> Int? {
130 | let result = readIntValue(from: data, for: rowerDataEnergyPerHourField)
131 | if result == 0xFFFF {
132 | return nil
133 | }
134 |
135 | return result
136 | }
137 |
138 | private static func energyPerMinute(from data: Data) -> Int? {
139 | let result = readIntValue(from: data, for: rowerDataEnergyPerMinuteField)
140 | if result == 0xFF {
141 | return nil
142 | }
143 |
144 | return result
145 | }
146 |
147 | private static func heartRate(from data: Data) -> Int? {
148 | return readIntValue(from: data, for: rowerDataHeartRateField)
149 | }
150 |
151 | private static func metabolicEquivalent(from data: Data) -> Double? {
152 | guard let intValue = readIntValue(from: data, for: rowerDataMetabolicEquivalentField) else {
153 | return nil
154 | }
155 |
156 | return Double(intValue) / 10
157 | }
158 |
159 | private static func elapsedTime(from data: Data) -> Int? {
160 | return readIntValue(from: data, for: rowerDataElapsedTimeField)
161 | }
162 |
163 | private static func remainingTime(from data: Data) -> Int? {
164 | return readIntValue(from: data, for: rowerDataRemainingTimeField)
165 | }
166 |
167 | private static func readIntValue(from data: Data, for field: Field) -> Int? {
168 | if !field.isPresent(in: data) {
169 | return nil
170 | }
171 |
172 | var offset = 0
173 | for i in 0.. Cancellable {
77 | listeners.append(listener)
78 | listener.onConnectionStateChanged(connectionState)
79 | return CancelListening(self, listener)
80 | }
81 |
82 | /**
83 | A `CBCentralManagerDelegate` which initiates a connection with the peripheral when the
84 | `CBCentralManager` is ready.
85 | */
86 | private class BLEConnectionDelegate: NSObject, CBCentralManagerDelegate {
87 |
88 | private unowned let parent: CBConnection
89 | private let peripheralIdentifier: UUID
90 |
91 | // swiftlint:disable weak_delegate
92 | private let peripheralDelegate: BleConnectionPeripheralDelegate
93 |
94 | init(
95 | _ peripheralIdentifier: UUID,
96 | _ parent: CBConnection
97 | ) {
98 | self.parent = parent
99 | self.peripheralIdentifier = peripheralIdentifier
100 | peripheralDelegate = BleConnectionPeripheralDelegate(parent)
101 | }
102 |
103 | /**
104 | The peripheral that is connected or being connected to.
105 | If the connection isn't set up yet or is disconnected, this value is nil.
106 | */
107 | private var peripheral: CBPeripheral?
108 |
109 | /**
110 | Whether the connection was cancelled, by invoking `disconnect()`.
111 | */
112 | private var connectionCancelled = false {
113 | didSet { peripheralDelegate.connectionCancelled = connectionCancelled }
114 | }
115 |
116 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
117 | os_log("centralManagerDidUpdateState %d", log: log, type: .debug, central.state.rawValue)
118 |
119 | guard peripheral == nil else {
120 | os_log("Already connected", log: log, type: .debug)
121 | return
122 | }
123 |
124 | if connectionCancelled {
125 | os_log("Connection cancelled", log: log, type: .debug)
126 | return
127 | }
128 |
129 | guard central.state == .poweredOn else {
130 | os_log("Invalid state, not connecting: %d", log: log, type: .error, central.state.rawValue)
131 | parent.connectionState = .failed
132 | return
133 | }
134 |
135 | guard let peripheral = central.retrievePeripherals(withIdentifiers: [peripheralIdentifier]).first else {
136 | os_log("Could not find peripheral with id %@", log: log, type: .error, peripheralIdentifier.uuidString)
137 | parent.connectionState = .failed
138 | return
139 | }
140 |
141 | os_log("Connecting to %@", log: log, type: .info, peripheral)
142 | self.peripheral = peripheral
143 | parent.connectionState = .connecting
144 | central.connect(peripheral, options: nil)
145 | }
146 |
147 | private var isDiscoveringServices = false
148 |
149 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
150 | if connectionCancelled {
151 | os_log("Manager did connect to peripheral, but connection was cancelled", log: log, type: .error)
152 | central.cancelPeripheralConnection(peripheral)
153 | return
154 | }
155 |
156 | if self.peripheral != peripheral {
157 | os_log("Manager did connect to different peripheral, disconnecting", log: log, type: .error)
158 | central.cancelPeripheralConnection(peripheral)
159 | return
160 | }
161 |
162 | os_log("centralManager didConnect %@", log: log, type: .debug, peripheral)
163 | if isDiscoveringServices {
164 | os_log("Already discovering services")
165 | return
166 | }
167 |
168 | os_log("Discovering services", log: log, type: .debug)
169 | isDiscoveringServices = true
170 | peripheral.delegate = peripheralDelegate
171 | peripheral.discoverServices(nil)
172 | }
173 |
174 | func centralManager(
175 | _ central: CBCentralManager,
176 | didDisconnectPeripheral peripheral: CBPeripheral,
177 | error: Error?
178 | ) {
179 | os_log("centralManager didDisconnectPeripheral %@", log: log, type: .debug, peripheral)
180 | if self.peripheral != peripheral {
181 | os_log("Disconnected peripheral differs from wanted peripheral")
182 | return
183 | }
184 |
185 | parent.delegate = nil
186 | parent.manager = nil
187 | parent.connectionState = .disconnected
188 | }
189 |
190 | func disconnect(_ manager: CBCentralManager) {
191 | guard let peripheral = peripheral else {
192 | os_log("Peripheral connection not yet initiated, cancelling", log: log, type: .error)
193 | connectionCancelled = true
194 |
195 | parent.delegate = nil
196 | parent.manager = nil
197 | return
198 | }
199 |
200 | manager.cancelPeripheralConnection(peripheral)
201 | connectionCancelled = true
202 | }
203 | }
204 |
205 | /**
206 | A `CBPeripheralDelegate` which sets up the connection with the peripheral once initiated.
207 | */
208 | private class BleConnectionPeripheralDelegate: NSObject, CBPeripheralDelegate {
209 |
210 | private unowned let parent: CBConnection
211 | var connectionCancelled = false
212 |
213 | init(
214 | _ parent: CBConnection
215 | ) {
216 | self.parent = parent
217 | }
218 |
219 | private var pendingServices: [CBService] = []
220 |
221 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
222 | if connectionCancelled {
223 | os_log("Peripheral did discover services but connection was cancelled.", log: log, type: .debug)
224 | return
225 | }
226 |
227 | if let error = error {
228 | os_log(
229 | "An error occurred while discovering services: %@",
230 | log: log,
231 | type: .error,
232 | String(describing: error)
233 | )
234 | parent.connectionState = .failed
235 | return
236 | }
237 |
238 | os_log("peripheral didDiscoverServices %@", log: log, type: .debug, peripheral.services!)
239 | guard let services = peripheral.services else {
240 | os_log("Device has no services", log: log, type: .error)
241 | parent.connectionState = .failed
242 | return
243 | }
244 |
245 | pendingServices = services
246 | continueDiscovering(peripheral)
247 | }
248 |
249 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
250 | if connectionCancelled {
251 | os_log("Peripheral did discover characteristics but connection was cancelled", log: log, type: .debug)
252 | return
253 | }
254 |
255 | if let error = error {
256 | os_log(
257 | "An error occurred while discovering characteristics: %@",
258 | log: log,
259 | type: .error,
260 | String(describing: error)
261 | )
262 | parent.connectionState = .failed
263 | return
264 | }
265 |
266 | os_log(
267 | "peripheral didDiscoverCharacteristicsFor %@(%@): %@",
268 | log: log,
269 | type: .debug,
270 | service,
271 | service.uuid.uuidString,
272 | service.characteristics!
273 | )
274 | continueDiscovering(peripheral)
275 | }
276 |
277 | private func continueDiscovering(_ peripheral: CBPeripheral) {
278 | guard let service = pendingServices.first else {
279 | os_log("Done discovering services and characteristics", log: log, type: .debug)
280 | parent.connectionState = .connected(device: CBConnectedDevice(from: peripheral))
281 | return
282 | }
283 |
284 | pendingServices.removeAll { s -> Bool in
285 | s === service
286 | }
287 | os_log("Discovering characteristics for %@(%@)", log: log, type: .debug, service, service.uuid.uuidString)
288 | peripheral.discoverCharacteristics(nil, for: service)
289 | }
290 | }
291 |
292 | private class CancelListening: Cancellable {
293 |
294 | private weak var parent: CBConnection?
295 | private let listener: CBConnectionStateListener
296 |
297 | init(
298 | _ parent: CBConnection,
299 | _ listener: CBConnectionStateListener
300 | ) {
301 | self.parent = parent
302 | self.listener = listener
303 | }
304 |
305 | func cancel() {
306 | parent?.listeners.removeAll { l in l === listener }
307 | }
308 |
309 | deinit {
310 | cancel()
311 | }
312 | }
313 | }
314 |
--------------------------------------------------------------------------------
/iOS Example/iOS Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8C40E20A24BF57D0004AE938 /* DeviceDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1E424BF57D0004AE938 /* DeviceDetailsViewModel.swift */; };
11 | 8C40E20B24BF57D0004AE938 /* ConnectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1E524BF57D0004AE938 /* ConnectionStatus.swift */; };
12 | 8C40E20C24BF57D0004AE938 /* DeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1E624BF57D0004AE938 /* DeviceDetailsView.swift */; };
13 | 8C40E20D24BF57D0004AE938 /* DeviceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1E824BF57D0004AE938 /* DeviceListView.swift */; };
14 | 8C40E20E24BF57D0004AE938 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1E924BF57D0004AE938 /* Device.swift */; };
15 | 8C40E20F24BF57D0004AE938 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1EA24BF57D0004AE938 /* DevicesViewModel.swift */; };
16 | 8C40E21024BF57D0004AE938 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1EB24BF57D0004AE938 /* DevicesView.swift */; };
17 | 8C40E21124BF57D0004AE938 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1EC24BF57D0004AE938 /* SceneDelegate.swift */; };
18 | 8C40E21224BF57D0004AE938 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8C40E1ED24BF57D0004AE938 /* Info.plist */; };
19 | 8C40E21324BF57D0004AE938 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8C40E1EF24BF57D0004AE938 /* LaunchScreen.storyboard */; };
20 | 8C40E21424BF57D0004AE938 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C40E1F224BF57D0004AE938 /* Preview Assets.xcassets */; };
21 | 8C40E21524BF57D0004AE938 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C40E1F324BF57D0004AE938 /* Assets.xcassets */; };
22 | 8C40E21724BF57D0004AE938 /* CBConnectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1F824BF57D0004AE938 /* CBConnectionFactory.swift */; };
23 | 8C40E21824BF57D0004AE938 /* CBConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1F924BF57D0004AE938 /* CBConnectedDevice.swift */; };
24 | 8C40E21924BF57D0004AE938 /* CBConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1FA24BF57D0004AE938 /* CBConnection.swift */; };
25 | 8C40E21B24BF57D0004AE938 /* CBConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1FC24BF57D0004AE938 /* CBConnectionState.swift */; };
26 | 8C40E21C24BF57D0004AE938 /* CBConnectionStateListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E1FD24BF57D0004AE938 /* CBConnectionStateListener.swift */; };
27 | 8C40E21F24BF57D0004AE938 /* CBScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E20224BF57D0004AE938 /* CBScanResult.swift */; };
28 | 8C40E22124BF57D0004AE938 /* ConnectedRowerDataDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E20524BF57D0004AE938 /* ConnectedRowerDataDevice.swift */; };
29 | 8C40E22224BF57D0004AE938 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E20724BF57D0004AE938 /* AppDelegate.swift */; };
30 | 8C40E22324BF57D0004AE938 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C40E20924BF57D0004AE938 /* Cancellable.swift */; };
31 | 8C66A09A24C99B07006DD6AE /* CBScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C66A09924C99B07006DD6AE /* CBScanner.swift */; };
32 | 8CA305E524C1BC480073B763 /* WaterRowerData-BLE in Frameworks */ = {isa = PBXBuildFile; productRef = 8CA305E424C1BC480073B763 /* WaterRowerData-BLE */; };
33 | /* End PBXBuildFile section */
34 |
35 | /* Begin PBXFileReference section */
36 | 8C40E18524BF555C004AE938 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
37 | 8C40E1E424BF57D0004AE938 /* DeviceDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDetailsViewModel.swift; sourceTree = ""; };
38 | 8C40E1E524BF57D0004AE938 /* ConnectionStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionStatus.swift; sourceTree = ""; };
39 | 8C40E1E624BF57D0004AE938 /* DeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDetailsView.swift; sourceTree = ""; };
40 | 8C40E1E824BF57D0004AE938 /* DeviceListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceListView.swift; sourceTree = ""; };
41 | 8C40E1E924BF57D0004AE938 /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; };
42 | 8C40E1EA24BF57D0004AE938 /* DevicesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; };
43 | 8C40E1EB24BF57D0004AE938 /* DevicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; };
44 | 8C40E1EC24BF57D0004AE938 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
45 | 8C40E1ED24BF57D0004AE938 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
46 | 8C40E1F024BF57D0004AE938 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = ""; };
47 | 8C40E1F224BF57D0004AE938 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
48 | 8C40E1F324BF57D0004AE938 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
49 | 8C40E1F824BF57D0004AE938 /* CBConnectionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBConnectionFactory.swift; sourceTree = ""; };
50 | 8C40E1F924BF57D0004AE938 /* CBConnectedDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBConnectedDevice.swift; sourceTree = ""; };
51 | 8C40E1FA24BF57D0004AE938 /* CBConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBConnection.swift; sourceTree = ""; };
52 | 8C40E1FC24BF57D0004AE938 /* CBConnectionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBConnectionState.swift; sourceTree = ""; };
53 | 8C40E1FD24BF57D0004AE938 /* CBConnectionStateListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBConnectionStateListener.swift; sourceTree = ""; };
54 | 8C40E20224BF57D0004AE938 /* CBScanResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBScanResult.swift; sourceTree = ""; };
55 | 8C40E20524BF57D0004AE938 /* ConnectedRowerDataDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedRowerDataDevice.swift; sourceTree = ""; };
56 | 8C40E20624BF57D0004AE938 /* iOS Example.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "iOS Example.entitlements"; sourceTree = ""; };
57 | 8C40E20724BF57D0004AE938 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
58 | 8C40E20924BF57D0004AE938 /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = ""; };
59 | 8C66A09924C99B07006DD6AE /* CBScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBScanner.swift; sourceTree = ""; };
60 | /* End PBXFileReference section */
61 |
62 | /* Begin PBXFrameworksBuildPhase section */
63 | 8C40E18224BF555C004AE938 /* Frameworks */ = {
64 | isa = PBXFrameworksBuildPhase;
65 | buildActionMask = 2147483647;
66 | files = (
67 | 8CA305E524C1BC480073B763 /* WaterRowerData-BLE in Frameworks */,
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | /* End PBXFrameworksBuildPhase section */
72 |
73 | /* Begin PBXGroup section */
74 | 8C40E17C24BF555C004AE938 = {
75 | isa = PBXGroup;
76 | children = (
77 | 8C40E18724BF555C004AE938 /* iOS Example */,
78 | 8C40E18624BF555C004AE938 /* Products */,
79 | 8C40E19C24BF5623004AE938 /* Frameworks */,
80 | );
81 | sourceTree = "";
82 | };
83 | 8C40E18624BF555C004AE938 /* Products */ = {
84 | isa = PBXGroup;
85 | children = (
86 | 8C40E18524BF555C004AE938 /* iOS Example.app */,
87 | );
88 | name = Products;
89 | sourceTree = "";
90 | };
91 | 8C40E18724BF555C004AE938 /* iOS Example */ = {
92 | isa = PBXGroup;
93 | children = (
94 | 8C40E20724BF57D0004AE938 /* AppDelegate.swift */,
95 | 8C40E1F324BF57D0004AE938 /* Assets.xcassets */,
96 | 8C40E1EE24BF57D0004AE938 /* Base.lproj */,
97 | 8C40E1F424BF57D0004AE938 /* Bluetooth */,
98 | 8C40E20624BF57D0004AE938 /* iOS Example.entitlements */,
99 | 8C40E1ED24BF57D0004AE938 /* Info.plist */,
100 | 8C40E1F124BF57D0004AE938 /* Preview Content */,
101 | 8C40E1EC24BF57D0004AE938 /* SceneDelegate.swift */,
102 | 8C40E1E224BF57D0004AE938 /* UI */,
103 | 8C40E20824BF57D0004AE938 /* Util */,
104 | );
105 | path = "iOS Example";
106 | sourceTree = "";
107 | };
108 | 8C40E19C24BF5623004AE938 /* Frameworks */ = {
109 | isa = PBXGroup;
110 | children = (
111 | );
112 | name = Frameworks;
113 | sourceTree = "";
114 | };
115 | 8C40E1E224BF57D0004AE938 /* UI */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 8C40E1E324BF57D0004AE938 /* DeviceDetails */,
119 | 8C40E1E724BF57D0004AE938 /* Devices */,
120 | );
121 | path = UI;
122 | sourceTree = "";
123 | };
124 | 8C40E1E324BF57D0004AE938 /* DeviceDetails */ = {
125 | isa = PBXGroup;
126 | children = (
127 | 8C40E1E424BF57D0004AE938 /* DeviceDetailsViewModel.swift */,
128 | 8C40E1E524BF57D0004AE938 /* ConnectionStatus.swift */,
129 | 8C40E1E624BF57D0004AE938 /* DeviceDetailsView.swift */,
130 | );
131 | path = DeviceDetails;
132 | sourceTree = "";
133 | };
134 | 8C40E1E724BF57D0004AE938 /* Devices */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 8C40E1E824BF57D0004AE938 /* DeviceListView.swift */,
138 | 8C40E1E924BF57D0004AE938 /* Device.swift */,
139 | 8C40E1EA24BF57D0004AE938 /* DevicesViewModel.swift */,
140 | 8C40E1EB24BF57D0004AE938 /* DevicesView.swift */,
141 | );
142 | path = Devices;
143 | sourceTree = "";
144 | };
145 | 8C40E1EE24BF57D0004AE938 /* Base.lproj */ = {
146 | isa = PBXGroup;
147 | children = (
148 | 8C40E1EF24BF57D0004AE938 /* LaunchScreen.storyboard */,
149 | );
150 | path = Base.lproj;
151 | sourceTree = "";
152 | };
153 | 8C40E1F124BF57D0004AE938 /* Preview Content */ = {
154 | isa = PBXGroup;
155 | children = (
156 | 8C40E1F224BF57D0004AE938 /* Preview Assets.xcassets */,
157 | );
158 | path = "Preview Content";
159 | sourceTree = "";
160 | };
161 | 8C40E1F424BF57D0004AE938 /* Bluetooth */ = {
162 | isa = PBXGroup;
163 | children = (
164 | 8C40E1F524BF57D0004AE938 /* Connection */,
165 | 8C40E1FE24BF57D0004AE938 /* Discovery */,
166 | 8C40E20424BF57D0004AE938 /* RowerData */,
167 | );
168 | path = Bluetooth;
169 | sourceTree = "";
170 | };
171 | 8C40E1F524BF57D0004AE938 /* Connection */ = {
172 | isa = PBXGroup;
173 | children = (
174 | 8C40E1FA24BF57D0004AE938 /* CBConnection.swift */,
175 | 8C40E1F824BF57D0004AE938 /* CBConnectionFactory.swift */,
176 | 8C40E1FC24BF57D0004AE938 /* CBConnectionState.swift */,
177 | 8C40E1FD24BF57D0004AE938 /* CBConnectionStateListener.swift */,
178 | 8C40E1F924BF57D0004AE938 /* CBConnectedDevice.swift */,
179 | );
180 | path = Connection;
181 | sourceTree = "";
182 | };
183 | 8C40E1FE24BF57D0004AE938 /* Discovery */ = {
184 | isa = PBXGroup;
185 | children = (
186 | 8C66A09924C99B07006DD6AE /* CBScanner.swift */,
187 | 8C40E20224BF57D0004AE938 /* CBScanResult.swift */,
188 | );
189 | path = Discovery;
190 | sourceTree = "";
191 | };
192 | 8C40E20424BF57D0004AE938 /* RowerData */ = {
193 | isa = PBXGroup;
194 | children = (
195 | 8C40E20524BF57D0004AE938 /* ConnectedRowerDataDevice.swift */,
196 | );
197 | path = RowerData;
198 | sourceTree = "";
199 | };
200 | 8C40E20824BF57D0004AE938 /* Util */ = {
201 | isa = PBXGroup;
202 | children = (
203 | 8C40E20924BF57D0004AE938 /* Cancellable.swift */,
204 | );
205 | path = Util;
206 | sourceTree = "";
207 | };
208 | /* End PBXGroup section */
209 |
210 | /* Begin PBXNativeTarget section */
211 | 8C40E18424BF555C004AE938 /* iOS Example */ = {
212 | isa = PBXNativeTarget;
213 | buildConfigurationList = 8C40E19924BF555D004AE938 /* Build configuration list for PBXNativeTarget "iOS Example" */;
214 | buildPhases = (
215 | 8C40E18124BF555C004AE938 /* Sources */,
216 | 8C40E18224BF555C004AE938 /* Frameworks */,
217 | 8C40E18324BF555C004AE938 /* Resources */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | );
223 | name = "iOS Example";
224 | packageProductDependencies = (
225 | 8CA305E424C1BC480073B763 /* WaterRowerData-BLE */,
226 | );
227 | productName = "iOS Example";
228 | productReference = 8C40E18524BF555C004AE938 /* iOS Example.app */;
229 | productType = "com.apple.product-type.application";
230 | };
231 | /* End PBXNativeTarget section */
232 |
233 | /* Begin PBXProject section */
234 | 8C40E17D24BF555C004AE938 /* Project object */ = {
235 | isa = PBXProject;
236 | attributes = {
237 | LastSwiftUpdateCheck = 1150;
238 | LastUpgradeCheck = 1150;
239 | ORGANIZATIONNAME = WaterRower;
240 | TargetAttributes = {
241 | 8C40E18424BF555C004AE938 = {
242 | CreatedOnToolsVersion = 11.5;
243 | LastSwiftMigration = 1150;
244 | };
245 | };
246 | };
247 | buildConfigurationList = 8C40E18024BF555C004AE938 /* Build configuration list for PBXProject "iOS Example" */;
248 | compatibilityVersion = "Xcode 9.3";
249 | developmentRegion = en;
250 | hasScannedForEncodings = 0;
251 | knownRegions = (
252 | en,
253 | Base,
254 | );
255 | mainGroup = 8C40E17C24BF555C004AE938;
256 | productRefGroup = 8C40E18624BF555C004AE938 /* Products */;
257 | projectDirPath = "";
258 | projectRoot = "";
259 | targets = (
260 | 8C40E18424BF555C004AE938 /* iOS Example */,
261 | );
262 | };
263 | /* End PBXProject section */
264 |
265 | /* Begin PBXResourcesBuildPhase section */
266 | 8C40E18324BF555C004AE938 /* Resources */ = {
267 | isa = PBXResourcesBuildPhase;
268 | buildActionMask = 2147483647;
269 | files = (
270 | 8C40E21224BF57D0004AE938 /* Info.plist in Resources */,
271 | 8C40E21424BF57D0004AE938 /* Preview Assets.xcassets in Resources */,
272 | 8C40E21524BF57D0004AE938 /* Assets.xcassets in Resources */,
273 | 8C40E21324BF57D0004AE938 /* LaunchScreen.storyboard in Resources */,
274 | );
275 | runOnlyForDeploymentPostprocessing = 0;
276 | };
277 | /* End PBXResourcesBuildPhase section */
278 |
279 | /* Begin PBXSourcesBuildPhase section */
280 | 8C40E18124BF555C004AE938 /* Sources */ = {
281 | isa = PBXSourcesBuildPhase;
282 | buildActionMask = 2147483647;
283 | files = (
284 | 8C40E20A24BF57D0004AE938 /* DeviceDetailsViewModel.swift in Sources */,
285 | 8C40E20B24BF57D0004AE938 /* ConnectionStatus.swift in Sources */,
286 | 8C40E20D24BF57D0004AE938 /* DeviceListView.swift in Sources */,
287 | 8C40E20C24BF57D0004AE938 /* DeviceDetailsView.swift in Sources */,
288 | 8C40E21724BF57D0004AE938 /* CBConnectionFactory.swift in Sources */,
289 | 8C40E20F24BF57D0004AE938 /* DevicesViewModel.swift in Sources */,
290 | 8C40E21824BF57D0004AE938 /* CBConnectedDevice.swift in Sources */,
291 | 8C40E21924BF57D0004AE938 /* CBConnection.swift in Sources */,
292 | 8C40E21B24BF57D0004AE938 /* CBConnectionState.swift in Sources */,
293 | 8C40E21F24BF57D0004AE938 /* CBScanResult.swift in Sources */,
294 | 8C66A09A24C99B07006DD6AE /* CBScanner.swift in Sources */,
295 | 8C40E21C24BF57D0004AE938 /* CBConnectionStateListener.swift in Sources */,
296 | 8C40E20E24BF57D0004AE938 /* Device.swift in Sources */,
297 | 8C40E21024BF57D0004AE938 /* DevicesView.swift in Sources */,
298 | 8C40E22124BF57D0004AE938 /* ConnectedRowerDataDevice.swift in Sources */,
299 | 8C40E22324BF57D0004AE938 /* Cancellable.swift in Sources */,
300 | 8C40E22224BF57D0004AE938 /* AppDelegate.swift in Sources */,
301 | 8C40E21124BF57D0004AE938 /* SceneDelegate.swift in Sources */,
302 | );
303 | runOnlyForDeploymentPostprocessing = 0;
304 | };
305 | /* End PBXSourcesBuildPhase section */
306 |
307 | /* Begin PBXVariantGroup section */
308 | 8C40E1EF24BF57D0004AE938 /* LaunchScreen.storyboard */ = {
309 | isa = PBXVariantGroup;
310 | children = (
311 | 8C40E1F024BF57D0004AE938 /* Base */,
312 | );
313 | name = LaunchScreen.storyboard;
314 | sourceTree = "";
315 | };
316 | /* End PBXVariantGroup section */
317 |
318 | /* Begin XCBuildConfiguration section */
319 | 8C40E19724BF555D004AE938 /* Debug */ = {
320 | isa = XCBuildConfiguration;
321 | buildSettings = {
322 | ALWAYS_SEARCH_USER_PATHS = NO;
323 | CLANG_ANALYZER_NONNULL = YES;
324 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
326 | CLANG_CXX_LIBRARY = "libc++";
327 | CLANG_ENABLE_MODULES = YES;
328 | CLANG_ENABLE_OBJC_ARC = YES;
329 | CLANG_ENABLE_OBJC_WEAK = YES;
330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
331 | CLANG_WARN_BOOL_CONVERSION = YES;
332 | CLANG_WARN_COMMA = YES;
333 | CLANG_WARN_CONSTANT_CONVERSION = YES;
334 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
336 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
337 | CLANG_WARN_EMPTY_BODY = YES;
338 | CLANG_WARN_ENUM_CONVERSION = YES;
339 | CLANG_WARN_INFINITE_RECURSION = YES;
340 | CLANG_WARN_INT_CONVERSION = YES;
341 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
342 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
346 | CLANG_WARN_STRICT_PROTOTYPES = YES;
347 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
349 | CLANG_WARN_UNREACHABLE_CODE = YES;
350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
351 | COPY_PHASE_STRIP = NO;
352 | DEBUG_INFORMATION_FORMAT = dwarf;
353 | ENABLE_STRICT_OBJC_MSGSEND = YES;
354 | ENABLE_TESTABILITY = YES;
355 | GCC_C_LANGUAGE_STANDARD = gnu11;
356 | GCC_DYNAMIC_NO_PIC = NO;
357 | GCC_NO_COMMON_BLOCKS = YES;
358 | GCC_OPTIMIZATION_LEVEL = 0;
359 | GCC_PREPROCESSOR_DEFINITIONS = (
360 | "DEBUG=1",
361 | "$(inherited)",
362 | );
363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
365 | GCC_WARN_UNDECLARED_SELECTOR = YES;
366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
367 | GCC_WARN_UNUSED_FUNCTION = YES;
368 | GCC_WARN_UNUSED_VARIABLE = YES;
369 | IPHONEOS_DEPLOYMENT_TARGET = 13.5;
370 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
371 | MTL_FAST_MATH = YES;
372 | ONLY_ACTIVE_ARCH = YES;
373 | SDKROOT = iphoneos;
374 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
375 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
376 | };
377 | name = Debug;
378 | };
379 | 8C40E19824BF555D004AE938 /* Release */ = {
380 | isa = XCBuildConfiguration;
381 | buildSettings = {
382 | ALWAYS_SEARCH_USER_PATHS = NO;
383 | CLANG_ANALYZER_NONNULL = YES;
384 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
385 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
386 | CLANG_CXX_LIBRARY = "libc++";
387 | CLANG_ENABLE_MODULES = YES;
388 | CLANG_ENABLE_OBJC_ARC = YES;
389 | CLANG_ENABLE_OBJC_WEAK = YES;
390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
391 | CLANG_WARN_BOOL_CONVERSION = YES;
392 | CLANG_WARN_COMMA = YES;
393 | CLANG_WARN_CONSTANT_CONVERSION = YES;
394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
396 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
397 | CLANG_WARN_EMPTY_BODY = YES;
398 | CLANG_WARN_ENUM_CONVERSION = YES;
399 | CLANG_WARN_INFINITE_RECURSION = YES;
400 | CLANG_WARN_INT_CONVERSION = YES;
401 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
402 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
405 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
406 | CLANG_WARN_STRICT_PROTOTYPES = YES;
407 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
408 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
409 | CLANG_WARN_UNREACHABLE_CODE = YES;
410 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
411 | COPY_PHASE_STRIP = NO;
412 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
413 | ENABLE_NS_ASSERTIONS = NO;
414 | ENABLE_STRICT_OBJC_MSGSEND = YES;
415 | GCC_C_LANGUAGE_STANDARD = gnu11;
416 | GCC_NO_COMMON_BLOCKS = YES;
417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
419 | GCC_WARN_UNDECLARED_SELECTOR = YES;
420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
421 | GCC_WARN_UNUSED_FUNCTION = YES;
422 | GCC_WARN_UNUSED_VARIABLE = YES;
423 | IPHONEOS_DEPLOYMENT_TARGET = 13.5;
424 | MTL_ENABLE_DEBUG_INFO = NO;
425 | MTL_FAST_MATH = YES;
426 | SDKROOT = iphoneos;
427 | SWIFT_COMPILATION_MODE = wholemodule;
428 | SWIFT_OPTIMIZATION_LEVEL = "-O";
429 | VALIDATE_PRODUCT = YES;
430 | };
431 | name = Release;
432 | };
433 | 8C40E19A24BF555D004AE938 /* Debug */ = {
434 | isa = XCBuildConfiguration;
435 | buildSettings = {
436 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
437 | CLANG_ENABLE_MODULES = YES;
438 | CODE_SIGN_ENTITLEMENTS = "iOS Example/iOS Example.entitlements";
439 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
440 | CODE_SIGN_STYLE = Automatic;
441 | DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\"";
442 | DEVELOPMENT_TEAM = CMQS4E74X4;
443 | ENABLE_PREVIEWS = YES;
444 | INFOPLIST_FILE = "iOS Example/Info.plist";
445 | LD_RUNPATH_SEARCH_PATHS = (
446 | "$(inherited)",
447 | "@executable_path/Frameworks",
448 | );
449 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.waterrower.iOS-Example";
450 | PRODUCT_NAME = "$(TARGET_NAME)";
451 | SUPPORTS_MACCATALYST = YES;
452 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
453 | SWIFT_VERSION = 5.0;
454 | TARGETED_DEVICE_FAMILY = "1,2";
455 | };
456 | name = Debug;
457 | };
458 | 8C40E19B24BF555D004AE938 /* Release */ = {
459 | isa = XCBuildConfiguration;
460 | buildSettings = {
461 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
462 | CLANG_ENABLE_MODULES = YES;
463 | CODE_SIGN_ENTITLEMENTS = "iOS Example/iOS Example.entitlements";
464 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
465 | CODE_SIGN_STYLE = Automatic;
466 | DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\"";
467 | DEVELOPMENT_TEAM = CMQS4E74X4;
468 | ENABLE_PREVIEWS = YES;
469 | INFOPLIST_FILE = "iOS Example/Info.plist";
470 | LD_RUNPATH_SEARCH_PATHS = (
471 | "$(inherited)",
472 | "@executable_path/Frameworks",
473 | );
474 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.waterrower.iOS-Example";
475 | PRODUCT_NAME = "$(TARGET_NAME)";
476 | SUPPORTS_MACCATALYST = YES;
477 | SWIFT_VERSION = 5.0;
478 | TARGETED_DEVICE_FAMILY = "1,2";
479 | };
480 | name = Release;
481 | };
482 | /* End XCBuildConfiguration section */
483 |
484 | /* Begin XCConfigurationList section */
485 | 8C40E18024BF555C004AE938 /* Build configuration list for PBXProject "iOS Example" */ = {
486 | isa = XCConfigurationList;
487 | buildConfigurations = (
488 | 8C40E19724BF555D004AE938 /* Debug */,
489 | 8C40E19824BF555D004AE938 /* Release */,
490 | );
491 | defaultConfigurationIsVisible = 0;
492 | defaultConfigurationName = Release;
493 | };
494 | 8C40E19924BF555D004AE938 /* Build configuration list for PBXNativeTarget "iOS Example" */ = {
495 | isa = XCConfigurationList;
496 | buildConfigurations = (
497 | 8C40E19A24BF555D004AE938 /* Debug */,
498 | 8C40E19B24BF555D004AE938 /* Release */,
499 | );
500 | defaultConfigurationIsVisible = 0;
501 | defaultConfigurationName = Release;
502 | };
503 | /* End XCConfigurationList section */
504 |
505 | /* Begin XCSwiftPackageProductDependency section */
506 | 8CA305E424C1BC480073B763 /* WaterRowerData-BLE */ = {
507 | isa = XCSwiftPackageProductDependency;
508 | productName = "WaterRowerData-BLE";
509 | };
510 | /* End XCSwiftPackageProductDependency section */
511 | };
512 | rootObject = 8C40E17D24BF555C004AE938 /* Project object */;
513 | }
514 |
--------------------------------------------------------------------------------
/WaterRowerData-iOS.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXAggregateTarget section */
10 | "WaterRowerData-iOS::WaterRowerData-iOSPackageTests::ProductTarget" /* WaterRowerData-iOSPackageTests */ = {
11 | isa = PBXAggregateTarget;
12 | buildConfigurationList = OBJ_102 /* Build configuration list for PBXAggregateTarget "WaterRowerData-iOSPackageTests" */;
13 | buildPhases = (
14 | );
15 | dependencies = (
16 | OBJ_105 /* PBXTargetDependency */,
17 | );
18 | name = "WaterRowerData-iOSPackageTests";
19 | productName = "WaterRowerData-iOSPackageTests";
20 | };
21 | /* End PBXAggregateTarget section */
22 |
23 | /* Begin PBXBuildFile section */
24 | OBJ_100 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
25 | OBJ_56 /* FitnessMachineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* FitnessMachineService.swift */; };
26 | OBJ_57 /* BitRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* BitRequirement.swift */; };
27 | OBJ_58 /* Data+ReadFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* Data+ReadFormat.swift */; };
28 | OBJ_59 /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Field.swift */; };
29 | OBJ_60 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Format.swift */; };
30 | OBJ_61 /* Requirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Requirement.swift */; };
31 | OBJ_62 /* RowerDataAveragePaceField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* RowerDataAveragePaceField.swift */; };
32 | OBJ_63 /* RowerDataAveragePowerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* RowerDataAveragePowerField.swift */; };
33 | OBJ_64 /* RowerDataAverageStrokeRateField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* RowerDataAverageStrokeRateField.swift */; };
34 | OBJ_65 /* RowerDataElapsedTimeField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* RowerDataElapsedTimeField.swift */; };
35 | OBJ_66 /* RowerDataEnergyPerHourField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* RowerDataEnergyPerHourField.swift */; };
36 | OBJ_67 /* RowerDataEnergyPerMinuteField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* RowerDataEnergyPerMinuteField.swift */; };
37 | OBJ_68 /* RowerDataFlagsField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* RowerDataFlagsField.swift */; };
38 | OBJ_69 /* RowerDataHeartRateField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* RowerDataHeartRateField.swift */; };
39 | OBJ_70 /* RowerDataInstantaneousPaceField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* RowerDataInstantaneousPaceField.swift */; };
40 | OBJ_71 /* RowerDataInstantaneousPowerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* RowerDataInstantaneousPowerField.swift */; };
41 | OBJ_72 /* RowerDataMetabolicEquivalentField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* RowerDataMetabolicEquivalentField.swift */; };
42 | OBJ_73 /* RowerDataRemainingTimeField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* RowerDataRemainingTimeField.swift */; };
43 | OBJ_74 /* RowerDataResistanceLevelField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* RowerDataResistanceLevelField.swift */; };
44 | OBJ_75 /* RowerDataStrokeCountField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* RowerDataStrokeCountField.swift */; };
45 | OBJ_76 /* RowerDataStrokeRateField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* RowerDataStrokeRateField.swift */; };
46 | OBJ_77 /* RowerDataTotalDistanceField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* RowerDataTotalDistanceField.swift */; };
47 | OBJ_78 /* RowerDataTotalEnergyField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* RowerDataTotalEnergyField.swift */; };
48 | OBJ_79 /* RowerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* RowerData.swift */; };
49 | OBJ_80 /* RowerDataCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* RowerDataCharacteristic.swift */; };
50 | OBJ_87 /* CharacteristicData.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_41 /* CharacteristicData.swift */; };
51 | OBJ_88 /* CharacteristicFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_42 /* CharacteristicFlags.swift */; };
52 | OBJ_89 /* RowerDataCharacteristicFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_43 /* RowerDataCharacteristicFlags.swift */; };
53 | OBJ_90 /* RowerDataCharacteristicTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_44 /* RowerDataCharacteristicTest.swift */; };
54 | OBJ_93 /* WaterRowerData_BLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "WaterRowerData-iOS::WaterRowerData-BLE::Product" /* WaterRowerData_BLE.framework */; };
55 | /* End PBXBuildFile section */
56 |
57 | /* Begin PBXContainerItemProxy section */
58 | 8CA966A624C1BB230002B6CD /* PBXContainerItemProxy */ = {
59 | isa = PBXContainerItemProxy;
60 | containerPortal = OBJ_1 /* Project object */;
61 | proxyType = 1;
62 | remoteGlobalIDString = "WaterRowerData-iOS::WaterRowerData-BLE";
63 | remoteInfo = "WaterRowerData-BLE";
64 | };
65 | 8CA966A724C1BB260002B6CD /* PBXContainerItemProxy */ = {
66 | isa = PBXContainerItemProxy;
67 | containerPortal = OBJ_1 /* Project object */;
68 | proxyType = 1;
69 | remoteGlobalIDString = "WaterRowerData-iOS::WaterRowerData-BLETests";
70 | remoteInfo = "WaterRowerData-BLETests";
71 | };
72 | /* End PBXContainerItemProxy section */
73 |
74 | /* Begin PBXFileReference section */
75 | OBJ_10 /* FitnessMachineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitnessMachineService.swift; sourceTree = ""; };
76 | OBJ_13 /* BitRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitRequirement.swift; sourceTree = ""; };
77 | OBJ_14 /* Data+ReadFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+ReadFormat.swift"; sourceTree = ""; };
78 | OBJ_15 /* Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Field.swift; sourceTree = ""; };
79 | OBJ_16 /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = ""; };
80 | OBJ_17 /* Requirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requirement.swift; sourceTree = ""; };
81 | OBJ_19 /* RowerDataAveragePaceField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataAveragePaceField.swift; sourceTree = ""; };
82 | OBJ_20 /* RowerDataAveragePowerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataAveragePowerField.swift; sourceTree = ""; };
83 | OBJ_21 /* RowerDataAverageStrokeRateField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataAverageStrokeRateField.swift; sourceTree = ""; };
84 | OBJ_22 /* RowerDataElapsedTimeField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataElapsedTimeField.swift; sourceTree = ""; };
85 | OBJ_23 /* RowerDataEnergyPerHourField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataEnergyPerHourField.swift; sourceTree = ""; };
86 | OBJ_24 /* RowerDataEnergyPerMinuteField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataEnergyPerMinuteField.swift; sourceTree = ""; };
87 | OBJ_25 /* RowerDataFlagsField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataFlagsField.swift; sourceTree = ""; };
88 | OBJ_26 /* RowerDataHeartRateField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataHeartRateField.swift; sourceTree = ""; };
89 | OBJ_27 /* RowerDataInstantaneousPaceField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataInstantaneousPaceField.swift; sourceTree = ""; };
90 | OBJ_28 /* RowerDataInstantaneousPowerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataInstantaneousPowerField.swift; sourceTree = ""; };
91 | OBJ_29 /* RowerDataMetabolicEquivalentField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataMetabolicEquivalentField.swift; sourceTree = ""; };
92 | OBJ_30 /* RowerDataRemainingTimeField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataRemainingTimeField.swift; sourceTree = ""; };
93 | OBJ_31 /* RowerDataResistanceLevelField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataResistanceLevelField.swift; sourceTree = ""; };
94 | OBJ_32 /* RowerDataStrokeCountField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataStrokeCountField.swift; sourceTree = ""; };
95 | OBJ_33 /* RowerDataStrokeRateField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataStrokeRateField.swift; sourceTree = ""; };
96 | OBJ_34 /* RowerDataTotalDistanceField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataTotalDistanceField.swift; sourceTree = ""; };
97 | OBJ_35 /* RowerDataTotalEnergyField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataTotalEnergyField.swift; sourceTree = ""; };
98 | OBJ_36 /* RowerData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerData.swift; sourceTree = ""; };
99 | OBJ_37 /* RowerDataCharacteristic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataCharacteristic.swift; sourceTree = ""; };
100 | OBJ_40 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
101 | OBJ_41 /* CharacteristicData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacteristicData.swift; sourceTree = ""; };
102 | OBJ_42 /* CharacteristicFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacteristicFlags.swift; sourceTree = ""; };
103 | OBJ_43 /* RowerDataCharacteristicFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataCharacteristicFlags.swift; sourceTree = ""; };
104 | OBJ_44 /* RowerDataCharacteristicTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowerDataCharacteristicTest.swift; sourceTree = ""; };
105 | OBJ_49 /* iOS Example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "iOS Example"; sourceTree = SOURCE_ROOT; };
106 | OBJ_50 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
107 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
108 | OBJ_9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
109 | "WaterRowerData-iOS::WaterRowerData-BLE::Product" /* WaterRowerData_BLE.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WaterRowerData_BLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
110 | "WaterRowerData-iOS::WaterRowerData-BLETests::Product" /* WaterRowerData_BLETests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; name = WaterRowerData_BLETests.xctest; path = "WaterRowerData-BLETests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
111 | /* End PBXFileReference section */
112 |
113 | /* Begin PBXFrameworksBuildPhase section */
114 | OBJ_81 /* Frameworks */ = {
115 | isa = PBXFrameworksBuildPhase;
116 | buildActionMask = 0;
117 | files = (
118 | );
119 | runOnlyForDeploymentPostprocessing = 0;
120 | };
121 | OBJ_92 /* Frameworks */ = {
122 | isa = PBXFrameworksBuildPhase;
123 | buildActionMask = 0;
124 | files = (
125 | OBJ_93 /* WaterRowerData_BLE.framework in Frameworks */,
126 | );
127 | runOnlyForDeploymentPostprocessing = 0;
128 | };
129 | /* End PBXFrameworksBuildPhase section */
130 |
131 | /* Begin PBXGroup section */
132 | OBJ_11 /* Internal */ = {
133 | isa = PBXGroup;
134 | children = (
135 | OBJ_12 /* GattSpecification */,
136 | OBJ_18 /* RowerDataSpecification */,
137 | );
138 | path = Internal;
139 | sourceTree = "";
140 | };
141 | OBJ_12 /* GattSpecification */ = {
142 | isa = PBXGroup;
143 | children = (
144 | OBJ_13 /* BitRequirement.swift */,
145 | OBJ_14 /* Data+ReadFormat.swift */,
146 | OBJ_15 /* Field.swift */,
147 | OBJ_16 /* Format.swift */,
148 | OBJ_17 /* Requirement.swift */,
149 | );
150 | path = GattSpecification;
151 | sourceTree = "";
152 | };
153 | OBJ_18 /* RowerDataSpecification */ = {
154 | isa = PBXGroup;
155 | children = (
156 | OBJ_19 /* RowerDataAveragePaceField.swift */,
157 | OBJ_20 /* RowerDataAveragePowerField.swift */,
158 | OBJ_21 /* RowerDataAverageStrokeRateField.swift */,
159 | OBJ_22 /* RowerDataElapsedTimeField.swift */,
160 | OBJ_23 /* RowerDataEnergyPerHourField.swift */,
161 | OBJ_24 /* RowerDataEnergyPerMinuteField.swift */,
162 | OBJ_25 /* RowerDataFlagsField.swift */,
163 | OBJ_26 /* RowerDataHeartRateField.swift */,
164 | OBJ_27 /* RowerDataInstantaneousPaceField.swift */,
165 | OBJ_28 /* RowerDataInstantaneousPowerField.swift */,
166 | OBJ_29 /* RowerDataMetabolicEquivalentField.swift */,
167 | OBJ_30 /* RowerDataRemainingTimeField.swift */,
168 | OBJ_31 /* RowerDataResistanceLevelField.swift */,
169 | OBJ_32 /* RowerDataStrokeCountField.swift */,
170 | OBJ_33 /* RowerDataStrokeRateField.swift */,
171 | OBJ_34 /* RowerDataTotalDistanceField.swift */,
172 | OBJ_35 /* RowerDataTotalEnergyField.swift */,
173 | );
174 | path = RowerDataSpecification;
175 | sourceTree = "";
176 | };
177 | OBJ_38 /* Tests */ = {
178 | isa = PBXGroup;
179 | children = (
180 | OBJ_39 /* WaterRowerData-BLETests */,
181 | );
182 | name = Tests;
183 | sourceTree = SOURCE_ROOT;
184 | };
185 | OBJ_39 /* WaterRowerData-BLETests */ = {
186 | isa = PBXGroup;
187 | children = (
188 | OBJ_40 /* Info.plist */,
189 | OBJ_41 /* CharacteristicData.swift */,
190 | OBJ_42 /* CharacteristicFlags.swift */,
191 | OBJ_43 /* RowerDataCharacteristicFlags.swift */,
192 | OBJ_44 /* RowerDataCharacteristicTest.swift */,
193 | );
194 | name = "WaterRowerData-BLETests";
195 | path = "Tests/WaterRowerData-BLETests";
196 | sourceTree = SOURCE_ROOT;
197 | };
198 | OBJ_46 /* Products */ = {
199 | isa = PBXGroup;
200 | children = (
201 | "WaterRowerData-iOS::WaterRowerData-BLE::Product" /* WaterRowerData_BLE.framework */,
202 | "WaterRowerData-iOS::WaterRowerData-BLETests::Product" /* WaterRowerData_BLETests.xctest */,
203 | );
204 | name = Products;
205 | sourceTree = BUILT_PRODUCTS_DIR;
206 | };
207 | OBJ_5 /* */ = {
208 | isa = PBXGroup;
209 | children = (
210 | OBJ_6 /* Package.swift */,
211 | OBJ_7 /* Sources */,
212 | OBJ_38 /* Tests */,
213 | OBJ_46 /* Products */,
214 | OBJ_49 /* iOS Example */,
215 | OBJ_50 /* README.md */,
216 | );
217 | name = "";
218 | sourceTree = "";
219 | };
220 | OBJ_7 /* Sources */ = {
221 | isa = PBXGroup;
222 | children = (
223 | OBJ_8 /* WaterRowerData-BLE */,
224 | );
225 | name = Sources;
226 | sourceTree = SOURCE_ROOT;
227 | };
228 | OBJ_8 /* WaterRowerData-BLE */ = {
229 | isa = PBXGroup;
230 | children = (
231 | OBJ_9 /* Info.plist */,
232 | OBJ_10 /* FitnessMachineService.swift */,
233 | OBJ_11 /* Internal */,
234 | OBJ_36 /* RowerData.swift */,
235 | OBJ_37 /* RowerDataCharacteristic.swift */,
236 | );
237 | name = "WaterRowerData-BLE";
238 | path = "Sources/WaterRowerData-BLE";
239 | sourceTree = SOURCE_ROOT;
240 | };
241 | /* End PBXGroup section */
242 |
243 | /* Begin PBXNativeTarget section */
244 | "WaterRowerData-iOS::SwiftPMPackageDescription" /* WaterRowerData-iOSPackageDescription */ = {
245 | isa = PBXNativeTarget;
246 | buildConfigurationList = OBJ_96 /* Build configuration list for PBXNativeTarget "WaterRowerData-iOSPackageDescription" */;
247 | buildPhases = (
248 | OBJ_99 /* Sources */,
249 | );
250 | buildRules = (
251 | );
252 | dependencies = (
253 | );
254 | name = "WaterRowerData-iOSPackageDescription";
255 | productName = "WaterRowerData-iOSPackageDescription";
256 | productType = "com.apple.product-type.framework";
257 | };
258 | "WaterRowerData-iOS::WaterRowerData-BLE" /* WaterRowerData-BLE */ = {
259 | isa = PBXNativeTarget;
260 | buildConfigurationList = OBJ_52 /* Build configuration list for PBXNativeTarget "WaterRowerData-BLE" */;
261 | buildPhases = (
262 | OBJ_55 /* Sources */,
263 | OBJ_81 /* Frameworks */,
264 | );
265 | buildRules = (
266 | );
267 | dependencies = (
268 | );
269 | name = "WaterRowerData-BLE";
270 | productName = WaterRowerData_BLE;
271 | productReference = "WaterRowerData-iOS::WaterRowerData-BLE::Product" /* WaterRowerData_BLE.framework */;
272 | productType = "com.apple.product-type.framework";
273 | };
274 | "WaterRowerData-iOS::WaterRowerData-BLETests" /* WaterRowerData-BLETests */ = {
275 | isa = PBXNativeTarget;
276 | buildConfigurationList = OBJ_83 /* Build configuration list for PBXNativeTarget "WaterRowerData-BLETests" */;
277 | buildPhases = (
278 | OBJ_86 /* Sources */,
279 | OBJ_92 /* Frameworks */,
280 | );
281 | buildRules = (
282 | );
283 | dependencies = (
284 | OBJ_94 /* PBXTargetDependency */,
285 | );
286 | name = "WaterRowerData-BLETests";
287 | productName = WaterRowerData_BLETests;
288 | productReference = "WaterRowerData-iOS::WaterRowerData-BLETests::Product" /* WaterRowerData_BLETests.xctest */;
289 | productType = "com.apple.product-type.bundle.unit-test";
290 | };
291 | /* End PBXNativeTarget section */
292 |
293 | /* Begin PBXProject section */
294 | OBJ_1 /* Project object */ = {
295 | isa = PBXProject;
296 | attributes = {
297 | LastSwiftMigration = 9999;
298 | LastUpgradeCheck = 9999;
299 | };
300 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "WaterRowerData-iOS" */;
301 | compatibilityVersion = "Xcode 3.2";
302 | developmentRegion = en;
303 | hasScannedForEncodings = 0;
304 | knownRegions = (
305 | en,
306 | );
307 | mainGroup = OBJ_5 /* */;
308 | productRefGroup = OBJ_46 /* Products */;
309 | projectDirPath = "";
310 | projectRoot = "";
311 | targets = (
312 | "WaterRowerData-iOS::WaterRowerData-BLE" /* WaterRowerData-BLE */,
313 | "WaterRowerData-iOS::WaterRowerData-BLETests" /* WaterRowerData-BLETests */,
314 | "WaterRowerData-iOS::SwiftPMPackageDescription" /* WaterRowerData-iOSPackageDescription */,
315 | "WaterRowerData-iOS::WaterRowerData-iOSPackageTests::ProductTarget" /* WaterRowerData-iOSPackageTests */,
316 | );
317 | };
318 | /* End PBXProject section */
319 |
320 | /* Begin PBXSourcesBuildPhase section */
321 | OBJ_55 /* Sources */ = {
322 | isa = PBXSourcesBuildPhase;
323 | buildActionMask = 0;
324 | files = (
325 | OBJ_56 /* FitnessMachineService.swift in Sources */,
326 | OBJ_57 /* BitRequirement.swift in Sources */,
327 | OBJ_58 /* Data+ReadFormat.swift in Sources */,
328 | OBJ_59 /* Field.swift in Sources */,
329 | OBJ_60 /* Format.swift in Sources */,
330 | OBJ_61 /* Requirement.swift in Sources */,
331 | OBJ_62 /* RowerDataAveragePaceField.swift in Sources */,
332 | OBJ_63 /* RowerDataAveragePowerField.swift in Sources */,
333 | OBJ_64 /* RowerDataAverageStrokeRateField.swift in Sources */,
334 | OBJ_65 /* RowerDataElapsedTimeField.swift in Sources */,
335 | OBJ_66 /* RowerDataEnergyPerHourField.swift in Sources */,
336 | OBJ_67 /* RowerDataEnergyPerMinuteField.swift in Sources */,
337 | OBJ_68 /* RowerDataFlagsField.swift in Sources */,
338 | OBJ_69 /* RowerDataHeartRateField.swift in Sources */,
339 | OBJ_70 /* RowerDataInstantaneousPaceField.swift in Sources */,
340 | OBJ_71 /* RowerDataInstantaneousPowerField.swift in Sources */,
341 | OBJ_72 /* RowerDataMetabolicEquivalentField.swift in Sources */,
342 | OBJ_73 /* RowerDataRemainingTimeField.swift in Sources */,
343 | OBJ_74 /* RowerDataResistanceLevelField.swift in Sources */,
344 | OBJ_75 /* RowerDataStrokeCountField.swift in Sources */,
345 | OBJ_76 /* RowerDataStrokeRateField.swift in Sources */,
346 | OBJ_77 /* RowerDataTotalDistanceField.swift in Sources */,
347 | OBJ_78 /* RowerDataTotalEnergyField.swift in Sources */,
348 | OBJ_79 /* RowerData.swift in Sources */,
349 | OBJ_80 /* RowerDataCharacteristic.swift in Sources */,
350 | );
351 | runOnlyForDeploymentPostprocessing = 0;
352 | };
353 | OBJ_86 /* Sources */ = {
354 | isa = PBXSourcesBuildPhase;
355 | buildActionMask = 0;
356 | files = (
357 | OBJ_87 /* CharacteristicData.swift in Sources */,
358 | OBJ_88 /* CharacteristicFlags.swift in Sources */,
359 | OBJ_89 /* RowerDataCharacteristicFlags.swift in Sources */,
360 | OBJ_90 /* RowerDataCharacteristicTest.swift in Sources */,
361 | );
362 | runOnlyForDeploymentPostprocessing = 0;
363 | };
364 | OBJ_99 /* Sources */ = {
365 | isa = PBXSourcesBuildPhase;
366 | buildActionMask = 0;
367 | files = (
368 | OBJ_100 /* Package.swift in Sources */,
369 | );
370 | runOnlyForDeploymentPostprocessing = 0;
371 | };
372 | /* End PBXSourcesBuildPhase section */
373 |
374 | /* Begin PBXTargetDependency section */
375 | OBJ_105 /* PBXTargetDependency */ = {
376 | isa = PBXTargetDependency;
377 | target = "WaterRowerData-iOS::WaterRowerData-BLETests" /* WaterRowerData-BLETests */;
378 | targetProxy = 8CA966A724C1BB260002B6CD /* PBXContainerItemProxy */;
379 | };
380 | OBJ_94 /* PBXTargetDependency */ = {
381 | isa = PBXTargetDependency;
382 | target = "WaterRowerData-iOS::WaterRowerData-BLE" /* WaterRowerData-BLE */;
383 | targetProxy = 8CA966A624C1BB230002B6CD /* PBXContainerItemProxy */;
384 | };
385 | /* End PBXTargetDependency section */
386 |
387 | /* Begin XCBuildConfiguration section */
388 | OBJ_103 /* Debug */ = {
389 | isa = XCBuildConfiguration;
390 | buildSettings = {
391 | };
392 | name = Debug;
393 | };
394 | OBJ_104 /* Release */ = {
395 | isa = XCBuildConfiguration;
396 | buildSettings = {
397 | };
398 | name = Release;
399 | };
400 | OBJ_3 /* Debug */ = {
401 | isa = XCBuildConfiguration;
402 | buildSettings = {
403 | CLANG_ENABLE_OBJC_ARC = YES;
404 | COMBINE_HIDPI_IMAGES = YES;
405 | COPY_PHASE_STRIP = NO;
406 | DEBUG_INFORMATION_FORMAT = dwarf;
407 | DYLIB_INSTALL_NAME_BASE = "@rpath";
408 | ENABLE_NS_ASSERTIONS = YES;
409 | GCC_OPTIMIZATION_LEVEL = 0;
410 | GCC_PREPROCESSOR_DEFINITIONS = (
411 | "$(inherited)",
412 | "SWIFT_PACKAGE=1",
413 | "DEBUG=1",
414 | );
415 | MACOSX_DEPLOYMENT_TARGET = 10.10;
416 | ONLY_ACTIVE_ARCH = YES;
417 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
418 | PRODUCT_NAME = "$(TARGET_NAME)";
419 | SDKROOT = macosx;
420 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
421 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
422 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
423 | USE_HEADERMAP = NO;
424 | };
425 | name = Debug;
426 | };
427 | OBJ_4 /* Release */ = {
428 | isa = XCBuildConfiguration;
429 | buildSettings = {
430 | CLANG_ENABLE_OBJC_ARC = YES;
431 | COMBINE_HIDPI_IMAGES = YES;
432 | COPY_PHASE_STRIP = YES;
433 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
434 | DYLIB_INSTALL_NAME_BASE = "@rpath";
435 | GCC_OPTIMIZATION_LEVEL = s;
436 | GCC_PREPROCESSOR_DEFINITIONS = (
437 | "$(inherited)",
438 | "SWIFT_PACKAGE=1",
439 | );
440 | MACOSX_DEPLOYMENT_TARGET = 10.10;
441 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
442 | PRODUCT_NAME = "$(TARGET_NAME)";
443 | SDKROOT = macosx;
444 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
445 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE";
446 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
447 | USE_HEADERMAP = NO;
448 | };
449 | name = Release;
450 | };
451 | OBJ_53 /* Debug */ = {
452 | isa = XCBuildConfiguration;
453 | buildSettings = {
454 | ENABLE_TESTABILITY = YES;
455 | FRAMEWORK_SEARCH_PATHS = (
456 | "$(inherited)",
457 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
458 | );
459 | HEADER_SEARCH_PATHS = "$(inherited)";
460 | INFOPLIST_FILE = "WaterRowerData-iOS.xcodeproj/WaterRowerData_BLE_Info.plist";
461 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
462 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
463 | MACOSX_DEPLOYMENT_TARGET = 10.10;
464 | OTHER_CFLAGS = "$(inherited)";
465 | OTHER_LDFLAGS = "$(inherited)";
466 | OTHER_SWIFT_FLAGS = "$(inherited)";
467 | PRODUCT_BUNDLE_IDENTIFIER = "WaterRowerData-BLE";
468 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
469 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
470 | SKIP_INSTALL = YES;
471 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
472 | SWIFT_VERSION = 5.0;
473 | TARGET_NAME = "WaterRowerData-BLE";
474 | TVOS_DEPLOYMENT_TARGET = 9.0;
475 | WATCHOS_DEPLOYMENT_TARGET = 2.0;
476 | };
477 | name = Debug;
478 | };
479 | OBJ_54 /* Release */ = {
480 | isa = XCBuildConfiguration;
481 | buildSettings = {
482 | ENABLE_TESTABILITY = YES;
483 | FRAMEWORK_SEARCH_PATHS = (
484 | "$(inherited)",
485 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
486 | );
487 | HEADER_SEARCH_PATHS = "$(inherited)";
488 | INFOPLIST_FILE = "WaterRowerData-iOS.xcodeproj/WaterRowerData_BLE_Info.plist";
489 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
490 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
491 | MACOSX_DEPLOYMENT_TARGET = 10.10;
492 | OTHER_CFLAGS = "$(inherited)";
493 | OTHER_LDFLAGS = "$(inherited)";
494 | OTHER_SWIFT_FLAGS = "$(inherited)";
495 | PRODUCT_BUNDLE_IDENTIFIER = "WaterRowerData-BLE";
496 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
497 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
498 | SKIP_INSTALL = YES;
499 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
500 | SWIFT_VERSION = 5.0;
501 | TARGET_NAME = "WaterRowerData-BLE";
502 | TVOS_DEPLOYMENT_TARGET = 9.0;
503 | WATCHOS_DEPLOYMENT_TARGET = 2.0;
504 | };
505 | name = Release;
506 | };
507 | OBJ_84 /* Debug */ = {
508 | isa = XCBuildConfiguration;
509 | buildSettings = {
510 | CLANG_ENABLE_MODULES = YES;
511 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
512 | FRAMEWORK_SEARCH_PATHS = (
513 | "$(inherited)",
514 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
515 | );
516 | HEADER_SEARCH_PATHS = "$(inherited)";
517 | INFOPLIST_FILE = "WaterRowerData-iOS.xcodeproj/WaterRowerData_BLETests_Info.plist";
518 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
519 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
520 | MACOSX_DEPLOYMENT_TARGET = 10.10;
521 | OTHER_CFLAGS = "$(inherited)";
522 | OTHER_LDFLAGS = "$(inherited)";
523 | OTHER_SWIFT_FLAGS = "$(inherited)";
524 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
525 | SWIFT_VERSION = 5.0;
526 | TARGET_NAME = "WaterRowerData-BLETests";
527 | TVOS_DEPLOYMENT_TARGET = 9.0;
528 | WATCHOS_DEPLOYMENT_TARGET = 2.0;
529 | };
530 | name = Debug;
531 | };
532 | OBJ_85 /* Release */ = {
533 | isa = XCBuildConfiguration;
534 | buildSettings = {
535 | CLANG_ENABLE_MODULES = YES;
536 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
537 | FRAMEWORK_SEARCH_PATHS = (
538 | "$(inherited)",
539 | "$(PLATFORM_DIR)/Developer/Library/Frameworks",
540 | );
541 | HEADER_SEARCH_PATHS = "$(inherited)";
542 | INFOPLIST_FILE = "WaterRowerData-iOS.xcodeproj/WaterRowerData_BLETests_Info.plist";
543 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
544 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
545 | MACOSX_DEPLOYMENT_TARGET = 10.10;
546 | OTHER_CFLAGS = "$(inherited)";
547 | OTHER_LDFLAGS = "$(inherited)";
548 | OTHER_SWIFT_FLAGS = "$(inherited)";
549 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
550 | SWIFT_VERSION = 5.0;
551 | TARGET_NAME = "WaterRowerData-BLETests";
552 | TVOS_DEPLOYMENT_TARGET = 9.0;
553 | WATCHOS_DEPLOYMENT_TARGET = 2.0;
554 | };
555 | name = Release;
556 | };
557 | OBJ_97 /* Debug */ = {
558 | isa = XCBuildConfiguration;
559 | buildSettings = {
560 | LD = /usr/bin/true;
561 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0";
562 | SWIFT_VERSION = 5.0;
563 | };
564 | name = Debug;
565 | };
566 | OBJ_98 /* Release */ = {
567 | isa = XCBuildConfiguration;
568 | buildSettings = {
569 | LD = /usr/bin/true;
570 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0";
571 | SWIFT_VERSION = 5.0;
572 | };
573 | name = Release;
574 | };
575 | /* End XCBuildConfiguration section */
576 |
577 | /* Begin XCConfigurationList section */
578 | OBJ_102 /* Build configuration list for PBXAggregateTarget "WaterRowerData-iOSPackageTests" */ = {
579 | isa = XCConfigurationList;
580 | buildConfigurations = (
581 | OBJ_103 /* Debug */,
582 | OBJ_104 /* Release */,
583 | );
584 | defaultConfigurationIsVisible = 0;
585 | defaultConfigurationName = Release;
586 | };
587 | OBJ_2 /* Build configuration list for PBXProject "WaterRowerData-iOS" */ = {
588 | isa = XCConfigurationList;
589 | buildConfigurations = (
590 | OBJ_3 /* Debug */,
591 | OBJ_4 /* Release */,
592 | );
593 | defaultConfigurationIsVisible = 0;
594 | defaultConfigurationName = Release;
595 | };
596 | OBJ_52 /* Build configuration list for PBXNativeTarget "WaterRowerData-BLE" */ = {
597 | isa = XCConfigurationList;
598 | buildConfigurations = (
599 | OBJ_53 /* Debug */,
600 | OBJ_54 /* Release */,
601 | );
602 | defaultConfigurationIsVisible = 0;
603 | defaultConfigurationName = Release;
604 | };
605 | OBJ_83 /* Build configuration list for PBXNativeTarget "WaterRowerData-BLETests" */ = {
606 | isa = XCConfigurationList;
607 | buildConfigurations = (
608 | OBJ_84 /* Debug */,
609 | OBJ_85 /* Release */,
610 | );
611 | defaultConfigurationIsVisible = 0;
612 | defaultConfigurationName = Release;
613 | };
614 | OBJ_96 /* Build configuration list for PBXNativeTarget "WaterRowerData-iOSPackageDescription" */ = {
615 | isa = XCConfigurationList;
616 | buildConfigurations = (
617 | OBJ_97 /* Debug */,
618 | OBJ_98 /* Release */,
619 | );
620 | defaultConfigurationIsVisible = 0;
621 | defaultConfigurationName = Release;
622 | };
623 | /* End XCConfigurationList section */
624 | };
625 | rootObject = OBJ_1 /* Project object */;
626 | }
627 |
--------------------------------------------------------------------------------
/Tests/WaterRowerData-BLETests/RowerDataCharacteristicTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import WaterRowerData_BLE
4 |
5 | class RowerDataCharacteristicTest: XCTestCase {
6 |
7 | // MARK: Stroke Rate
8 |
9 | func test_strokeRate_notPresent_resultsIn_nilValue() {
10 | /* Given */
11 | let flags = RowerDataCharacteristicFlags.create(moreDataPresent: false)
12 | let data = CharacteristicData.create(flags: flags)
13 |
14 | /* When */
15 | let result = RowerDataCharacteristic.decode(data: data)
16 |
17 | /* Then */
18 | XCTAssertNil(result.strokeRate)
19 | }
20 |
21 | func test_strokeRate_present_resultsIn_uint8Value_withBinaryExponentMinusOne() {
22 | /* Given */
23 | let flags = RowerDataCharacteristicFlags.create(moreDataPresent: true)
24 | let data = CharacteristicData.create(
25 | flags: flags,
26 | values: 7, // Stroke rate of 3.5
27 | 0, // Stroke count value
28 | 0
29 | )
30 |
31 | /* When */
32 | let result = RowerDataCharacteristic.decode(data: data)
33 |
34 | /* Then */
35 | XCTAssertEqual(result.strokeRate, 3.5)
36 | }
37 |
38 | // MARK: Stroke Count
39 |
40 | func test_strokeCount_notPresent_resultsIn_nilValue() {
41 | /* Given */
42 | let flags = RowerDataCharacteristicFlags.create(moreDataPresent: false)
43 | let data = CharacteristicData.create(flags: flags)
44 |
45 | /* When */
46 | let result = RowerDataCharacteristic.decode(data: data)
47 |
48 | /* Then */
49 | XCTAssertNil(result.strokeCount)
50 | }
51 |
52 | func test_strokeCount_present_resultsIn_uint16Value_lowValue() {
53 | /* Given */
54 | let flags = RowerDataCharacteristicFlags.create(moreDataPresent: true)
55 | let data = CharacteristicData.create(
56 | flags: flags,
57 | values: 0, // Stroke Rate
58 | 1, // Stroke count of 1
59 | 0
60 | )
61 |
62 | /* When */
63 | let result = RowerDataCharacteristic.decode(data: data)
64 |
65 | /* Then */
66 | XCTAssertEqual(result.strokeCount, 1)
67 | }
68 |
69 | func test_strokeCount_present_resultsIn_uint16Value_highValue() {
70 | /* Given */
71 | let flags = RowerDataCharacteristicFlags.create(moreDataPresent: true)
72 | let data = CharacteristicData.create(
73 | flags: flags,
74 | values: 0, // Stroke Rate
75 | 1, // Stroke count of 1 + 256
76 | 1
77 | )
78 |
79 | /* When */
80 | let result = RowerDataCharacteristic.decode(data: data)
81 |
82 | /* Then */
83 | XCTAssertEqual(result.strokeCount, 257)
84 | }
85 |
86 | // MARK: Average Stroke Rate
87 |
88 | func test_averageStrokeRate_notPresent_resultsIn_nilValue() {
89 | /* Given */
90 | let flags = RowerDataCharacteristicFlags.create(averageStrokeRatePresent: false)
91 | let data = CharacteristicData.create(flags: flags)
92 |
93 | /* When */
94 | let result = RowerDataCharacteristic.decode(data: data)
95 |
96 | /* Then */
97 | XCTAssertNil(result.averageStrokeRate)
98 | }
99 |
100 | func test_averageStrokeRate_present_resultsIn_uint8Value_withBinaryExponentMinusOne() {
101 | /* Given */
102 | let flags = RowerDataCharacteristicFlags.create(averageStrokeRatePresent: true)
103 | let data = CharacteristicData.create(flags: flags, values: 7)
104 |
105 | /* When */
106 | let result = RowerDataCharacteristic.decode(data: data)
107 |
108 | /* Then */
109 | XCTAssertEqual(result.averageStrokeRate, 3.5)
110 | }
111 |
112 | // MARK: Total Distance
113 |
114 | func test_totalDistance_notPresent_resultsIn_nilValue() {
115 | /* Given */
116 | let flags = RowerDataCharacteristicFlags.create(totalDistancePresent: false)
117 | let data = CharacteristicData.create(flags: flags)
118 |
119 | /* When */
120 | let result = RowerDataCharacteristic.decode(data: data)
121 |
122 | /* Then */
123 | XCTAssertNil(result.totalDistanceMeters)
124 | }
125 |
126 | func test_totalDistance_present_resultsIn_uint24Value_forLowValue() {
127 | /* Given */
128 | let flags = RowerDataCharacteristicFlags.create(totalDistancePresent: true)
129 | let data = CharacteristicData.create(flags: flags, values: 1, 0, 0)
130 |
131 | /* When */
132 | let result = RowerDataCharacteristic.decode(data: data)
133 |
134 | /* Then */
135 | XCTAssertEqual(result.totalDistanceMeters, 1)
136 | }
137 |
138 | func test_totalDistance_present_resultsIn_uint24Value_forMediumValue() {
139 | /* Given */
140 | let flags = RowerDataCharacteristicFlags.create(totalDistancePresent: true)
141 | let data = CharacteristicData.create(flags: flags, values: 1, 2, 0) // 1 + 512
142 |
143 | /* When */
144 | let result = RowerDataCharacteristic.decode(data: data)
145 |
146 | /* Then */
147 | XCTAssertEqual(result.totalDistanceMeters, 513)
148 | }
149 |
150 | func test_totalDistance_present_resultsIn_uint24Value_forHighValue() {
151 | /* Given */
152 | let flags = RowerDataCharacteristicFlags.create(totalDistancePresent: true)
153 | let data = CharacteristicData.create(flags: flags, values: 1, 2, 4) // 1 + 512 + 262144
154 |
155 | /* When */
156 | let result = RowerDataCharacteristic.decode(data: data)
157 |
158 | /* Then */
159 | XCTAssertEqual(result.totalDistanceMeters, 262657)
160 | }
161 |
162 | // MARK: Instantaneous Pace
163 |
164 | func test_instantaneousPace_notPresent_resultsIn_nilValue() {
165 | /* Given */
166 | let flags = RowerDataCharacteristicFlags.create(instantaneousPacePresent: false)
167 | let data = CharacteristicData.create(flags: flags)
168 |
169 | /* When */
170 | let result = RowerDataCharacteristic.decode(data: data)
171 |
172 | /* Then */
173 | XCTAssertNil(result.instantaneousPaceSeconds)
174 | }
175 |
176 | func test_instantaneousPace_notPresent_resultsIn_uint16Value_forLowValue() {
177 | /* Given */
178 | let flags = RowerDataCharacteristicFlags.create(instantaneousPacePresent: true)
179 | let data = CharacteristicData.create(flags: flags, values: 1, 0)
180 |
181 | /* When */
182 | let result = RowerDataCharacteristic.decode(data: data)
183 |
184 | /* Then */
185 | XCTAssertEqual(result.instantaneousPaceSeconds, 1)
186 | }
187 |
188 | func test_instantaneousPace_notPresent_resultsIn_uint16Value_forHighValue() {
189 | /* Given */
190 | let flags = RowerDataCharacteristicFlags.create(instantaneousPacePresent: true)
191 | let data = CharacteristicData.create(flags: flags, values: 1, 2) // 1 + 512
192 |
193 | /* When */
194 | let result = RowerDataCharacteristic.decode(data: data)
195 |
196 | /* Then */
197 | XCTAssertEqual(result.instantaneousPaceSeconds, 513)
198 | }
199 |
200 | // MARK: Average Pace
201 |
202 | func test_averagePace_notPresent_resultsIn_nilValue() {
203 | /* Given */
204 | let flags = RowerDataCharacteristicFlags.create(averagePacePresent: false)
205 | let data = CharacteristicData.create(flags: flags)
206 |
207 | /* When */
208 | let result = RowerDataCharacteristic.decode(data: data)
209 |
210 | /* Then */
211 | XCTAssertNil(result.averagePaceSeconds)
212 | }
213 |
214 | func test_averagePace_present_resultsIn_uint16Value_forLowValue() {
215 | /* Given */
216 | let flags = RowerDataCharacteristicFlags.create(averagePacePresent: true)
217 | let data = CharacteristicData.create(flags: flags, values: 1, 0)
218 |
219 | /* When */
220 | let result = RowerDataCharacteristic.decode(data: data)
221 |
222 | /* Then */
223 | XCTAssertEqual(result.averagePaceSeconds, 1)
224 | }
225 |
226 | func test_averagePace_present_resultsIn_uint16Value_forHighValue() {
227 | /* Given */
228 | let flags = RowerDataCharacteristicFlags.create(averagePacePresent: true)
229 | let data = CharacteristicData.create(flags: flags, values: 1, 2) // 1 + 512
230 |
231 | /* When */
232 | let result = RowerDataCharacteristic.decode(data: data)
233 |
234 | /* Then */
235 | XCTAssertEqual(result.averagePaceSeconds, 513)
236 | }
237 |
238 | // MARK: Instantaneous Power
239 |
240 | func test_instantaneousPower_notPresent_resultsIn_nilValue() {
241 | /* Given */
242 | let flags = RowerDataCharacteristicFlags.create(instantaneousPowerPresent: false)
243 | let data = CharacteristicData.create(flags: flags)
244 |
245 | /* When */
246 | let result = RowerDataCharacteristic.decode(data: data)
247 |
248 | /* Then */
249 | XCTAssertNil(result.instantaneousPowerWatts)
250 | }
251 |
252 | func test_instantaneousPower_present_resultsIn_sint16Value_forLowValue() {
253 | /* Given */
254 | let flags = RowerDataCharacteristicFlags.create(instantaneousPowerPresent: true)
255 | let data = CharacteristicData.create(flags: flags, values: 1, 0)
256 |
257 | /* When */
258 | let result = RowerDataCharacteristic.decode(data: data)
259 |
260 | /* Then */
261 | XCTAssertEqual(result.instantaneousPowerWatts, 1)
262 | }
263 |
264 | func test_instantaneousPower_present_resultsIn_sint16Value_forHighValue() {
265 | /* Given */
266 | let flags = RowerDataCharacteristicFlags.create(instantaneousPowerPresent: true)
267 | let data = CharacteristicData.create(flags: flags, values: 1, 2) // 1 + 512
268 |
269 | /* When */
270 | let result = RowerDataCharacteristic.decode(data: data)
271 |
272 | /* Then */
273 | XCTAssertEqual(result.instantaneousPowerWatts, 513)
274 | }
275 |
276 | func test_instantaneousPower_present_resultsIn_sint16Value_forLowNegativeValue() {
277 | /* Given */
278 | let flags = RowerDataCharacteristicFlags.create(instantaneousPowerPresent: true)
279 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111111)
280 |
281 | /* When */
282 | let result = RowerDataCharacteristic.decode(data: data)
283 |
284 | /* Then */
285 | XCTAssertEqual(result.instantaneousPowerWatts, -1)
286 | }
287 |
288 | func test_instantaneousPower_present_resultsIn_sint16Value_forHighNegativeValue() {
289 | /* Given */
290 | let flags = RowerDataCharacteristicFlags.create(instantaneousPowerPresent: true)
291 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111110)
292 |
293 | /* When */
294 | let result = RowerDataCharacteristic.decode(data: data)
295 |
296 | /* Then */
297 | XCTAssertEqual(result.instantaneousPowerWatts, -257)
298 | }
299 |
300 | // MARK: Average Power
301 |
302 | func test_averagePower_notPresent_resultsIn_nilValue() {
303 | /* Given */
304 | let flags = RowerDataCharacteristicFlags.create(averagePowerPresent: false)
305 | let data = CharacteristicData.create(flags: flags)
306 |
307 | /* When */
308 | let result = RowerDataCharacteristic.decode(data: data)
309 |
310 | /* Then */
311 | XCTAssertNil(result.averagePowerWatts)
312 | }
313 |
314 | func test_averagePower_present_resultsIn_sint16Value_forLowValue() {
315 | /* Given */
316 | let flags = RowerDataCharacteristicFlags.create(averagePowerPresent: true)
317 | let data = CharacteristicData.create(flags: flags, values: 1, 0)
318 |
319 | /* When */
320 | let result = RowerDataCharacteristic.decode(data: data)
321 |
322 | /* Then */
323 | XCTAssertEqual(result.averagePowerWatts, 1)
324 | }
325 |
326 | func test_averagePower_present_resultsIn_sint16Value_forHighValue() {
327 | /* Given */
328 | let flags = RowerDataCharacteristicFlags.create(averagePowerPresent: true)
329 | let data = CharacteristicData.create(flags: flags, values: 1, 2) // 1 + 512
330 |
331 | /* When */
332 | let result = RowerDataCharacteristic.decode(data: data)
333 |
334 | /* Then */
335 | XCTAssertEqual(result.averagePowerWatts, 513)
336 | }
337 |
338 | func test_averagePower_present_resultsIn_sint16Value_forLowNegativeValue() {
339 | /* Given */
340 | let flags = RowerDataCharacteristicFlags.create(averagePowerPresent: true)
341 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111111)
342 |
343 | /* When */
344 | let result = RowerDataCharacteristic.decode(data: data)
345 |
346 | /* Then */
347 | XCTAssertEqual(result.averagePowerWatts, -1)
348 | }
349 |
350 | func test_averagePower_present_resultsIn_sint16Value_forHighNegativeValue() {
351 | /* Given */
352 | let flags = RowerDataCharacteristicFlags.create(averagePowerPresent: true)
353 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111110)
354 |
355 | /* When */
356 | let result = RowerDataCharacteristic.decode(data: data)
357 |
358 | /* Then */
359 | XCTAssertEqual(result.averagePowerWatts, -257)
360 | }
361 |
362 | // MARK: Resistance level
363 |
364 | func test_resistanceLevel_notPresent_resultsIn_nilValue() {
365 | /* Given */
366 | let flags = RowerDataCharacteristicFlags.create(resistanceLevelPresent: false)
367 | let data = CharacteristicData.create(flags: flags)
368 |
369 | /* When */
370 | let result = RowerDataCharacteristic.decode(data: data)
371 |
372 | /* Then */
373 | XCTAssertNil(result.resistanceLevel)
374 | }
375 |
376 | func test_resistanceLevel_present_resultsIn_sint16Value_forLowValue() {
377 | /* Given */
378 | let flags = RowerDataCharacteristicFlags.create(resistanceLevelPresent: true)
379 | let data = CharacteristicData.create(flags: flags, values: 1, 0)
380 |
381 | /* When */
382 | let result = RowerDataCharacteristic.decode(data: data)
383 |
384 | /* Then */
385 | XCTAssertEqual(result.resistanceLevel, 1)
386 | }
387 |
388 | func test_resistanceLevel_present_resultsIn_sint16Value_forHighValue() {
389 | /* Given */
390 | let flags = RowerDataCharacteristicFlags.create(resistanceLevelPresent: true)
391 | let data = CharacteristicData.create(flags: flags, values: 1, 2) // 1 + 512
392 |
393 | /* When */
394 | let result = RowerDataCharacteristic.decode(data: data)
395 |
396 | /* Then */
397 | XCTAssertEqual(result.resistanceLevel, 513)
398 | }
399 |
400 | func test_resistanceLevel_present_resultsIn_sint16Value_forLowNegativeValue() {
401 | /* Given */
402 | let flags = RowerDataCharacteristicFlags.create(resistanceLevelPresent: true)
403 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111111)
404 |
405 | /* When */
406 | let result = RowerDataCharacteristic.decode(data: data)
407 |
408 | /* Then */
409 | XCTAssertEqual(result.resistanceLevel, -1)
410 | }
411 |
412 | func test_resistanceLevel_present_resultsIn_sint16Value_forHighNegativeValue() {
413 | /* Given */
414 | let flags = RowerDataCharacteristicFlags.create(resistanceLevelPresent: true)
415 | let data = CharacteristicData.create(flags: flags, values: 0b11111111, 0b11111110)
416 |
417 | /* When */
418 | let result = RowerDataCharacteristic.decode(data: data)
419 |
420 | /* Then */
421 | XCTAssertEqual(result.resistanceLevel, -257)
422 | }
423 |
424 | // MARK: Total Energy
425 |
426 | func test_totalEnergy_notPresent_resultsIn_nilValue() {
427 | /* Given */
428 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: false)
429 | let data = CharacteristicData.create(flags: flags)
430 |
431 | /* When */
432 | let result = RowerDataCharacteristic.decode(data: data)
433 |
434 | /* Then */
435 | XCTAssertNil(result.totalEnergyKiloCalories)
436 | }
437 |
438 | func test_totalEnergy_present_resultsIn_uint16Value_forLowValue() {
439 | /* Given */
440 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
441 | let data = CharacteristicData.create(
442 | flags: flags,
443 | values: 1, // Total energy value 1
444 | 0,
445 | 0, // Energy per hour value
446 | 0,
447 | 0, // Energy per minute value
448 | 0
449 | )
450 |
451 | /* When */
452 | let result = RowerDataCharacteristic.decode(data: data)
453 |
454 | /* Then */
455 | XCTAssertEqual(result.totalEnergyKiloCalories, 1)
456 | }
457 |
458 | func test_totalEnergy_present_resultsIn_uint16Value_forHighValue() {
459 | /* Given */
460 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
461 | let data = CharacteristicData.create(
462 | flags: flags,
463 | values: 1, // Total energy value 1 + 512
464 | 2,
465 | 0, // Energy per hour value
466 | 0,
467 | 0, // Energy per minute value
468 | 0
469 | )
470 |
471 | /* When */
472 | let result = RowerDataCharacteristic.decode(data: data)
473 |
474 | /* Then */
475 | XCTAssertEqual(result.totalEnergyKiloCalories, 513)
476 | }
477 |
478 | // See section 4.8.1.11 of the FTMS Bluetooth Service specification.
479 | func test_totalEnergy_presentButNotSupported_resultsIn_nilValue() {
480 | // If this field has to be present (i.e., if the Expended Energy Present bit of the Flags field is set to 1)
481 | // but the Server does not support the calculation of the Total Energy, the Server shall use the special
482 | // value 0xFFFF (i.e., decimal value of 65535 in UINT16 format), which means ‘Data Not Available’.
483 |
484 | /* Given */
485 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
486 | let data = CharacteristicData.create(
487 | flags: flags,
488 | values: 0xff, // Total energy value 65535
489 | 0xff,
490 | 0, // Energy per hour value
491 | 0,
492 | 0, // Energy per minute value
493 | 0
494 | )
495 |
496 | /* When */
497 | let result = RowerDataCharacteristic.decode(data: data)
498 |
499 | /* Then */
500 | XCTAssertNil(result.totalEnergyKiloCalories)
501 | }
502 |
503 | // MARK: Energy Per Hour
504 |
505 | func test_energyPerHour_notPresent_resultsIn_nilValue() {
506 | /* Given */
507 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: false)
508 | let data = CharacteristicData.create(flags: flags)
509 |
510 | /* When */
511 | let result = RowerDataCharacteristic.decode(data: data)
512 |
513 | /* Then */
514 | XCTAssertNil(result.energyPerHourKiloCalories)
515 | }
516 |
517 | func test_energyPerHour_present_resultsIn_uint16Value_forLowValue() {
518 | /* Given */
519 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
520 | let data = CharacteristicData.create(
521 | flags: flags,
522 | values: 0, // Total energy value
523 | 0,
524 | 1, // Energy per hour value 1
525 | 0,
526 | 0, // Energy per minute value
527 | 0
528 | )
529 |
530 | /* When */
531 | let result = RowerDataCharacteristic.decode(data: data)
532 |
533 | /* Then */
534 | XCTAssertEqual(result.energyPerHourKiloCalories, 1)
535 | }
536 |
537 | func test_energyPerHour_present_resultsIn_uint16Value_forHighValue() {
538 | /* Given */
539 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
540 | let data = CharacteristicData.create(
541 | flags: flags,
542 | values: 0, // Total energy value
543 | 0,
544 | 1, // Energy per hour value 1 + 512
545 | 2,
546 | 0, // Energy per minute value
547 | 0
548 | )
549 |
550 | /* When */
551 | let result = RowerDataCharacteristic.decode(data: data)
552 |
553 | /* Then */
554 | XCTAssertEqual(result.energyPerHourKiloCalories, 513)
555 | }
556 |
557 | // See section 4.8.1.12 of the FTMS Bluetooth Service specification.
558 | func test_energyPerHour_presentButNotSupported_resultsIn_nilValue() {
559 | // If this field has to be present (i.e., if the Expended Energy Present bit of the Flags field is set to 1)
560 | // but the Server does not support the calculation of the Energy per Hour, the Server shall use the special
561 | // value 0xFFFF (i.e., decimal value of 65535 in UINT16 format), which means ‘Data Not Available’.
562 |
563 | /* Given */
564 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
565 | let data = CharacteristicData.create(
566 | flags: flags,
567 | values: 0, // Total energy value
568 | 0,
569 | 0xFF, // Energy per hour value 65535
570 | 0xFF,
571 | 0, // Energy per minute value
572 | 0
573 | )
574 |
575 | /* When */
576 | let result = RowerDataCharacteristic.decode(data: data)
577 |
578 | /* Then */
579 | XCTAssertNil(result.energyPerHourKiloCalories)
580 | }
581 |
582 | // MARK: Energy Per Minute
583 |
584 | func test_energyPerMinute_notPresent_resultsIn_nilValue() {
585 | /* Given */
586 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: false)
587 | let data = CharacteristicData.create(flags: flags)
588 |
589 | /* When */
590 | let result = RowerDataCharacteristic.decode(data: data)
591 |
592 | /* Then */
593 | XCTAssertNil(result.energyPerMinuteKiloCalories)
594 | }
595 |
596 | func test_energyPerMinute_present_resultsIn_uint8Value() {
597 | /* Given */
598 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
599 | let data = CharacteristicData.create(
600 | flags: flags,
601 | values: 0, // Total energy value
602 | 0,
603 | 0, // Energy per hour value
604 | 0,
605 | 1 // Energy per minute value 1
606 | )
607 |
608 | /* When */
609 | let result = RowerDataCharacteristic.decode(data: data)
610 |
611 | /* Then */
612 | XCTAssertEqual(result.energyPerMinuteKiloCalories, 1)
613 | }
614 |
615 | // See section 4.8.1.13 of the FTMS Bluetooth Service specification.
616 | func test_energyPerMinute_presentButNotSupported_resultsIn_nilValue() {
617 | // If this field has to be present (i.e., if the Expended Energy Present bit of the Flags field is set to 1)
618 | // but the Server does not support the calculation of the Energy per Minute, the Server shall use the special
619 | // value 0xFF (i.e., decimal value of 255 in UINT16 format), which means ‘Data Not Available’.
620 |
621 | /* Given */
622 | let flags = RowerDataCharacteristicFlags.create(expendedEnergyPresent: true)
623 | let data = CharacteristicData.create(
624 | flags: flags,
625 | values: 0, // Total energy value
626 | 0,
627 | 0, // Energy per hour value
628 | 0,
629 | 0xFF // Energy per minute value 255
630 | )
631 |
632 | /* When */
633 | let result = RowerDataCharacteristic.decode(data: data)
634 |
635 | /* Then */
636 | XCTAssertNil(result.energyPerMinuteKiloCalories)
637 | }
638 |
639 | // MARK: Heart Rate
640 |
641 | func test_heartRate_notPresent_resultsIn_nilValue() {
642 | /* Given */
643 | let flags = RowerDataCharacteristicFlags.create(heartRatePresent: false)
644 | let data = CharacteristicData.create(flags: flags)
645 |
646 | /* When */
647 | let result = RowerDataCharacteristic.decode(data: data)
648 |
649 | /* Then */
650 | XCTAssertNil(result.heartRate)
651 | }
652 |
653 | func test_heartRate_present_resultsIn_uint8Value() {
654 | /* Given */
655 | let flags = RowerDataCharacteristicFlags.create(heartRatePresent: true)
656 | let data = CharacteristicData.create(
657 | flags: flags,
658 | values: 170 // Heart rate of 170
659 | )
660 |
661 | /* When */
662 | let result = RowerDataCharacteristic.decode(data: data)
663 |
664 | /* Then */
665 | XCTAssertEqual(result.heartRate, 170)
666 | }
667 |
668 | // MARK: Metabolic Equivalent
669 |
670 | func test_metabolicEquivalent_notPresent_resultsIn_nilValue() {
671 | /* Given */
672 | let flags = RowerDataCharacteristicFlags.create(metabolicEquivalentPresent: false)
673 | let data = CharacteristicData.create(flags: flags)
674 |
675 | /* When */
676 | let result = RowerDataCharacteristic.decode(data: data)
677 |
678 | /* Then */
679 | XCTAssertNil(result.metabolicEquivalent)
680 | }
681 |
682 | func test_metabolicEquivalent_present_resultsIn_uint8Value() {
683 | /* Given */
684 | let flags = RowerDataCharacteristicFlags.create(metabolicEquivalentPresent: true)
685 | let data = CharacteristicData.create(
686 | flags: flags,
687 | values: 123 // Metabolic Equivalent of 12.3
688 | )
689 |
690 | /* When */
691 | let result = RowerDataCharacteristic.decode(data: data)
692 |
693 | /* Then */
694 | XCTAssertEqual(result.metabolicEquivalent, 12.3)
695 | }
696 |
697 | // MARK: Elapsed Time
698 |
699 | func test_elapsedTime_notPresent_resultsIn_nilValue() {
700 | /* Given */
701 | let flags = RowerDataCharacteristicFlags.create(elapsedTimePresent: false)
702 | let data = CharacteristicData.create(flags: flags)
703 |
704 | /* When */
705 | let result = RowerDataCharacteristic.decode(data: data)
706 |
707 | /* Then */
708 | XCTAssertNil(result.elapsedTimeSeconds)
709 | }
710 |
711 | func test_elapsedTime_present_resultsIn_uint16Value_forLowValue() {
712 | /* Given */
713 | let flags = RowerDataCharacteristicFlags.create(elapsedTimePresent: true)
714 | let data = CharacteristicData.create(
715 | flags: flags,
716 | values: 3,
717 | 0
718 | )
719 |
720 | /* When */
721 | let result = RowerDataCharacteristic.decode(data: data)
722 |
723 | /* Then */
724 | XCTAssertEqual(result.elapsedTimeSeconds, 3)
725 | }
726 |
727 | func test_elapsedTime_present_resultsIn_uint16Value_forHighValue() {
728 | /* Given */
729 | let flags = RowerDataCharacteristicFlags.create(elapsedTimePresent: true)
730 | let data = CharacteristicData.create(
731 | flags: flags,
732 | values: 1, // 1 + 512
733 | 2
734 | )
735 |
736 | /* When */
737 | let result = RowerDataCharacteristic.decode(data: data)
738 |
739 | /* Then */
740 | XCTAssertEqual(result.elapsedTimeSeconds, 513)
741 | }
742 |
743 | // MARK: Remaining Time
744 |
745 | func test_remainingTime_notPresent_resultsIn_nilValue() {
746 | /* Given */
747 | let flags = RowerDataCharacteristicFlags.create(remainingTimePresent: false)
748 | let data = CharacteristicData.create(flags: flags)
749 |
750 | /* When */
751 | let result = RowerDataCharacteristic.decode(data: data)
752 |
753 | /* Then */
754 | XCTAssertNil(result.remainingTimeSeconds)
755 | }
756 |
757 | func test_remainingTime_present_resultsIn_uint16Value_forLowValue() {
758 | /* Given */
759 | let flags = RowerDataCharacteristicFlags.create(remainingTimePresent: true)
760 | let data = CharacteristicData.create(
761 | flags: flags,
762 | values: 3,
763 | 0
764 | )
765 |
766 | /* When */
767 | let result = RowerDataCharacteristic.decode(data: data)
768 |
769 | /* Then */
770 | XCTAssertEqual(result.remainingTimeSeconds, 3)
771 | }
772 |
773 | func test_remainingTime_present_resultsIn_uint16Value_forHighValue() {
774 | /* Given */
775 | let flags = RowerDataCharacteristicFlags.create(remainingTimePresent: true)
776 | let data = CharacteristicData.create(
777 | flags: flags,
778 | values: 1, // 1 + 512
779 | 2
780 | )
781 |
782 | /* When */
783 | let result = RowerDataCharacteristic.decode(data: data)
784 |
785 | /* Then */
786 | XCTAssertEqual(result.remainingTimeSeconds, 513)
787 | }
788 |
789 | // MARK: Multiple properties present
790 |
791 | func test_multiplePropertiesPresent_properlyOffsetsValues_forAverageStrokeRateAndTotalDistance() {
792 | /* Given */
793 | let flags = RowerDataCharacteristicFlags.create(
794 | averageStrokeRatePresent: true,
795 | totalDistancePresent: true
796 | )
797 | let data = CharacteristicData.create(
798 | flags: flags,
799 | values: 7, // Average stroke rate of 3.5
800 | 16, // Total distance of 16
801 | 0,
802 | 0
803 | )
804 |
805 | /* When */
806 | let result = RowerDataCharacteristic.decode(data: data)
807 |
808 | /* Then */
809 | XCTAssertEqual(result.averageStrokeRate, 3.5)
810 | XCTAssertEqual(result.totalDistanceMeters, 16)
811 | }
812 |
813 | func test_multiplePropertiesPresent_properlyOffsetsValues_forAverageStrokeRateAndInstantaneousPace() {
814 | /* Given */
815 | let flags = RowerDataCharacteristicFlags.create(
816 | averageStrokeRatePresent: true,
817 | instantaneousPacePresent: true
818 | )
819 | let data = CharacteristicData.create(
820 | flags: flags,
821 | values: 7, // Average stroke rate of 3.5
822 | 16, // Instantaneous pace of 16
823 | 0
824 | )
825 |
826 | /* When */
827 | let result = RowerDataCharacteristic.decode(data: data)
828 |
829 | /* Then */
830 | XCTAssertEqual(result.averageStrokeRate, 3.5)
831 | XCTAssertEqual(result.instantaneousPaceSeconds, 16)
832 | }
833 |
834 | func test_multiplePropertiesPresent_properlyOffsetsValues_forTotalDistanceAndInstantaneousPace() {
835 | /* Given */
836 | let flags = RowerDataCharacteristicFlags.create(
837 | totalDistancePresent: true,
838 | instantaneousPacePresent: true
839 | )
840 | let data = CharacteristicData.create(
841 | flags: flags,
842 | values: 32, // Total distance of 32
843 | 0,
844 | 0,
845 | 16, // Instantaneous pace of 16
846 | 0
847 | )
848 |
849 | /* When */
850 | let result = RowerDataCharacteristic.decode(data: data)
851 |
852 | /* Then */
853 | XCTAssertEqual(result.totalDistanceMeters, 32)
854 | XCTAssertEqual(result.instantaneousPaceSeconds, 16)
855 | }
856 |
857 | // This is not really representative (not enough available bytes in a real world scenario),
858 | // but a good test to execute anyway to test dependencies.
859 | func test_allPropertiesPresent() {
860 | /* Given */
861 | let flags = RowerDataCharacteristicFlags.create(
862 | moreDataPresent: true,
863 | averageStrokeRatePresent: true,
864 | totalDistancePresent: true,
865 | instantaneousPacePresent: true,
866 | averagePacePresent: true,
867 | instantaneousPowerPresent: true,
868 | averagePowerPresent: true,
869 | resistanceLevelPresent: true,
870 | expendedEnergyPresent: true,
871 | heartRatePresent: true,
872 | metabolicEquivalentPresent: true,
873 | elapsedTimePresent: true,
874 | remainingTimePresent: true
875 | )
876 |
877 | let data = CharacteristicData.create(
878 | flags: flags,
879 | values: 1, // Stroke rate of 0.5
880 | 2, // Stroke count of 2
881 | 0,
882 | 3, // Average stroke rate of 1.5
883 | 4, // Total distance of 4
884 | 0,
885 | 0,
886 | 5, // Instantaneous pace of 5
887 | 0,
888 | 6, // Average pace of 6
889 | 0,
890 | 7, // Instantaneous power of 7,
891 | 0,
892 | 8, // Average power of 8
893 | 0,
894 | 9, // Resistance level of 9
895 | 0,
896 | 10, // Total energy of 10
897 | 0,
898 | 11, // Energy per hour of 11
899 | 0,
900 | 12, // Energy per minute of 12
901 | 13, // Heart rate of 13
902 | 14, // Metabolic equivalent of 1.4
903 | 15, // Elapsed time of 15
904 | 0,
905 | 16, // Time remaining of 16
906 | 0
907 | )
908 |
909 | /* When */
910 | let result = RowerDataCharacteristic.decode(data: data)
911 |
912 | /* Then */
913 | XCTAssertEqual(result.strokeRate, 0.5)
914 | XCTAssertEqual(result.strokeCount, 2)
915 | XCTAssertEqual(result.averageStrokeRate, 1.5)
916 | XCTAssertEqual(result.totalDistanceMeters, 4)
917 | XCTAssertEqual(result.instantaneousPaceSeconds, 5)
918 | XCTAssertEqual(result.averagePaceSeconds, 6)
919 | XCTAssertEqual(result.instantaneousPowerWatts, 7)
920 | XCTAssertEqual(result.averagePowerWatts, 8)
921 | XCTAssertEqual(result.resistanceLevel, 9)
922 | XCTAssertEqual(result.totalEnergyKiloCalories, 10)
923 | XCTAssertEqual(result.energyPerHourKiloCalories, 11)
924 | XCTAssertEqual(result.energyPerMinuteKiloCalories, 12)
925 | XCTAssertEqual(result.heartRate, 13)
926 | XCTAssertEqual(result.metabolicEquivalent, 1.4)
927 | XCTAssertEqual(result.elapsedTimeSeconds, 15)
928 | XCTAssertEqual(result.remainingTimeSeconds, 16)
929 | }
930 | }
931 |
--------------------------------------------------------------------------------