Skip to content

Commit

Permalink
Issue 130: BLE Battery Service client did not update the changes for …
Browse files Browse the repository at this point in the history
…observer listening the battery status.
  • Loading branch information
JOikarinen committed Mar 2, 2021
1 parent 3264462 commit c4211e6
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@
6CEA10E821786B0000E16FBF /* Timestamp.pbobjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 6CEA10E421786B0000E16FBF /* Timestamp.pbobjc.h */; };
6CEA10E921786B0000E16FBF /* Type.pbobjc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CEA10E521786B0000E16FBF /* Type.pbobjc.m */; };
6CEA10EA21786B0000E16FBF /* Timestamp.pbobjc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CEA10E621786B0000E16FBF /* Timestamp.pbobjc.m */; };
A53468CB25ECDB38006752CA /* RxTest.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A53468CA25ECDB38006752CA /* RxTest.xcframework */; };
A580139725B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A580139625B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift */; };
A580139825B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A580139625B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift */; };
A580139925B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A580139625B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift */; };
A5AD08B725E67064002DA200 /* BleBasClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD08B625E67064002DA200 /* BleBasClientTest.swift */; };
A5C3EC3525A9CB33003565A9 /* RxSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5C3EC3425A9CB33003565A9 /* RxSwift.xcframework */; };
A5C3EC3825A9CB43003565A9 /* RxSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5C3EC3425A9CB33003565A9 /* RxSwift.xcframework */; };
A5C3EC3B25A9CB4F003565A9 /* RxSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5C3EC3425A9CB33003565A9 /* RxSwift.xcframework */; };
Expand Down Expand Up @@ -434,7 +436,9 @@
6CEA10E421786B0000E16FBF /* Timestamp.pbobjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Timestamp.pbobjc.h; path = sdk/impl/protobuf/google/Timestamp.pbobjc.h; sourceTree = "<group>"; };
6CEA10E521786B0000E16FBF /* Type.pbobjc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Type.pbobjc.m; path = sdk/impl/protobuf/google/Type.pbobjc.m; sourceTree = "<group>"; };
6CEA10E621786B0000E16FBF /* Timestamp.pbobjc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Timestamp.pbobjc.m; path = sdk/impl/protobuf/google/Timestamp.pbobjc.m; sourceTree = "<group>"; };
A53468CA25ECDB38006752CA /* RxTest.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RxTest.xcframework; path = dependencyFrameworks/RxTest.xcframework; sourceTree = "<group>"; };
A580139625B1636A00654E60 /* BlePolarDeviceCapabilitiesUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlePolarDeviceCapabilitiesUtility.swift; path = ble/api/model/polar/BlePolarDeviceCapabilitiesUtility.swift; sourceTree = "<group>"; };
A5AD08B625E67064002DA200 /* BleBasClientTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BleBasClientTest.swift; sourceTree = "<group>"; };
A5C3EC3425A9CB33003565A9 /* RxSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RxSwift.xcframework; path = dependencyFrameworks/RxSwift.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand All @@ -452,6 +456,7 @@
buildActionMask = 2147483647;
files = (
A5C3EC3B25A9CB4F003565A9 /* RxSwift.xcframework in Frameworks */,
A53468CB25ECDB38006752CA /* RxTest.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -477,6 +482,7 @@
6C1079211D6D6EA000931948 /* iOSCommunicationsTests */ = {
isa = PBXGroup;
children = (
A5AD08B625E67064002DA200 /* BleBasClientTest.swift */,
6C1079221D6D6EA000931948 /* iOSCommunicationsTests.swift */,
6C1079241D6D6EA000931948 /* Info.plist */,
);
Expand Down Expand Up @@ -640,6 +646,7 @@
6C64AB1C1D8ABF980074340A /* Frameworks */ = {
isa = PBXGroup;
children = (
A53468CA25ECDB38006752CA /* RxTest.xcframework */,
A5C3EC3425A9CB33003565A9 /* RxSwift.xcframework */,
);
name = Frameworks;
Expand Down Expand Up @@ -1187,6 +1194,7 @@
buildActionMask = 2147483647;
files = (
6C1079231D6D6EA000931948 /* iOSCommunicationsTests.swift in Sources */,
A5AD08B725E67064002DA200 /* BleBasClientTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1671,7 +1679,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.polar.PolarBleSdkWatchOs;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
Expand Down Expand Up @@ -1723,7 +1731,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.polar.PolarBleSdkWatchOs;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
Expand Down Expand Up @@ -1760,6 +1768,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
EXCLUDED_ARCHS = "";
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
GCC_C_LANGUAGE_STANDARD = gnu11;
Expand All @@ -1775,7 +1784,7 @@
"@loader_path/Frameworks",
);
MACH_O_TYPE = mh_dylib;
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
"OTHER_CFLAGS[arch=*]" = "-fembed-bitcode";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.polar.PolarBleSdk;
Expand Down Expand Up @@ -1817,6 +1826,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -1831,7 +1841,7 @@
"@loader_path/Frameworks",
);
MACH_O_TYPE = mh_dylib;
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
"OTHER_CFLAGS[arch=*]" = "-fembed-bitcode";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.polar.PolarBleSdk;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,59 @@ import CoreBluetooth
import RxSwift

public class BleBasClient: BleGattClientBase {

public static let BATTERY_SERVICE = CBUUID(string: "180F")
let BATTERY_LEVEL = CBUUID(string: "2A19")

var batteryLevel = AtomicInteger(initialValue: -1)
var observers = AtomicList<RxObserver<Int>>()
private static let BATTERY_LEVEL_CHARACTERISTIC = CBUUID(string: "2A19")
private static let UNDEFINED_BATTERY_PERCENTAGE = -1

var cachedBatteryPercentage = AtomicInteger(initialValue: UNDEFINED_BATTERY_PERCENTAGE)
var observers = AtomicList<RxObserver<Int>>()

public init(gattServiceTransmitter: BleAttributeTransportProtocol){
super.init(serviceUuid: BleBasClient.BATTERY_SERVICE, gattServiceTransmitter: gattServiceTransmitter)
addCharacteristicRead(BATTERY_LEVEL)
addCharacteristicNotification(BleBasClient.BATTERY_LEVEL_CHARACTERISTIC)
addCharacteristicRead(BleBasClient.BATTERY_LEVEL_CHARACTERISTIC)
}

// from base
override public func disconnected() {
super.disconnected()
self.batteryLevel.set(-1)
cachedBatteryPercentage.set(BleBasClient.UNDEFINED_BATTERY_PERCENTAGE)
RxUtils.postErrorAndClearList(observers, error: BleGattException.gattDisconnected)
}

override public func processServiceData(_ chr: CBUUID , data: Data , err: Int ){
var trace = "BleBasClient process data. chr: \(chr.uuidString)"
if( err == 0 ){
var level: Int8=0
var level: UInt8=0
(data as NSData).getBytes(&level, length: MemoryLayout<UInt8>.size)
batteryLevel.set(Int(level))
RxUtils.emitNext(observers) { (observer) in
observer.obs.onNext(Int(level))
}
trace.append(" battery percentage: \(level)")
BleLogger.trace(trace)
cachedBatteryPercentage.set(Int(level))
RxUtils.emitNext(observers) { (observer) in observer.obs.onNext(Int(level))}
} else {
trace.append(" err: \(err)")
BleLogger.error(trace)
}
}

// apis
public func readLevel() throws {
try self.gattServiceTransmitter?.readValue(self, serviceUuid: BleBasClient.BATTERY_SERVICE, characteristicUuid: self.BATTERY_LEVEL)
try self.gattServiceTransmitter?.readValue(self, serviceUuid: BleBasClient.BATTERY_SERVICE, characteristicUuid: BleBasClient.BATTERY_LEVEL_CHARACTERISTIC)
}

/// wait/monitor battery level update, either returns cached value, or waits initial read value
/// Get observable for monitoring battery status updates on connected device. If battery level is already cached then the cached value is emitted immidiately.
///
/// - Parameter checkConnection: check initial connection
/// - Returns: Observable stream, complete: non produced
public func waitBatteryLevelUpdate(_ checkConnection: Bool) -> Observable<Int> {
var object: RxObserver<Int>!
return Observable.create{ observer in
object = RxObserver<Int>.init(obs: observer)
if !checkConnection || self.gattServiceTransmitter?.isConnected() ?? false {
self.observers.append(object)
if self.batteryLevel.get() != -1 {
object.obs.onNext(Int(self.batteryLevel.get()))
}
} else {
observer.onError(BleGattException.gattDisconnected)
}
return Disposables.create {
self.observers.remove({ (item) -> Bool in
return item === object
})
}
}
/// - Returns: Observable stream of battery status.
/// onNext, on every battery status update received from connected device. The value is the device battery level as a percentage from 0% to 100%
/// onError, if client is not initially connected or ble disconnect's
public func monitorBatteryStatus(_ checkConnection: Bool) -> Observable<Int> {
return RxUtils.monitor(observers, transport: gattServiceTransmitter, checkConnection: checkConnection)
.startWith(self.cachedBatteryPercentage.get())
.filter { value in self.isValidBatteryPercentage(value)};
}

private func isValidBatteryPercentage(_ batteryPercentage:Int) -> Bool {
let batteryPercentageRange = 0...100
return batteryPercentageRange.contains(batteryPercentage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class RxUtils {
}
list.removeAll()
}

/// helper to emit next object
static func emitNext<T>(_ list: AtomicList<T>, emitter: (_ item: T) -> Void ) {
let objects = list.list()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,8 @@ import RxSwift
let hrClient = client as! BleHrClient
self.startHrObserver(hrClient, deviceId: deviceId)
case BleBasClient.BATTERY_SERVICE:
return (client as! BleBasClient).waitBatteryLevelUpdate(true)
return (client as! BleBasClient).monitorBatteryStatus(true)
.observe(on: self.scheduler)
.take(1)
.do(onNext: { (level: Int) in
self.deviceInfoObserver?.batteryLevelReceived(
deviceId, batteryLevel: UInt(level))
Expand Down Expand Up @@ -295,8 +294,9 @@ import RxSwift
}

private func startHrObserver(_ client: BleHrClient, deviceId: String) {
_ = client.observeHrNotifications(true).observe(
on: self.scheduler).subscribe{ e in
_ = client.observeHrNotifications(true)
.observe(on: self.scheduler)
.subscribe{ e in
switch e {
case .completed:
break
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright © 2021 Polar. All rights reserved.

import XCTest
import iOSCommunications
import CoreBluetooth
import RxTest
import RxSwift

class BleBasClientTest: XCTestCase {
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
var bleBasClient:BleBasClient!
var mockGattServiceTransmitterImpl:MockGattServiceTransmitterImpl!

override func setUpWithError() throws {
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
mockGattServiceTransmitterImpl = MockGattServiceTransmitterImpl()
bleBasClient = BleBasClient(gattServiceTransmitter: mockGattServiceTransmitterImpl)
}

override func tearDownWithError() throws {
scheduler = nil
disposeBag = nil
mockGattServiceTransmitterImpl = nil
bleBasClient = nil
}

// GIVEN that BLE Battery Service client receives battery data updates
// WHEN battery level observable is subscribed
// THEN the latest cached battery value is emitted
func testTheCachedValue() throws {

// Arrange
let characteristic: CBUUID = CBUUID(string: "2A19")
let deviceNotifyingBatteryData1 = Data([100])
let deviceNotifyingBatteryData2 = Data([80])
let error = 0
let observer = scheduler.createObserver(Int.self)

// Act
bleBasClient.processServiceData(characteristic, data: deviceNotifyingBatteryData1, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingBatteryData2, err: error)

let observable = bleBasClient.monitorBatteryStatus(true)
observable.subscribe(observer).disposed(by: disposeBag)

scheduler.start()

// Assert
XCTAssertEqual(observer.events, [
.next(0, Int(deviceNotifyingBatteryData2[0]))
])
}

// GIVEN that BLE Battery Service client receives battery data updates
// WHEN battery level observable is subscribed
// THEN the correct values are received by observer
func testBatteryValuesStreamEmitsCorrectValues() throws {

// Arrange
let characteristic: CBUUID = CBUUID(string: "2A19")
let deviceNotifyingValidBatteryData1 = Data([100])
let deviceNotifyingInvalidBatteryData1 = Data([250])
let deviceNotifyingValidBatteryData2 = Data([80])
let deviceNotifyingInvalidBatteryData2 = Data([0xFF])
let deviceNotifyingValidBatteryData3 = Data([00])

let error = 0
let observer = scheduler.createObserver(Int.self)

// Act
bleBasClient.processServiceData(characteristic, data: deviceNotifyingValidBatteryData1, err: error)
let observable = bleBasClient.monitorBatteryStatus(true)
observable.subscribe(observer).disposed(by: disposeBag)
scheduler.start()

bleBasClient.processServiceData(characteristic, data: deviceNotifyingInvalidBatteryData1, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingValidBatteryData2, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingInvalidBatteryData2, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingValidBatteryData3, err: error)

// Assert
XCTAssertEqual(observer.events, [
.next(0, Int(deviceNotifyingValidBatteryData1[0])),
.next(0, Int(deviceNotifyingValidBatteryData2[0])),
.next(0, Int(deviceNotifyingValidBatteryData3[0]))
])
}

// GIVEN that BLE Battery Service client receives battery data updates
// WHEN battery level observable is subscribed
// THEN at some point device connection is lost
func testDeviceDisconnectsWhileStreaming() throws {

// Arrange
let characteristic: CBUUID = CBUUID(string: "2A19")
let deviceNotifyingBatteryData1 = Data([100])
let deviceNotifyingBatteryData2 = Data([90])
let deviceNotifyingBatteryData3 = Data([80])

let error = 0
let observer = scheduler.createObserver(Int.self)

// Act
let observable = bleBasClient.monitorBatteryStatus(true)
observable.subscribe(observer).disposed(by: disposeBag)
scheduler.start()

bleBasClient.processServiceData(characteristic, data: deviceNotifyingBatteryData1, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingBatteryData2, err: error)
bleBasClient.processServiceData(characteristic, data: deviceNotifyingBatteryData3, err: error)
bleBasClient.disconnected()

// Assert
XCTAssertEqual(observer.events, [
.next(0, Int(deviceNotifyingBatteryData1[0])),
.next(0, Int(deviceNotifyingBatteryData2[0])),
.next(0, Int(deviceNotifyingBatteryData3[0])),
.error(0, BleGattException.gattDisconnected)
])
}
}

class MockGattServiceTransmitterImpl: BleAttributeTransportProtocol {
func isConnected() -> Bool {
return true
}

func transmitMessage(_ parent: BleGattClientBase, serviceUuid: CBUUID , characteristicUuid: CBUUID , packet: Data, withResponse: Bool) throws {
// Do nothing
}

func characteristicWith(uuid: CBUUID) throws -> CBCharacteristic? {
return nil
}

func characteristicNameWith(uuid: CBUUID) -> String? {
return nil
}

func readValue(_ parent: BleGattClientBase, serviceUuid: CBUUID , characteristicUuid: CBUUID ) throws {
// Do nothing
}

func setCharacteristicNotify(_ parent: BleGattClientBase, serviceUuid: CBUUID, characteristicUuid: CBUUID, notify: Bool) throws {
// Do nothing
}

func attributeOperationStarted(){
// Do nothing
}

func attributeOperationFinished(){
// Do nothing
}
}

0 comments on commit c4211e6

Please sign in to comment.