Skip to content

Commit

Permalink
5.6.0-beta1
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuli Määttä committed Dec 5, 2023
1 parent c68c75b commit 132dfc5
Show file tree
Hide file tree
Showing 19 changed files with 1,420 additions and 162 deletions.
2 changes: 1 addition & 1 deletion PolarBleSdk.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'PolarBleSdk'
s.version = '5.5.0'
s.version = '5.6.0-beta1'
s.summary = 'SDK for Polar sensors'
s.homepage = 'https://github.com/polarofficial/polar-ble-sdk'
s.license = { :type => 'Custom', :file => 'Polar_SDK_License.txt' }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.polar.androidcommunications.api.ble.model

/**
* DIS info key-value pair.
*/
data class DisInfo(
val key: String,
val value: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@

import com.polar.androidcommunications.api.ble.exceptions.BleAttributeError;
import com.polar.androidcommunications.api.ble.exceptions.BleDisconnected;
import com.polar.androidcommunications.api.ble.model.DisInfo;
import com.polar.androidcommunications.api.ble.model.gatt.BleGattBase;
import com.polar.androidcommunications.api.ble.model.gatt.BleGattTxInterface;
import com.polar.androidcommunications.common.ble.AtomicSet;
import com.polar.androidcommunications.common.ble.RxUtils;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
Expand All @@ -35,10 +42,15 @@ public class BleDisClient extends BleGattBase {
public static final UUID IEEE_11073_20601 = UUID.fromString("00002a2a-0000-1000-8000-00805f9b34fb");
public static final UUID PNP_ID = UUID.fromString("00002a50-0000-1000-8000-00805f9b34fb");

public static final String SYSTEM_ID_HEX = "SYSTEM_ID_HEX";

// store in map
private final HashMap<UUID, String> disInformation = new HashMap<>();
private final AtomicSet<FlowableEmitter<? super Pair<UUID, String>>> disObserverAtomicList = new AtomicSet<>();

private final Set<DisInfo> disInformationDataSet = new HashSet<>();
private final AtomicSet<FlowableEmitter<DisInfo>> disInfoObservers = new AtomicSet<>();

public BleDisClient(BleGattTxInterface txInterface) {
super(txInterface, DIS_SERVICE);
addCharacteristicRead(MODEL_NUMBER_STRING);
Expand All @@ -57,15 +69,26 @@ public void reset() {
super.reset();
synchronized (disInformation) {
disInformation.clear();
disInformationDataSet.clear();
}
RxUtils.postDisconnectedAndClearList(disObserverAtomicList);
RxUtils.postDisconnectedAndClearList(disInfoObservers);
}

@Override
public void processServiceData(final UUID characteristic, final byte[] data, int status, boolean notifying) {
if (status == 0) {
final String asciiRepresentation = new String(data, StandardCharsets.UTF_8);
synchronized (disInformation) {
disInformation.put(characteristic, new String(data, StandardCharsets.UTF_8));
disInformation.put(characteristic, asciiRepresentation);
}
synchronized (disInformationDataSet) {
if (characteristic.equals(BleDisClient.SYSTEM_ID)) {
final String hexRepresentation = systemIdBytesToHex(data);
disInformationDataSet.add(new DisInfo(BleDisClient.SYSTEM_ID_HEX, hexRepresentation));
} else {
disInformationDataSet.add(new DisInfo(characteristic.toString(), asciiRepresentation));
}
}
RxUtils.emitNext(disObserverAtomicList, object -> {
object.onNext(new Pair<>(characteristic, new String(data, StandardCharsets.UTF_8)));
Expand All @@ -75,6 +98,26 @@ public void processServiceData(final UUID characteristic, final byte[] data, int
}
}
});

RxUtils.emitNext(disInfoObservers, object -> {
disInformationDataSet.stream()
.filter(info -> (characteristic.equals(BleDisClient.SYSTEM_ID)
&& info.getKey().equals(BleDisClient.SYSTEM_ID_HEX))
|| (characteristic.toString().equals(info.getKey())))
.findFirst().ifPresent(object::onNext);

synchronized (disInformationDataSet) {
final Set<UUID> validUuids = disInformationDataSet.stream()
.map(DisInfo::getKey)
.filter(this::isValidUUIDString)
.map(UUID::fromString)
.collect(Collectors.toSet());

if (hasAllAvailableReadableCharacteristics(validUuids)) {
object.onComplete();
}
}
});
} else {
RxUtils.postError(disObserverAtomicList, new BleAttributeError("dis ", status));
}
Expand Down Expand Up @@ -118,5 +161,58 @@ public Flowable<Pair<UUID, String>> observeDisInfo(final boolean checkConnection
}
}, BackpressureStrategy.BUFFER).doFinally(() -> disObserverAtomicList.remove(observer[0]));
}

/**
* Produces: onNext, when a {@link DisInfo} has been read <BR>
* onCompleted, after all available {@link DisInfo} has been read <BR>
* onError, if client is not initially connected or ble disconnect's <BR>
*
* @param checkConnection, optionally check connection on subscribe <BR>
* @return Flowable stream emitting {@link DisInfo} <BR>
*/
public Flowable<DisInfo> observeDisInfoWithKeysAsStrings(final boolean checkConnection) {
final FlowableEmitter<DisInfo>[] observer = new FlowableEmitter[1];
return Flowable.create((FlowableOnSubscribe<DisInfo>) subscriber -> {
if (!checkConnection || BleDisClient.this.txInterface.isConnected()) {
observer[0] = subscriber;
disInfoObservers.add(subscriber);

synchronized (disInformationDataSet) {
for (DisInfo disInfo : disInformationDataSet) {
subscriber.onNext(disInfo);
}

final Set<UUID> validUuids = disInformationDataSet.stream()
.filter(disInfo -> isValidUUIDString(disInfo.getKey()))
.map(disInfo -> UUID.fromString(disInfo.getKey()))
.collect(Collectors.toSet());

if (hasAllAvailableReadableCharacteristics(validUuids)) {
subscriber.onComplete();
}
}
} else if (!subscriber.isCancelled()) {
subscriber.tryOnError(new BleDisconnected());
}
}, BackpressureStrategy.BUFFER)
.doFinally(() -> disInfoObservers.remove(observer[0]));
}

private String systemIdBytesToHex(final byte[] bytes) {
final StringBuilder hex = new StringBuilder(2 * bytes.length);
for (int i = bytes.length - 1; i >= 0; i--) {
hex.append(String.format("%02X", bytes[i]));
}
return hex.toString();
}

private boolean isValidUUIDString(final String s) {
try {
UUID.fromString(s);
return true;
} catch (final IllegalArgumentException e) {
return false;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class BleHtsClient(txInterface: BleGattTxInterface?) :

private val htsObserverAtomicList = AtomicSet<FlowableEmitter<in TemperatureMeasurement>>()

override fun reset() {
super.reset()
RxUtils.postDisconnectedAndClearList(htsObserverAtomicList)
}

override fun processServiceData(
characteristic: UUID?,
data: ByteArray?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdMeasurem
internal object OfflineRecordingUtility {

fun mapOfflineRecordingFileNameToMeasurementType(fileName: String): PmdMeasurementType {
return when (fileName) {
"ACC.REC" -> PmdMeasurementType.ACC
"GYRO.REC" -> PmdMeasurementType.GYRO
"MAG.REC" -> PmdMeasurementType.MAGNETOMETER
"PPG.REC" -> PmdMeasurementType.PPG
"PPI.REC" -> PmdMeasurementType.PPI
"HR.REC" -> PmdMeasurementType.OFFLINE_HR
else -> throw Exception("Unknown offline file $fileName")
val fileNameWithoutExtension = fileName.substringBeforeLast(".")
return when (fileNameWithoutExtension.replace(Regex("\\d+"), "")) {
"ACC" -> PmdMeasurementType.ACC
"GYRO" -> PmdMeasurementType.GYRO
"MAG" -> PmdMeasurementType.MAGNETOMETER
"PPG" -> PmdMeasurementType.PPG
"PPI" -> PmdMeasurementType.PPI
"HR" -> PmdMeasurementType.OFFLINE_HR
else -> throw IllegalArgumentException("Unknown offline file $fileName")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright © 2019 Polar Electro Oy. All rights reserved.
package com.polar.sdk.api

import com.polar.androidcommunications.api.ble.model.DisInfo
import com.polar.sdk.api.PolarBleApi.PolarDeviceDataType
import com.polar.sdk.api.model.PolarDeviceInfo
import com.polar.sdk.api.model.PolarHrData
Expand Down Expand Up @@ -83,6 +84,14 @@ interface PolarBleApiCallbackProvider {
*/
fun disInformationReceived(identifier: String, uuid: UUID, value: String)

/**
* DIS information received. Requires feature [PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO]
*
* @param identifier Polar device id or bt address
* @param disInfo [DisInfo] key-value pair
*/
fun disInformationReceived(identifier: String, disInfo: DisInfo)

/**
* Battery level received. Requires feature [PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO]
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ object PolarBleApiDefaultImpl {
*/
@JvmStatic
fun versionInfo(): String {
return "5.5.0"
return "5.6.0-beta"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,34 @@ interface PolarOfflineRecordingApi {
*/
fun getOfflineRecord(identifier: String, entry: PolarOfflineRecordingEntry, secret: PolarRecordingSecret? = null): Single<PolarOfflineRecordingData>

/**
* List split offline recordings stored in the device.
*
* @param identifier Polar device id found printed on the sensor/device or bt address
* @return [Flowable] stream
* <BR></BR> - onNext the found offline recording entry in [PolarOfflineRecordingEntry]
* <BR></BR> - onComplete the listing completed
* <BR></BR> - onError listing request failed
*/
fun listSplitOfflineRecordings(identifier: String): Flowable<PolarOfflineRecordingEntry>

/**
* Fetch split recording from the Polar device.
*
* Note, the fetching of the recording may take several seconds if the recording is big.
*
* @param identifier Polar device id found printed on the sensor/device or bt address
* @param entry The offline recording to be fetched
* @param secret If the secret is provided in [startOfflineRecording] or [setOfflineRecordingTrigger]
* then the same secret must be provided when fetching the offline record
*
* @return [Single]
* Produces:
* <BR></BR> - onSuccess the offline recording data
* <BR></BR> - onError fetch recording request failed
*/
fun getSplitOfflineRecord(identifier: String, entry: PolarOfflineRecordingEntry, secret: PolarRecordingSecret? = null): Single<PolarOfflineRecordingData>

/**
* Removes offline recording from the device
*
Expand Down
Loading

0 comments on commit 132dfc5

Please sign in to comment.