react-native-polar-bridge 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,12 +17,14 @@ import io.reactivex.rxjava3.core.Flowable
17
17
  import io.reactivex.rxjava3.core.Single
18
18
  import java.util.*
19
19
  import java.util.concurrent.atomic.AtomicInteger
20
+ import java.util.concurrent.TimeUnit
20
21
  import java.time.Instant
21
22
 
22
23
  @ReactModule(name = PolarBridgeModule.NAME)
23
24
  class PolarBridgeModule(reactContext: ReactApplicationContext) :
24
25
  NativePolarBridgeSpec(reactContext) {
25
26
  private val reactContext: ReactApplicationContext = reactContext
27
+ private val SENSOR_BUFFER_SECONDS = 10L
26
28
 
27
29
  override fun getName(): String {
28
30
  return NAME
@@ -617,17 +619,34 @@ class PolarBridgeModule(reactContext: ReactApplicationContext) :
617
619
  }
618
620
  }
619
621
 
622
+ data class HrSampleWithTimestamp(
623
+ val data: PolarHrData.PolarHrSample,
624
+ val timestampMs: Long
625
+ )
626
+
620
627
  override fun fetchHrData(deviceId: String) {
621
628
  Log.e(TAG, "Fetch Heart Data called on: $deviceId ")
622
629
  val isDisposed = hrDisposable?.isDisposed ?: true
623
630
  try{
624
631
  if (isDisposed) {
625
632
  hrDisposable = api.startHrStreaming(deviceId)
633
+ .flatMapIterable { hrData ->
634
+ hrData.samples.map { sample ->
635
+ HrSampleWithTimestamp(
636
+ data = sample,
637
+ timestampMs = System.currentTimeMillis()
638
+ )
639
+ }
640
+ }
641
+ .buffer(SENSOR_BUFFER_SECONDS, TimeUnit.SECONDS)
626
642
  .observeOn(AndroidSchedulers.mainThread())
627
643
  .subscribe(
628
- { hrData: PolarHrData ->
629
- Log.i(TAG, "PolarHrData ${hrData.samples.size}")
630
- for (sample in hrData.samples) {
644
+ { samples ->
645
+ if (samples.isEmpty()) return@subscribe
646
+
647
+ Log.d(TAG, "Flushing HR buffer (${samples.size} samples)")
648
+ for (item in samples) {
649
+ val sample = item.data
631
650
  Log.d(TAG, "HR bpm: ${sample.hr} " +
632
651
  "rrs: ${sample.rrsMs} " +
633
652
  "rrAvailable: ${sample.rrAvailable} " +
@@ -645,6 +664,7 @@ class PolarBridgeModule(reactContext: ReactApplicationContext) :
645
664
  event.putBoolean("rrAvailable", sample.rrAvailable)
646
665
  event.putBoolean("contactStatus", sample.contactStatus)
647
666
  event.putBoolean("contactStatusSupported", sample.contactStatusSupported)
667
+ event.putDouble("timestamp", item.timestampMs.toDouble())
648
668
 
649
669
  sendEvent("PolarHrData", event)
650
670
  }
@@ -719,10 +739,15 @@ class PolarBridgeModule(reactContext: ReactApplicationContext) :
719
739
  .flatMap { settings: PolarSensorSetting ->
720
740
  api.startAccStreaming(deviceId, settings)
721
741
  }
742
+ .flatMapIterable { it.samples }
743
+ .buffer(SENSOR_BUFFER_SECONDS, TimeUnit.SECONDS)
722
744
  .observeOn(AndroidSchedulers.mainThread())
723
745
  .subscribe(
724
- { polarAccelerometerData: PolarAccelerometerData ->
725
- for (data in polarAccelerometerData.samples) {
746
+ { samples ->
747
+ if (samples.isEmpty()) return@subscribe
748
+ Log.d(TAG, "Flushing ACC buffer (${samples.size} samples)")
749
+
750
+ for (data in samples) {
726
751
  Log.d(TAG, "ACC x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp}")
727
752
 
728
753
  val event: WritableMap = Arguments.createMap()
@@ -770,10 +795,16 @@ class PolarBridgeModule(reactContext: ReactApplicationContext) :
770
795
  .flatMap { settings: PolarSensorSetting ->
771
796
  api.startGyroStreaming(deviceId, settings)
772
797
  }
798
+ .flatMapIterable { it.samples }
799
+ .buffer(SENSOR_BUFFER_SECONDS, TimeUnit.SECONDS)
773
800
  .observeOn(AndroidSchedulers.mainThread())
774
801
  .subscribe(
775
- { polarGyroData: PolarGyroData ->
776
- for (data in polarGyroData.samples) {
802
+ { samples ->
803
+ if (samples.isEmpty()) return@subscribe
804
+
805
+ Log.d(TAG, "Flushing GYR buffer (${samples.size} samples)")
806
+
807
+ for (data in samples) {
777
808
  Log.d(TAG, "GYR x: ${data.x} y: ${data.y} z: ${data.z} timeStamp: ${data.timeStamp}")
778
809
 
779
810
  val event: WritableMap = Arguments.createMap()
@@ -822,25 +853,31 @@ class PolarBridgeModule(reactContext: ReactApplicationContext) :
822
853
  .flatMap { settings: PolarSensorSetting ->
823
854
  api.startPpgStreaming(deviceId, settings)
824
855
  }
856
+ .filter { it.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1 }
857
+ .flatMapIterable { it.samples }
858
+ .buffer(SENSOR_BUFFER_SECONDS, TimeUnit.SECONDS)
859
+ .observeOn(AndroidSchedulers.mainThread())
825
860
  .subscribe(
826
- { polarPpgData: PolarPpgData ->
827
- if (polarPpgData.type == PolarPpgData.PpgDataType.PPG3_AMBIENT1) {
828
- for (data in polarPpgData.samples) {
829
- Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}")
830
-
831
- val event: WritableMap = Arguments.createMap()
832
- // Float not supported
833
- // See: https://github.com/facebook/react-native/issues/9685
834
- // See: https://javadoc.io/doc/com.facebook.react/react-native/0.20.1/com/facebook/react/bridge/WritableMap.html
835
- event.putString("ppg0", "${data.channelSamples[0]}")
836
- event.putString("ppg1", "${data.channelSamples[1]}")
837
- event.putString("ppg2", "${data.channelSamples[2]}")
838
- event.putString("ambient", "${data.channelSamples[3]}")
839
- // Long not supported, use double as workaround
840
- event.putDouble("ppgTimestamp", data.timeStamp.toDouble())
841
-
842
- sendEvent("PolarPpgData", event)
843
- }
861
+ { samples ->
862
+ if (samples.isEmpty()) return@subscribe
863
+
864
+ Log.d(TAG, "Flushing PPG buffer (${samples.size} samples)")
865
+
866
+ for (data in samples) {
867
+ Log.d(TAG, "PPG ppg0: ${data.channelSamples[0]} ppg1: ${data.channelSamples[1]} ppg2: ${data.channelSamples[2]} ambient: ${data.channelSamples[3]} timeStamp: ${data.timeStamp}")
868
+
869
+ val event: WritableMap = Arguments.createMap()
870
+ // Float not supported
871
+ // See: https://github.com/facebook/react-native/issues/9685
872
+ // See: https://javadoc.io/doc/com.facebook.react/react-native/0.20.1/com/facebook/react/bridge/WritableMap.html
873
+ event.putString("ppg0", "${data.channelSamples[0]}")
874
+ event.putString("ppg1", "${data.channelSamples[1]}")
875
+ event.putString("ppg2", "${data.channelSamples[2]}")
876
+ event.putString("ambient", "${data.channelSamples[3]}")
877
+ // Long not supported, use double as workaround
878
+ event.putDouble("ppgTimestamp", data.timeStamp.toDouble())
879
+
880
+ sendEvent("PolarPpgData", event)
844
881
  }
845
882
  },
846
883
  { error: Throwable ->
@@ -46,6 +46,9 @@ class PolarBridge: RCTEventEmitter, ObservableObject
46
46
  private var isPpgStreaming = false
47
47
  private let disposeBag = DisposeBag()
48
48
 
49
+ /// Flush interval for all sensor buffers (seconds)
50
+ private let sensorFlushInterval: TimeInterval = 10.0
51
+
49
52
  @objc
50
53
  func multiply(_ a: NSNumber,withB b: NSNumber) -> NSNumber {
51
54
  let result = a.doubleValue * b.doubleValue
@@ -158,6 +161,53 @@ class PolarBridge: RCTEventEmitter, ObservableObject
158
161
  .asObservable()
159
162
  }
160
163
 
164
+ private var hrBuffer: [[String: Any]] = []
165
+ private let hrBufferQueue = DispatchQueue(label: "com.polarbridge.hrBufferQueue")
166
+ private var hrFlushTimer: Timer?
167
+
168
+ private func startHrFlushTimer() {
169
+ stopHrFlushTimer()
170
+
171
+ DispatchQueue.main.async { [weak self] in
172
+ guard let self = self else { return }
173
+
174
+ self.hrFlushTimer = Timer(
175
+ timeInterval: self.sensorFlushInterval,
176
+ repeats: true
177
+ ) { [weak self] _ in
178
+ self?.flushHrBuffer()
179
+ }
180
+
181
+ RunLoop.main.add(self.hrFlushTimer!, forMode: .common)
182
+ }
183
+ }
184
+
185
+ private func stopHrFlushTimer() {
186
+ DispatchQueue.main.async { [weak self] in
187
+ self?.hrFlushTimer?.invalidate()
188
+ self?.hrFlushTimer = nil
189
+ }
190
+ }
191
+
192
+ private func flushHrBuffer() {
193
+ hrBufferQueue.async { [weak self] in
194
+ guard let self = self else { return }
195
+ guard !self.hrBuffer.isEmpty else { return }
196
+
197
+ let events = self.hrBuffer
198
+ self.hrBuffer.removeAll()
199
+
200
+ DispatchQueue.main.async {
201
+ for event in events {
202
+ self.sendEvent(
203
+ withName: PolarEvent.PolarHrData.rawValue,
204
+ body: event
205
+ )
206
+ }
207
+ }
208
+ }
209
+ }
210
+
161
211
  @objc(fetchHrData:)
162
212
  func fetchHrData(_ deviceId: String) {
163
213
  NSLog("PolarBridge: Fetch HR Data called on: \(deviceId)")
@@ -170,12 +220,17 @@ class PolarBridge: RCTEventEmitter, ObservableObject
170
220
  // Dispose previous subscription if running
171
221
  if isHrStreaming {
172
222
  disposeHrStream()
223
+ stopHrFlushTimer()
224
+ flushHrBuffer()
225
+ isHrStreaming = false
226
+
173
227
  NSLog("PolarBridge: HR Stream stopped")
174
228
  sendEvent(withName: PolarEvent.PolarHrComplete.rawValue, body: ["message": "HR Stream stopped"])
175
229
  return
176
230
  }
177
231
 
178
232
  isHrStreaming = true
233
+ startHrFlushTimer()
179
234
 
180
235
  // Start HR streaming
181
236
  hrDisposable = api.startHrStreaming(deviceId)
@@ -185,28 +240,39 @@ class PolarBridge: RCTEventEmitter, ObservableObject
185
240
  guard let self = self else { return }
186
241
  NSLog("PolarBridge: HR data samples count: \(hrData.count)")
187
242
 
188
- for sample in hrData {
189
- NSLog("HR bpm: \(sample.hr), rrs: \(sample.rrsMs), rrAvailable: \(sample.rrAvailable), contactStatus: \(sample.contactStatus), contactStatusSupported: \(sample.contactStatusSupported)")
190
-
191
- // Create dictionary to send as event
192
- var event: [String: Any] = [:]
193
- event["hr"] = sample.hr
194
- event["rrsMs"] = sample.rrsMs
195
- event["rrAvailable"] = sample.rrAvailable
196
- event["contactStatus"] = sample.contactStatus
197
- event["contactStatusSupported"] = sample.contactStatusSupported
243
+ self.hrBufferQueue.async {
244
+ for sample in hrData {
245
+ NSLog("HR bpm: \(sample.hr), rrs: \(sample.rrsMs), rrAvailable: \(sample.rrAvailable), contactStatus: \(sample.contactStatus), contactStatusSupported: \(sample.contactStatusSupported)")
198
246
 
199
- self.sendEvent(withName: PolarEvent.PolarHrData.rawValue, body: event)
247
+ // Create dictionary to send as event
248
+ var event: [String: Any] = [:]
249
+ event["hr"] = sample.hr
250
+ event["rrsMs"] = sample.rrsMs
251
+ event["rrAvailable"] = sample.rrAvailable
252
+ event["contactStatus"] = sample.contactStatus
253
+ event["contactStatusSupported"] = sample.contactStatusSupported
254
+ event["timestamp"] = Date().timeIntervalSince1970 * 1000
255
+
256
+ // self.sendEvent(withName: PolarEvent.PolarHrData.rawValue, body: event)
257
+ self.hrBuffer.append(event)
258
+ }
200
259
  }
201
260
  },
202
261
  onError: { [weak self] error in
203
262
  guard let self = self else { return }
204
263
  NSLog("PolarBridge: HR stream failed: \(error.localizedDescription)")
264
+ self.stopHrFlushTimer()
265
+ self.flushHrBuffer()
266
+ self.isHrStreaming = false
267
+
205
268
  self.sendEvent(withName: PolarEvent.PolarHrError.rawValue, body: ["error": error.localizedDescription])
206
269
  },
207
270
  onCompleted: { [weak self] in
208
271
  guard let self = self else { return }
209
272
  NSLog("PolarBridge: HR stream complete")
273
+ self.stopHrFlushTimer()
274
+ self.flushHrBuffer()
275
+ self.isHrStreaming = false
210
276
  self.sendEvent(withName: PolarEvent.PolarHrComplete.rawValue, body: ["message": "HR stream complete"])
211
277
  }
212
278
  )
@@ -215,6 +281,64 @@ class PolarBridge: RCTEventEmitter, ObservableObject
215
281
 
216
282
  }
217
283
 
284
+ // ACC
285
+ private var accBuffer: [[String: Any]] = []
286
+ private let accBufferQueue = DispatchQueue(label: "com.polarbridge.accBufferQueue")
287
+ private var accFlushTimer: Timer?
288
+
289
+ // GYR
290
+ private var gyrBuffer: [[String: Any]] = []
291
+ private let gyrBufferQueue = DispatchQueue(label: "com.polarbridge.gyrBufferQueue")
292
+ private var gyrFlushTimer: Timer?
293
+
294
+ // PPG
295
+ private var ppgBuffer: [[String: Any]] = []
296
+ private let ppgBufferQueue = DispatchQueue(label: "com.polarbridge.ppgBufferQueue")
297
+ private var ppgFlushTimer: Timer?
298
+
299
+ private func startAccFlushTimer() {
300
+ stopAccFlushTimer()
301
+
302
+ DispatchQueue.main.async { [weak self] in
303
+ guard let self = self else { return }
304
+
305
+ self.accFlushTimer = Timer(
306
+ timeInterval: self.sensorFlushInterval,
307
+ repeats: true
308
+ ) { [weak self] _ in
309
+ self?.flushAccBuffer()
310
+ }
311
+
312
+ RunLoop.main.add(self.accFlushTimer!, forMode: .common)
313
+ }
314
+ }
315
+
316
+ private func stopAccFlushTimer() {
317
+ DispatchQueue.main.async { [weak self] in
318
+ self?.accFlushTimer?.invalidate()
319
+ self?.accFlushTimer = nil
320
+ }
321
+ }
322
+
323
+ private func flushAccBuffer() {
324
+ accBufferQueue.async { [weak self] in
325
+ guard let self = self else { return }
326
+ guard !self.accBuffer.isEmpty else { return }
327
+
328
+ let events = self.accBuffer
329
+ self.accBuffer.removeAll()
330
+
331
+ DispatchQueue.main.async {
332
+ for event in events {
333
+ self.sendEvent(
334
+ withName: PolarEvent.PolarAccData.rawValue,
335
+ body: event
336
+ )
337
+ }
338
+ }
339
+ }
340
+ }
341
+
218
342
  @objc(fetchAccData:)
219
343
  func fetchAccData(_ deviceId: String) {
220
344
  NSLog("PolarBridge: Fetch ACC Data called on: \(deviceId)")
@@ -227,12 +351,17 @@ class PolarBridge: RCTEventEmitter, ObservableObject
227
351
  // Stop existing ACC stream if running
228
352
  if isAccStreaming {
229
353
  disposeAccStream()
354
+ stopAccFlushTimer()
355
+ flushAccBuffer()
356
+ isAccStreaming = false
357
+
230
358
  NSLog("PolarBridge: ACC Stream stopped")
231
359
  sendEvent(withName: PolarEvent.PolarAccComplete.rawValue, body: ["message": "ACC Stream stopped"])
232
360
  return
233
361
  }
234
362
 
235
363
  isAccStreaming = true
364
+ startAccFlushTimer()
236
365
 
237
366
  accDisposable = requestStreamSettings(deviceId, feature: .acc)
238
367
  .flatMap { settings in
@@ -243,21 +372,28 @@ class PolarBridge: RCTEventEmitter, ObservableObject
243
372
  onNext: { [weak self] accData in
244
373
  guard let self = self else { return }
245
374
 
246
- for sample in accData.samples {
247
- NSLog("ACC x: \(sample.x) y: \(sample.y) z: \(sample.z) timestamp: \(sample.timeStamp)")
375
+ self.accBufferQueue.async {
376
+ for sample in accData.samples {
377
+ NSLog("ACC x: \(sample.x) y: \(sample.y) z: \(sample.z) timestamp: \(sample.timeStamp)")
248
378
 
249
- var event: [String: Any] = [:]
250
- event["accX"] = sample.x
251
- event["accY"] = sample.y
252
- event["accZ"] = sample.z
253
- event["accTimestamp"] = Double(sample.timeStamp) // RN doesn’t support int64
379
+ var event: [String: Any] = [:]
380
+ event["accX"] = sample.x
381
+ event["accY"] = sample.y
382
+ event["accZ"] = sample.z
383
+ event["accTimestamp"] = Double(sample.timeStamp) // RN doesn’t support int64
254
384
 
255
- self.sendEvent(withName: PolarEvent.PolarAccData.rawValue, body: event)
385
+ self.accBuffer.append(event)
386
+ // self.sendEvent(withName: PolarEvent.PolarAccData.rawValue, body: event)
387
+ }
256
388
  }
257
389
  },
258
390
  onError: { [weak self] error in
259
391
  guard let self = self else { return }
260
392
 
393
+ self.stopAccFlushTimer()
394
+ self.flushAccBuffer()
395
+ self.isAccStreaming = false
396
+
261
397
  NSLog("PolarBridge: ACC stream failed: \(error.localizedDescription)")
262
398
 
263
399
  self.sendEvent(
@@ -270,6 +406,10 @@ class PolarBridge: RCTEventEmitter, ObservableObject
270
406
  onCompleted: { [weak self] in
271
407
  guard let self = self else { return }
272
408
 
409
+ self.stopAccFlushTimer()
410
+ self.flushAccBuffer()
411
+ self.isAccStreaming = false
412
+
273
413
  NSLog("PolarBridge: ACC stream complete")
274
414
 
275
415
  self.sendEvent(
@@ -284,6 +424,49 @@ class PolarBridge: RCTEventEmitter, ObservableObject
284
424
  accDisposable?.disposed(by: disposeBag)
285
425
  }
286
426
 
427
+ private func startGyrFlushTimer() {
428
+ stopGyrFlushTimer()
429
+
430
+ DispatchQueue.main.async { [weak self] in
431
+ guard let self = self else { return }
432
+
433
+ self.gyrFlushTimer = Timer(
434
+ timeInterval: self.sensorFlushInterval,
435
+ repeats: true
436
+ ) { [weak self] _ in
437
+ self?.flushGyrBuffer()
438
+ }
439
+
440
+ RunLoop.main.add(self.gyrFlushTimer!, forMode: .common)
441
+ }
442
+ }
443
+
444
+ private func stopGyrFlushTimer() {
445
+ DispatchQueue.main.async { [weak self] in
446
+ self?.gyrFlushTimer?.invalidate()
447
+ self?.gyrFlushTimer = nil
448
+ }
449
+ }
450
+
451
+ private func flushGyrBuffer() {
452
+ gyrBufferQueue.async { [weak self] in
453
+ guard let self = self else { return }
454
+ guard !self.gyrBuffer.isEmpty else { return }
455
+
456
+ let events = self.gyrBuffer
457
+ self.gyrBuffer.removeAll()
458
+
459
+ DispatchQueue.main.async {
460
+ for event in events {
461
+ self.sendEvent(
462
+ withName: PolarEvent.PolarGyrData.rawValue,
463
+ body: event
464
+ )
465
+ }
466
+ }
467
+ }
468
+ }
469
+
287
470
  @objc(fetchGyrData:)
288
471
  func fetchGyrData(_ deviceId: String) {
289
472
  NSLog("PolarBridge: Fetch Gyroscope Data called on: \(deviceId)")
@@ -295,12 +478,17 @@ class PolarBridge: RCTEventEmitter, ObservableObject
295
478
 
296
479
  if isGyrStreaming {
297
480
  disposeGyrStream()
481
+ stopGyrFlushTimer()
482
+ flushGyrBuffer()
483
+ isGyrStreaming = false
484
+
298
485
  NSLog("PolarBridge: GYR Stream stopped")
299
486
  sendEvent(withName: PolarEvent.PolarAccComplete.rawValue, body: ["message": "GYR Stream stopped"])
300
487
  return
301
488
  }
302
489
 
303
490
  isGyrStreaming = true
491
+ startGyrFlushTimer()
304
492
 
305
493
  gyrDisposable = requestStreamSettings(deviceId, feature: .gyro)
306
494
  .flatMap { settings in
@@ -311,28 +499,35 @@ class PolarBridge: RCTEventEmitter, ObservableObject
311
499
  onNext: { [weak self] gyrData in
312
500
  guard let self = self else { return }
313
501
 
314
- for sample in gyrData.samples {
315
- NSLog("GYR x: \(sample.x) y: \(sample.y) z: \(sample.z) timestamp: \(sample.timeStamp)")
502
+ self.gyrBufferQueue.async {
503
+ for sample in gyrData.samples {
504
+ NSLog("GYR x: \(sample.x) y: \(sample.y) z: \(sample.z) timestamp: \(sample.timeStamp)")
316
505
 
317
- var event: [String: Any] = [:]
506
+ var event: [String: Any] = [:]
318
507
 
319
- // JS does NOT support Float — convert to String
320
- event["gyrX"] = "\(sample.x)"
321
- event["gyrY"] = "\(sample.y)"
322
- event["gyrZ"] = "\(sample.z)"
508
+ // JS does NOT support Float — convert to String
509
+ event["gyrX"] = "\(sample.x)"
510
+ event["gyrY"] = "\(sample.y)"
511
+ event["gyrZ"] = "\(sample.z)"
323
512
 
324
- // JS does NOT support Int64 — convert to Double
325
- event["gyrTimestamp"] = Double(sample.timeStamp)
513
+ // JS does NOT support Int64 — convert to Double
514
+ event["gyrTimestamp"] = Double(sample.timeStamp)
326
515
 
327
- self.sendEvent(
328
- withName: PolarEvent.PolarGyrData.rawValue,
329
- body: event
330
- )
516
+ self.gyrBuffer.append(event)
517
+ // self.sendEvent(
518
+ // withName: PolarEvent.PolarGyrData.rawValue,
519
+ // body: event
520
+ // )
521
+ }
331
522
  }
332
523
  },
333
524
  onError: { [weak self] error in
334
525
  guard let self = self else { return }
335
526
 
527
+ stopGyrFlushTimer()
528
+ flushGyrBuffer()
529
+ isGyrStreaming = false
530
+
336
531
  NSLog("PolarBridge: GYR stream failed: \(error.localizedDescription)")
337
532
 
338
533
  self.sendEvent(
@@ -345,6 +540,10 @@ class PolarBridge: RCTEventEmitter, ObservableObject
345
540
  onCompleted: { [weak self] in
346
541
  guard let self = self else { return }
347
542
 
543
+ stopGyrFlushTimer()
544
+ flushGyrBuffer()
545
+ isGyrStreaming = false
546
+
348
547
  NSLog("PolarBridge: GYR stream complete")
349
548
 
350
549
  self.sendEvent(
@@ -359,6 +558,49 @@ class PolarBridge: RCTEventEmitter, ObservableObject
359
558
  gyrDisposable?.disposed(by: disposeBag)
360
559
  }
361
560
 
561
+ private func startPpgFlushTimer() {
562
+ stopPpgFlushTimer()
563
+
564
+ DispatchQueue.main.async { [weak self] in
565
+ guard let self = self else { return }
566
+
567
+ self.ppgFlushTimer = Timer(
568
+ timeInterval: self.sensorFlushInterval,
569
+ repeats: true
570
+ ) { [weak self] _ in
571
+ self?.flushPpgBuffer()
572
+ }
573
+
574
+ RunLoop.main.add(self.ppgFlushTimer!, forMode: .common)
575
+ }
576
+ }
577
+
578
+ private func stopPpgFlushTimer() {
579
+ DispatchQueue.main.async { [weak self] in
580
+ self?.ppgFlushTimer?.invalidate()
581
+ self?.ppgFlushTimer = nil
582
+ }
583
+ }
584
+
585
+ private func flushPpgBuffer() {
586
+ ppgBufferQueue.async { [weak self] in
587
+ guard let self = self else { return }
588
+ guard !self.ppgBuffer.isEmpty else { return }
589
+
590
+ let events = self.ppgBuffer
591
+ self.ppgBuffer.removeAll()
592
+
593
+ DispatchQueue.main.async {
594
+ for event in events {
595
+ self.sendEvent(
596
+ withName: PolarEvent.PolarPpgData.rawValue,
597
+ body: event
598
+ )
599
+ }
600
+ }
601
+ }
602
+ }
603
+
362
604
  @objc(fetchPpgData:)
363
605
  func fetchPpgData(_ deviceId: String) {
364
606
  NSLog("PolarBridge: Fetch PPG Data called on: \(deviceId)")
@@ -370,13 +612,17 @@ class PolarBridge: RCTEventEmitter, ObservableObject
370
612
 
371
613
  if isPpgStreaming {
372
614
  disposePpgStream()
615
+ stopPpgFlushTimer()
616
+ flushPpgBuffer()
617
+ isPpgStreaming = false
618
+
373
619
  NSLog("PolarBridge: PPG Stream stopped")
374
620
  sendEvent(withName: PolarEvent.PolarAccComplete.rawValue, body: ["message": "PPG Stream stopped"])
375
621
  return
376
622
  }
377
623
 
378
624
  isPpgStreaming = true
379
-
625
+ startPpgFlushTimer()
380
626
 
381
627
  ppgDisposable = requestStreamSettings(deviceId, feature: .ppg)
382
628
  .flatMap { settings in
@@ -389,31 +635,38 @@ class PolarBridge: RCTEventEmitter, ObservableObject
389
635
 
390
636
  // Only handle PPG3_AMBIENT1 type (just like Kotlin)
391
637
  if ppgData.type == PpgDataType.ppg3_ambient1 {
392
- for sample in ppgData.samples {
638
+ self.ppgBufferQueue.async {
639
+ for sample in ppgData.samples {
393
640
 
394
- NSLog("PPG ppg0: \(sample.channelSamples[0]) ppg1: \(sample.channelSamples[1]) ppg2: \(sample.channelSamples[2]) ambient: \(sample.channelSamples[3]) ts: \(sample.timeStamp)")
641
+ NSLog("PPG ppg0: \(sample.channelSamples[0]) ppg1: \(sample.channelSamples[1]) ppg2: \(sample.channelSamples[2]) ambient: \(sample.channelSamples[3]) ts: \(sample.timeStamp)")
395
642
 
396
- var event: [String: Any] = [:]
643
+ var event: [String: Any] = [:]
397
644
 
398
- // Float → String (React Native cannot handle Float)
399
- event["ppg0"] = "\(sample.channelSamples[0])"
400
- event["ppg1"] = "\(sample.channelSamples[1])"
401
- event["ppg2"] = "\(sample.channelSamples[2])"
402
- event["ambient"] = "\(sample.channelSamples[3])"
645
+ // Float → String (React Native cannot handle Float)
646
+ event["ppg0"] = "\(sample.channelSamples[0])"
647
+ event["ppg1"] = "\(sample.channelSamples[1])"
648
+ event["ppg2"] = "\(sample.channelSamples[2])"
649
+ event["ambient"] = "\(sample.channelSamples[3])"
403
650
 
404
- // Int64 timestamp → Double
405
- event["ppgTimestamp"] = Double(sample.timeStamp)
651
+ // Int64 timestamp → Double
652
+ event["ppgTimestamp"] = Double(sample.timeStamp)
406
653
 
407
- self.sendEvent(
408
- withName: PolarEvent.PolarPpgData.rawValue,
409
- body: event
410
- )
654
+ self.ppgBuffer.append(event)
655
+ // self.sendEvent(
656
+ // withName: PolarEvent.PolarPpgData.rawValue,
657
+ // body: event
658
+ // )
659
+ }
411
660
  }
412
661
  }
413
662
  },
414
663
  onError: { [weak self] error in
415
664
  guard let self = self else { return }
416
665
 
666
+ self.stopPpgFlushTimer()
667
+ self.flushPpgBuffer()
668
+ self.isPpgStreaming = false
669
+
417
670
  NSLog("PolarBridge: PPG stream failed: \(error.localizedDescription)")
418
671
 
419
672
  self.sendEvent(
@@ -426,6 +679,10 @@ class PolarBridge: RCTEventEmitter, ObservableObject
426
679
  onCompleted: { [weak self] in
427
680
  guard let self = self else { return }
428
681
 
682
+ self.stopPpgFlushTimer()
683
+ self.flushPpgBuffer()
684
+ self.isPpgStreaming = false
685
+
429
686
  NSLog("PolarBridge: PPG stream complete")
430
687
 
431
688
  self.sendEvent(
@@ -21,6 +21,7 @@ export type HrData = {
21
21
  rrAvailable: boolean;
22
22
  contactStatus: boolean;
23
23
  contactStatusSupported: boolean;
24
+ timestamp: number;
24
25
  };
25
26
  export type AccData = {
26
27
  accX: number;
@@ -1 +1 @@
1
- {"version":3,"file":"PolarDataModel.d.ts","sourceRoot":"","sources":["../../../src/PolarDataModel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAG,MAAM,CAAC;CACvB,CAAC"}
1
+ {"version":3,"file":"PolarDataModel.d.ts","sourceRoot":"","sources":["../../../src/PolarDataModel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,OAAO,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAG,MAAM,CAAC;CACvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-polar-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Polar SDK for React Native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -23,6 +23,7 @@ export type HrData = {
23
23
  rrAvailable: boolean;
24
24
  contactStatus: boolean;
25
25
  contactStatusSupported: boolean;
26
+ timestamp: number;
26
27
  };
27
28
 
28
29
  export type AccData = {