Closed robin-glimp closed 2 years ago
Can you link the documentation for this feature? I'm not sure if/when I'll have time to implement it (depending on how complicated it is), but that information will be useful regardless.
Well the "documentation" I was referring to was the main README.md on the polar-ble-sdk repo.
H10 Heart rate sensor
- ...
- Start and stop of internal recording and request for internal recording status. Recording supports RR, HR with one second sampletime or HR with five second sampletime.
- List, read and remove for stored internal recording (sensor supports only one recording at the time).
The best overview of methods to be made available in this library would be found here: https://github.dev/polarofficial/polar-ble-sdk/blob/master/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApi.java#L296-L297
startRecording
stopRecording
requestRecordingStatus
listExercises
fetchExercise
removeExercise
For my personal use case it would suffice to have a hard-coded exercise ID and drop the list functionality and simplify the signature of all the methods (dropping the exerciseId), however it is not according to the polar spec anymore then.
The latest commit has the code for this, but it has literally zero testing. If you want to try it anyways you can use this in your pubspec:
polar:
git:
url: https://github.com/Rexios80/polar
ref: 5bcf251137220d6e108a7e020223b8c74c10d16f
Thanks for the quick turnaround! Tested it out with my Android phone and Polar H10 sensor.
The listExercises
and requestRecordingStatus
return as expected: with an empty list and a filled object as a response.
The issue I face is with the startRecording
method, I am getting this error message:
PlatformException(error, java.lang.Integer cannot be cast to java.lang.String, null, java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at dev.rexios.polar.PolarPlugin.startRecording(PolarPlugin.kt:217)
at dev.rexios.polar.PolarPlugin.onMethodCall(PolarPlugin.kt:76)
at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262)
at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:265)
at android.app.ActivityThread.main(ActivityThread.java:8360)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
With a further stackTrace:
#0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:607:7)
#1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:167:18)
<asynchronous suspension>
#2 PolarProvider.startRecording (package:pebbles/providers/polar_provider.dart:234:7)
<asynchronous suspension>
#3 _DevToolsState._startRecording (package:pebbles/screens/dev_tools.dart:42:7)
<asynchronous suspension>
Looked at the code and it seems to be a conversion issue for the interval field or something? I cant write kotlin so not entirely sure.
Yep that's what not testing it does 🙃
Other things are probably still broken, but try this ref: "5d2c72a9c3adf2c0f39ceddec9534916c4a3800c"
Bit by bit you'll figure it out with me being your tester haha. I'll give the new ref a go right now!
Alright looks like I was able to make a successful recording.
The next bug is in the listExercises
command and the decoding of the response into a flutter object.
I have adapted your code so you can see the response object.
Code:
Future<List<PolarExerciseEntry>> listExercises(String identifier) async {
final result = await _channel.invokeListMethod('listExercises', identifier);
if (result == null) {
return [];
}
print(result);
return result
.cast<String>()
.map((e) => PolarExerciseEntry.fromJson(jsonDecode(e)))
.toList();
}
The printed line is:
[{"date":"Oct 11, 2022 19:17:27","identifier":"test","path":"/test/SAMPLES.BPB"}]
And the stack trace for the operation is:
E/flutter (20684): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: type 'String' is not a subtype of type 'int'
E/flutter (20684): #0 new PolarExerciseEntry.fromJson (package:polar/src/model/polar_recording.dart:76:56)
E/flutter (20684): #1 Polar.listExercises.<anonymous closure> (package:polar/src/polar_base.dart:450:40)
E/flutter (20684): #2 MappedListIterable.elementAt (dart:_internal/iterable.dart:413:31)
E/flutter (20684): #3 ListIterator.moveNext (dart:_internal/iterable.dart:342:26)
E/flutter (20684): #4 new _GrowableList._ofEfficientLengthIterable (dart:core-patch/growable_array.dart:189:27)
E/flutter (20684): #5 new _GrowableList.of (dart:core-patch/growable_array.dart:150:28)
E/flutter (20684): #6 new List.of (dart:core-patch/array_patch.dart:51:28)
E/flutter (20684): #7 ListIterable.toList (dart:_internal/iterable.dart:213:44)
E/flutter (20684): #8 Polar.listExercises (package:polar/src/polar_base.dart:451:10)
E/flutter (20684): <asynchronous suspension>
E/flutter (20684): #9 PolarProvider.listExercises (package:pebbles/providers/polar_provider.dart:217:11)
E/flutter (20684): <asynchronous suspension>
E/flutter (20684): #10 _DevToolsState._listExercises (package:pebbles/screens/dev_tools.dart:57:9)
E/flutter (20684): <asynchronous suspension>
So it seems to be the date parsing.
Also another thing I found out: I wanted to try and get a second recording in there (which isnt possible for the H10 I believe). So I changed the exerciseId to test2
and hit record again. This gave me a PlatformException(OPERATION_NOT_PERMITTED, null, null, null)
. However as this library is purely a wrapper, this isnt your concern, as this is how the API specification is telling us how it should be. (Just a note to self to just use a hard-coded ID)
Knowing the timestamp from the message above I also tried the fetchExercise
method but there is another casting bug there.
polar.fetchExercise(
PolarExerciseEntry(
path: "/test/SAMPLES.BPB",
date: DateTime(2022, 10, 11, 19, 17, 27),
entryId: "test",
),
);
Results in:
PlatformException(error, java.util.HashMap cannot be cast to java.lang.String, null, java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
at dev.rexios.polar.PolarPlugin.fetchExercise(PolarPlugin.kt:287)
at dev.rexios.polar.PolarPlugin.onMethodCall(PolarPlugin.kt:80)
at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262)
at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:265)
at android.app.ActivityThread.main(ActivityThread.java:8360)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
The same applies to the removeExercise
parsing logic of the date.
Try the ref "72034c005c7e74e1262511b9af73732e85df55fe"
listExercises
works good now!
3 new bugs:
entryId
and identifier
. This seems to be an issue in polar_recording.dart
in the PolarExerciseEntry.fromJson
method. The json name is identifier
but it expects it to be entryId
.fetchExercises
fails with the following stacktrace:
PlatformException(error, java.util.HashMap cannot be cast to java.lang.String, null, java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
at dev.rexios.polar.PolarPlugin.fetchExercise(PolarPlugin.kt:309)
at dev.rexios.polar.PolarPlugin.onMethodCall(PolarPlugin.kt:102)
at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262)
at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:265)
at android.app.ActivityThread.main(ActivityThread.java:8360)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
removeExercise
fails with the following stacktrace:
PlatformException(error, java.util.HashMap cannot be cast to java.lang.String, null, java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
at dev.rexios.polar.PolarPlugin.removeExercise(PolarPlugin.kt:327)
at dev.rexios.polar.PolarPlugin.onMethodCall(PolarPlugin.kt:103)
at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262)
at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:265)
at android.app.ActivityThread.main(ActivityThread.java:8360)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
Try "ffa98e5154a5f0952b95571f0591a30a8cba637a"
@Rexios80 the issues on fetchExercise
and removeExercise
remains the same it seems:
PlatformException(error, java.util.HashMap cannot be cast to java.lang.String, null, java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
at dev.rexios.polar.PolarPlugin.removeExercise(PolarPlugin.kt:327)
at dev.rexios.polar.PolarPlugin.onMethodCall(PolarPlugin.kt:103)
at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262)
at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:265)
at android.app.ActivityThread.main(ActivityThread.java:8360)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
I think this fixes that "eb48cd071e46304181e63d12ae5e154340ef6331"
You're a genius! I managed to start, stop, fetch, list and remove an exercise!
I also love the named parameter addition for startRecording
.
To get this all fully production ready we might need to look into error messages a bit more. So what happens if there are no entries found to fetch or starting another session while the H10 already has one saved.
Also I fixed some more issues on ref "99431a1b72e1ab6dafc3092949e5502a9dbb884b"
If you are able to test on iOS too so I could get another pair of eyes on the whole thing I would greatly appreciate it. If everything looks good to you, I'll go ahead and release it.
One thing I keep seeing when I connect the polar is this stacktrace, it is not breaking, but it doesnt look good.
E/EventChannel#polar/streaming(30288): java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter arguments
E/EventChannel#polar/streaming(30288): at dev.rexios.polar.PolarPlugin$streamingHandler$1.onCancel(Unknown Source:2)
E/EventChannel#polar/streaming(30288): at io.flutter.plugin.common.EventChannel$IncomingStreamRequestHandler.onListen(EventChannel.java:212)
E/EventChannel#polar/streaming(30288): at io.flutter.plugin.common.EventChannel$IncomingStreamRequestHandler.onMessage(EventChannel.java:197)
E/EventChannel#polar/streaming(30288): at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)
E/EventChannel#polar/streaming(30288): at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:319)
E/EventChannel#polar/streaming(30288): at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12)
E/EventChannel#polar/streaming(30288): at android.os.Handler.handleCallback(Handler.java:938)
E/EventChannel#polar/streaming(30288): at android.os.Handler.dispatchMessage(Handler.java:99)
E/EventChannel#polar/streaming(30288): at android.os.Looper.loop(Looper.java:265)
E/EventChannel#polar/streaming(30288): at android.app.ActivityThread.main(ActivityThread.java:8360)
E/EventChannel#polar/streaming(30288): at java.lang.reflect.Method.invoke(Native Method)
E/EventChannel#polar/streaming(30288): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:632)
E/EventChannel#polar/streaming(30288): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049)
One more small issue on the latest ref. When I wake my phone up after a minute of inactivity and trigger the requestRecordingStatus
I get this brief disconnect or something with the following Stacktrace. Only happened once or twice so not 100% sure whether I can consistently reproduce and whether its coming from the library, the polar library or android itself.
[log] PlatformException(com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpUtils$PftpOperationTimeout: Air packet was not received in required timeline, null, null, null)
[log] #0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:607:7)
#1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:167:18)
<asynchronous suspension>
#2 MethodChannel.invokeListMethod (package:flutter/src/services/platform_channel.dart:353:35)
<asynchronous suspension>
#3 Polar.requestRecordingStatus (package:polar/src/polar_base.dart:428:9)
<asynchronous suspension>
#4 PolarProvider.requestRecordingStatus (package:pebbles/providers/polar_provider.dart:259:14)
<asynchronous suspension>
I'm pretty sure you're supposed to call list before you call fetch, so that doesn't matter If you try to start a session with one already saved, you get an exception, so you should probably call list before trying that too
As this library is a wrapper I would say that is fine. But I do think some documentation on this would help. Maybe I can write some of that for you.
If you are able to test on iOS too so I could get another pair of eyes on the whole thing I would greatly appreciate it.
I should be able to do that for you later this week with a colleague of mine.
If everything looks good to you, I'll go ahead and release it.
Sure, any other tests beside iOS you want me to do? I also have a Polar verity sense here that I can use, however I havent been able to get good data from it so far. I really prefer the H10.
So I recorded a 6 hour segment of RR intervals to test. The result of that is some errors..
PlatformException(com.polar.androidcommunications.api.ble.exceptions.BleCharacteristicNotificationNotEnabled: PS-FTP MTU not enabled, null, null, null)
PlatformException(com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpUtils$PftpOperationTimeout: Air packet was not received in required timeline, null, null, null)
PlatformException(Unknown error listing exercises, null, null, null)
Unfortunately I dont have stacktraces as these errors come from sentry.
I got the second one the most often, but I think a clue lies in the first error --> FTP isnt enabled.
This is also mentioned in the polar-ble-sdk
, but I dont know how this plays into your wrapper and whether the app has to call a new function.
Wasn't able to test with iOS just yet. But thats in the pipeline.
So the starting, stopping, listing, fetching recordings works but only for small recordings (few minutes max I think). After that it needs the file transfer but thats not working just yet.
Are you getting an ftpFeatureReady
call?
It looks like you need ftp enabled to do anything with recording, so maybe you're just calling it too early?
I managed to get a download. the ftpFeatureReadyStream
always just gives me the device identifier whenever I connect. But maybe that was sufficient to have the download. Still half of the time it fails and I cant really put my finger on the issue.
Bigger issue though: iOS cant connect to the polar anymore :( Here is the crash report from sentry.
OS Version: iOS 16.0.2 (20A380)
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Crashed Thread: 0
Application Specific Information:
State restoration of CBCentralManager is only allowed for applications that have specified the "bluetooth-central" background mode
Thread 0 Crashed:
0 CoreFoundation 0x348a4a248 __exceptionPreprocess
1 libobjc.A.dylib 0x33b1d7a64 objc_exception_throw
2 Foundation 0x33d826818 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
3 CoreBluetooth 0x388351dc4 <redacted>
4 PolarBleSdk 0x1014ec270 <unknown> + 292
5 PolarBleSdk 0x1014f248c <unknown> + 96
6 PolarBleSdk 0x101547c7c <unknown> + 1484
7 PolarBleSdk 0x101546d54 PolarBleApiDefaultImpl.polarImplementation
8 polar 0x1026c3c9c <unknown> + 172
9 polar 0x1026caef4 <unknown> + 116
10 polar 0x1026c4828 <unknown> + 84
11 Flutter 0x102e62e50 <redacted>
12 Flutter 0x10294acd0 <redacted>
13 libdispatch.dylib 0x3573464b0 _dispatch_call_block_and_release
14 libdispatch.dylib 0x357347fd8 _dispatch_client_callout
15 libdispatch.dylib 0x3573567f0 _dispatch_main_queue_drain
16 libdispatch.dylib 0x357356440 _dispatch_main_queue_callback_4CF
17 CoreFoundation 0x348adaa04 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
18 CoreFoundation 0x348abc364 __CFRunLoopRun
19 CoreFoundation 0x348ac11e0 CFRunLoopRunSpecific
20 GraphicsServices 0x3ba781364 GSEventRunModal
21 UIKitCore 0x34d0fed84 -[UIApplication _run]
22 UIKitCore 0x34d0fe9e8 UIApplicationMain
23 Runner 0x200140444 <redacted>
24 <unknown> 0x1e00ed948 <redacted>
Thread 0 Crashed:
0 CoreFoundation 0x348a4a248 __exceptionPreprocess
1 libobjc.A.dylib 0x33b1d7a64 objc_exception_throw
2 Foundation 0x33d826818 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
3 CoreBluetooth 0x388351dc4 <redacted>
4 PolarBleSdk 0x1014ec270 <unknown> + 292
5 PolarBleSdk 0x1014f248c <unknown> + 96
6 PolarBleSdk 0x101547c7c <unknown> + 1484
7 PolarBleSdk 0x101546d54 PolarBleApiDefaultImpl.polarImplementation
8 polar 0x1026c3c9c <unknown> + 172
9 polar 0x1026caef4 <unknown> + 116
10 polar 0x1026c4828 <unknown> + 84
11 Flutter 0x102e62e50 <redacted>
12 Flutter 0x10294acd0 <redacted>
13 libdispatch.dylib 0x3573464b0 _dispatch_call_block_and_release
14 libdispatch.dylib 0x357347fd8 _dispatch_client_callout
15 libdispatch.dylib 0x3573567f0 _dispatch_main_queue_drain
16 libdispatch.dylib 0x357356440 _dispatch_main_queue_callback_4CF
17 CoreFoundation 0x348adaa04 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
18 CoreFoundation 0x348abc364 __CFRunLoopRun
19 CoreFoundation 0x348ac11e0 CFRunLoopRunSpecific
20 GraphicsServices 0x3ba781364 GSEventRunModal
21 UIKitCore 0x34d0fed84 -[UIApplication _run]
22 UIKitCore 0x34d0fe9e8 UIApplicationMain
23 Runner 0x200140444 <redacted>
24 <unknown> 0x1e00ed948 <redacted>
My current Info.plist
looks like this (I redacted my explanation)
<key>NSBluetoothAlwaysUsageDescription</key>
<string>...</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>...</string>
I will try this fix found in https://github.com/dotintent/FlutterBleLib/issues/530, maybe that helps. but this is then important for the documentation
Well do you have the Bluetooth background mode enabled?
Well do you have the Bluetooth background mode enabled?
Nope, so I will add that line in the Info.plist
, however that was never required for me using this library before.
It sounds like it's because of the state restoration stuff Polar added. I'm thinking my wrapper is good, and any remaining issues are to be discussed with the Polar devs themselves unless you can show me errors generated by my code.
I released this to pub.dev in version 3.2.0
Feel free to reopen this if it needs more attention
I was going through the polar SDK and spotted the ability to record RR intervals over longer periods of time. I wanted to use this library to implement this feature in my app.
However I didnt see the right methods to do this. The only traces I can find for this support are:
PolarExerciseData
class in the streaming data list but its unusedftpFeatureReadyStream
but that only seems to return the device id.Can you explain whether this feature is either not there, not scheduled to be implemented, still scheduled, something else?
Would love to have this as a part of the library and would be happy to test if needed.
My own devices: