From 1c2b5d689c5fa805550078b0f07a6db7045d3864 Mon Sep 17 00:00:00 2001 From: Bastiaan Date: Wed, 8 May 2024 12:36:17 +0200 Subject: [PATCH 01/25] Add project version to gradle properties --- build.gradle | 2 +- gradle.properties | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a43325925..428119e33 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ allprojects { ext.issueUrl = 'https://github.com/' + githubRepoName + '/issues' ext.website = 'http://radar-base.org' - version = "1.2.4" + version = "$project_version" group = 'org.radarbase' ext.versionCode = 52 diff --git a/gradle.properties b/gradle.properties index 10aa7c04f..0302caa2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,6 +23,8 @@ android.jetifier.ignorelist=jackson-core-2.16.1.jar # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +project_version=1.2.5 + java_version=17 kotlin_version=1.9.23 gradle_version=8.7 From 95825a95975f15e7b2c63969d391c138432a4041 Mon Sep 17 00:00:00 2001 From: Bastiaan Date: Wed, 8 May 2024 13:55:01 +0200 Subject: [PATCH 02/25] Add project version to gradle properties --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0302caa2e..836b77bf1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ android.jetifier.ignorelist=jackson-core-2.16.1.jar # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -project_version=1.2.5 +project_version=1.2.5 java_version=17 kotlin_version=1.9.23 From d9c6645dc07efcf463b5efc5ed405082a8854e65 Mon Sep 17 00:00:00 2001 From: Bastiaan Date: Wed, 1 May 2024 13:13:51 +0200 Subject: [PATCH 03/25] Add PolarPlugin --- build.gradle | 1 + gradle.properties | 2 +- plugins/radar-android-polar/README.md | 42 ++ plugins/radar-android-polar/build.gradle | 29 ++ .../src/main/AndroidManifest.xml | 19 + .../radarbase/passive/polar/PolarManager.kt | 362 ++++++++++++++++++ .../radarbase/passive/polar/PolarProvider.kt | 54 +++ .../radarbase/passive/polar/PolarService.kt | 35 ++ .../org/radarbase/passive/polar/PolarState.kt | 21 + .../src/main/res/values/strings.xml | 5 + .../src/main/res/values/styles.xml | 8 + 11 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 plugins/radar-android-polar/README.md create mode 100644 plugins/radar-android-polar/build.gradle create mode 100644 plugins/radar-android-polar/src/main/AndroidManifest.xml create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarService.kt create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarState.kt create mode 100644 plugins/radar-android-polar/src/main/res/values/strings.xml create mode 100644 plugins/radar-android-polar/src/main/res/values/styles.xml diff --git a/build.gradle b/build.gradle index 428119e33..87a948233 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,7 @@ subprojects { repositories { google() mavenCentral() + mavenLocal() // maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } } diff --git a/gradle.properties b/gradle.properties index 836b77bf1..a62c8b842 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,7 +37,7 @@ publish_plugin_version=2.0.0 versions_plugin_version=0.51.0 radar_commons_version=0.15.0 -radar_schemas_commons_version=0.8.7 +radar_schemas_commons_version=0.8.800 radar_faros_sdk_version=0.1.0 diff --git a/plugins/radar-android-polar/README.md b/plugins/radar-android-polar/README.md new file mode 100644 index 000000000..1b1027006 --- /dev/null +++ b/plugins/radar-android-polar/README.md @@ -0,0 +1,42 @@ +# Polar plugin RADAR-pRMT + +Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. + +The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. + +This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. + +The following H10 features have been implemented: +- BatteryLevel +- Heart Rate (as bpm) with sample rate of 1Hz. +- Electrocardiography (ECG) data in µV with sample rate 130Hz. +- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. +**** +## Installation + +To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. + +```gradle +repositories { + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" + implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' +} +``` + +Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. + +## Configuration + +Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. + +## Contributing + +This plugin was build using the [POLAR BLE SDK][1]. + +[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polar/build.gradle b/plugins/radar-android-polar/build.gradle new file mode 100644 index 000000000..b07976e3e --- /dev/null +++ b/plugins/radar-android-polar/build.gradle @@ -0,0 +1,29 @@ +apply from: "$rootDir/gradle/android.gradle" + +android { + namespace "org.radarbase.passive.polar" +} + +//---------------------------------------------------------------------------// +// Configuration // +//---------------------------------------------------------------------------// + +description = "Polar plugin for RADAR passive remote monitoring app" + +//---------------------------------------------------------------------------// +// Sources and classpath configurations // +//---------------------------------------------------------------------------// + +repositories { + maven { url "https://jitpack.io" } +} + +dependencies { + api project(":radar-commons-android") + + implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" + implementation "io.reactivex.rxjava3:rxjava:3.1.6" + implementation "io.reactivex.rxjava3:rxandroid:3.0.2" +} + +apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polar/src/main/AndroidManifest.xml b/plugins/radar-android-polar/src/main/AndroidManifest.xml new file mode 100644 index 000000000..be34bbdba --- /dev/null +++ b/plugins/radar-android-polar/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt new file mode 100644 index 000000000..a41713b78 --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -0,0 +1,362 @@ +package org.radarbase.passive.polar + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.os.PowerManager +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import android.util.Log +import com.polar.sdk.api.PolarBleApi +import com.polar.sdk.api.PolarBleApiCallback +import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation +import com.polar.sdk.api.model.* +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import org.radarbase.android.data.DataCache +import org.radarbase.android.source.AbstractSourceManager +import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.SafeHandler +import org.radarcns.kafka.ObservationKey +import org.radarcns.passive.polar.* +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.* + +class PolarManager( + polarService: PolarService, + private val applicationContext: Context +) : AbstractSourceManager(polarService) { + + private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) + private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) + private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) + private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + + private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) + private var wakeLock: PowerManager.WakeLock? = null + + private lateinit var api: PolarBleApi + private var deviceId: String? = null + private var isDeviceConnected: Boolean = false + + private var autoConnectDisposable: Disposable? = null + private var hrDisposable: Disposable? = null + private var ecgDisposable: Disposable? = null + private var accDisposable: Disposable? = null + private var ppiDisposable: Disposable? = null + companion object { + private const val TAG = "POLAR" + + } + + init { + status = SourceStatusListener.Status.DISCONNECTED // red icon + name = service.getString(R.string.polarDisplayName) + } + + @SuppressLint("WakelockTimeout") + override fun start(acceptableIds: Set) { + + status = SourceStatusListener.Status.READY // blue loading + Log.d(TAG, "RB Device name is currently $deviceId") + + disconnectToPolarSDK(deviceId) + connectToPolarSDK() + + register() + mHandler.start() + mHandler.execute { + wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> + pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarManager") + .also { it.acquire() } + } + } + + } + fun connectToPolarSDK() { + api = defaultImplementation( + applicationContext, + setOf( + PolarBleApi.PolarBleSdkFeature.FEATURE_HR, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, + PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, + PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO + ) + ) + api.setApiLogger { str: String -> Log.d("P-SDK", str) } + api.setApiCallback(object : PolarBleApiCallback() { + override fun blePowerStateChanged(powered: Boolean) { + Log.d(TAG, "BluetoothStateChanged $powered") + if (powered == false) { + status = SourceStatusListener.Status.DISCONNECTED // red circle + } else { + status = SourceStatusListener.Status.READY // blue loading + } + } + + override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") + Log.d(TAG, "RB Does it come here again?") + deviceId = polarDeviceInfo.deviceId + name = service.getString(R.string.polarDeviceName, deviceId) + + if (deviceId != null) { + isDeviceConnected = true + status = SourceStatusListener.Status.CONNECTED // green circle + } + } + + override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { + status = SourceStatusListener.Status.CONNECTING // green dots + Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") + } + + override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") + isDeviceConnected = false + status = SourceStatusListener.Status.DISCONNECTED // red circle + + } + + override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { + + if (isDeviceConnected) { + Log.d(TAG, "Feature ready $feature for $deviceId") + + + when (feature) { + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { + streamHR() + streamEcg() + streamAcc() +// streamPpi() + } + + else -> { + Log.d(TAG, "No feature was ready") + } + } + } else { + Log.d(TAG, "No device was connected") + } + } + + override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { + if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { + Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) + } + } + + override fun batteryLevelReceived(identifier: String, level: Int) { + var batteryLevel = (level/100).toFloat() + state.batteryLevel = batteryLevel + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTime()) + send(batteryLevelTopic, PolarBatteryLevel(getTime(), getTime(), batteryLevel)) + } + + }) + + try { + if (autoConnectDisposable != null) { + autoConnectDisposable?.dispose() + } + autoConnectDisposable = api.autoConnectToDevice(-60, "180D", null) + .subscribe( + { Log.d(TAG, "auto connect search complete") }, + { throwable: Throwable -> Log.e(TAG, "" + throwable.toString()) } + ) + } catch (e: Exception) { + Log.e(TAG, "Could not find polar device") + } + } + + override fun disconnect() { + super.disconnect() + api.disconnectFromDevice(deviceId!!) + api.shutDown() + } + + fun disconnectToPolarSDK(deviceId: String?) { + try { + api.disconnectFromDevice(deviceId!!) + api.shutDown() + } catch (e: Exception) { + Log.e(TAG, "Error occurred during shutdown: ${e.message}") + } + } + + fun getTime(): Double { + return (System.currentTimeMillis() / 1000).toDouble() + } + + // Since in Polar sensors (H10, H9, VeritySense and OH1) the epoch time is chosen to be 2000-01-01T00:00:00Z, this function will convert + // this to the traditional Unix epoch of 1970-01-01T00:00:00Z + fun epoch2000NanosTo1970Seconds(epochNanos: Long): Double { + val epochSeconds = epochNanos / 1_000_000_000.0 // Convert nanoseconds to seconds + val epochStart = LocalDateTime.of(2000, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC) + return epochSeconds + epochStart + } + + fun streamHR() { + Log.d(TAG, "log Famke start streamHR for ${deviceId}") + val isDisposed = hrDisposable?.isDisposed ?: true + if (isDisposed) { + Log.d(TAG, "log Famke start streamHR isDisposed is ${isDisposed}") + hrDisposable = deviceId?.let { + api.startHrStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } + .subscribe( + { hrData: PolarHrData -> + for (sample in hrData.samples) { + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTime()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + send( + heartRateTopic, + PolarHeartRate( + getTime(), + getTime(), + sample.hr, + sample.rrsMs, + sample.rrAvailable, + sample.contactStatus, + sample.contactStatusSupported + ) + ) + + } + }, + { error: Throwable -> + Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") + hrDisposable = null + }, + { Log.d(TAG, "HR stream for ${deviceId} complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + hrDisposable?.dispose() + Log.d(TAG, "HR stream disposed") + hrDisposable = null + } + } + + fun streamEcg() { + val isDisposed = ecgDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 130, + PolarSensorSetting.SettingType.RESOLUTION to 14 + ) + val ecgSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) + .subscribe( + { polarEcgData: PolarEcgData -> + for (data in polarEcgData.samples) { + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} currentTime: ${epoch2000NanosTo1970Seconds(data.timeStamp)}") + send( + ecgTopic, + PolarEcg( + epoch2000NanosTo1970Seconds(data.timeStamp), + getTime(), + data.voltage + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ECG stream failed. Reason $error") + }, + { Log.d(TAG, "ECG stream complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + ecgDisposable?.dispose() + } + } + fun streamAcc() { + val isDisposed = accDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] + PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] + PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] + ) + val accSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + accDisposable = api.startAccStreaming(deviceId, accSettings) + .subscribe( + { polarAccelerometerData: PolarAccelerometerData -> + for (data in polarAccelerometerData.samples) { + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} time: ${getTime()}") + send( + accelerationTopic, + PolarAcceleration( + getTime(), + getTime(), + data.x, + data.y, + data.z + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ACC stream failed. Reason $error") + }, + { + Log.d(TAG, "ACC stream complete") + } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + accDisposable?.dispose() + } + } + + fun streamPpi() { + val isDisposed = ppiDisposable?.isDisposed ?: true + if (isDisposed) { + ppiDisposable = deviceId?.let { + api.startPpiStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { ppiData: PolarPpiData -> + for (sample in ppiData.samples) { + Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") + send( + ppIntervalTopic, + PolarPpInterval( + getTime(), + getTime(), + sample.blockerBit, + sample.errorEstimate, + sample.hr, + sample.ppi, + sample.skinContactStatus, + sample.skinContactSupported + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "PPI stream failed. Reason $error") + }, + { Log.d(TAG, "PPI stream complete") } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + ppiDisposable?.dispose() + } + } + +} + + diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt new file mode 100644 index 000000000..975fb045b --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt @@ -0,0 +1,54 @@ +package org.radarbase.passive.polar + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import org.radarbase.android.BuildConfig +import org.radarbase.android.RadarService +import org.radarbase.android.source.SourceProvider + +open class PolarProvider(radarService: RadarService) : SourceProvider(radarService) { + override val serviceClass: Class = PolarService::class.java + + override val pluginNames = listOf( + "polar_sensors", + "polar_sensor", + ".polar.PolarProvider", + "org.radarbase.passive.polar.PolarProvider", + "org.radarcns.polar.PolarProvider") + + override val description: String + get() = radarService.getString(R.string.polarSensorsDescription) + override val hasDetailView = true + + override val displayName: String + get() = radarService.getString(R.string.polarDisplayName) + + override val permissionsNeeded = buildList { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add(Manifest.permission.BLUETOOTH_SCAN) + add(Manifest.permission.BLUETOOTH_CONNECT) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + } + + override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) + + override val sourceProducer: String = PRODUCER + + override val sourceModel: String = MODEL + + override val version: String = BuildConfig.VERSION_NAME + + override val isFilterable = true + companion object { + const val PRODUCER = "Polar" + const val MODEL = "Generic" + } +} diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarService.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarService.kt new file mode 100644 index 000000000..7a2f04d71 --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarService.kt @@ -0,0 +1,35 @@ +package org.radarbase.passive.polar + +import android.content.Context +import android.hardware.Sensor +import android.os.Process +import android.util.SparseIntArray +import org.radarbase.android.config.SingleRadarConfiguration +import org.radarbase.android.source.SourceManager +import org.radarbase.android.source.SourceService +import org.radarbase.android.util.SafeHandler +import org.slf4j.LoggerFactory +import java.util.concurrent.TimeUnit + +/** + * A service that manages the Polar manager and a TableDataHandler to send store the data of + * the phone sensors and send it to a Kafka REST proxy. + */ +class PolarService : SourceService() { + private lateinit var handler: SafeHandler +// private lateinit var context: Context + override val defaultState: PolarState + get() = PolarState() + + override fun onCreate() { + super.onCreate() + handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) + } + + override fun createSourceManager() = PolarManager(this, applicationContext) + + override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { + manager as PolarManager + } +} + diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarState.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarState.kt new file mode 100644 index 000000000..81d0f5abf --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarState.kt @@ -0,0 +1,21 @@ +package org.radarbase.passive.polar + +import org.radarbase.android.source.BaseSourceState + +/** + * The status on a single point in time + */ +class PolarState : BaseSourceState() { + override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) + @set:Synchronized + override var batteryLevel = Float.NaN + + override val hasAcceleration: Boolean = true + + @Synchronized + fun setAcceleration(x: Float, y: Float, z: Float) { + this.acceleration[0] = x + this.acceleration[1] = y + this.acceleration[2] = z + } +} diff --git a/plugins/radar-android-polar/src/main/res/values/strings.xml b/plugins/radar-android-polar/src/main/res/values/strings.xml new file mode 100644 index 000000000..ed5ea2c7c --- /dev/null +++ b/plugins/radar-android-polar/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Polar + Polar %1$s + Polar sensors like acceleration and step counts. + \ No newline at end of file diff --git a/plugins/radar-android-polar/src/main/res/values/styles.xml b/plugins/radar-android-polar/src/main/res/values/styles.xml new file mode 100644 index 000000000..8acc56071 --- /dev/null +++ b/plugins/radar-android-polar/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + From 8e2a761ce9ee6a94677ae270897ace86e049671d Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Thu, 2 May 2024 17:48:53 +0200 Subject: [PATCH 04/25] Fix interop pRMT --- gradle.properties | 5 +++-- gradle/android.gradle | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index a62c8b842..cafc0373b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,7 @@ org.gradle.parallel=true org.gradle.vfs.watch=true kotlin.code.style=official android.jetifier.ignorelist=jackson-core-2.16.1.jar +android.defaults.buildfeatures.buildconfig=true # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -29,7 +30,7 @@ java_version=17 kotlin_version=1.9.23 gradle_version=8.7 -gradle_android_version=8.3.2 +gradle_android_version=8.2.0 unmock_plugin_version=0.7.9 dokka_android_gradle_plugin_version=0.9.18 dokka_version=1.9.20 @@ -46,7 +47,7 @@ appcompat_version=1.6.1 okhttp_version=4.12.0 localbroadcastmanager_version=1.1.0 legacy_support_version=1.0.0 -lifecycle_service_version=2.7.0 +lifecycle_service_version=2.6.1 firebase_bom_version=32.8.1 slf4j_handroid_version=2.0.4 material_version=1.11.0 diff --git a/gradle/android.gradle b/gradle/android.gradle index 3df30ed56..2aa7f96de 100644 --- a/gradle/android.gradle +++ b/gradle/android.gradle @@ -1,6 +1,6 @@ android { compileSdkVersion 33 - buildToolsVersion '34.0.0' + buildToolsVersion '32.0.0' defaultConfig { minSdkVersion 26 From a4b37f477ab01cffabfe11b310639894221e2775 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 8 May 2024 09:52:24 +0200 Subject: [PATCH 05/25] Fixed time of ECG topic using devices timestamp and made Unit test for this of PolarEpoch conversion --- plugins/radar-android-polar/build.gradle | 5 ++ .../radarbase/passive/polar/PolarManager.kt | 71 +++++++++++-------- .../org/radarbase/passive/polar/PolarUtils.kt | 12 ++++ .../passive/polar/PolarEpochConversionTest.kt | 25 +++++++ 4 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt create mode 100644 plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt diff --git a/plugins/radar-android-polar/build.gradle b/plugins/radar-android-polar/build.gradle index b07976e3e..c0f908fda 100644 --- a/plugins/radar-android-polar/build.gradle +++ b/plugins/radar-android-polar/build.gradle @@ -24,6 +24,11 @@ dependencies { implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" implementation "io.reactivex.rxjava3:rxjava:3.1.6" implementation "io.reactivex.rxjava3:rxandroid:3.0.2" + + implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' + implementation 'joda-time:joda-time:2.9.4' + + testImplementation 'junit:junit:4.13' } apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index a41713b78..37a745da2 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -12,6 +12,8 @@ import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation import com.polar.sdk.api.model.* import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single import org.radarbase.android.data.DataCache import org.radarbase.android.source.AbstractSourceManager import org.radarbase.android.source.SourceStatusListener @@ -45,6 +47,8 @@ class PolarManager( private var ecgDisposable: Disposable? = null private var accDisposable: Disposable? = null private var ppiDisposable: Disposable? = null + private var timeDisposable: Disposable? = null + companion object { private const val TAG = "POLAR" @@ -127,15 +131,16 @@ class PolarManager( if (isDeviceConnected) { Log.d(TAG, "Feature ready $feature for $deviceId") + if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { + setDeviceTime(deviceId) + } when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { streamHR() streamEcg() streamAcc() -// streamPpi() } - else -> { Log.d(TAG, "No feature was ready") } @@ -154,8 +159,8 @@ class PolarManager( override fun batteryLevelReceived(identifier: String, level: Int) { var batteryLevel = (level/100).toFloat() state.batteryLevel = batteryLevel - Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTime()) - send(batteryLevelTopic, PolarBatteryLevel(getTime(), getTime(), batteryLevel)) + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) + send(batteryLevelTopic, PolarBatteryLevel(getTimeSec(), getTimeSec(), batteryLevel)) } }) @@ -174,12 +179,6 @@ class PolarManager( } } - override fun disconnect() { - super.disconnect() - api.disconnectFromDevice(deviceId!!) - api.shutDown() - } - fun disconnectToPolarSDK(deviceId: String?) { try { api.disconnectFromDevice(deviceId!!) @@ -189,23 +188,32 @@ class PolarManager( } } - fun getTime(): Double { - return (System.currentTimeMillis() / 1000).toDouble() + fun setDeviceTime(deviceId: String?) { + deviceId?.let { id -> + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.time = Date() + api.setLocalTime(id, calendar) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val timeSetString = "time ${calendar.time} set to device" + Log.d(TAG, timeSetString) + }, + { error: Throwable -> Log.e(TAG, "set time failed: $error") } + ) + } ?: run { + Log.e(TAG, "Device ID is null. Cannot set device time.") + } } - // Since in Polar sensors (H10, H9, VeritySense and OH1) the epoch time is chosen to be 2000-01-01T00:00:00Z, this function will convert - // this to the traditional Unix epoch of 1970-01-01T00:00:00Z - fun epoch2000NanosTo1970Seconds(epochNanos: Long): Double { - val epochSeconds = epochNanos / 1_000_000_000.0 // Convert nanoseconds to seconds - val epochStart = LocalDateTime.of(2000, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC) - return epochSeconds + epochStart + fun getTimeSec(): Double { + return (System.currentTimeMillis() / 1000).toDouble() } fun streamHR() { - Log.d(TAG, "log Famke start streamHR for ${deviceId}") + Log.d(TAG, "start streamHR for ${deviceId}") val isDisposed = hrDisposable?.isDisposed ?: true if (isDisposed) { - Log.d(TAG, "log Famke start streamHR isDisposed is ${isDisposed}") hrDisposable = deviceId?.let { api.startHrStreaming(it) .observeOn(AndroidSchedulers.mainThread()) @@ -213,12 +221,12 @@ class PolarManager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTime()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( - getTime(), - getTime(), + getTimeSec(), + getTimeSec(), sample.hr, sample.rrsMs, sample.rrAvailable, @@ -245,6 +253,7 @@ class PolarManager( } fun streamEcg() { + Log.d(TAG, "start streamECG for ${deviceId}") val isDisposed = ecgDisposable?.isDisposed ?: true if (isDisposed) { val settingMap = mapOf( @@ -257,12 +266,12 @@ class PolarManager( .subscribe( { polarEcgData: PolarEcgData -> for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} currentTime: ${epoch2000NanosTo1970Seconds(data.timeStamp)}") + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") send( ecgTopic, PolarEcg( - epoch2000NanosTo1970Seconds(data.timeStamp), - getTime(), + PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), data.voltage ) ) @@ -293,12 +302,12 @@ class PolarManager( .subscribe( { polarAccelerometerData: PolarAccelerometerData -> for (data in polarAccelerometerData.samples) { - Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} time: ${getTime()}") + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") send( accelerationTopic, PolarAcceleration( - getTime(), - getTime(), + PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), data.x, data.y, data.z @@ -333,8 +342,8 @@ class PolarManager( send( ppIntervalTopic, PolarPpInterval( - getTime(), - getTime(), + getTimeSec(), + getTimeSec(), sample.blockerBit, sample.errorEstimate, sample.hr, diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt new file mode 100644 index 000000000..6e41967b9 --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt @@ -0,0 +1,12 @@ +object PolarUtils { + + @JvmStatic + fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { + val thirtyYearsInNanoSec = 946771200000000000 + val oneDayInNanoSec = 86400000000000 + + val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec + + return unixEpoch + } +} \ No newline at end of file diff --git a/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt new file mode 100644 index 000000000..219bbec82 --- /dev/null +++ b/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt @@ -0,0 +1,25 @@ +import org.junit.Assert.assertEquals +import org.junit.Test +import org.radarbase.passive.polar.* + +class PolarEpochConversionTest { + + @Test + fun testConvertEpochPolarToUnixEpoch() { + val testData = mapOf( + 768408772990080000L to 1715093572990080000L, + 768408772997784576L to 1715093572997784576L, + 768408773005489152L to 1715093573005489152L, + 768408773013193728L to 1715093573013193728L, + 768408773020898176L to 1715093573020898176L, + 768408773028602752L to 1715093573028602752L, + 768408773036307328L to 1715093573036307328L, + 768408773044011776L to 1715093573044011776L + ) + + testData.forEach { (epochPolar, expectedUnixEpoch) -> + val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) + assertEquals(expectedUnixEpoch, result) + } + } +} From e41d7d7cb9a5b38a910bbe8ac9cbe6e6d51aa705 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 8 May 2024 10:27:23 +0200 Subject: [PATCH 06/25] Fix batteryLevel calculation --- .../src/main/java/org/radarbase/passive/polar/PolarManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 37a745da2..99aef922c 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -157,7 +157,7 @@ class PolarManager( } override fun batteryLevelReceived(identifier: String, level: Int) { - var batteryLevel = (level/100).toFloat() + var batteryLevel = level.toFloat() / 100.0f state.batteryLevel = batteryLevel Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) send(batteryLevelTopic, PolarBatteryLevel(getTimeSec(), getTimeSec(), batteryLevel)) From a5871a6a412ea92a5255b09e9eb28e58a101876d Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 14 May 2024 16:23:14 +0200 Subject: [PATCH 07/25] Change app UI to specific device name --- .../src/main/java/org/radarbase/passive/polar/PolarManager.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 99aef922c..ea8b0582b 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -106,7 +106,8 @@ class PolarManager( Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId - name = service.getString(R.string.polarDeviceName, deviceId) + name = polarDeviceInfo.name +// name = service.getString(R.string.polarDeviceName, deviceId) if (deviceId != null) { isDeviceConnected = true From 8135e0d1fe23bdea837b40db7d73ea226af01bcd Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 14 May 2024 16:23:55 +0200 Subject: [PATCH 08/25] Add specific PolarVantageV3 plugin files --- .../passive/polar/PolarVantageV3Manager.kt | 383 ++++++++++++++++++ .../passive/polar/PolarVantageV3Provider.kt | 54 +++ .../passive/polar/PolarVantageV3Service.kt | 30 ++ 3 files changed, 467 insertions(+) create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt create mode 100644 plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt new file mode 100644 index 000000000..6108989cf --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt @@ -0,0 +1,383 @@ +package org.radarbase.passive.polar + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.os.PowerManager +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import android.util.Log +import com.polar.sdk.api.PolarBleApi +import com.polar.sdk.api.PolarBleApiCallback +import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation +import com.polar.sdk.api.errors.PolarInvalidArgument +import com.polar.sdk.api.model.* +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import org.radarbase.android.data.DataCache +import org.radarbase.android.source.AbstractSourceManager +import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.SafeHandler +import org.radarcns.kafka.ObservationKey +import org.radarcns.passive.polar.* +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.* + +class PolarVantageV3Manager( + polarService: PolarVantageV3Service, + private val applicationContext: Context +) : AbstractSourceManager(polarService) { + + private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) + private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) + private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) + private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + + private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) + private var wakeLock: PowerManager.WakeLock? = null + + private lateinit var api: PolarBleApi + private var deviceId: String = "D733F724" + private var isDeviceConnected: Boolean = false + + private var autoConnectDisposable: Disposable? = null + private var hrDisposable: Disposable? = null + private var ecgDisposable: Disposable? = null + private var accDisposable: Disposable? = null + private var ppiDisposable: Disposable? = null + private var timeDisposable: Disposable? = null + + companion object { + private const val TAG = "POLAR-VantageV3" + + } + + init { + status = SourceStatusListener.Status.DISCONNECTED // red icon + name = service.getString(R.string.polarDisplayName) + } + + @SuppressLint("WakelockTimeout") + override fun start(acceptableIds: Set) { + + status = SourceStatusListener.Status.READY // blue loading + Log.d(TAG, "RB Device name is $deviceId") + + disconnectToPolarSDK(deviceId) + connectToPolarSDK() + + register() + mHandler.start() + mHandler.execute { + wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> + pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarVantageV3Manager") + .also { it.acquire() } + } + } + + } + fun connectToPolarSDK() { + api = defaultImplementation( + applicationContext, + setOf( + PolarBleApi.PolarBleSdkFeature.FEATURE_HR, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, + PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, + PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO + ) + ) + api.setApiLogger { str: String -> Log.d("P-SDK", str) } + api.setApiCallback(object : PolarBleApiCallback() { + override fun blePowerStateChanged(powered: Boolean) { + Log.d(TAG, "BluetoothStateChanged $powered") + if (powered == false) { + status = SourceStatusListener.Status.DISCONNECTED // red circle + } else { + status = SourceStatusListener.Status.READY // blue loading + } + } + + override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") + Log.d(TAG, "RB Does it come here again?") + deviceId = polarDeviceInfo.deviceId + name = service.getString(R.string.polarDeviceName, deviceId) + + if (deviceId != null) { + isDeviceConnected = true + status = SourceStatusListener.Status.CONNECTED // green circle + } + } + + override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { + status = SourceStatusListener.Status.CONNECTING // green dots + Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") + } + + override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") + isDeviceConnected = false + status = SourceStatusListener.Status.DISCONNECTED // red circle + + } + + override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { + + if (isDeviceConnected) { + Log.d(TAG, "Feature ready $feature for $deviceId") + + if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { + setDeviceTime(deviceId) + } + + + when (feature) { + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { + Log.d(TAG, "Start recording now") + Thread.sleep(10_000L) + streamHR() + streamEcg() + streamAcc() + } + else -> { + Log.d(TAG, "No feature was ready") + } + } + } else { + Log.d(TAG, "No device was connected") + } + } + + override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { + if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { + Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) + } + } + + override fun batteryLevelReceived(identifier: String, level: Int) { + var batteryLevel = level.toFloat() / 100.0f + state.batteryLevel = batteryLevel + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) + send(batteryLevelTopic, PolarBatteryLevel(getTimeSec(), getTimeSec(), batteryLevel)) + } + + }) + + try { + api.connectToDevice(deviceId) + } catch (a: PolarInvalidArgument) { + a.printStackTrace() + } + + } + +// try { +// if (autoConnectDisposable != null) { +// autoConnectDisposable?.dispose() +// } +// autoConnectDisposable = api.autoConnectToDevice(-60, "180D", null) +// .subscribe( +// { Log.d(TAG, "auto connect search complete") }, +// { throwable: Throwable -> Log.e(TAG, "" + throwable.toString()) } +// ) +// } catch (e: Exception) { +// Log.e(TAG, "Could not find polar device") +// } +// } + + fun disconnectToPolarSDK(deviceId: String?) { + try { + api.disconnectFromDevice(deviceId!!) + api.shutDown() + } catch (e: Exception) { + Log.e(TAG, "Error occurred during shutdown: ${e.message}") + } + } + + fun setDeviceTime(deviceId: String?) { + deviceId?.let { id -> + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.time = Date() + api.setLocalTime(id, calendar) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val timeSetString = "time ${calendar.time} set to device" + Log.d(TAG, timeSetString) + }, + { error: Throwable -> Log.e(TAG, "set time failed: $error") } + ) + } ?: run { + Log.e(TAG, "Device ID is null. Cannot set device time.") + } + } + + fun getTimeSec(): Double { + return (System.currentTimeMillis() / 1000).toDouble() + } + + fun streamHR() { + Log.d(TAG, "start streamHR for ${deviceId}") + val isDisposed = hrDisposable?.isDisposed ?: true + if (isDisposed) { + hrDisposable = deviceId?.let { + api.startHrStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } + .subscribe( + { hrData: PolarHrData -> + for (sample in hrData.samples) { + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + send( + heartRateTopic, + PolarHeartRate( + getTimeSec(), + getTimeSec(), + sample.hr, + sample.rrsMs, + sample.rrAvailable, + sample.contactStatus, + sample.contactStatusSupported + ) + ) + + } + }, + { error: Throwable -> + Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") + hrDisposable = null + }, + { Log.d(TAG, "HR stream for ${deviceId} complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + hrDisposable?.dispose() + Log.d(TAG, "HR stream disposed") + hrDisposable = null + } + } + + fun streamEcg() { + Log.d(TAG, "start streamECG for ${deviceId}") + val isDisposed = ecgDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 130, + PolarSensorSetting.SettingType.RESOLUTION to 14 + ) + val ecgSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) + .subscribe( + { polarEcgData: PolarEcgData -> + for (data in polarEcgData.samples) { + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") + send( + ecgTopic, + PolarEcg( + PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.voltage + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ECG stream failed. Reason $error") + }, + { Log.d(TAG, "ECG stream complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + ecgDisposable?.dispose() + } + } + fun streamAcc() { + val isDisposed = accDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] + PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] + PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] + ) + val accSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + accDisposable = api.startAccStreaming(deviceId, accSettings) + .subscribe( + { polarAccelerometerData: PolarAccelerometerData -> + for (data in polarAccelerometerData.samples) { + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") + send( + accelerationTopic, + PolarAcceleration( + PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.x, + data.y, + data.z + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ACC stream failed. Reason $error") + }, + { + Log.d(TAG, "ACC stream complete") + } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + accDisposable?.dispose() + } + } + + fun streamPpi() { + val isDisposed = ppiDisposable?.isDisposed ?: true + if (isDisposed) { + ppiDisposable = deviceId?.let { + api.startPpiStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { ppiData: PolarPpiData -> + for (sample in ppiData.samples) { + Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") + send( + ppIntervalTopic, + PolarPpInterval( + getTimeSec(), + getTimeSec(), + sample.blockerBit, + sample.errorEstimate, + sample.hr, + sample.ppi, + sample.skinContactStatus, + sample.skinContactSupported + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "PPI stream failed. Reason $error") + }, + { Log.d(TAG, "PPI stream complete") } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + ppiDisposable?.dispose() + } + } + +} + + diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt new file mode 100644 index 000000000..87f0eb28b --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt @@ -0,0 +1,54 @@ +package org.radarbase.passive.polar + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import org.radarbase.android.BuildConfig +import org.radarbase.android.RadarService +import org.radarbase.android.source.SourceProvider + +open class PolarVantageV3Provider(radarService: RadarService) : SourceProvider(radarService) { + override val serviceClass: Class = PolarVantageV3Service::class.java + + override val pluginNames = listOf( + "PolarVantageV3_sensors", + "PolarVantageV3_sensor", + ".polar.PolarVantageV3Provider", + "org.radarbase.passive.polar.PolarVantageV3Provider", + "org.radarcns.polar.PolarVantageV3Provider") + + override val description: String + get() = radarService.getString(R.string.polarSensorsDescription) + override val hasDetailView = true + + override val displayName: String + get() = radarService.getString(R.string.polarDisplayName) + + override val permissionsNeeded = buildList { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add(Manifest.permission.BLUETOOTH_SCAN) + add(Manifest.permission.BLUETOOTH_CONNECT) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + } + + override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) + + override val sourceProducer: String = PRODUCER + + override val sourceModel: String = MODEL + + override val version: String = BuildConfig.VERSION_NAME + + override val isFilterable = true + companion object { + const val PRODUCER = "Polar" + const val MODEL = "Generic" + } +} diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt new file mode 100644 index 000000000..d31f2e6c3 --- /dev/null +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt @@ -0,0 +1,30 @@ +package org.radarbase.passive.polar + +import android.os.Process +import org.radarbase.android.config.SingleRadarConfiguration +import org.radarbase.android.source.SourceManager +import org.radarbase.android.source.SourceService +import org.radarbase.android.util.SafeHandler + +/** + * A service that manages the Polar manager and a TableDataHandler to send store the data of + * the phone sensors and send it to a Kafka REST proxy. + */ +class PolarVantageV3Service : SourceService() { + private lateinit var handler: SafeHandler +// private lateinit var context: Context + override val defaultState: PolarState + get() = PolarState() + + override fun onCreate() { + super.onCreate() + handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) + } + + override fun createSourceManager() = PolarVantageV3Manager(this, applicationContext) + + override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { + manager as PolarManager + } +} + From 266229ac60d1d0186bd25afe05809402dd943538 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 15 May 2024 10:53:45 +0200 Subject: [PATCH 09/25] Add specific PolarVantageV3 plugin files --- .../radar-android-polarvantagev3/README.md | 42 +++++++++++++++++++ .../radar-android-polarvantagev3/build.gradle | 34 +++++++++++++++ .../src/main/AndroidManifest.xml | 19 +++++++++ .../polarvantagev3}/PolarVantageV3Provider.kt | 4 +- .../polarvantagev3}/PolarVantageV3Service.kt | 12 +++--- .../polarvantagev3/PolarVantageV3State.kt | 21 ++++++++++ .../polarvantagev3/PolarVantageV3Utils.kt | 12 ++++++ .../src/main/res/values/strings.xml | 5 +++ .../src/main/res/values/styles.xml | 8 ++++ .../passive/polar/PolarEpochConversionTest.kt | 25 +++++++++++ 10 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 plugins/radar-android-polarvantagev3/README.md create mode 100644 plugins/radar-android-polarvantagev3/build.gradle create mode 100644 plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml rename plugins/{radar-android-polar/src/main/java/org/radarbase/passive/polar => radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3}/PolarVantageV3Provider.kt (95%) rename plugins/{radar-android-polar/src/main/java/org/radarbase/passive/polar => radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3}/PolarVantageV3Service.kt (72%) create mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt create mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt create mode 100644 plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml create mode 100644 plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml create mode 100644 plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt diff --git a/plugins/radar-android-polarvantagev3/README.md b/plugins/radar-android-polarvantagev3/README.md new file mode 100644 index 000000000..1b1027006 --- /dev/null +++ b/plugins/radar-android-polarvantagev3/README.md @@ -0,0 +1,42 @@ +# Polar plugin RADAR-pRMT + +Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. + +The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. + +This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. + +The following H10 features have been implemented: +- BatteryLevel +- Heart Rate (as bpm) with sample rate of 1Hz. +- Electrocardiography (ECG) data in µV with sample rate 130Hz. +- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. +**** +## Installation + +To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. + +```gradle +repositories { + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" + implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' +} +``` + +Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. + +## Configuration + +Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. + +## Contributing + +This plugin was build using the [POLAR BLE SDK][1]. + +[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarvantagev3/build.gradle b/plugins/radar-android-polarvantagev3/build.gradle new file mode 100644 index 000000000..78b2da649 --- /dev/null +++ b/plugins/radar-android-polarvantagev3/build.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/android.gradle" + +android { + namespace "org.radarbase.passive.polarvantagev3" +} + +//---------------------------------------------------------------------------// +// Configuration // +//---------------------------------------------------------------------------// + +description = "Polar plugin for RADAR passive remote monitoring app" + +//---------------------------------------------------------------------------// +// Sources and classpath configurations // +//---------------------------------------------------------------------------// + +repositories { + maven { url "https://jitpack.io" } +} + +dependencies { + api project(":radar-commons-android") + + implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" + implementation "io.reactivex.rxjava3:rxjava:3.1.6" + implementation "io.reactivex.rxjava3:rxandroid:3.0.2" + + implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' + implementation 'joda-time:joda-time:2.9.4' + + testImplementation 'junit:junit:4.13' +} + +apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml b/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml new file mode 100644 index 000000000..79f518b4a --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt similarity index 95% rename from plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt rename to plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt index 87f0eb28b..18d2cea39 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Provider.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt @@ -1,4 +1,4 @@ -package org.radarbase.passive.polar +package org.radarbase.passive.polarvantagev3 import android.Manifest import android.content.pm.PackageManager @@ -7,7 +7,7 @@ import org.radarbase.android.BuildConfig import org.radarbase.android.RadarService import org.radarbase.android.source.SourceProvider -open class PolarVantageV3Provider(radarService: RadarService) : SourceProvider(radarService) { +open class PolarVantageV3Provider(radarService: RadarService) : SourceProvider(radarService) { override val serviceClass: Class = PolarVantageV3Service::class.java override val pluginNames = listOf( diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt similarity index 72% rename from plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt rename to plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt index d31f2e6c3..0119317b5 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Service.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt @@ -1,4 +1,4 @@ -package org.radarbase.passive.polar +package org.radarbase.passive.polarvantagev3 import android.os.Process import org.radarbase.android.config.SingleRadarConfiguration @@ -10,11 +10,11 @@ import org.radarbase.android.util.SafeHandler * A service that manages the Polar manager and a TableDataHandler to send store the data of * the phone sensors and send it to a Kafka REST proxy. */ -class PolarVantageV3Service : SourceService() { +class PolarVantageV3Service : SourceService() { private lateinit var handler: SafeHandler // private lateinit var context: Context - override val defaultState: PolarState - get() = PolarState() + override val defaultState: PolarVantageV3State + get() = PolarVantageV3State() override fun onCreate() { super.onCreate() @@ -23,8 +23,8 @@ class PolarVantageV3Service : SourceService() { override fun createSourceManager() = PolarVantageV3Manager(this, applicationContext) - override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { - manager as PolarManager + override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { + manager as PolarVantageV3Manager } } diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt new file mode 100644 index 000000000..3b0b42f56 --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt @@ -0,0 +1,21 @@ +package org.radarbase.passive.polarvantagev3 + +import org.radarbase.android.source.BaseSourceState + +/** + * The status on a single point in time + */ +class PolarVantageV3State : BaseSourceState() { + override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) + @set:Synchronized + override var batteryLevel = Float.NaN + + override val hasAcceleration: Boolean = true + + @Synchronized + fun setAcceleration(x: Float, y: Float, z: Float) { + this.acceleration[0] = x + this.acceleration[1] = y + this.acceleration[2] = z + } +} diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt new file mode 100644 index 000000000..cf4b848bf --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt @@ -0,0 +1,12 @@ +object PolarVantageV3Utils { + + @JvmStatic + fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { + val thirtyYearsInNanoSec = 946771200000000000 + val oneDayInNanoSec = 86400000000000 + + val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec + + return unixEpoch + } +} \ No newline at end of file diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml new file mode 100644 index 000000000..ed5ea2c7c --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Polar + Polar %1$s + Polar sensors like acceleration and step counts. + \ No newline at end of file diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml new file mode 100644 index 000000000..8acc56071 --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt new file mode 100644 index 000000000..219bbec82 --- /dev/null +++ b/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt @@ -0,0 +1,25 @@ +import org.junit.Assert.assertEquals +import org.junit.Test +import org.radarbase.passive.polar.* + +class PolarEpochConversionTest { + + @Test + fun testConvertEpochPolarToUnixEpoch() { + val testData = mapOf( + 768408772990080000L to 1715093572990080000L, + 768408772997784576L to 1715093572997784576L, + 768408773005489152L to 1715093573005489152L, + 768408773013193728L to 1715093573013193728L, + 768408773020898176L to 1715093573020898176L, + 768408773028602752L to 1715093573028602752L, + 768408773036307328L to 1715093573036307328L, + 768408773044011776L to 1715093573044011776L + ) + + testData.forEach { (epochPolar, expectedUnixEpoch) -> + val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) + assertEquals(expectedUnixEpoch, result) + } + } +} From a2095d62974cc215212e8dfbf6ea0bc006e0b3e8 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 15 May 2024 10:55:35 +0200 Subject: [PATCH 10/25] Add Polar deviceName to topics --- .../radarbase/passive/polar/PolarManager.kt | 7 +++- .../polarvantagev3}/PolarVantageV3Manager.kt | 37 ++++++------------- 2 files changed, 17 insertions(+), 27 deletions(-) rename plugins/{radar-android-polar/src/main/java/org/radarbase/passive/polar => radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3}/PolarVantageV3Manager.kt (92%) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index ea8b0582b..d21f40034 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -107,7 +107,6 @@ class PolarManager( Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId name = polarDeviceInfo.name -// name = service.getString(R.string.polarDeviceName, deviceId) if (deviceId != null) { isDeviceConnected = true @@ -161,7 +160,7 @@ class PolarManager( var batteryLevel = level.toFloat() / 100.0f state.batteryLevel = batteryLevel Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(getTimeSec(), getTimeSec(), batteryLevel)) + send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) } }) @@ -226,6 +225,7 @@ class PolarManager( send( heartRateTopic, PolarHeartRate( + name, getTimeSec(), getTimeSec(), sample.hr, @@ -271,6 +271,7 @@ class PolarManager( send( ecgTopic, PolarEcg( + name, PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), getTimeSec(), data.voltage @@ -307,6 +308,7 @@ class PolarManager( send( accelerationTopic, PolarAcceleration( + name, PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), getTimeSec(), data.x, @@ -343,6 +345,7 @@ class PolarManager( send( ppIntervalTopic, PolarPpInterval( + name, getTimeSec(), getTimeSec(), sample.blockerBit, diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt similarity index 92% rename from plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt rename to plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt index 6108989cf..a9847f12e 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarVantageV3Manager.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt @@ -1,4 +1,4 @@ -package org.radarbase.passive.polar +package org.radarbase.passive.polarvantagev3 import android.annotation.SuppressLint import android.content.Context @@ -20,7 +20,7 @@ import org.radarbase.android.source.AbstractSourceManager import org.radarbase.android.source.SourceStatusListener import org.radarbase.android.util.SafeHandler import org.radarcns.kafka.ObservationKey -import org.radarcns.passive.polar.* +import org.radarcns.passive.polar.* // schemas import java.time.LocalDateTime import java.time.ZoneOffset import java.util.* @@ -28,7 +28,7 @@ import java.util.* class PolarVantageV3Manager( polarService: PolarVantageV3Service, private val applicationContext: Context -) : AbstractSourceManager(polarService) { +) : AbstractSourceManager(polarService) { private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) @@ -107,7 +107,7 @@ class PolarVantageV3Manager( Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId - name = service.getString(R.string.polarDeviceName, deviceId) + name = polarDeviceInfo.name if (deviceId != null) { isDeviceConnected = true @@ -140,10 +140,7 @@ class PolarVantageV3Manager( when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { Log.d(TAG, "Start recording now") - Thread.sleep(10_000L) streamHR() - streamEcg() - streamAcc() } else -> { Log.d(TAG, "No feature was ready") @@ -164,7 +161,7 @@ class PolarVantageV3Manager( var batteryLevel = level.toFloat() / 100.0f state.batteryLevel = batteryLevel Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(getTimeSec(), getTimeSec(), batteryLevel)) + send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) } }) @@ -177,20 +174,6 @@ class PolarVantageV3Manager( } -// try { -// if (autoConnectDisposable != null) { -// autoConnectDisposable?.dispose() -// } -// autoConnectDisposable = api.autoConnectToDevice(-60, "180D", null) -// .subscribe( -// { Log.d(TAG, "auto connect search complete") }, -// { throwable: Throwable -> Log.e(TAG, "" + throwable.toString()) } -// ) -// } catch (e: Exception) { -// Log.e(TAG, "Could not find polar device") -// } -// } - fun disconnectToPolarSDK(deviceId: String?) { try { api.disconnectFromDevice(deviceId!!) @@ -237,6 +220,7 @@ class PolarVantageV3Manager( send( heartRateTopic, PolarHeartRate( + name, getTimeSec(), getTimeSec(), sample.hr, @@ -278,11 +262,12 @@ class PolarVantageV3Manager( .subscribe( { polarEcgData: PolarEcgData -> for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp)}") send( ecgTopic, PolarEcg( - PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + name, + PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp), getTimeSec(), data.voltage ) @@ -318,7 +303,8 @@ class PolarVantageV3Manager( send( accelerationTopic, PolarAcceleration( - PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + name, + PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp), getTimeSec(), data.x, data.y, @@ -354,6 +340,7 @@ class PolarVantageV3Manager( send( ppIntervalTopic, PolarPpInterval( + name, getTimeSec(), getTimeSec(), sample.blockerBit, From 19fb526c6e4f0529a647c6ce2d6fb3c07a21d25a Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Thu, 16 May 2024 15:32:38 +0200 Subject: [PATCH 11/25] Fix permissions --- .../org/radarbase/passive/polar/PolarProvider.kt | 16 +++++++++------- .../polarvantagev3/PolarVantageV3Provider.kt | 15 ++++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt index 975fb045b..ea18ac992 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarProvider.kt @@ -24,17 +24,19 @@ open class PolarProvider(radarService: RadarService) : SourceProvider= Build.VERSION_CODES.S) { add(Manifest.permission.BLUETOOTH_SCAN) add(Manifest.permission.BLUETOOTH_CONNECT) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } else { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } } } diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt index 18d2cea39..330a626a9 100644 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt @@ -25,16 +25,17 @@ open class PolarVantageV3Provider(radarService: RadarService) : SourceProvider

= Build.VERSION_CODES.S) { add(Manifest.permission.BLUETOOTH_SCAN) add(Manifest.permission.BLUETOOTH_CONNECT) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } else { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } } } From 0f92e9290c6237ad42133372d4b18c81f046b771 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 29 May 2024 14:18:01 +0200 Subject: [PATCH 12/25] Change Polar V3 display name --- .../src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml index ed5ea2c7c..2957e8866 100644 --- a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml +++ b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Polar + Polar V3 Polar %1$s Polar sensors like acceleration and step counts. \ No newline at end of file From e46a4e85ffd5648c63f4ad828ed4808f0b0d80f3 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 29 May 2024 14:19:39 +0200 Subject: [PATCH 13/25] Add nanosec timestamp for HRstream --- .../radarbase/passive/polar/PolarManager.kt | 20 ++++++++++++++----- .../polarvantagev3/PolarVantageV3Manager.kt | 8 ++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index d21f40034..8100425b8 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -78,7 +78,10 @@ class PolarManager( } } + fun connectToPolarSDK() { + + Log.d(TAG, "Connecting to Polar API") api = defaultImplementation( applicationContext, setOf( @@ -138,8 +141,8 @@ class PolarManager( when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { streamHR() - streamEcg() - streamAcc() +// streamEcg() +// streamAcc() } else -> { Log.d(TAG, "No feature was ready") @@ -172,13 +175,16 @@ class PolarManager( autoConnectDisposable = api.autoConnectToDevice(-60, "180D", null) .subscribe( { Log.d(TAG, "auto connect search complete") }, - { throwable: Throwable -> Log.e(TAG, "" + throwable.toString()) } + { throwable: Throwable -> + Log.e(TAG, "" + throwable.toString()) + } ) } catch (e: Exception) { Log.e(TAG, "Could not find polar device") } } + fun disconnectToPolarSDK(deviceId: String?) { try { api.disconnectFromDevice(deviceId!!) @@ -210,6 +216,10 @@ class PolarManager( return (System.currentTimeMillis() / 1000).toDouble() } + fun getTimeNano(): Long { + return (System.currentTimeMillis() * 1_000_000L) + } + fun streamHR() { Log.d(TAG, "start streamHR for ${deviceId}") val isDisposed = hrDisposable?.isDisposed ?: true @@ -221,12 +231,12 @@ class PolarManager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( name, - getTimeSec(), + getTimeNano(), getTimeSec(), sample.hr, sample.rrsMs, diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt index a9847f12e..7dd3eda61 100644 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt @@ -205,6 +205,10 @@ class PolarVantageV3Manager( return (System.currentTimeMillis() / 1000).toDouble() } + fun getTimeNano(): Long { + return (System.currentTimeMillis() * 1_000_000L) + } + fun streamHR() { Log.d(TAG, "start streamHR for ${deviceId}") val isDisposed = hrDisposable?.isDisposed ?: true @@ -216,12 +220,12 @@ class PolarVantageV3Manager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( name, - getTimeSec(), + getTimeNano(), getTimeSec(), sample.hr, sample.rrsMs, From 96f3b3732804b3af7b80dcb166c7b22597b05067 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 4 Jun 2024 16:59:27 +0200 Subject: [PATCH 14/25] Add H10 plugin --- plugins/radar-android-polarh10/README.md | 42 ++ plugins/radar-android-polarh10/build.gradle | 34 ++ .../src/main/AndroidManifest.xml | 19 + .../passive/polarh10/PolarH10Manager.kt | 371 ++++++++++++++++++ .../passive/polarh10/PolarH10Provider.kt | 55 +++ .../passive/polarh10/PolarH10Service.kt | 30 ++ .../passive/polarh10/PolarH10State.kt | 21 + .../passive/polarh10/PolarH10Utils.kt | 12 + .../src/main/res/values/strings.xml | 5 + .../src/main/res/values/styles.xml | 8 + .../passive/polar/PolarEpochConversionTest.kt | 25 ++ 11 files changed, 622 insertions(+) create mode 100644 plugins/radar-android-polarh10/README.md create mode 100644 plugins/radar-android-polarh10/build.gradle create mode 100644 plugins/radar-android-polarh10/src/main/AndroidManifest.xml create mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt create mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt create mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt create mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt create mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt create mode 100644 plugins/radar-android-polarh10/src/main/res/values/strings.xml create mode 100644 plugins/radar-android-polarh10/src/main/res/values/styles.xml create mode 100644 plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt diff --git a/plugins/radar-android-polarh10/README.md b/plugins/radar-android-polarh10/README.md new file mode 100644 index 000000000..1b1027006 --- /dev/null +++ b/plugins/radar-android-polarh10/README.md @@ -0,0 +1,42 @@ +# Polar plugin RADAR-pRMT + +Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. + +The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. + +This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. + +The following H10 features have been implemented: +- BatteryLevel +- Heart Rate (as bpm) with sample rate of 1Hz. +- Electrocardiography (ECG) data in µV with sample rate 130Hz. +- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. +**** +## Installation + +To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. + +```gradle +repositories { + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" + implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' +} +``` + +Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. + +## Configuration + +Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. + +## Contributing + +This plugin was build using the [POLAR BLE SDK][1]. + +[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarh10/build.gradle b/plugins/radar-android-polarh10/build.gradle new file mode 100644 index 000000000..4cf817c4b --- /dev/null +++ b/plugins/radar-android-polarh10/build.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/android.gradle" + +android { + namespace "org.radarbase.passive.polarh10" +} + +//---------------------------------------------------------------------------// +// Configuration // +//---------------------------------------------------------------------------// + +description = "Polar plugin for RADAR passive remote monitoring app" + +//---------------------------------------------------------------------------// +// Sources and classpath configurations // +//---------------------------------------------------------------------------// + +repositories { + maven { url "https://jitpack.io" } +} + +dependencies { + api project(":radar-commons-android") + + implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" + implementation "io.reactivex.rxjava3:rxjava:3.1.6" + implementation "io.reactivex.rxjava3:rxandroid:3.0.2" + + implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' + implementation 'joda-time:joda-time:2.9.4' + + testImplementation 'junit:junit:4.13' +} + +apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarh10/src/main/AndroidManifest.xml b/plugins/radar-android-polarh10/src/main/AndroidManifest.xml new file mode 100644 index 000000000..21646b312 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt new file mode 100644 index 000000000..fcadb7c2e --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt @@ -0,0 +1,371 @@ +package org.radarbase.passive.polarh10 + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.os.PowerManager +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import android.util.Log +import com.polar.sdk.api.PolarBleApi +import com.polar.sdk.api.PolarBleApiCallback +import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation +import com.polar.sdk.api.errors.PolarInvalidArgument +import com.polar.sdk.api.model.* +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import org.radarbase.android.data.DataCache +import org.radarbase.android.source.AbstractSourceManager +import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.SafeHandler +import org.radarcns.kafka.ObservationKey +import org.radarcns.passive.polar.* // schemas +import java.util.* + +class PolarH10Manager( + polarService: PolarH10Service, + private val applicationContext: Context +) : AbstractSourceManager(polarService) { + + private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) + private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) + private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) + private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + + private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) + private var wakeLock: PowerManager.WakeLock? = null + + private lateinit var api: PolarBleApi + private var deviceId: String = "D6B33A2A" + private var isDeviceConnected: Boolean = false + + private var autoConnectDisposable: Disposable? = null + private var hrDisposable: Disposable? = null + private var ecgDisposable: Disposable? = null + private var accDisposable: Disposable? = null + private var ppiDisposable: Disposable? = null + private var timeDisposable: Disposable? = null + + companion object { + private const val TAG = "POLAR-H10" + + } + + init { + status = SourceStatusListener.Status.DISCONNECTED // red icon + name = service.getString(R.string.polarDisplayName) + } + + @SuppressLint("WakelockTimeout") + override fun start(acceptableIds: Set) { + + status = SourceStatusListener.Status.READY // blue loading + Log.d(TAG, "RB Device name is $deviceId") + + disconnectToPolarSDK(deviceId) + connectToPolarSDK() + + register() + mHandler.start() + mHandler.execute { + wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> + pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarH10Manager") + .also { it.acquire() } + } + } + + } + fun connectToPolarSDK() { + api = defaultImplementation( + applicationContext, + setOf( + PolarBleApi.PolarBleSdkFeature.FEATURE_HR, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, + PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, + PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO + ) + ) + api.setApiLogger { str: String -> Log.d("P-SDK", str) } + api.setApiCallback(object : PolarBleApiCallback() { + override fun blePowerStateChanged(powered: Boolean) { + Log.d(TAG, "BluetoothStateChanged $powered") + if (powered == false) { + status = SourceStatusListener.Status.DISCONNECTED // red circle + } else { + status = SourceStatusListener.Status.READY // blue loading + } + } + + override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") + Log.d(TAG, "RB Does it come here again?") + deviceId = polarDeviceInfo.deviceId + name = polarDeviceInfo.name + + if (deviceId != null) { + isDeviceConnected = true + status = SourceStatusListener.Status.CONNECTED // green circle + } + } + + override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { + status = SourceStatusListener.Status.CONNECTING // green dots + Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") + } + + override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") + isDeviceConnected = false + status = SourceStatusListener.Status.DISCONNECTED // red circle + + } + + override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { + + if (isDeviceConnected) { + Log.d(TAG, "Feature ready $feature for $deviceId") + + if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { + setDeviceTime(deviceId) + } + + + when (feature) { + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { + Log.d(TAG, "Start recording now") + streamHR() + streamEcg() + } + else -> { + Log.d(TAG, "No feature was ready") + } + } + } else { + Log.d(TAG, "No device was connected") + } + } + + override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { + if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { + Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) + } + } + + override fun batteryLevelReceived(identifier: String, level: Int) { + var batteryLevel = level.toFloat() / 100.0f + state.batteryLevel = batteryLevel + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) + send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) + } + + }) + + try { + api.connectToDevice(deviceId) + } catch (a: PolarInvalidArgument) { + a.printStackTrace() + } + + } + + fun disconnectToPolarSDK(deviceId: String?) { + try { + api.disconnectFromDevice(deviceId!!) + api.shutDown() + } catch (e: Exception) { + Log.e(TAG, "Error occurred during shutdown: ${e.message}") + } + } + + fun setDeviceTime(deviceId: String?) { + deviceId?.let { id -> + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.time = Date() + api.setLocalTime(id, calendar) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val timeSetString = "time ${calendar.time} set to device" + Log.d(TAG, timeSetString) + }, + { error: Throwable -> Log.e(TAG, "set time failed: $error") } + ) + } ?: run { + Log.e(TAG, "Device ID is null. Cannot set device time.") + } + } + + fun getTimeSec(): Double { + return (System.currentTimeMillis() / 1000).toDouble() + } + + fun getTimeNano(): Long { + return (System.currentTimeMillis() * 1_000_000L) + } + + fun streamHR() { + Log.d(TAG, "start streamHR for ${deviceId}") + val isDisposed = hrDisposable?.isDisposed ?: true + if (isDisposed) { + hrDisposable = deviceId?.let { + api.startHrStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } + .subscribe( + { hrData: PolarHrData -> + for (sample in hrData.samples) { + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + send( + heartRateTopic, + PolarHeartRate( + name, + getTimeNano(), + getTimeSec(), + sample.hr, + sample.rrsMs, + sample.rrAvailable, + sample.contactStatus, + sample.contactStatusSupported + ) + ) + + } + }, + { error: Throwable -> + Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") + hrDisposable = null + }, + { Log.d(TAG, "HR stream for ${deviceId} complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + hrDisposable?.dispose() + Log.d(TAG, "HR stream disposed") + hrDisposable = null + } + } + + fun streamEcg() { + Log.d(TAG, "start streamECG for ${deviceId}") + val isDisposed = ecgDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 130, + PolarSensorSetting.SettingType.RESOLUTION to 14 + ) + val ecgSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) + .subscribe( + { polarEcgData: PolarEcgData -> + for (data in polarEcgData.samples) { + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp)}") + send( + ecgTopic, + PolarEcg( + name, + PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.voltage + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ECG stream failed. Reason $error") + }, + { Log.d(TAG, "ECG stream complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + ecgDisposable?.dispose() + } + } + fun streamAcc() { + val isDisposed = accDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] + PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] + PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] + ) + val accSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + accDisposable = api.startAccStreaming(deviceId, accSettings) + .subscribe( + { polarAccelerometerData: PolarAccelerometerData -> + for (data in polarAccelerometerData.samples) { + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") + send( + accelerationTopic, + PolarAcceleration( + name, + PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.x, + data.y, + data.z + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ACC stream failed. Reason $error") + }, + { + Log.d(TAG, "ACC stream complete") + } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + accDisposable?.dispose() + } + } + + fun streamPpi() { + val isDisposed = ppiDisposable?.isDisposed ?: true + if (isDisposed) { + ppiDisposable = deviceId?.let { + api.startPpiStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { ppiData: PolarPpiData -> + for (sample in ppiData.samples) { + Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") + send( + ppIntervalTopic, + PolarPpInterval( + name, + getTimeSec(), + getTimeSec(), + sample.blockerBit, + sample.errorEstimate, + sample.hr, + sample.ppi, + sample.skinContactStatus, + sample.skinContactSupported + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "PPI stream failed. Reason $error") + }, + { Log.d(TAG, "PPI stream complete") } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + ppiDisposable?.dispose() + } + } + +} + + diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt new file mode 100644 index 000000000..a2641902c --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt @@ -0,0 +1,55 @@ +package org.radarbase.passive.polarh10 + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import org.radarbase.android.BuildConfig +import org.radarbase.android.RadarService +import org.radarbase.android.source.SourceProvider + +open class PolarH10Provider(radarService: RadarService) : SourceProvider(radarService) { + override val serviceClass: Class = PolarH10Service::class.java + + override val pluginNames = listOf( + "PolarH10_sensors", + "PolarH10_sensor", + ".polar.PolarH10Provider", + "org.radarbase.passive.polar.PolarH10Provider", + "org.radarcns.polar.PolarH10Provider") + + override val description: String + get() = radarService.getString(R.string.polarSensorsDescription) + override val hasDetailView = true + + override val displayName: String + get() = radarService.getString(R.string.polarDisplayName) + + override val permissionsNeeded = buildList { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add(Manifest.permission.BLUETOOTH_SCAN) + add(Manifest.permission.BLUETOOTH_CONNECT) + } else { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + } + } + + override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) + + override val sourceProducer: String = PRODUCER + + override val sourceModel: String = MODEL + + override val version: String = BuildConfig.VERSION_NAME + + override val isFilterable = true + companion object { + const val PRODUCER = "Polar" + const val MODEL = "Generic" + } +} diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt new file mode 100644 index 000000000..3703a80b4 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt @@ -0,0 +1,30 @@ +package org.radarbase.passive.polarh10 + +import android.os.Process +import org.radarbase.android.config.SingleRadarConfiguration +import org.radarbase.android.source.SourceManager +import org.radarbase.android.source.SourceService +import org.radarbase.android.util.SafeHandler + +/** + * A service that manages the Polar manager and a TableDataHandler to send store the data of + * the phone sensors and send it to a Kafka REST proxy. + */ +class PolarH10Service : SourceService() { + private lateinit var handler: SafeHandler +// private lateinit var context: Context + override val defaultState: PolarH10State + get() = PolarH10State() + + override fun onCreate() { + super.onCreate() + handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) + } + + override fun createSourceManager() = PolarH10Manager(this, applicationContext) + + override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { + manager as PolarH10Manager + } +} + diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt new file mode 100644 index 000000000..b69f8fba6 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt @@ -0,0 +1,21 @@ +package org.radarbase.passive.polarh10 + +import org.radarbase.android.source.BaseSourceState + +/** + * The status on a single point in time + */ +class PolarH10State : BaseSourceState() { + override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) + @set:Synchronized + override var batteryLevel = Float.NaN + + override val hasAcceleration: Boolean = true + + @Synchronized + fun setAcceleration(x: Float, y: Float, z: Float) { + this.acceleration[0] = x + this.acceleration[1] = y + this.acceleration[2] = z + } +} diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt new file mode 100644 index 000000000..64b061977 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt @@ -0,0 +1,12 @@ +object PolarH10Utils { + + @JvmStatic + fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { + val thirtyYearsInNanoSec = 946771200000000000 + val oneDayInNanoSec = 86400000000000 + + val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec + + return unixEpoch + } +} \ No newline at end of file diff --git a/plugins/radar-android-polarh10/src/main/res/values/strings.xml b/plugins/radar-android-polarh10/src/main/res/values/strings.xml new file mode 100644 index 000000000..c618e7411 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Polar H10 + Polar H10 %1$s + Polar sensors like acceleration and step counts. + \ No newline at end of file diff --git a/plugins/radar-android-polarh10/src/main/res/values/styles.xml b/plugins/radar-android-polarh10/src/main/res/values/styles.xml new file mode 100644 index 000000000..8acc56071 --- /dev/null +++ b/plugins/radar-android-polarh10/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt new file mode 100644 index 000000000..219bbec82 --- /dev/null +++ b/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt @@ -0,0 +1,25 @@ +import org.junit.Assert.assertEquals +import org.junit.Test +import org.radarbase.passive.polar.* + +class PolarEpochConversionTest { + + @Test + fun testConvertEpochPolarToUnixEpoch() { + val testData = mapOf( + 768408772990080000L to 1715093572990080000L, + 768408772997784576L to 1715093572997784576L, + 768408773005489152L to 1715093573005489152L, + 768408773013193728L to 1715093573013193728L, + 768408773020898176L to 1715093573020898176L, + 768408773028602752L to 1715093573028602752L, + 768408773036307328L to 1715093573036307328L, + 768408773044011776L to 1715093573044011776L + ) + + testData.forEach { (epochPolar, expectedUnixEpoch) -> + val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) + assertEquals(expectedUnixEpoch, result) + } + } +} From f49908acbfbe540ccb9035e33b9cfd302645476e Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 4 Jun 2024 16:59:56 +0200 Subject: [PATCH 15/25] Add Sense plugin --- plugins/radar-android-polarsense/README.md | 42 ++ plugins/radar-android-polarsense/build.gradle | 34 ++ .../src/main/AndroidManifest.xml | 19 + .../passive/polarsense/PolarSenseManager.kt | 371 ++++++++++++++++++ .../passive/polarsense/PolarSenseProvider.kt | 55 +++ .../passive/polarsense/PolarSenseService.kt | 30 ++ .../passive/polarsense/PolarSenseState.kt | 21 + .../passive/polarsense/PolarSenseUtils.kt | 12 + .../src/main/res/values/strings.xml | 5 + .../src/main/res/values/styles.xml | 8 + .../passive/polar/PolarEpochConversionTest.kt | 25 ++ 11 files changed, 622 insertions(+) create mode 100644 plugins/radar-android-polarsense/README.md create mode 100644 plugins/radar-android-polarsense/build.gradle create mode 100644 plugins/radar-android-polarsense/src/main/AndroidManifest.xml create mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt create mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt create mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt create mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt create mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt create mode 100644 plugins/radar-android-polarsense/src/main/res/values/strings.xml create mode 100644 plugins/radar-android-polarsense/src/main/res/values/styles.xml create mode 100644 plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt diff --git a/plugins/radar-android-polarsense/README.md b/plugins/radar-android-polarsense/README.md new file mode 100644 index 000000000..1b1027006 --- /dev/null +++ b/plugins/radar-android-polarsense/README.md @@ -0,0 +1,42 @@ +# Polar plugin RADAR-pRMT + +Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. + +The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. + +This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. + +The following H10 features have been implemented: +- BatteryLevel +- Heart Rate (as bpm) with sample rate of 1Hz. +- Electrocardiography (ECG) data in µV with sample rate 130Hz. +- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. +**** +## Installation + +To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. + +```gradle +repositories { + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" + implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' +} +``` + +Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. + +## Configuration + +Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. + +## Contributing + +This plugin was build using the [POLAR BLE SDK][1]. + +[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarsense/build.gradle b/plugins/radar-android-polarsense/build.gradle new file mode 100644 index 000000000..613b727cf --- /dev/null +++ b/plugins/radar-android-polarsense/build.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/android.gradle" + +android { + namespace "org.radarbase.passive.polarsense" +} + +//---------------------------------------------------------------------------// +// Configuration // +//---------------------------------------------------------------------------// + +description = "Polar plugin for RADAR passive remote monitoring app" + +//---------------------------------------------------------------------------// +// Sources and classpath configurations // +//---------------------------------------------------------------------------// + +repositories { + maven { url "https://jitpack.io" } +} + +dependencies { + api project(":radar-commons-android") + + implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" + implementation "io.reactivex.rxjava3:rxjava:3.1.6" + implementation "io.reactivex.rxjava3:rxandroid:3.0.2" + + implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' + implementation 'joda-time:joda-time:2.9.4' + + testImplementation 'junit:junit:4.13' +} + +apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarsense/src/main/AndroidManifest.xml b/plugins/radar-android-polarsense/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a8024fe1a --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt new file mode 100644 index 000000000..8d0b5cffb --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt @@ -0,0 +1,371 @@ +package org.radarbase.passive.polarsense + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.os.PowerManager +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import android.util.Log +import com.polar.sdk.api.PolarBleApi +import com.polar.sdk.api.PolarBleApiCallback +import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation +import com.polar.sdk.api.errors.PolarInvalidArgument +import com.polar.sdk.api.model.* +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import org.radarbase.android.data.DataCache +import org.radarbase.android.source.AbstractSourceManager +import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.SafeHandler +import org.radarcns.kafka.ObservationKey +import org.radarcns.passive.polar.* // schemas +import java.util.* + +class PolarSenseManager( + polarService: PolarSenseService, + private val applicationContext: Context +) : AbstractSourceManager(polarService) { + + private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) + private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) + private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) + private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + + private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) + private var wakeLock: PowerManager.WakeLock? = null + + private lateinit var api: PolarBleApi + private var deviceId: String = "D833AF2E" + private var isDeviceConnected: Boolean = false + + private var autoConnectDisposable: Disposable? = null + private var hrDisposable: Disposable? = null + private var ecgDisposable: Disposable? = null + private var accDisposable: Disposable? = null + private var ppiDisposable: Disposable? = null + private var timeDisposable: Disposable? = null + + companion object { + private const val TAG = "POLAR-Sense" + + } + + init { + status = SourceStatusListener.Status.DISCONNECTED // red icon + name = service.getString(R.string.polarDisplayName) + } + + @SuppressLint("WakelockTimeout") + override fun start(acceptableIds: Set) { + + status = SourceStatusListener.Status.READY // blue loading + Log.d(TAG, "RB Device name is $deviceId") + + disconnectToPolarSDK(deviceId) + connectToPolarSDK() + + register() + mHandler.start() + mHandler.execute { + wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> + pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarSenseManager") + .also { it.acquire() } + } + } + + } + fun connectToPolarSDK() { + api = defaultImplementation( + applicationContext, + setOf( + PolarBleApi.PolarBleSdkFeature.FEATURE_HR, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, + PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, + PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO + ) + ) + api.setApiLogger { str: String -> Log.d("P-SDK", str) } + api.setApiCallback(object : PolarBleApiCallback() { + override fun blePowerStateChanged(powered: Boolean) { + Log.d(TAG, "BluetoothStateChanged $powered") + if (powered == false) { + status = SourceStatusListener.Status.DISCONNECTED // red circle + } else { + status = SourceStatusListener.Status.READY // blue loading + } + } + + override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") + Log.d(TAG, "RB Does it come here again?") + deviceId = polarDeviceInfo.deviceId + name = polarDeviceInfo.name + + if (deviceId != null) { + isDeviceConnected = true + status = SourceStatusListener.Status.CONNECTED // green circle + } + } + + override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { + status = SourceStatusListener.Status.CONNECTING // green dots + Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") + } + + override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { + Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") + isDeviceConnected = false + status = SourceStatusListener.Status.DISCONNECTED // red circle + + } + + override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { + + if (isDeviceConnected) { + Log.d(TAG, "Feature ready $feature for $deviceId") + + if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { + setDeviceTime(deviceId) + } + + + when (feature) { + PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { + Log.d(TAG, "Start recording now") + streamHR() + streamPpi() + } + else -> { + Log.d(TAG, "No feature was ready") + } + } + } else { + Log.d(TAG, "No device was connected") + } + } + + override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { + if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { + Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) + } + } + + override fun batteryLevelReceived(identifier: String, level: Int) { + var batteryLevel = level.toFloat() / 100.0f + state.batteryLevel = batteryLevel + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) + send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) + } + + }) + + try { + api.connectToDevice(deviceId) + } catch (a: PolarInvalidArgument) { + a.printStackTrace() + } + + } + + fun disconnectToPolarSDK(deviceId: String?) { + try { + api.disconnectFromDevice(deviceId!!) + api.shutDown() + } catch (e: Exception) { + Log.e(TAG, "Error occurred during shutdown: ${e.message}") + } + } + + fun setDeviceTime(deviceId: String?) { + deviceId?.let { id -> + val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) + calendar.time = Date() + api.setLocalTime(id, calendar) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val timeSetString = "time ${calendar.time} set to device" + Log.d(TAG, timeSetString) + }, + { error: Throwable -> Log.e(TAG, "set time failed: $error") } + ) + } ?: run { + Log.e(TAG, "Device ID is null. Cannot set device time.") + } + } + + fun getTimeSec(): Double { + return (System.currentTimeMillis() / 1000).toDouble() + } + + fun getTimeNano(): Long { + return (System.currentTimeMillis() * 1_000_000L) + } + + fun streamHR() { + Log.d(TAG, "start streamHR for ${deviceId}") + val isDisposed = hrDisposable?.isDisposed ?: true + if (isDisposed) { + hrDisposable = deviceId?.let { + api.startHrStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } + .subscribe( + { hrData: PolarHrData -> + for (sample in hrData.samples) { + Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + send( + heartRateTopic, + PolarHeartRate( + name, + getTimeNano(), + getTimeSec(), + sample.hr, + sample.rrsMs, + sample.rrAvailable, + sample.contactStatus, + sample.contactStatusSupported + ) + ) + + } + }, + { error: Throwable -> + Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") + hrDisposable = null + }, + { Log.d(TAG, "HR stream for ${deviceId} complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + hrDisposable?.dispose() + Log.d(TAG, "HR stream disposed") + hrDisposable = null + } + } + + fun streamEcg() { + Log.d(TAG, "start streamECG for ${deviceId}") + val isDisposed = ecgDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 130, + PolarSensorSetting.SettingType.RESOLUTION to 14 + ) + val ecgSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) + .subscribe( + { polarEcgData: PolarEcgData -> + for (data in polarEcgData.samples) { + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") + send( + ecgTopic, + PolarEcg( + name, + PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.voltage + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ECG stream failed. Reason $error") + }, + { Log.d(TAG, "ECG stream complete") } + ) + } + } else { + // NOTE stops streaming if it is "running" + ecgDisposable?.dispose() + } + } + fun streamAcc() { + val isDisposed = accDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] + PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] + PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] + ) + val accSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + accDisposable = api.startAccStreaming(deviceId, accSettings) + .subscribe( + { polarAccelerometerData: PolarAccelerometerData -> + for (data in polarAccelerometerData.samples) { + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") + send( + accelerationTopic, + PolarAcceleration( + name, + PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.x, + data.y, + data.z + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "ACC stream failed. Reason $error") + }, + { + Log.d(TAG, "ACC stream complete") + } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + accDisposable?.dispose() + } + } + + fun streamPpi() { + val isDisposed = ppiDisposable?.isDisposed ?: true + if (isDisposed) { + ppiDisposable = deviceId?.let { + api.startPpiStreaming(it) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { ppiData: PolarPpiData -> + for (sample in ppiData.samples) { + Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") + send( + ppIntervalTopic, + PolarPpInterval( + name, + getTimeSec(), + getTimeSec(), + sample.blockerBit, + sample.errorEstimate, + sample.hr, + sample.ppi, + sample.skinContactStatus, + sample.skinContactSupported + ) + ) + } + }, + { error: Throwable -> + Log.e(TAG, "PPI stream failed. Reason $error") + }, + { Log.d(TAG, "PPI stream complete") } + ) + } + } else { + // NOTE dispose will stop streaming if it is "running" + ppiDisposable?.dispose() + } + } + +} + + diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt new file mode 100644 index 000000000..9a7792fd3 --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt @@ -0,0 +1,55 @@ +package org.radarbase.passive.polarsense + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import org.radarbase.android.BuildConfig +import org.radarbase.android.RadarService +import org.radarbase.android.source.SourceProvider + +open class PolarSenseProvider(radarService: RadarService) : SourceProvider(radarService) { + override val serviceClass: Class = PolarSenseService::class.java + + override val pluginNames = listOf( + "PolarSense_sensors", + "PolarSense_sensor", + ".polar.PolarSenseProvider", + "org.radarbase.passive.polar.PolarSenseProvider", + "org.radarcns.polar.PolarSenseProvider") + + override val description: String + get() = radarService.getString(R.string.polarSensorsDescription) + override val hasDetailView = true + + override val displayName: String + get() = radarService.getString(R.string.polarDisplayName) + + override val permissionsNeeded = buildList { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add(Manifest.permission.BLUETOOTH_SCAN) + add(Manifest.permission.BLUETOOTH_CONNECT) + } else { + add(Manifest.permission.ACCESS_COARSE_LOCATION) + add(Manifest.permission.ACCESS_FINE_LOCATION) + add(Manifest.permission.BLUETOOTH) + add(Manifest.permission.BLUETOOTH_ADMIN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + } + } + + override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) + + override val sourceProducer: String = PRODUCER + + override val sourceModel: String = MODEL + + override val version: String = BuildConfig.VERSION_NAME + + override val isFilterable = true + companion object { + const val PRODUCER = "Polar" + const val MODEL = "Generic" + } +} diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt new file mode 100644 index 000000000..63dfc5864 --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt @@ -0,0 +1,30 @@ +package org.radarbase.passive.polarsense + +import android.os.Process +import org.radarbase.android.config.SingleRadarConfiguration +import org.radarbase.android.source.SourceManager +import org.radarbase.android.source.SourceService +import org.radarbase.android.util.SafeHandler + +/** + * A service that manages the Polar manager and a TableDataHandler to send store the data of + * the phone sensors and send it to a Kafka REST proxy. + */ +class PolarSenseService : SourceService() { + private lateinit var handler: SafeHandler +// private lateinit var context: Context + override val defaultState: PolarSenseState + get() = PolarSenseState() + + override fun onCreate() { + super.onCreate() + handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) + } + + override fun createSourceManager() = PolarSenseManager(this, applicationContext) + + override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { + manager as PolarSenseManager + } +} + diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt new file mode 100644 index 000000000..6d5e1e5b0 --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt @@ -0,0 +1,21 @@ +package org.radarbase.passive.polarsense + +import org.radarbase.android.source.BaseSourceState + +/** + * The status on a single point in time + */ +class PolarSenseState : BaseSourceState() { + override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) + @set:Synchronized + override var batteryLevel = Float.NaN + + override val hasAcceleration: Boolean = true + + @Synchronized + fun setAcceleration(x: Float, y: Float, z: Float) { + this.acceleration[0] = x + this.acceleration[1] = y + this.acceleration[2] = z + } +} diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt new file mode 100644 index 000000000..28bb3b39a --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt @@ -0,0 +1,12 @@ +object PolarSenseUtils { + + @JvmStatic + fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { + val thirtyYearsInNanoSec = 946771200000000000 + val oneDayInNanoSec = 86400000000000 + + val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec + + return unixEpoch + } +} \ No newline at end of file diff --git a/plugins/radar-android-polarsense/src/main/res/values/strings.xml b/plugins/radar-android-polarsense/src/main/res/values/strings.xml new file mode 100644 index 000000000..9e6a485ea --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Polar Sense + Polar Sense %1$s + Polar sensors like acceleration and step counts. + \ No newline at end of file diff --git a/plugins/radar-android-polarsense/src/main/res/values/styles.xml b/plugins/radar-android-polarsense/src/main/res/values/styles.xml new file mode 100644 index 000000000..8acc56071 --- /dev/null +++ b/plugins/radar-android-polarsense/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt new file mode 100644 index 000000000..219bbec82 --- /dev/null +++ b/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt @@ -0,0 +1,25 @@ +import org.junit.Assert.assertEquals +import org.junit.Test +import org.radarbase.passive.polar.* + +class PolarEpochConversionTest { + + @Test + fun testConvertEpochPolarToUnixEpoch() { + val testData = mapOf( + 768408772990080000L to 1715093572990080000L, + 768408772997784576L to 1715093572997784576L, + 768408773005489152L to 1715093573005489152L, + 768408773013193728L to 1715093573013193728L, + 768408773020898176L to 1715093573020898176L, + 768408773028602752L to 1715093573028602752L, + 768408773036307328L to 1715093573036307328L, + 768408773044011776L to 1715093573044011776L + ) + + testData.forEach { (epochPolar, expectedUnixEpoch) -> + val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) + assertEquals(expectedUnixEpoch, result) + } + } +} From 030266e79b0ffc5d8e825193026338178218c197 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 4 Jun 2024 17:00:20 +0200 Subject: [PATCH 16/25] Change Device name --- plugins/radar-android-polar/src/main/res/values/strings.xml | 4 ++-- .../src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/radar-android-polar/src/main/res/values/strings.xml b/plugins/radar-android-polar/src/main/res/values/strings.xml index ed5ea2c7c..985c92fb6 100644 --- a/plugins/radar-android-polar/src/main/res/values/strings.xml +++ b/plugins/radar-android-polar/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Polar - Polar %1$s + Polar auto + Polar auto %1$s Polar sensors like acceleration and step counts. \ No newline at end of file diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml index 2957e8866..975f5cff1 100644 --- a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml +++ b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ Polar V3 - Polar %1$s + Polar V3 %1$s Polar sensors like acceleration and step counts. \ No newline at end of file From 5b7344ad852c6a4e5478076bc0aa473afb9c3265 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 4 Jun 2024 17:00:39 +0200 Subject: [PATCH 17/25] Add phone-ppg gradle.skip --- plugins/radar-android-ppg/gradle.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/radar-android-ppg/gradle.skip diff --git a/plugins/radar-android-ppg/gradle.skip b/plugins/radar-android-ppg/gradle.skip deleted file mode 100644 index e69de29bb..000000000 From b4fddf3556dd9939636e55d2eac6796b5369b3c8 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 5 Jun 2024 10:35:37 +0200 Subject: [PATCH 18/25] Fix android_polar_pulse_to_pulse_interval scheme name --- .../radarbase/passive/polar/PolarManager.kt | 2 +- .../passive/polarh10/PolarH10Manager.kt | 2 +- .../passive/polarsense/PolarSenseManager.kt | 48 +++++++++++-------- .../polarvantagev3/PolarVantageV3Manager.kt | 41 +++++++++++++++- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 8100425b8..4eb8582e4 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -33,7 +33,7 @@ class PolarManager( private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) private var wakeLock: PowerManager.WakeLock? = null diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt index fcadb7c2e..91d8b7058 100644 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt @@ -30,7 +30,7 @@ class PolarH10Manager( private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) private var wakeLock: PowerManager.WakeLock? = null diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt index 8d0b5cffb..a7f940905 100644 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt @@ -30,7 +30,8 @@ class PolarSenseManager( private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) + private val ppgTopic: DataCache = createCache("android_polar_ppg", PolarPpg()) private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) private var wakeLock: PowerManager.WakeLock? = null @@ -45,6 +46,7 @@ class PolarSenseManager( private var accDisposable: Disposable? = null private var ppiDisposable: Disposable? = null private var timeDisposable: Disposable? = null + private var ppgDisposable: Disposable? = null companion object { private const val TAG = "POLAR-Sense" @@ -138,6 +140,7 @@ class PolarSenseManager( Log.d(TAG, "Start recording now") streamHR() streamPpi() + streamPpg() } else -> { Log.d(TAG, "No feature was ready") @@ -249,30 +252,36 @@ class PolarSenseManager( } } - fun streamEcg() { - Log.d(TAG, "start streamECG for ${deviceId}") - val isDisposed = ecgDisposable?.isDisposed ?: true + fun streamPpg() { + Log.d(TAG, "start streamPpg for ${deviceId}") + val isDisposed = ppgDisposable?.isDisposed ?: true if (isDisposed) { val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 130, - PolarSensorSetting.SettingType.RESOLUTION to 14 + PolarSensorSetting.SettingType.SAMPLE_RATE to 55, + PolarSensorSetting.SettingType.RESOLUTION to 22, + PolarSensorSetting.SettingType.CHANNELS to 4 ) - val ecgSettings = PolarSensorSetting(settingMap) + val ppgSettings = PolarSensorSetting(settingMap) deviceId?.let { deviceId -> - ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) + ppgDisposable = api.startPpgStreaming(deviceId, ppgSettings) .subscribe( - { polarEcgData: PolarEcgData -> - for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") - send( - ecgTopic, - PolarEcg( - name, - PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.voltage + { polarPpgData: PolarPpgData -> + if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) { + for (data in polarPpgData.samples) { + Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}") + send( + ppgTopic, + PolarPpg( + name, + PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + getTimeSec(), + data.channelSamples[0], + data.channelSamples[1], + data.channelSamples[2], + data.channelSamples[3] + ) ) - ) + } } }, { error: Throwable -> @@ -286,6 +295,7 @@ class PolarSenseManager( ecgDisposable?.dispose() } } + fun streamAcc() { val isDisposed = accDisposable?.isDisposed ?: true if (isDisposed) { diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt index 7dd3eda61..6832fd0e7 100644 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt @@ -24,6 +24,9 @@ import org.radarcns.passive.polar.* // schemas import java.time.LocalDateTime import java.time.ZoneOffset import java.util.* +import com.polar.androidcommunications.api.* +import com.polar.sdk.api.PolarBleApiDefaultImpl +import com.polar.sdk.api.PolarH10OfflineExerciseApi class PolarVantageV3Manager( polarService: PolarVantageV3Service, @@ -34,7 +37,7 @@ class PolarVantageV3Manager( private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pp_interval", PolarPpInterval()) + private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) private var wakeLock: PowerManager.WakeLock? = null @@ -136,11 +139,13 @@ class PolarVantageV3Manager( setDeviceTime(deviceId) } +// PPI online stream or offline recording is not supported in SDK MODE when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { Log.d(TAG, "Start recording now") streamHR() +// streamPpi() } else -> { Log.d(TAG, "No feature was ready") @@ -289,6 +294,7 @@ class PolarVantageV3Manager( ecgDisposable?.dispose() } } + fun streamAcc() { val isDisposed = accDisposable?.isDisposed ?: true if (isDisposed) { @@ -331,6 +337,39 @@ class PolarVantageV3Manager( } } +// fun streamPpg() { +// val isDisposed = ppgDisposable?.isDisposed ?: true +// if (isDisposed) { +// val settingMap = mapOf( +// PolarSensorSetting.SettingType.SAMPLE_RATE to 13, // +// PolarSensorSetting.SettingType.RESOLUTION to 22, // +// PolarSensorSetting.SettingType.CHANNELS to 24 // +// ) +// val ppgSettings = PolarSensorSetting(settingMap) +// deviceId?.let { deviceId -> +// ppgDisposable = api.startPpgStreaming(deviceId, ppgSettings) +// .subscribe( +// { polarPpgData: PolarPpgData -> +// if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) { +// for (data in polarPpgData.samples) { +// Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}") +// } +// } +// }, +// { error: Throwable -> +// Log.e(TAG, "PPG stream failed. Reason $error") +// }, +// { +// Log.d(TAG, "PPG stream complete") +// } +// ) +// } +// } else { +// // NOTE dispose will stop streaming if it is "running" +// ppgDisposable?.dispose() +// } +// } + fun streamPpi() { val isDisposed = ppiDisposable?.isDisposed ?: true if (isDisposed) { From 02c8342084d62189e30dd723417f5898d76f881e Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 17 Jul 2024 10:45:29 +0200 Subject: [PATCH 19/25] Clean up unnecessary logs --- .../radarbase/passive/polar/PolarManager.kt | 9 ++-- .../passive/polarh10/PolarH10Manager.kt | 5 +-- .../passive/polarsense/PolarSenseManager.kt | 4 +- .../polarvantagev3/PolarVantageV3Manager.kt | 44 ++----------------- 4 files changed, 12 insertions(+), 50 deletions(-) diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 4eb8582e4..065062862 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -63,7 +63,7 @@ class PolarManager( override fun start(acceptableIds: Set) { status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "RB Device name is currently $deviceId") + Log.d(TAG, "Polar Device is $deviceId") disconnectToPolarSDK(deviceId) connectToPolarSDK() @@ -107,7 +107,6 @@ class PolarManager( override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId name = polarDeviceInfo.name @@ -141,8 +140,8 @@ class PolarManager( when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { streamHR() -// streamEcg() -// streamAcc() + streamEcg() + streamAcc() } else -> { Log.d(TAG, "No feature was ready") @@ -231,7 +230,7 @@ class PolarManager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt index 91d8b7058..27df82d2c 100644 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt +++ b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt @@ -60,7 +60,7 @@ class PolarH10Manager( override fun start(acceptableIds: Set) { status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "RB Device name is $deviceId") + Log.d(TAG, "Polar Device is $deviceId") disconnectToPolarSDK(deviceId) connectToPolarSDK() @@ -101,7 +101,6 @@ class PolarH10Manager( override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId name = polarDeviceInfo.name @@ -217,7 +216,7 @@ class PolarH10Manager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt index a7f940905..af43c4dee 100644 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt +++ b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt @@ -62,7 +62,7 @@ class PolarSenseManager( override fun start(acceptableIds: Set) { status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "RB Device name is $deviceId") + Log.d(TAG, "Polar Device is $deviceId") disconnectToPolarSDK(deviceId) connectToPolarSDK() @@ -220,7 +220,7 @@ class PolarSenseManager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt index 6832fd0e7..85d91e556 100644 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt +++ b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt @@ -67,7 +67,7 @@ class PolarVantageV3Manager( override fun start(acceptableIds: Set) { status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "RB Device name is $deviceId") + Log.d(TAG, "Polar Device is $deviceId") disconnectToPolarSDK(deviceId) connectToPolarSDK() @@ -108,7 +108,6 @@ class PolarVantageV3Manager( override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - Log.d(TAG, "RB Does it come here again?") deviceId = polarDeviceInfo.deviceId name = polarDeviceInfo.name @@ -139,13 +138,11 @@ class PolarVantageV3Manager( setDeviceTime(deviceId) } -// PPI online stream or offline recording is not supported in SDK MODE - when (feature) { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { Log.d(TAG, "Start recording now") streamHR() -// streamPpi() + streamPpi() } else -> { Log.d(TAG, "No feature was ready") @@ -225,11 +222,11 @@ class PolarVantageV3Manager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( - name, + deviceId, getTimeNano(), getTimeSec(), sample.hr, @@ -337,39 +334,6 @@ class PolarVantageV3Manager( } } -// fun streamPpg() { -// val isDisposed = ppgDisposable?.isDisposed ?: true -// if (isDisposed) { -// val settingMap = mapOf( -// PolarSensorSetting.SettingType.SAMPLE_RATE to 13, // -// PolarSensorSetting.SettingType.RESOLUTION to 22, // -// PolarSensorSetting.SettingType.CHANNELS to 24 // -// ) -// val ppgSettings = PolarSensorSetting(settingMap) -// deviceId?.let { deviceId -> -// ppgDisposable = api.startPpgStreaming(deviceId, ppgSettings) -// .subscribe( -// { polarPpgData: PolarPpgData -> -// if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) { -// for (data in polarPpgData.samples) { -// Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}") -// } -// } -// }, -// { error: Throwable -> -// Log.e(TAG, "PPG stream failed. Reason $error") -// }, -// { -// Log.d(TAG, "PPG stream complete") -// } -// ) -// } -// } else { -// // NOTE dispose will stop streaming if it is "running" -// ppgDisposable?.dispose() -// } -// } - fun streamPpi() { val isDisposed = ppiDisposable?.isDisposed ?: true if (isDisposed) { From 3d8bf7266cae4a338197ea985ce6ffa93166b5b8 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Sun, 21 Jul 2024 13:43:36 +0200 Subject: [PATCH 20/25] Remove specific Polar device plugins --- plugins/radar-android-polarh10/README.md | 42 -- plugins/radar-android-polarh10/build.gradle | 34 -- .../src/main/AndroidManifest.xml | 19 - .../passive/polarh10/PolarH10Manager.kt | 370 ----------------- .../passive/polarh10/PolarH10Provider.kt | 55 --- .../passive/polarh10/PolarH10Service.kt | 30 -- .../passive/polarh10/PolarH10State.kt | 21 - .../passive/polarh10/PolarH10Utils.kt | 12 - .../src/main/res/values/strings.xml | 5 - .../src/main/res/values/styles.xml | 8 - .../passive/polar/PolarEpochConversionTest.kt | 25 -- plugins/radar-android-polarsense/README.md | 42 -- plugins/radar-android-polarsense/build.gradle | 34 -- .../src/main/AndroidManifest.xml | 19 - .../passive/polarsense/PolarSenseManager.kt | 381 ------------------ .../passive/polarsense/PolarSenseProvider.kt | 55 --- .../passive/polarsense/PolarSenseService.kt | 30 -- .../passive/polarsense/PolarSenseState.kt | 21 - .../passive/polarsense/PolarSenseUtils.kt | 12 - .../src/main/res/values/strings.xml | 5 - .../src/main/res/values/styles.xml | 8 - .../passive/polar/PolarEpochConversionTest.kt | 25 -- .../radar-android-polarvantagev3/README.md | 42 -- .../radar-android-polarvantagev3/build.gradle | 34 -- .../src/main/AndroidManifest.xml | 19 - .../polarvantagev3/PolarVantageV3Manager.kt | 377 ----------------- .../polarvantagev3/PolarVantageV3Provider.kt | 55 --- .../polarvantagev3/PolarVantageV3Service.kt | 30 -- .../polarvantagev3/PolarVantageV3State.kt | 21 - .../polarvantagev3/PolarVantageV3Utils.kt | 12 - .../src/main/res/values/strings.xml | 5 - .../src/main/res/values/styles.xml | 8 - .../passive/polar/PolarEpochConversionTest.kt | 25 -- 33 files changed, 1881 deletions(-) delete mode 100644 plugins/radar-android-polarh10/README.md delete mode 100644 plugins/radar-android-polarh10/build.gradle delete mode 100644 plugins/radar-android-polarh10/src/main/AndroidManifest.xml delete mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt delete mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt delete mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt delete mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt delete mode 100644 plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt delete mode 100644 plugins/radar-android-polarh10/src/main/res/values/strings.xml delete mode 100644 plugins/radar-android-polarh10/src/main/res/values/styles.xml delete mode 100644 plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt delete mode 100644 plugins/radar-android-polarsense/README.md delete mode 100644 plugins/radar-android-polarsense/build.gradle delete mode 100644 plugins/radar-android-polarsense/src/main/AndroidManifest.xml delete mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt delete mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt delete mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt delete mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt delete mode 100644 plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt delete mode 100644 plugins/radar-android-polarsense/src/main/res/values/strings.xml delete mode 100644 plugins/radar-android-polarsense/src/main/res/values/styles.xml delete mode 100644 plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt delete mode 100644 plugins/radar-android-polarvantagev3/README.md delete mode 100644 plugins/radar-android-polarvantagev3/build.gradle delete mode 100644 plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml delete mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt delete mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt delete mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt delete mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt delete mode 100644 plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt delete mode 100644 plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml delete mode 100644 plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml delete mode 100644 plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt diff --git a/plugins/radar-android-polarh10/README.md b/plugins/radar-android-polarh10/README.md deleted file mode 100644 index 1b1027006..000000000 --- a/plugins/radar-android-polarh10/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Polar plugin RADAR-pRMT - -Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. - -The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. - -This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. - -The following H10 features have been implemented: -- BatteryLevel -- Heart Rate (as bpm) with sample rate of 1Hz. -- Electrocardiography (ECG) data in µV with sample rate 130Hz. -- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. -**** -## Installation - -To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. - -```gradle -repositories { - maven { url 'https://jitpack.io' } -} - -dependencies { - implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" - implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' - implementation 'io.reactivex.rxjava3:rxjava:3.1.6' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' -} -``` - -Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. - -## Configuration - -Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. - -## Contributing - -This plugin was build using the [POLAR BLE SDK][1]. - -[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarh10/build.gradle b/plugins/radar-android-polarh10/build.gradle deleted file mode 100644 index 4cf817c4b..000000000 --- a/plugins/radar-android-polarh10/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -apply from: "$rootDir/gradle/android.gradle" - -android { - namespace "org.radarbase.passive.polarh10" -} - -//---------------------------------------------------------------------------// -// Configuration // -//---------------------------------------------------------------------------// - -description = "Polar plugin for RADAR passive remote monitoring app" - -//---------------------------------------------------------------------------// -// Sources and classpath configurations // -//---------------------------------------------------------------------------// - -repositories { - maven { url "https://jitpack.io" } -} - -dependencies { - api project(":radar-commons-android") - - implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" - implementation "io.reactivex.rxjava3:rxjava:3.1.6" - implementation "io.reactivex.rxjava3:rxandroid:3.0.2" - - implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' - implementation 'joda-time:joda-time:2.9.4' - - testImplementation 'junit:junit:4.13' -} - -apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarh10/src/main/AndroidManifest.xml b/plugins/radar-android-polarh10/src/main/AndroidManifest.xml deleted file mode 100644 index 21646b312..000000000 --- a/plugins/radar-android-polarh10/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt deleted file mode 100644 index 27df82d2c..000000000 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Manager.kt +++ /dev/null @@ -1,370 +0,0 @@ -package org.radarbase.passive.polarh10 - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Context.POWER_SERVICE -import android.os.PowerManager -import android.os.Process.THREAD_PRIORITY_BACKGROUND -import android.util.Log -import com.polar.sdk.api.PolarBleApi -import com.polar.sdk.api.PolarBleApiCallback -import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation -import com.polar.sdk.api.errors.PolarInvalidArgument -import com.polar.sdk.api.model.* -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import org.radarbase.android.data.DataCache -import org.radarbase.android.source.AbstractSourceManager -import org.radarbase.android.source.SourceStatusListener -import org.radarbase.android.util.SafeHandler -import org.radarcns.kafka.ObservationKey -import org.radarcns.passive.polar.* // schemas -import java.util.* - -class PolarH10Manager( - polarService: PolarH10Service, - private val applicationContext: Context -) : AbstractSourceManager(polarService) { - - private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) - private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) - private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) - private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) - - private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) - private var wakeLock: PowerManager.WakeLock? = null - - private lateinit var api: PolarBleApi - private var deviceId: String = "D6B33A2A" - private var isDeviceConnected: Boolean = false - - private var autoConnectDisposable: Disposable? = null - private var hrDisposable: Disposable? = null - private var ecgDisposable: Disposable? = null - private var accDisposable: Disposable? = null - private var ppiDisposable: Disposable? = null - private var timeDisposable: Disposable? = null - - companion object { - private const val TAG = "POLAR-H10" - - } - - init { - status = SourceStatusListener.Status.DISCONNECTED // red icon - name = service.getString(R.string.polarDisplayName) - } - - @SuppressLint("WakelockTimeout") - override fun start(acceptableIds: Set) { - - status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "Polar Device is $deviceId") - - disconnectToPolarSDK(deviceId) - connectToPolarSDK() - - register() - mHandler.start() - mHandler.execute { - wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> - pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarH10Manager") - .also { it.acquire() } - } - } - - } - fun connectToPolarSDK() { - api = defaultImplementation( - applicationContext, - setOf( - PolarBleApi.PolarBleSdkFeature.FEATURE_HR, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, - PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, - PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO - ) - ) - api.setApiLogger { str: String -> Log.d("P-SDK", str) } - api.setApiCallback(object : PolarBleApiCallback() { - override fun blePowerStateChanged(powered: Boolean) { - Log.d(TAG, "BluetoothStateChanged $powered") - if (powered == false) { - status = SourceStatusListener.Status.DISCONNECTED // red circle - } else { - status = SourceStatusListener.Status.READY // blue loading - } - } - - override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - deviceId = polarDeviceInfo.deviceId - name = polarDeviceInfo.name - - if (deviceId != null) { - isDeviceConnected = true - status = SourceStatusListener.Status.CONNECTED // green circle - } - } - - override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { - status = SourceStatusListener.Status.CONNECTING // green dots - Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") - } - - override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") - isDeviceConnected = false - status = SourceStatusListener.Status.DISCONNECTED // red circle - - } - - override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { - - if (isDeviceConnected) { - Log.d(TAG, "Feature ready $feature for $deviceId") - - if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { - setDeviceTime(deviceId) - } - - - when (feature) { - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { - Log.d(TAG, "Start recording now") - streamHR() - streamEcg() - } - else -> { - Log.d(TAG, "No feature was ready") - } - } - } else { - Log.d(TAG, "No device was connected") - } - } - - override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { - if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { - Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) - } - } - - override fun batteryLevelReceived(identifier: String, level: Int) { - var batteryLevel = level.toFloat() / 100.0f - state.batteryLevel = batteryLevel - Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) - } - - }) - - try { - api.connectToDevice(deviceId) - } catch (a: PolarInvalidArgument) { - a.printStackTrace() - } - - } - - fun disconnectToPolarSDK(deviceId: String?) { - try { - api.disconnectFromDevice(deviceId!!) - api.shutDown() - } catch (e: Exception) { - Log.e(TAG, "Error occurred during shutdown: ${e.message}") - } - } - - fun setDeviceTime(deviceId: String?) { - deviceId?.let { id -> - val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) - calendar.time = Date() - api.setLocalTime(id, calendar) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - val timeSetString = "time ${calendar.time} set to device" - Log.d(TAG, timeSetString) - }, - { error: Throwable -> Log.e(TAG, "set time failed: $error") } - ) - } ?: run { - Log.e(TAG, "Device ID is null. Cannot set device time.") - } - } - - fun getTimeSec(): Double { - return (System.currentTimeMillis() / 1000).toDouble() - } - - fun getTimeNano(): Long { - return (System.currentTimeMillis() * 1_000_000L) - } - - fun streamHR() { - Log.d(TAG, "start streamHR for ${deviceId}") - val isDisposed = hrDisposable?.isDisposed ?: true - if (isDisposed) { - hrDisposable = deviceId?.let { - api.startHrStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } - .subscribe( - { hrData: PolarHrData -> - for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") - send( - heartRateTopic, - PolarHeartRate( - name, - getTimeNano(), - getTimeSec(), - sample.hr, - sample.rrsMs, - sample.rrAvailable, - sample.contactStatus, - sample.contactStatusSupported - ) - ) - - } - }, - { error: Throwable -> - Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") - hrDisposable = null - }, - { Log.d(TAG, "HR stream for ${deviceId} complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - hrDisposable?.dispose() - Log.d(TAG, "HR stream disposed") - hrDisposable = null - } - } - - fun streamEcg() { - Log.d(TAG, "start streamECG for ${deviceId}") - val isDisposed = ecgDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 130, - PolarSensorSetting.SettingType.RESOLUTION to 14 - ) - val ecgSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) - .subscribe( - { polarEcgData: PolarEcgData -> - for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp)}") - send( - ecgTopic, - PolarEcg( - name, - PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.voltage - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "ECG stream failed. Reason $error") - }, - { Log.d(TAG, "ECG stream complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - ecgDisposable?.dispose() - } - } - fun streamAcc() { - val isDisposed = accDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] - PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] - PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] - ) - val accSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - accDisposable = api.startAccStreaming(deviceId, accSettings) - .subscribe( - { polarAccelerometerData: PolarAccelerometerData -> - for (data in polarAccelerometerData.samples) { - Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") - send( - accelerationTopic, - PolarAcceleration( - name, - PolarH10Utils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.x, - data.y, - data.z - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "ACC stream failed. Reason $error") - }, - { - Log.d(TAG, "ACC stream complete") - } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - accDisposable?.dispose() - } - } - - fun streamPpi() { - val isDisposed = ppiDisposable?.isDisposed ?: true - if (isDisposed) { - ppiDisposable = deviceId?.let { - api.startPpiStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { ppiData: PolarPpiData -> - for (sample in ppiData.samples) { - Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") - send( - ppIntervalTopic, - PolarPpInterval( - name, - getTimeSec(), - getTimeSec(), - sample.blockerBit, - sample.errorEstimate, - sample.hr, - sample.ppi, - sample.skinContactStatus, - sample.skinContactSupported - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "PPI stream failed. Reason $error") - }, - { Log.d(TAG, "PPI stream complete") } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - ppiDisposable?.dispose() - } - } - -} - - diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt deleted file mode 100644 index a2641902c..000000000 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Provider.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.radarbase.passive.polarh10 - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import org.radarbase.android.BuildConfig -import org.radarbase.android.RadarService -import org.radarbase.android.source.SourceProvider - -open class PolarH10Provider(radarService: RadarService) : SourceProvider(radarService) { - override val serviceClass: Class = PolarH10Service::class.java - - override val pluginNames = listOf( - "PolarH10_sensors", - "PolarH10_sensor", - ".polar.PolarH10Provider", - "org.radarbase.passive.polar.PolarH10Provider", - "org.radarcns.polar.PolarH10Provider") - - override val description: String - get() = radarService.getString(R.string.polarSensorsDescription) - override val hasDetailView = true - - override val displayName: String - get() = radarService.getString(R.string.polarDisplayName) - - override val permissionsNeeded = buildList { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - add(Manifest.permission.BLUETOOTH_SCAN) - add(Manifest.permission.BLUETOOTH_CONNECT) - } else { - add(Manifest.permission.ACCESS_COARSE_LOCATION) - add(Manifest.permission.ACCESS_FINE_LOCATION) - add(Manifest.permission.BLUETOOTH) - add(Manifest.permission.BLUETOOTH_ADMIN) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } - } - } - - override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) - - override val sourceProducer: String = PRODUCER - - override val sourceModel: String = MODEL - - override val version: String = BuildConfig.VERSION_NAME - - override val isFilterable = true - companion object { - const val PRODUCER = "Polar" - const val MODEL = "Generic" - } -} diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt deleted file mode 100644 index 3703a80b4..000000000 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Service.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.radarbase.passive.polarh10 - -import android.os.Process -import org.radarbase.android.config.SingleRadarConfiguration -import org.radarbase.android.source.SourceManager -import org.radarbase.android.source.SourceService -import org.radarbase.android.util.SafeHandler - -/** - * A service that manages the Polar manager and a TableDataHandler to send store the data of - * the phone sensors and send it to a Kafka REST proxy. - */ -class PolarH10Service : SourceService() { - private lateinit var handler: SafeHandler -// private lateinit var context: Context - override val defaultState: PolarH10State - get() = PolarH10State() - - override fun onCreate() { - super.onCreate() - handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) - } - - override fun createSourceManager() = PolarH10Manager(this, applicationContext) - - override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { - manager as PolarH10Manager - } -} - diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt deleted file mode 100644 index b69f8fba6..000000000 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10State.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.radarbase.passive.polarh10 - -import org.radarbase.android.source.BaseSourceState - -/** - * The status on a single point in time - */ -class PolarH10State : BaseSourceState() { - override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) - @set:Synchronized - override var batteryLevel = Float.NaN - - override val hasAcceleration: Boolean = true - - @Synchronized - fun setAcceleration(x: Float, y: Float, z: Float) { - this.acceleration[0] = x - this.acceleration[1] = y - this.acceleration[2] = z - } -} diff --git a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt b/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt deleted file mode 100644 index 64b061977..000000000 --- a/plugins/radar-android-polarh10/src/main/java/org/radarbase/passive/polarh10/PolarH10Utils.kt +++ /dev/null @@ -1,12 +0,0 @@ -object PolarH10Utils { - - @JvmStatic - fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { - val thirtyYearsInNanoSec = 946771200000000000 - val oneDayInNanoSec = 86400000000000 - - val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec - - return unixEpoch - } -} \ No newline at end of file diff --git a/plugins/radar-android-polarh10/src/main/res/values/strings.xml b/plugins/radar-android-polarh10/src/main/res/values/strings.xml deleted file mode 100644 index c618e7411..000000000 --- a/plugins/radar-android-polarh10/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - Polar H10 - Polar H10 %1$s - Polar sensors like acceleration and step counts. - \ No newline at end of file diff --git a/plugins/radar-android-polarh10/src/main/res/values/styles.xml b/plugins/radar-android-polarh10/src/main/res/values/styles.xml deleted file mode 100644 index 8acc56071..000000000 --- a/plugins/radar-android-polarh10/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt deleted file mode 100644 index 219bbec82..000000000 --- a/plugins/radar-android-polarh10/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -import org.junit.Assert.assertEquals -import org.junit.Test -import org.radarbase.passive.polar.* - -class PolarEpochConversionTest { - - @Test - fun testConvertEpochPolarToUnixEpoch() { - val testData = mapOf( - 768408772990080000L to 1715093572990080000L, - 768408772997784576L to 1715093572997784576L, - 768408773005489152L to 1715093573005489152L, - 768408773013193728L to 1715093573013193728L, - 768408773020898176L to 1715093573020898176L, - 768408773028602752L to 1715093573028602752L, - 768408773036307328L to 1715093573036307328L, - 768408773044011776L to 1715093573044011776L - ) - - testData.forEach { (epochPolar, expectedUnixEpoch) -> - val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) - assertEquals(expectedUnixEpoch, result) - } - } -} diff --git a/plugins/radar-android-polarsense/README.md b/plugins/radar-android-polarsense/README.md deleted file mode 100644 index 1b1027006..000000000 --- a/plugins/radar-android-polarsense/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Polar plugin RADAR-pRMT - -Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. - -The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. - -This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. - -The following H10 features have been implemented: -- BatteryLevel -- Heart Rate (as bpm) with sample rate of 1Hz. -- Electrocardiography (ECG) data in µV with sample rate 130Hz. -- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. -**** -## Installation - -To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. - -```gradle -repositories { - maven { url 'https://jitpack.io' } -} - -dependencies { - implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" - implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' - implementation 'io.reactivex.rxjava3:rxjava:3.1.6' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' -} -``` - -Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. - -## Configuration - -Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. - -## Contributing - -This plugin was build using the [POLAR BLE SDK][1]. - -[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarsense/build.gradle b/plugins/radar-android-polarsense/build.gradle deleted file mode 100644 index 613b727cf..000000000 --- a/plugins/radar-android-polarsense/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -apply from: "$rootDir/gradle/android.gradle" - -android { - namespace "org.radarbase.passive.polarsense" -} - -//---------------------------------------------------------------------------// -// Configuration // -//---------------------------------------------------------------------------// - -description = "Polar plugin for RADAR passive remote monitoring app" - -//---------------------------------------------------------------------------// -// Sources and classpath configurations // -//---------------------------------------------------------------------------// - -repositories { - maven { url "https://jitpack.io" } -} - -dependencies { - api project(":radar-commons-android") - - implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" - implementation "io.reactivex.rxjava3:rxjava:3.1.6" - implementation "io.reactivex.rxjava3:rxandroid:3.0.2" - - implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' - implementation 'joda-time:joda-time:2.9.4' - - testImplementation 'junit:junit:4.13' -} - -apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarsense/src/main/AndroidManifest.xml b/plugins/radar-android-polarsense/src/main/AndroidManifest.xml deleted file mode 100644 index a8024fe1a..000000000 --- a/plugins/radar-android-polarsense/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt deleted file mode 100644 index af43c4dee..000000000 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseManager.kt +++ /dev/null @@ -1,381 +0,0 @@ -package org.radarbase.passive.polarsense - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Context.POWER_SERVICE -import android.os.PowerManager -import android.os.Process.THREAD_PRIORITY_BACKGROUND -import android.util.Log -import com.polar.sdk.api.PolarBleApi -import com.polar.sdk.api.PolarBleApiCallback -import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation -import com.polar.sdk.api.errors.PolarInvalidArgument -import com.polar.sdk.api.model.* -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import org.radarbase.android.data.DataCache -import org.radarbase.android.source.AbstractSourceManager -import org.radarbase.android.source.SourceStatusListener -import org.radarbase.android.util.SafeHandler -import org.radarcns.kafka.ObservationKey -import org.radarcns.passive.polar.* // schemas -import java.util.* - -class PolarSenseManager( - polarService: PolarSenseService, - private val applicationContext: Context -) : AbstractSourceManager(polarService) { - - private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) - private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) - private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) - private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) - private val ppgTopic: DataCache = createCache("android_polar_ppg", PolarPpg()) - - private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) - private var wakeLock: PowerManager.WakeLock? = null - - private lateinit var api: PolarBleApi - private var deviceId: String = "D833AF2E" - private var isDeviceConnected: Boolean = false - - private var autoConnectDisposable: Disposable? = null - private var hrDisposable: Disposable? = null - private var ecgDisposable: Disposable? = null - private var accDisposable: Disposable? = null - private var ppiDisposable: Disposable? = null - private var timeDisposable: Disposable? = null - private var ppgDisposable: Disposable? = null - - companion object { - private const val TAG = "POLAR-Sense" - - } - - init { - status = SourceStatusListener.Status.DISCONNECTED // red icon - name = service.getString(R.string.polarDisplayName) - } - - @SuppressLint("WakelockTimeout") - override fun start(acceptableIds: Set) { - - status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "Polar Device is $deviceId") - - disconnectToPolarSDK(deviceId) - connectToPolarSDK() - - register() - mHandler.start() - mHandler.execute { - wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> - pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarSenseManager") - .also { it.acquire() } - } - } - - } - fun connectToPolarSDK() { - api = defaultImplementation( - applicationContext, - setOf( - PolarBleApi.PolarBleSdkFeature.FEATURE_HR, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, - PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, - PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO - ) - ) - api.setApiLogger { str: String -> Log.d("P-SDK", str) } - api.setApiCallback(object : PolarBleApiCallback() { - override fun blePowerStateChanged(powered: Boolean) { - Log.d(TAG, "BluetoothStateChanged $powered") - if (powered == false) { - status = SourceStatusListener.Status.DISCONNECTED // red circle - } else { - status = SourceStatusListener.Status.READY // blue loading - } - } - - override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - Log.d(TAG, "RB Does it come here again?") - deviceId = polarDeviceInfo.deviceId - name = polarDeviceInfo.name - - if (deviceId != null) { - isDeviceConnected = true - status = SourceStatusListener.Status.CONNECTED // green circle - } - } - - override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { - status = SourceStatusListener.Status.CONNECTING // green dots - Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") - } - - override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") - isDeviceConnected = false - status = SourceStatusListener.Status.DISCONNECTED // red circle - - } - - override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { - - if (isDeviceConnected) { - Log.d(TAG, "Feature ready $feature for $deviceId") - - if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { - setDeviceTime(deviceId) - } - - - when (feature) { - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { - Log.d(TAG, "Start recording now") - streamHR() - streamPpi() - streamPpg() - } - else -> { - Log.d(TAG, "No feature was ready") - } - } - } else { - Log.d(TAG, "No device was connected") - } - } - - override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { - if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { - Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) - } - } - - override fun batteryLevelReceived(identifier: String, level: Int) { - var batteryLevel = level.toFloat() / 100.0f - state.batteryLevel = batteryLevel - Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) - } - - }) - - try { - api.connectToDevice(deviceId) - } catch (a: PolarInvalidArgument) { - a.printStackTrace() - } - - } - - fun disconnectToPolarSDK(deviceId: String?) { - try { - api.disconnectFromDevice(deviceId!!) - api.shutDown() - } catch (e: Exception) { - Log.e(TAG, "Error occurred during shutdown: ${e.message}") - } - } - - fun setDeviceTime(deviceId: String?) { - deviceId?.let { id -> - val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) - calendar.time = Date() - api.setLocalTime(id, calendar) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - val timeSetString = "time ${calendar.time} set to device" - Log.d(TAG, timeSetString) - }, - { error: Throwable -> Log.e(TAG, "set time failed: $error") } - ) - } ?: run { - Log.e(TAG, "Device ID is null. Cannot set device time.") - } - } - - fun getTimeSec(): Double { - return (System.currentTimeMillis() / 1000).toDouble() - } - - fun getTimeNano(): Long { - return (System.currentTimeMillis() * 1_000_000L) - } - - fun streamHR() { - Log.d(TAG, "start streamHR for ${deviceId}") - val isDisposed = hrDisposable?.isDisposed ?: true - if (isDisposed) { - hrDisposable = deviceId?.let { - api.startHrStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } - .subscribe( - { hrData: PolarHrData -> - for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") - send( - heartRateTopic, - PolarHeartRate( - name, - getTimeNano(), - getTimeSec(), - sample.hr, - sample.rrsMs, - sample.rrAvailable, - sample.contactStatus, - sample.contactStatusSupported - ) - ) - - } - }, - { error: Throwable -> - Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") - hrDisposable = null - }, - { Log.d(TAG, "HR stream for ${deviceId} complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - hrDisposable?.dispose() - Log.d(TAG, "HR stream disposed") - hrDisposable = null - } - } - - fun streamPpg() { - Log.d(TAG, "start streamPpg for ${deviceId}") - val isDisposed = ppgDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 55, - PolarSensorSetting.SettingType.RESOLUTION to 22, - PolarSensorSetting.SettingType.CHANNELS to 4 - ) - val ppgSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - ppgDisposable = api.startPpgStreaming(deviceId, ppgSettings) - .subscribe( - { polarPpgData: PolarPpgData -> - if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) { - for (data in polarPpgData.samples) { - Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}") - send( - ppgTopic, - PolarPpg( - name, - PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.channelSamples[0], - data.channelSamples[1], - data.channelSamples[2], - data.channelSamples[3] - ) - ) - } - } - }, - { error: Throwable -> - Log.e(TAG, "ECG stream failed. Reason $error") - }, - { Log.d(TAG, "ECG stream complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - ecgDisposable?.dispose() - } - } - - fun streamAcc() { - val isDisposed = accDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] - PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] - PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] - ) - val accSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - accDisposable = api.startAccStreaming(deviceId, accSettings) - .subscribe( - { polarAccelerometerData: PolarAccelerometerData -> - for (data in polarAccelerometerData.samples) { - Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") - send( - accelerationTopic, - PolarAcceleration( - name, - PolarSenseUtils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.x, - data.y, - data.z - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "ACC stream failed. Reason $error") - }, - { - Log.d(TAG, "ACC stream complete") - } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - accDisposable?.dispose() - } - } - - fun streamPpi() { - val isDisposed = ppiDisposable?.isDisposed ?: true - if (isDisposed) { - ppiDisposable = deviceId?.let { - api.startPpiStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { ppiData: PolarPpiData -> - for (sample in ppiData.samples) { - Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") - send( - ppIntervalTopic, - PolarPpInterval( - name, - getTimeSec(), - getTimeSec(), - sample.blockerBit, - sample.errorEstimate, - sample.hr, - sample.ppi, - sample.skinContactStatus, - sample.skinContactSupported - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "PPI stream failed. Reason $error") - }, - { Log.d(TAG, "PPI stream complete") } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - ppiDisposable?.dispose() - } - } - -} - - diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt deleted file mode 100644 index 9a7792fd3..000000000 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseProvider.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.radarbase.passive.polarsense - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import org.radarbase.android.BuildConfig -import org.radarbase.android.RadarService -import org.radarbase.android.source.SourceProvider - -open class PolarSenseProvider(radarService: RadarService) : SourceProvider(radarService) { - override val serviceClass: Class = PolarSenseService::class.java - - override val pluginNames = listOf( - "PolarSense_sensors", - "PolarSense_sensor", - ".polar.PolarSenseProvider", - "org.radarbase.passive.polar.PolarSenseProvider", - "org.radarcns.polar.PolarSenseProvider") - - override val description: String - get() = radarService.getString(R.string.polarSensorsDescription) - override val hasDetailView = true - - override val displayName: String - get() = radarService.getString(R.string.polarDisplayName) - - override val permissionsNeeded = buildList { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - add(Manifest.permission.BLUETOOTH_SCAN) - add(Manifest.permission.BLUETOOTH_CONNECT) - } else { - add(Manifest.permission.ACCESS_COARSE_LOCATION) - add(Manifest.permission.ACCESS_FINE_LOCATION) - add(Manifest.permission.BLUETOOTH) - add(Manifest.permission.BLUETOOTH_ADMIN) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } - } - } - - override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) - - override val sourceProducer: String = PRODUCER - - override val sourceModel: String = MODEL - - override val version: String = BuildConfig.VERSION_NAME - - override val isFilterable = true - companion object { - const val PRODUCER = "Polar" - const val MODEL = "Generic" - } -} diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt deleted file mode 100644 index 63dfc5864..000000000 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseService.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.radarbase.passive.polarsense - -import android.os.Process -import org.radarbase.android.config.SingleRadarConfiguration -import org.radarbase.android.source.SourceManager -import org.radarbase.android.source.SourceService -import org.radarbase.android.util.SafeHandler - -/** - * A service that manages the Polar manager and a TableDataHandler to send store the data of - * the phone sensors and send it to a Kafka REST proxy. - */ -class PolarSenseService : SourceService() { - private lateinit var handler: SafeHandler -// private lateinit var context: Context - override val defaultState: PolarSenseState - get() = PolarSenseState() - - override fun onCreate() { - super.onCreate() - handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) - } - - override fun createSourceManager() = PolarSenseManager(this, applicationContext) - - override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { - manager as PolarSenseManager - } -} - diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt deleted file mode 100644 index 6d5e1e5b0..000000000 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.radarbase.passive.polarsense - -import org.radarbase.android.source.BaseSourceState - -/** - * The status on a single point in time - */ -class PolarSenseState : BaseSourceState() { - override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) - @set:Synchronized - override var batteryLevel = Float.NaN - - override val hasAcceleration: Boolean = true - - @Synchronized - fun setAcceleration(x: Float, y: Float, z: Float) { - this.acceleration[0] = x - this.acceleration[1] = y - this.acceleration[2] = z - } -} diff --git a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt b/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt deleted file mode 100644 index 28bb3b39a..000000000 --- a/plugins/radar-android-polarsense/src/main/java/org/radarbase/passive/polarsense/PolarSenseUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -object PolarSenseUtils { - - @JvmStatic - fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { - val thirtyYearsInNanoSec = 946771200000000000 - val oneDayInNanoSec = 86400000000000 - - val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec - - return unixEpoch - } -} \ No newline at end of file diff --git a/plugins/radar-android-polarsense/src/main/res/values/strings.xml b/plugins/radar-android-polarsense/src/main/res/values/strings.xml deleted file mode 100644 index 9e6a485ea..000000000 --- a/plugins/radar-android-polarsense/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - Polar Sense - Polar Sense %1$s - Polar sensors like acceleration and step counts. - \ No newline at end of file diff --git a/plugins/radar-android-polarsense/src/main/res/values/styles.xml b/plugins/radar-android-polarsense/src/main/res/values/styles.xml deleted file mode 100644 index 8acc56071..000000000 --- a/plugins/radar-android-polarsense/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt deleted file mode 100644 index 219bbec82..000000000 --- a/plugins/radar-android-polarsense/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -import org.junit.Assert.assertEquals -import org.junit.Test -import org.radarbase.passive.polar.* - -class PolarEpochConversionTest { - - @Test - fun testConvertEpochPolarToUnixEpoch() { - val testData = mapOf( - 768408772990080000L to 1715093572990080000L, - 768408772997784576L to 1715093572997784576L, - 768408773005489152L to 1715093573005489152L, - 768408773013193728L to 1715093573013193728L, - 768408773020898176L to 1715093573020898176L, - 768408773028602752L to 1715093573028602752L, - 768408773036307328L to 1715093573036307328L, - 768408773044011776L to 1715093573044011776L - ) - - testData.forEach { (epochPolar, expectedUnixEpoch) -> - val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) - assertEquals(expectedUnixEpoch, result) - } - } -} diff --git a/plugins/radar-android-polarvantagev3/README.md b/plugins/radar-android-polarvantagev3/README.md deleted file mode 100644 index 1b1027006..000000000 --- a/plugins/radar-android-polarvantagev3/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Polar plugin RADAR-pRMT - -Application to be run on an Android 5.0 (or later) device with Bluetooth Low Energy (Bluetooth 4.0 or later), to interact with a Polar device. - -The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. - -This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. - -The following H10 features have been implemented: -- BatteryLevel -- Heart Rate (as bpm) with sample rate of 1Hz. -- Electrocardiography (ECG) data in µV with sample rate 130Hz. -- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. -**** -## Installation - -To add the plugin code to your app, add the following snippet to your app's `build.gradle` file. - -```gradle -repositories { - maven { url 'https://jitpack.io' } -} - -dependencies { - implementation "org.radarbase:radar-android-polar:$radarCommonsAndroidVersion" - implementation 'com.github.polarofficial:polar-ble-sdk:5.5.0' - implementation 'io.reactivex.rxjava3:rxjava:3.1.6' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' -} -``` - -Add `org.radarbase.passive.polar.PolarProvider` to the `plugins` variable of the `RadarService` instance in your app. - -## Configuration - -Add the provider `.polar.PolarProvider` to the Firebase Remote Config `plugins` variable. - -## Contributing - -This plugin was build using the [POLAR BLE SDK][1]. - -[1]: https://github.com/polarofficial/polar-ble-sdk diff --git a/plugins/radar-android-polarvantagev3/build.gradle b/plugins/radar-android-polarvantagev3/build.gradle deleted file mode 100644 index 78b2da649..000000000 --- a/plugins/radar-android-polarvantagev3/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -apply from: "$rootDir/gradle/android.gradle" - -android { - namespace "org.radarbase.passive.polarvantagev3" -} - -//---------------------------------------------------------------------------// -// Configuration // -//---------------------------------------------------------------------------// - -description = "Polar plugin for RADAR passive remote monitoring app" - -//---------------------------------------------------------------------------// -// Sources and classpath configurations // -//---------------------------------------------------------------------------// - -repositories { - maven { url "https://jitpack.io" } -} - -dependencies { - api project(":radar-commons-android") - - implementation "com.github.polarofficial:polar-ble-sdk:5.5.0" - implementation "io.reactivex.rxjava3:rxjava:3.1.6" - implementation "io.reactivex.rxjava3:rxandroid:3.0.2" - - implementation group: 'org.joda', name: 'joda-convert', version: '2.0.1', classifier: 'classic' - implementation 'joda-time:joda-time:2.9.4' - - testImplementation 'junit:junit:4.13' -} - -apply from: "$rootDir/gradle/publishing.gradle" diff --git a/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml b/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml deleted file mode 100644 index 79f518b4a..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt deleted file mode 100644 index 85d91e556..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Manager.kt +++ /dev/null @@ -1,377 +0,0 @@ -package org.radarbase.passive.polarvantagev3 - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Context.POWER_SERVICE -import android.os.PowerManager -import android.os.Process.THREAD_PRIORITY_BACKGROUND -import android.util.Log -import com.polar.sdk.api.PolarBleApi -import com.polar.sdk.api.PolarBleApiCallback -import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation -import com.polar.sdk.api.errors.PolarInvalidArgument -import com.polar.sdk.api.model.* -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import org.radarbase.android.data.DataCache -import org.radarbase.android.source.AbstractSourceManager -import org.radarbase.android.source.SourceStatusListener -import org.radarbase.android.util.SafeHandler -import org.radarcns.kafka.ObservationKey -import org.radarcns.passive.polar.* // schemas -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.* -import com.polar.androidcommunications.api.* -import com.polar.sdk.api.PolarBleApiDefaultImpl -import com.polar.sdk.api.PolarH10OfflineExerciseApi - -class PolarVantageV3Manager( - polarService: PolarVantageV3Service, - private val applicationContext: Context -) : AbstractSourceManager(polarService) { - - private val accelerationTopic: DataCache = createCache("android_polar_acceleration", PolarAcceleration()) - private val batteryLevelTopic: DataCache = createCache("android_polar_battery_level", PolarBatteryLevel()) - private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) - private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) - private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) - - private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) - private var wakeLock: PowerManager.WakeLock? = null - - private lateinit var api: PolarBleApi - private var deviceId: String = "D733F724" - private var isDeviceConnected: Boolean = false - - private var autoConnectDisposable: Disposable? = null - private var hrDisposable: Disposable? = null - private var ecgDisposable: Disposable? = null - private var accDisposable: Disposable? = null - private var ppiDisposable: Disposable? = null - private var timeDisposable: Disposable? = null - - companion object { - private const val TAG = "POLAR-VantageV3" - - } - - init { - status = SourceStatusListener.Status.DISCONNECTED // red icon - name = service.getString(R.string.polarDisplayName) - } - - @SuppressLint("WakelockTimeout") - override fun start(acceptableIds: Set) { - - status = SourceStatusListener.Status.READY // blue loading - Log.d(TAG, "Polar Device is $deviceId") - - disconnectToPolarSDK(deviceId) - connectToPolarSDK() - - register() - mHandler.start() - mHandler.execute { - wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm -> - pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarVantageV3Manager") - .also { it.acquire() } - } - } - - } - fun connectToPolarSDK() { - api = defaultImplementation( - applicationContext, - setOf( - PolarBleApi.PolarBleSdkFeature.FEATURE_HR, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, - PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_H10_EXERCISE_RECORDING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP, - PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO - ) - ) - api.setApiLogger { str: String -> Log.d("P-SDK", str) } - api.setApiCallback(object : PolarBleApiCallback() { - override fun blePowerStateChanged(powered: Boolean) { - Log.d(TAG, "BluetoothStateChanged $powered") - if (powered == false) { - status = SourceStatusListener.Status.DISCONNECTED // red circle - } else { - status = SourceStatusListener.Status.READY // blue loading - } - } - - override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device connected ${polarDeviceInfo.deviceId}") - deviceId = polarDeviceInfo.deviceId - name = polarDeviceInfo.name - - if (deviceId != null) { - isDeviceConnected = true - status = SourceStatusListener.Status.CONNECTED // green circle - } - } - - override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) { - status = SourceStatusListener.Status.CONNECTING // green dots - Log.d(TAG, "Device connecting ${polarDeviceInfo.deviceId}") - } - - override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) { - Log.d(TAG, "Device disconnected ${polarDeviceInfo.deviceId}") - isDeviceConnected = false - status = SourceStatusListener.Status.DISCONNECTED // red circle - - } - - override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) { - - if (isDeviceConnected) { - Log.d(TAG, "Feature ready $feature for $deviceId") - - if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) { - setDeviceTime(deviceId) - } - - when (feature) { - PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING -> { - Log.d(TAG, "Start recording now") - streamHR() - streamPpi() - } - else -> { - Log.d(TAG, "No feature was ready") - } - } - } else { - Log.d(TAG, "No device was connected") - } - } - - override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { - if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { - Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) - } - } - - override fun batteryLevelReceived(identifier: String, level: Int) { - var batteryLevel = level.toFloat() / 100.0f - state.batteryLevel = batteryLevel - Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) - } - - }) - - try { - api.connectToDevice(deviceId) - } catch (a: PolarInvalidArgument) { - a.printStackTrace() - } - - } - - fun disconnectToPolarSDK(deviceId: String?) { - try { - api.disconnectFromDevice(deviceId!!) - api.shutDown() - } catch (e: Exception) { - Log.e(TAG, "Error occurred during shutdown: ${e.message}") - } - } - - fun setDeviceTime(deviceId: String?) { - deviceId?.let { id -> - val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")) - calendar.time = Date() - api.setLocalTime(id, calendar) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - val timeSetString = "time ${calendar.time} set to device" - Log.d(TAG, timeSetString) - }, - { error: Throwable -> Log.e(TAG, "set time failed: $error") } - ) - } ?: run { - Log.e(TAG, "Device ID is null. Cannot set device time.") - } - } - - fun getTimeSec(): Double { - return (System.currentTimeMillis() / 1000).toDouble() - } - - fun getTimeNano(): Long { - return (System.currentTimeMillis() * 1_000_000L) - } - - fun streamHR() { - Log.d(TAG, "start streamHR for ${deviceId}") - val isDisposed = hrDisposable?.isDisposed ?: true - if (isDisposed) { - hrDisposable = deviceId?.let { - api.startHrStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { Log.d(TAG, "Subscribed to HrStreaming for ${deviceId}") } - .subscribe( - { hrData: PolarHrData -> - for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") - send( - heartRateTopic, - PolarHeartRate( - deviceId, - getTimeNano(), - getTimeSec(), - sample.hr, - sample.rrsMs, - sample.rrAvailable, - sample.contactStatus, - sample.contactStatusSupported - ) - ) - - } - }, - { error: Throwable -> - Log.e(TAG, "HR stream failed for ${deviceId}. Reason $error") - hrDisposable = null - }, - { Log.d(TAG, "HR stream for ${deviceId} complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - hrDisposable?.dispose() - Log.d(TAG, "HR stream disposed") - hrDisposable = null - } - } - - fun streamEcg() { - Log.d(TAG, "start streamECG for ${deviceId}") - val isDisposed = ecgDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 130, - PolarSensorSetting.SettingType.RESOLUTION to 14 - ) - val ecgSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - ecgDisposable = api.startEcgStreaming(deviceId, ecgSettings) - .subscribe( - { polarEcgData: PolarEcgData -> - for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp)}") - send( - ecgTopic, - PolarEcg( - name, - PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.voltage - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "ECG stream failed. Reason $error") - }, - { Log.d(TAG, "ECG stream complete") } - ) - } - } else { - // NOTE stops streaming if it is "running" - ecgDisposable?.dispose() - } - } - - fun streamAcc() { - val isDisposed = accDisposable?.isDisposed ?: true - if (isDisposed) { - val settingMap = mapOf( - PolarSensorSetting.SettingType.SAMPLE_RATE to 25, // [50, 100, 200, 25] - PolarSensorSetting.SettingType.RESOLUTION to 16, // [16] - PolarSensorSetting.SettingType.RANGE to 2 // [2, 4, 8] - ) - val accSettings = PolarSensorSetting(settingMap) - deviceId?.let { deviceId -> - accDisposable = api.startAccStreaming(deviceId, accSettings) - .subscribe( - { polarAccelerometerData: PolarAccelerometerData -> - for (data in polarAccelerometerData.samples) { - Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") - send( - accelerationTopic, - PolarAcceleration( - name, - PolarVantageV3Utils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), - data.x, - data.y, - data.z - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "ACC stream failed. Reason $error") - }, - { - Log.d(TAG, "ACC stream complete") - } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - accDisposable?.dispose() - } - } - - fun streamPpi() { - val isDisposed = ppiDisposable?.isDisposed ?: true - if (isDisposed) { - ppiDisposable = deviceId?.let { - api.startPpiStreaming(it) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { ppiData: PolarPpiData -> - for (sample in ppiData.samples) { - Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") - send( - ppIntervalTopic, - PolarPpInterval( - name, - getTimeSec(), - getTimeSec(), - sample.blockerBit, - sample.errorEstimate, - sample.hr, - sample.ppi, - sample.skinContactStatus, - sample.skinContactSupported - ) - ) - } - }, - { error: Throwable -> - Log.e(TAG, "PPI stream failed. Reason $error") - }, - { Log.d(TAG, "PPI stream complete") } - ) - } - } else { - // NOTE dispose will stop streaming if it is "running" - ppiDisposable?.dispose() - } - } - -} - - diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt deleted file mode 100644 index 330a626a9..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Provider.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.radarbase.passive.polarvantagev3 - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import org.radarbase.android.BuildConfig -import org.radarbase.android.RadarService -import org.radarbase.android.source.SourceProvider - -open class PolarVantageV3Provider(radarService: RadarService) : SourceProvider(radarService) { - override val serviceClass: Class = PolarVantageV3Service::class.java - - override val pluginNames = listOf( - "PolarVantageV3_sensors", - "PolarVantageV3_sensor", - ".polar.PolarVantageV3Provider", - "org.radarbase.passive.polar.PolarVantageV3Provider", - "org.radarcns.polar.PolarVantageV3Provider") - - override val description: String - get() = radarService.getString(R.string.polarSensorsDescription) - override val hasDetailView = true - - override val displayName: String - get() = radarService.getString(R.string.polarDisplayName) - - override val permissionsNeeded = buildList { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - add(Manifest.permission.BLUETOOTH_SCAN) - add(Manifest.permission.BLUETOOTH_CONNECT) - } else { - add(Manifest.permission.ACCESS_COARSE_LOCATION) - add(Manifest.permission.ACCESS_FINE_LOCATION) - add(Manifest.permission.BLUETOOTH) - add(Manifest.permission.BLUETOOTH_ADMIN) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } - } - } - - override val featuresNeeded = listOf(PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_BLUETOOTH_LE) - - override val sourceProducer: String = PRODUCER - - override val sourceModel: String = MODEL - - override val version: String = BuildConfig.VERSION_NAME - - override val isFilterable = true - companion object { - const val PRODUCER = "Polar" - const val MODEL = "Generic" - } -} diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt deleted file mode 100644 index 0119317b5..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Service.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.radarbase.passive.polarvantagev3 - -import android.os.Process -import org.radarbase.android.config.SingleRadarConfiguration -import org.radarbase.android.source.SourceManager -import org.radarbase.android.source.SourceService -import org.radarbase.android.util.SafeHandler - -/** - * A service that manages the Polar manager and a TableDataHandler to send store the data of - * the phone sensors and send it to a Kafka REST proxy. - */ -class PolarVantageV3Service : SourceService() { - private lateinit var handler: SafeHandler -// private lateinit var context: Context - override val defaultState: PolarVantageV3State - get() = PolarVantageV3State() - - override fun onCreate() { - super.onCreate() - handler = SafeHandler.getInstance("Polar", Process.THREAD_PRIORITY_FOREGROUND) - } - - override fun createSourceManager() = PolarVantageV3Manager(this, applicationContext) - - override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) { - manager as PolarVantageV3Manager - } -} - diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt deleted file mode 100644 index 3b0b42f56..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3State.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.radarbase.passive.polarvantagev3 - -import org.radarbase.android.source.BaseSourceState - -/** - * The status on a single point in time - */ -class PolarVantageV3State : BaseSourceState() { - override val acceleration = floatArrayOf(Float.NaN, Float.NaN, Float.NaN) - @set:Synchronized - override var batteryLevel = Float.NaN - - override val hasAcceleration: Boolean = true - - @Synchronized - fun setAcceleration(x: Float, y: Float, z: Float) { - this.acceleration[0] = x - this.acceleration[1] = y - this.acceleration[2] = z - } -} diff --git a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt b/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt deleted file mode 100644 index cf4b848bf..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/java/org/radarbase/passive/polarvantagev3/PolarVantageV3Utils.kt +++ /dev/null @@ -1,12 +0,0 @@ -object PolarVantageV3Utils { - - @JvmStatic - fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { - val thirtyYearsInNanoSec = 946771200000000000 - val oneDayInNanoSec = 86400000000000 - - val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec - - return unixEpoch - } -} \ No newline at end of file diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml deleted file mode 100644 index 975f5cff1..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - Polar V3 - Polar V3 %1$s - Polar sensors like acceleration and step counts. - \ No newline at end of file diff --git a/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml b/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml deleted file mode 100644 index 8acc56071..000000000 --- a/plugins/radar-android-polarvantagev3/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt deleted file mode 100644 index 219bbec82..000000000 --- a/plugins/radar-android-polarvantagev3/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -import org.junit.Assert.assertEquals -import org.junit.Test -import org.radarbase.passive.polar.* - -class PolarEpochConversionTest { - - @Test - fun testConvertEpochPolarToUnixEpoch() { - val testData = mapOf( - 768408772990080000L to 1715093572990080000L, - 768408772997784576L to 1715093572997784576L, - 768408773005489152L to 1715093573005489152L, - 768408773013193728L to 1715093573013193728L, - 768408773020898176L to 1715093573020898176L, - 768408773028602752L to 1715093573028602752L, - 768408773036307328L to 1715093573036307328L, - 768408773044011776L to 1715093573044011776L - ) - - testData.forEach { (epochPolar, expectedUnixEpoch) -> - val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) - assertEquals(expectedUnixEpoch, result) - } - } -} From 2b96db9fdc6d061bd3cbfcffaec2e62a4c12674a Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Sun, 21 Jul 2024 13:47:26 +0200 Subject: [PATCH 21/25] Fix time calculation --- build.gradle | 1 + gradle.properties | 2 +- .../radarbase/passive/polar/PolarManager.kt | 107 ++++++++++++------ .../org/radarbase/passive/polar/PolarUtils.kt | 6 +- .../passive/polar/PolarEpochConversionTest.kt | 11 +- 5 files changed, 77 insertions(+), 50 deletions(-) diff --git a/build.gradle b/build.gradle index 87a948233..03de52343 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ buildscript { repositories { google() mavenCentral() + mavenLocal() } dependencies { diff --git a/gradle.properties b/gradle.properties index cafc0373b..6ef1d0be9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ publish_plugin_version=2.0.0 versions_plugin_version=0.51.0 radar_commons_version=0.15.0 -radar_schemas_commons_version=0.8.800 +radar_schemas_commons_version=0.8.10-SNAPSHOT radar_faros_sdk_version=0.1.0 diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 065062862..5b407b64b 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -9,6 +9,7 @@ import android.util.Log import com.polar.sdk.api.PolarBleApi import com.polar.sdk.api.PolarBleApiCallback import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation +import com.polar.sdk.api.errors.PolarInvalidArgument import com.polar.sdk.api.model.* import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable @@ -34,12 +35,13 @@ class PolarManager( private val ecgTopic: DataCache = createCache("android_polar_ecg", PolarEcg()) private val heartRateTopic: DataCache = createCache("android_polar_heart_rate", PolarHeartRate()) private val ppIntervalTopic: DataCache = createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval()) + private val ppgTopic: DataCache = createCache("android_polar_ppg", PolarPpg()) private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND) private var wakeLock: PowerManager.WakeLock? = null private lateinit var api: PolarBleApi - private var deviceId: String? = null + private var deviceId: String = "D733F724" private var isDeviceConnected: Boolean = false private var autoConnectDisposable: Disposable? = null @@ -47,6 +49,7 @@ class PolarManager( private var ecgDisposable: Disposable? = null private var accDisposable: Disposable? = null private var ppiDisposable: Disposable? = null + private var ppgDisposable: Disposable? = null private var timeDisposable: Disposable? = null companion object { @@ -142,6 +145,8 @@ class PolarManager( streamHR() streamEcg() streamAcc() + streamPpi() + streamPpg() } else -> { Log.d(TAG, "No feature was ready") @@ -161,28 +166,19 @@ class PolarManager( override fun batteryLevelReceived(identifier: String, level: Int) { var batteryLevel = level.toFloat() / 100.0f state.batteryLevel = batteryLevel - Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + getTimeSec()) - send(batteryLevelTopic, PolarBatteryLevel(name, getTimeSec(), getTimeSec(), batteryLevel)) + Log.d(TAG, "Battery level $level%, which is $batteryLevel at " + currentTime) + send(batteryLevelTopic, PolarBatteryLevel(name, currentTime, currentTime, batteryLevel)) } }) try { - if (autoConnectDisposable != null) { - autoConnectDisposable?.dispose() - } - autoConnectDisposable = api.autoConnectToDevice(-60, "180D", null) - .subscribe( - { Log.d(TAG, "auto connect search complete") }, - { throwable: Throwable -> - Log.e(TAG, "" + throwable.toString()) - } - ) - } catch (e: Exception) { - Log.e(TAG, "Could not find polar device") + api.connectToDevice(deviceId) + } catch (a: PolarInvalidArgument) { + a.printStackTrace() } - } + } fun disconnectToPolarSDK(deviceId: String?) { try { @@ -201,22 +197,19 @@ class PolarManager( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { - val timeSetString = "time ${calendar.time} set to device" + val timeSetString = "Time ${calendar.time} set to device" Log.d(TAG, timeSetString) }, - { error: Throwable -> Log.e(TAG, "set time failed: $error") } + { error: Throwable -> Log.e(TAG, "Set time failed: $error") } ) } ?: run { Log.e(TAG, "Device ID is null. Cannot set device time.") } } - fun getTimeSec(): Double { - return (System.currentTimeMillis() / 1000).toDouble() - } - - fun getTimeNano(): Long { - return (System.currentTimeMillis() * 1_000_000L) + fun getTimeNano(): Double { + var nano = (System.currentTimeMillis() * 1_000_000L).toDouble() + return nano/1000_000_000L } fun streamHR() { @@ -230,13 +223,13 @@ class PolarManager( .subscribe( { hrData: PolarHrData -> for (sample in hrData.samples) { - Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} time ${getTimeNano()} ${getTimeSec()} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") + Log.d(TAG, "HeartRate data for ${name}, ${deviceId}: HR ${sample.hr} timeStamp: ${getTimeNano()} currentTime: ${currentTime} R ${sample.rrsMs} rrAvailable: ${sample.rrAvailable} contactStatus: ${sample.contactStatus} contactStatusSupported: ${sample.contactStatusSupported}") send( heartRateTopic, PolarHeartRate( name, getTimeNano(), - getTimeSec(), + currentTime, sample.hr, sample.rrsMs, sample.rrAvailable, @@ -255,7 +248,6 @@ class PolarManager( ) } } else { - // NOTE stops streaming if it is "running" hrDisposable?.dispose() Log.d(TAG, "HR stream disposed") hrDisposable = null @@ -276,13 +268,13 @@ class PolarManager( .subscribe( { polarEcgData: PolarEcgData -> for (data in polarEcgData.samples) { - Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${data.timeStamp} time: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)}") + Log.d(TAG, "ECG yV: ${data.voltage} timeStamp: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)} currentTime: ${currentTime} PolarTimeStamp: ${data.timeStamp}") send( ecgTopic, PolarEcg( name, PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), + currentTime, data.voltage ) ) @@ -295,10 +287,10 @@ class PolarManager( ) } } else { - // NOTE stops streaming if it is "running" ecgDisposable?.dispose() } } + fun streamAcc() { val isDisposed = accDisposable?.isDisposed ?: true if (isDisposed) { @@ -313,13 +305,13 @@ class PolarManager( .subscribe( { polarAccelerometerData: PolarAccelerometerData -> for (data in polarAccelerometerData.samples) { - Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp} getTimeSec: ${getTimeSec()}") + Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)} currentTime: ${currentTime} PolarTimeStamp: ${data.timeStamp}") send( accelerationTopic, PolarAcceleration( name, PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), - getTimeSec(), + currentTime, data.x, data.y, data.z @@ -336,11 +328,53 @@ class PolarManager( ) } } else { - // NOTE dispose will stop streaming if it is "running" accDisposable?.dispose() } } + fun streamPpg() { + Log.d(TAG, "start streamPpg for ${deviceId}") + val isDisposed = ppgDisposable?.isDisposed ?: true + if (isDisposed) { + val settingMap = mapOf( + PolarSensorSetting.SettingType.SAMPLE_RATE to 55, + PolarSensorSetting.SettingType.RESOLUTION to 22, + PolarSensorSetting.SettingType.CHANNELS to 4 + ) + val ppgSettings = PolarSensorSetting(settingMap) + deviceId?.let { deviceId -> + ppgDisposable = api.startPpgStreaming(deviceId, ppgSettings) + .subscribe( + { polarPpgData: PolarPpgData -> + if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) { + for (data in polarPpgData.samples) { + Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)} currentTime: ${currentTime} PolarTimeStamp: ${data.timeStamp}") + send( + ppgTopic, + PolarPpg( + name, + PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp), + currentTime, + data.channelSamples[0], + data.channelSamples[1], + data.channelSamples[2], + data.channelSamples[3] + ) + ) + } + } + }, + { error: Throwable -> + Log.e(TAG, "ECG stream failed. Reason $error") + }, + { Log.d(TAG, "ECG stream complete") } + ) + } + } else { + ecgDisposable?.dispose() + } + } + fun streamPpi() { val isDisposed = ppiDisposable?.isDisposed ?: true if (isDisposed) { @@ -350,13 +384,13 @@ class PolarManager( .subscribe( { ppiData: PolarPpiData -> for (sample in ppiData.samples) { - Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate}") + Log.d(TAG, "PPI ppi: ${sample.ppi} blocker: ${sample.blockerBit} errorEstimate: ${sample.errorEstimate} currentTime: ${currentTime}") send( ppIntervalTopic, PolarPpInterval( name, - getTimeSec(), - getTimeSec(), + currentTime, + currentTime, sample.blockerBit, sample.errorEstimate, sample.hr, @@ -374,7 +408,6 @@ class PolarManager( ) } } else { - // NOTE dispose will stop streaming if it is "running" ppiDisposable?.dispose() } } diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt index 6e41967b9..afc879bf9 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarUtils.kt @@ -1,12 +1,12 @@ object PolarUtils { @JvmStatic - fun convertEpochPolarToUnixEpoch(epochPolar: Long): Long { + fun convertEpochPolarToUnixEpoch(epochPolar: Long): Double { val thirtyYearsInNanoSec = 946771200000000000 val oneDayInNanoSec = 86400000000000 - val unixEpoch = epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec + val unixEpoch = (epochPolar + thirtyYearsInNanoSec - oneDayInNanoSec).toDouble() - return unixEpoch + return unixEpoch/1000_000_000L } } \ No newline at end of file diff --git a/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt b/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt index 219bbec82..492e3211d 100644 --- a/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt +++ b/plugins/radar-android-polar/src/test/java/org/radarbase/passive/polar/PolarEpochConversionTest.kt @@ -7,19 +7,12 @@ class PolarEpochConversionTest { @Test fun testConvertEpochPolarToUnixEpoch() { val testData = mapOf( - 768408772990080000L to 1715093572990080000L, - 768408772997784576L to 1715093572997784576L, - 768408773005489152L to 1715093573005489152L, - 768408773013193728L to 1715093573013193728L, - 768408773020898176L to 1715093573020898176L, - 768408773028602752L to 1715093573028602752L, - 768408773036307328L to 1715093573036307328L, - 768408773044011776L to 1715093573044011776L + 774625951321350016L to 1.72131075132135E9 ) testData.forEach { (epochPolar, expectedUnixEpoch) -> val result = PolarUtils.convertEpochPolarToUnixEpoch(epochPolar) - assertEquals(expectedUnixEpoch, result) + assertEquals(expectedUnixEpoch, result, 0.01) } } } From 358d0fb5d47d7f89ae9b4865719b329de9b2b211 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Sun, 21 Jul 2024 13:48:06 +0200 Subject: [PATCH 22/25] Update Readme --- plugins/radar-android-polar/README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/radar-android-polar/README.md b/plugins/radar-android-polar/README.md index 1b1027006..a3969e376 100644 --- a/plugins/radar-android-polar/README.md +++ b/plugins/radar-android-polar/README.md @@ -4,13 +4,23 @@ Application to be run on an Android 5.0 (or later) device with Bluetooth Low Ene The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. -This plugin has currently been tested using Polar's H10 heart rate sensor, but should also be compatible with the Polar H9 Heart rate sensor, Polar Verity Sense Optical heart rate sensor, OH1 Optical heart rate sensor, Ignite 3 watch and Vantage V3 watch, as listed on the [POLAR BLE SDK] GitHub [1]. +This plugin connects to a Polar device, of which the deviceId is hardcoded (on line 44, in PolarManager.kt). + +This plugin has currently been tested with the Polar H10, Polar Vantage V3 and Polar Verity Sense, of which the following topics are implemented: + +| Polar device | Topic | Description | +|--------------------------------|-----------------------------|------------------------------------------------------------| +| Polar H10 | android_polar_battery_level | Battery level | +| | android_polar_heart_rate | Heart rate (bpm) with sample rate 1Hz| +| | android_polar_ecg | Electrocardiography (ECG) data in µV with sample rate 130Hz| +| | android_polar_acceleration | Accelerometer data with a sample rate of 25Hz, a resoltion of 16 and range of 2G. Axis specific acceleration data in mG.| +| Polar Vantage V3 | android_polar_battery_level | Battery level| +| | android_polar_heart_rate | Heart rate (bpm) in 1Hz frequency| +| Polar Verity Sense | android_polar_battery_level | Battery level| +| | android_polar_heart_rate | Heart rate (bpm) with sample rate 1Hz| +| | android_polar_ppg | PPG data with a sample rate of 55Hz, a resolution of 22 using 4 channels. | +| | android_polar_ppi | PP interval representing cardiac pulse-to-pulse interval extracted from PPG signal.| -The following H10 features have been implemented: -- BatteryLevel -- Heart Rate (as bpm) with sample rate of 1Hz. -- Electrocardiography (ECG) data in µV with sample rate 130Hz. -- Accelerometer data with a sample rate of 25Hz and range of 2G. Axis specific acceleration data in mG. **** ## Installation From cdddd7b4ca8d760e9b5775004e5b2431629d8e3d Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Tue, 23 Jul 2024 10:49:50 +0200 Subject: [PATCH 23/25] Clean up code --- plugins/radar-android-polar/README.md | 2 +- .../main/java/org/radarbase/passive/polar/PolarManager.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/radar-android-polar/README.md b/plugins/radar-android-polar/README.md index a3969e376..320f8f156 100644 --- a/plugins/radar-android-polar/README.md +++ b/plugins/radar-android-polar/README.md @@ -4,7 +4,7 @@ Application to be run on an Android 5.0 (or later) device with Bluetooth Low Ene The plugin application uses Bluetooth Low Energy requirement, making it require coarse location permissions. This plugin does not collect location information. -This plugin connects to a Polar device, of which the deviceId is hardcoded (on line 44, in PolarManager.kt). +This plugin connects to a Polar device, of which the deviceId is hardcoded in PolarManager. This plugin has currently been tested with the Polar H10, Polar Vantage V3 and Polar Verity Sense, of which the following topics are implemented: diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt index 5b407b64b..56951d500 100644 --- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt +++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt @@ -41,7 +41,8 @@ class PolarManager( private var wakeLock: PowerManager.WakeLock? = null private lateinit var api: PolarBleApi - private var deviceId: String = "D733F724" + // Polar Device ID example given: D733F724 + private var deviceId: String = "ReplaceMe" private var isDeviceConnected: Boolean = false private var autoConnectDisposable: Disposable? = null @@ -158,9 +159,7 @@ class PolarManager( } override fun disInformationReceived(identifier: String, uuid: UUID, value: String) { - if (uuid == UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")) { - Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) - } + Log.d(TAG, "Firmware: " + identifier + " " + value.trim { it <= ' ' }) } override fun batteryLevelReceived(identifier: String, level: Int) { From cb069563cef1349f5c0152a2aeb2312476dac733 Mon Sep 17 00:00:00 2001 From: Bastiaan Date: Wed, 24 Jul 2024 14:50:43 +0200 Subject: [PATCH 24/25] Enable use of snapshot packages --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 87a948233..a951f2dff 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ subprojects { google() mavenCentral() mavenLocal() -// maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } } dependencies { From 63434e529517784f0d8ad56c81a5db4a9ac2edd3 Mon Sep 17 00:00:00 2001 From: Famke Schulting Date: Wed, 24 Jul 2024 16:47:35 +0200 Subject: [PATCH 25/25] Check in gradle.skip --- .gitignore | 1 - plugins/radar-android-ppg/gradle.skip | 0 2 files changed, 1 deletion(-) create mode 100644 plugins/radar-android-ppg/gradle.skip diff --git a/.gitignore b/.gitignore index d3e815421..4879cb373 100644 --- a/.gitignore +++ b/.gitignore @@ -98,4 +98,3 @@ fabric.properties ## Pebble 2 .lock* .skip -gradle.skip diff --git a/plugins/radar-android-ppg/gradle.skip b/plugins/radar-android-ppg/gradle.skip new file mode 100644 index 000000000..e69de29bb