diff --git a/PolarBleSdk.podspec b/PolarBleSdk.podspec index 8ddb067f..30b6bc30 100644 --- a/PolarBleSdk.podspec +++ b/PolarBleSdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PolarBleSdk' - s.version = '5.6.0-beta1' + s.version = '5.6.0-beta2' s.summary = 'SDK for Polar sensors' s.homepage = 'https://github.com/polarofficial/polar-ble-sdk' s.license = { :type => 'Custom', :file => 'Polar_SDK_License.txt' } diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/offlinerecording/OfflineRecordingData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/offlinerecording/OfflineRecordingData.kt index d0566ddc..6e0693fd 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/offlinerecording/OfflineRecordingData.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/offlinerecording/OfflineRecordingData.kt @@ -46,8 +46,8 @@ internal class OfflineRecordingData( private const val PACKET_SIZE_LENGTH = 2 @Throws(Exception::class) - fun parseDataFromOfflineFile(fileData: ByteArray, type: PmdMeasurementType, secret: PmdSecret? = null): OfflineRecordingData { - BleLogger.d(TAG, "Start offline file parsing. File size is ${fileData.size} and type $type") + fun parseDataFromOfflineFile(fileData: ByteArray, type: PmdMeasurementType, secret: PmdSecret? = null, lastTimestamp: ULong = 0uL): OfflineRecordingData { + BleLogger.d(TAG, "Start offline file parsing. File size is ${fileData.size} and type $type, previous file last timestamp: $lastTimestamp") // guard if (fileData.isEmpty()) { @@ -70,7 +70,8 @@ internal class OfflineRecordingData( val parsedData = parseData( dataBytes = payloadDataBytes, metaData = metaData, - builder = getDataBuilder(type) + builder = getDataBuilder(type), + lastTimestamp = lastTimestamp ) return OfflineRecordingData( @@ -271,9 +272,9 @@ internal class OfflineRecordingData( return TypeUtils.convertArrayToUnsignedInt(packetSize.toByteArray(), 0, 2) } - private fun parseData(dataBytes: List, metaData: OfflineRecordingMetaData, builder: T): T { + private fun parseData(dataBytes: List, metaData: OfflineRecordingMetaData, builder: T, lastTimestamp: ULong = 0uL): T { - var previousTimeStamp: ULong = 0uL + var previousTimeStamp: ULong = lastTimestamp var packetSize = metaData.dataPayloadSize val sampleRate = metaData.recordingSettings?.settings?.get(PmdSetting.PmdSettingType.SAMPLE_RATE)?.first() ?: 0 val factor = metaData.recordingSettings?.settings?.get(PmdSetting.PmdSettingType.FACTOR)?.first()?.let { diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt index 5a7b83af..60ced941 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt @@ -25,6 +25,6 @@ object PolarBleApiDefaultImpl { */ @JvmStatic fun versionInfo(): String { - return "5.6.0-beta" + return "5.6.0-beta2" } } \ No newline at end of file diff --git a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift index 145d4c97..78933c33 100644 --- a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift +++ b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift @@ -1239,32 +1239,33 @@ extension PolarBleApiImpl: PolarBleApi { var polarPpiData: PolarOfflineRecordingData? var polarHrData: PolarOfflineRecordingData? + var lastTimestamp: UInt64 = 0 _ = subRecordingCountObservable .flatMap { count -> Observable in return Observable.range(start: 0, count: count) .flatMap { subRecordingIndex -> Observable in Observable.create { observer in - + let subRecordingPath: String if entry.path.range(of: ".*\\.REC$", options: .regularExpression) != nil && count > 0 { subRecordingPath = entry.path.replacingOccurrences(of: "\\d(?=\\.REC$)", with: "\(subRecordingIndex)", options: .regularExpression) } else { subRecordingPath = entry.path } - + do { var operation = Protocol_PbPFtpOperation() operation.command = Protocol_PbPFtpOperation.Command.get operation.path = subRecordingPath.isEmpty ? entry.path : subRecordingPath let request = try operation.serializedData() - + BleLogger.trace("Offline record get. Device: \(identifier) Path: \(subRecordingPath) Secret used: \(secret != nil)") - + let notificationResult = client.sendNotification( Protocol_PbPFtpHostToDevNotification.initializeSession.rawValue, parameters: nil ) - + let requestResult = notificationResult .andThen(Single.deferred { client.request(request) }) .map { dataResult in @@ -1273,20 +1274,22 @@ extension PolarBleApiImpl: PolarBleApi { let offlineRecordingData: OfflineRecordingData = try OfflineRecordingData.parseDataFromOfflineFile( fileData: dataResult as Data, type: PolarDataUtils.mapToPmdClientMeasurementType(from: entry.type), - secret: pmdSecret + secret: pmdSecret, + lastTimestamp: lastTimestamp ) return offlineRecordingData } catch { throw PolarErrors.polarOfflineRecordingError(description: "Failed to parse data") } } - + _ = requestResult.subscribe( onSuccess: { offlineRecordingData in do { let settings: PolarSensorSetting = offlineRecordingData.recordingSettings?.mapToPolarSettings() ?? PolarSensorSetting() switch offlineRecordingData.data { case let accData as AccData: + lastTimestamp = accData.samples.last?.timeStamp ?? 0 switch polarAccData { case let .accOfflineRecordingData(existingData, startTime, existingSettings): let newSamples = existingData.samples + accData.samples.map { (timeStamp: $0.timeStamp, x: $0.x, y: $0.y, z: $0.z) } @@ -1305,6 +1308,7 @@ extension PolarBleApiImpl: PolarBleApi { observer.onNext(polarAccData!) } case let gyroData as GyrData: + lastTimestamp = gyroData.samples.last?.timeStamp ?? 0 switch polarGyroData { case let .gyroOfflineRecordingData(existingData, startTime, existingSettings): let newSamples = existingData.samples + gyroData.samples.map { (timeStamp: $0.timeStamp, x: $0.x, y: $0.y, z: $0.z) } @@ -1323,6 +1327,7 @@ extension PolarBleApiImpl: PolarBleApi { observer.onNext(polarGyroData!) } case let magData as MagData: + lastTimestamp = magData.samples.last?.timeStamp ?? 0 switch polarMagData { case let .magOfflineRecordingData(existingData, startTime, existingSettings): let newSamples = existingData.samples + magData.samples.map { (timeStamp: $0.timeStamp, x: $0.x, y: $0.y, z: $0.z) } @@ -1341,6 +1346,7 @@ extension PolarBleApiImpl: PolarBleApi { observer.onNext(polarMagData!) } case let ppgData as PpgData: + lastTimestamp = ppgData.samples.last?.timeStamp ?? 0 switch polarPpgData { case let .ppgOfflineRecordingData(existingData, startTime, existingSettings): let newSamples = existingData.samples + ppgData.samples.map { (timeStamp: $0.timeStamp, channelSamples: $0.ppgDataSamples) } diff --git a/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/offlinerecording/OfflineRecordingData.swift b/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/offlinerecording/OfflineRecordingData.swift index b61171c2..f5b682f6 100644 --- a/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/offlinerecording/OfflineRecordingData.swift +++ b/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/offlinerecording/OfflineRecordingData.swift @@ -33,35 +33,36 @@ public struct OfflineRecordingData { let recordingSettings: PmdSetting? let data: DataType - public static func parseDataFromOfflineFile(fileData: Data, type: PmdMeasurementType, secret: PmdSecret? = nil) throws -> OfflineRecordingData { - BleLogger.trace("Start offline file parsing. File size is \(fileData.count) and type \(type)") - + public static func parseDataFromOfflineFile(fileData: Data, type: PmdMeasurementType, secret: PmdSecret? = nil, lastTimestamp: UInt64 = 0) throws -> OfflineRecordingData { + BleLogger.trace("Start offline file parsing. File size is \(fileData.count) and type \(type), previous file last timestamp: \(lastTimestamp)") + guard !fileData.isEmpty else { throw OfflineRecordingError.emptyFile } - + let (metaData, metaDataLength):(OfflineRecordingMetaData, Int) do { (metaData, metaDataLength) = try parseMetaData(fileData, secret) } catch { throw OfflineRecordingError.offlineRecordingErrorMetaDataParseFailed(description: "\(error)") } - + let payloadDataBytes = fileData.subdata(in: metaDataLength..(offlineRecordingHeader: metaData.offlineRecordingHeader, startTime: metaData.startTime, recordingSettings: metaData.recordingSettings, data: parsedData) } - + private static func getDataBuilder(type: PmdMeasurementType) throws -> Any { switch(type) { case .ecg: @@ -82,41 +83,41 @@ public struct OfflineRecordingData { throw OfflineRecordingError.offlineRecordingErrorNoParserForData } } - + private static func parseMetaData(_ fileBytes: Data,_ secret: PmdSecret?) throws -> (OfflineRecordingMetaData, Int) { var securityOffset = 0 let offlineRecordingSecurityStrategy = try parseSecurityStrategy(strategyBytes: fileBytes.subdataSafe(in: SECURITY_STRATEGY_INDEX.. { ) return (metaData, securityOffset + metaDataOffset) } - + private static func parseSecurityStrategy(strategyBytes: Data) throws -> PmdSecret.SecurityStrategy { guard strategyBytes.count == 1, let strategyByte = strategyBytes.first else { throw OfflineRecordingError.offlineRecordingErrorMetaDataParseFailed(description: "security strategy parse failed. Strategy bytes size was \(strategyBytes.count)") } return try PmdSecret.SecurityStrategy.fromByte(strategyByte: strategyByte) } - + private static func decryptMetaData(offlineRecordingSecurityStrategy: PmdSecret.SecurityStrategy, metaData: Data, secret: PmdSecret?) throws -> Data { switch (offlineRecordingSecurityStrategy) { - + case .none: return metaData case .xor: @@ -144,22 +145,22 @@ public struct OfflineRecordingData { throw OfflineRecordingError.offlineRecordingErrorSecretMissing } return try s.decryptArray(cipherArray: metaData) - + case .aes128, .aes256: guard let s = secret else { throw OfflineRecordingError.offlineRecordingErrorSecretMissing } - + guard s.strategy == offlineRecordingSecurityStrategy else { throw OfflineRecordingError.offlineRecordingSecurityStrategyMissMatch(description: "Offline file is encrypted using \(offlineRecordingSecurityStrategy). The key provided is \(s.strategy)") } - + let endOffset = (metaData.count / 16 * 16) let metaDataChunk = try metaData.subdataSafe(in: 0.. OfflineRecordingHeader { var offset = 0 let step = 4 @@ -171,41 +172,41 @@ public struct OfflineRecordingData { offset += step let eswHash = TypeUtils.convertArrayToUnsignedInt(headerBytes, offset: offset, size: step) offset += step - + return OfflineRecordingHeader(magic: magic, version: version, free: free, eswHash: eswHash) } - + private static func parseStartTime(startTimeBytes: Data) throws -> Date { let expectedStartTime: String = String(decoding: startTimeBytes, as: UTF8.self) let trimmedString = String(expectedStartTime.replacingOccurrences(of: " ", with: "T").dropLast() + "Z") - + let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime] - + guard let result = dateFormatter.date(from: trimmedString) else { throw OfflineRecordingError.offlineRecordingErrorMetaDataParseFailed(description: "Couldn't parse start time from \(trimmedString)") } return result } - + private static func parseSettings(metaDataBytes: Data) throws -> (PmdSetting?, Int) { var offset = 0 let settingsLength = Int(metaDataBytes[offset]) offset += OFFLINE_SETTINGS_SIZE_FIELD_LENGTH let settingBytes = try metaDataBytes.subdataSafe(in:offset..<(offset + settingsLength)) var pmdSetting: PmdSetting? = nil - + if (!settingBytes.isEmpty) { pmdSetting = PmdSetting(settingBytes) } return (pmdSetting, offset + settingsLength) } - + private static func parseSecurityInfo(securityInfoBytes: Data, secret: PmdSecret?) throws -> (PmdSecret, Int) { var offset = 0 let infoLength = securityInfoBytes[offset] offset += 1 - + if infoLength == 0 { if let s = secret { return (s, offset) @@ -213,11 +214,11 @@ public struct OfflineRecordingData { return try (PmdSecret(strategy : PmdSecret.SecurityStrategy.none, key : Data()), offset) } } - + let strategy = securityInfoBytes[offset] offset += 1 switch(try PmdSecret.SecurityStrategy.fromByte(strategyByte: strategy)) { - + case .none: return try (PmdSecret(strategy : PmdSecret.SecurityStrategy.none, key : Data()), offset) case .xor: @@ -239,7 +240,7 @@ public struct OfflineRecordingData { return try (PmdSecret(strategy : PmdSecret.SecurityStrategy.aes256, key :key), offset) } } - + private static func parsePaddingBytes(metaDataOffset: Int, offlineRecordingSecurityStrategy: PmdSecret.SecurityStrategy) -> Int { switch (offlineRecordingSecurityStrategy) { case .none, .xor: @@ -248,27 +249,27 @@ public struct OfflineRecordingData { return (16 - metaDataOffset % 16) } } - + private static func parsePacketSize(packetSize: Data) -> Int { return Int(TypeUtils.convertArrayToUnsignedInt(packetSize, offset: 0, size: 2)) } - - private static func parseData(dataBytes: Data, metaData: OfflineRecordingMetaData, builder: Any) throws -> Any { - var previousTimeStamp: UInt64 = 0 - + + private static func parseData(dataBytes: Data, metaData: OfflineRecordingMetaData, builder: Any, lastTimestamp: UInt64 = 0) throws -> Any { + var previousTimeStamp: UInt64 = lastTimestamp + var packetSize = metaData.dataPayloadSize let sampleRate = UInt(metaData.recordingSettings?.settings[PmdSetting.PmdSettingType.sampleRate]?.first ?? 0) - + let factor:Float if let data = metaData.recordingSettings?.settings[PmdSetting.PmdSettingType.factor]?.first { factor = Float(bitPattern: data) } else { factor = 1.0 } - + var offset = 0 let decryptedData = try metaData.securityInfo.decryptArray(cipherArray: dataBytes) - + repeat { let data = try decryptedData.subdataSafe(in:offset..<(packetSize + offset)) offset += packetSize @@ -276,9 +277,9 @@ public struct OfflineRecordingData { { _ in previousTimeStamp } , { _ in factor }, { _ in sampleRate }) - + previousTimeStamp = dataFrame.timeStamp - + switch(builder) { case is EcgData: let ecgData = try EcgData.parseDataFromDataFrame(frame: dataFrame) @@ -304,7 +305,7 @@ public struct OfflineRecordingData { default: throw OfflineRecordingError.offlineRecordingErrorSecretMissing } - + if (offset < decryptedData.count) { packetSize = try parsePacketSize(packetSize: decryptedData.subdataSafe(in:offset..<(offset + PACKET_SIZE_LENGTH))) offset += PACKET_SIZE_LENGTH