@blife/rn-step-counter 1.0.0
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.
- package/LICENSE +21 -0
- package/README.kr.md +159 -0
- package/README.md +158 -0
- package/StepCounter.podspec +22 -0
- package/android/README.md +65 -0
- package/android/build.gradle +66 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/stepcounter/StepCounterModule.kt +190 -0
- package/android/src/main/java/com/stepcounter/StepCounterPackage.kt +53 -0
- package/android/src/main/java/com/stepcounter/services/AccelerometerService.kt +147 -0
- package/android/src/main/java/com/stepcounter/services/SensorListenService.kt +305 -0
- package/android/src/main/java/com/stepcounter/services/StepCounterService.kt +79 -0
- package/android/src/main/java/com/stepcounter/utils/AndroidVersionHelper.kt +33 -0
- package/android/src/main/java/com/stepcounter/utils/SensorFusionMath.kt +104 -0
- package/ios/SOMotionDetecter.h +44 -0
- package/ios/SOMotionDetecter.m +91 -0
- package/ios/StepCounter.h +7 -0
- package/ios/StepCounter.mm +264 -0
- package/lib/module/NativeStepCounter.js +21 -0
- package/lib/module/NativeStepCounter.js.map +1 -0
- package/lib/module/index.js +194 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/jest.setup.d.ts +2 -0
- package/lib/typescript/jest.setup.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeStepCounter.d.ts +60 -0
- package/lib/typescript/src/NativeStepCounter.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +80 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +138 -0
- package/src/NativeStepCounter.ts +62 -0
- package/src/index.tsx +263 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.stepcounter.services
|
|
2
|
+
|
|
3
|
+
import android.hardware.Sensor
|
|
4
|
+
import android.hardware.SensorManager
|
|
5
|
+
import com.stepcounter.StepCounterModule
|
|
6
|
+
import java.util.concurrent.TimeUnit
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This class is responsible for listening to the step counter sensor.
|
|
10
|
+
* It is used to count the steps of the user.
|
|
11
|
+
* @param counterModule The module that is responsible for the communication with the react-native layer
|
|
12
|
+
* @param sensorManager The sensor manager that is responsible for the sensor
|
|
13
|
+
* @property sensorType The type of the sensor always Sensor.TYPE_STEP_COUNTER
|
|
14
|
+
* @property sensorTypeString The type of the sensor as a string. so always "Step Counter"
|
|
15
|
+
* @property sensorDelay The integer enum value of delay of the sensor.
|
|
16
|
+
* choose between SensorManager.SENSOR_DELAY_NORMAL or SensorManager.SENSOR_DELAY_UI
|
|
17
|
+
* @property detectedSensor The sensor that is detected
|
|
18
|
+
* @property previousSteps The initial steps or the previous steps.
|
|
19
|
+
* step counter sensor is recording since the last reboot.
|
|
20
|
+
* so if previous step is null, we need to initialize the previous steps with the current steps minus 1.
|
|
21
|
+
* @property currentSteps The current steps
|
|
22
|
+
* @constructor Creates a new StepCounterService
|
|
23
|
+
* @see SensorListenService
|
|
24
|
+
* @see Sensor
|
|
25
|
+
* @see SensorManager
|
|
26
|
+
* @see StepCounterModule
|
|
27
|
+
* @see TimeUnit
|
|
28
|
+
* @see SensorManager.SENSOR_DELAY_NORMAL
|
|
29
|
+
* @see Sensor.TYPE_STEP_COUNTER
|
|
30
|
+
* @see SensorManager.getDefaultSensor {@link SensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)}
|
|
31
|
+
* @see SensorListenService.updateCurrentSteps
|
|
32
|
+
* @see TimeUnit.toMillis
|
|
33
|
+
*/
|
|
34
|
+
class StepCounterService(
|
|
35
|
+
counterModule: StepCounterModule,
|
|
36
|
+
sensorManager: SensorManager,
|
|
37
|
+
) : SensorListenService(counterModule, sensorManager) {
|
|
38
|
+
override val sensorTypeString = "Step Counter"
|
|
39
|
+
override val sensorType = Sensor.TYPE_STEP_COUNTER
|
|
40
|
+
override val detectedSensor: Sensor? = sensorManager.getDefaultSensor(sensorType)
|
|
41
|
+
private var previousSteps: Double = 0.0
|
|
42
|
+
set(value) {
|
|
43
|
+
if (field < value) {
|
|
44
|
+
field = value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
override var currentSteps: Double = 0.0
|
|
48
|
+
set(value) {
|
|
49
|
+
if (field < value) {
|
|
50
|
+
field = value
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun resetSessionState() {
|
|
55
|
+
previousSteps = 0.0
|
|
56
|
+
currentSteps = 0.0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This function is responsible for updating the current steps.
|
|
61
|
+
* @param [eventData][FloatArray(1) values][android.hardware.SensorEvent.values] The step counter event data
|
|
62
|
+
* @return The current steps
|
|
63
|
+
* @see android.hardware.SensorEvent
|
|
64
|
+
* @see android.hardware.SensorEvent.values
|
|
65
|
+
* @see android.hardware.SensorEvent.timestamp
|
|
66
|
+
*/
|
|
67
|
+
override fun updateCurrentSteps(eventData: FloatArray): Boolean {
|
|
68
|
+
// if the time difference is greater than the delay, set the current steps to the step count minus the initial steps
|
|
69
|
+
// if the previous steps aren't initialized yet,
|
|
70
|
+
return if (previousSteps == 0.0) {
|
|
71
|
+
previousSteps = eventData[0].toDouble()
|
|
72
|
+
false
|
|
73
|
+
} else {
|
|
74
|
+
currentSteps = eventData[0].toDouble().minus(previousSteps)
|
|
75
|
+
// set the last update to the current time
|
|
76
|
+
true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.stepcounter.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager.FEATURE_SENSOR_ACCELEROMETER
|
|
5
|
+
import android.content.pm.PackageManager.FEATURE_SENSOR_STEP_COUNTER
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.annotation.RequiresFeature
|
|
8
|
+
import com.stepcounter.utils.AndroidVersionHelper.TAG_NAME
|
|
9
|
+
import com.stepcounter.utils.AndroidVersionHelper.isHardwareAccelerometerEnabled
|
|
10
|
+
import com.stepcounter.utils.AndroidVersionHelper.isHardwareStepCounterEnabled
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This class is responsible for the version check of the android device.
|
|
14
|
+
* It is used to check whether the device supports hardware step detection.
|
|
15
|
+
* @property TAG_NAME The name of the class
|
|
16
|
+
* @property isHardwareStepCounterEnabled Decides whether the hardware step counter should be used
|
|
17
|
+
* @property isHardwareAccelerometerEnabled Decides whether the hardware accelerometer should be used
|
|
18
|
+
*/
|
|
19
|
+
object AndroidVersionHelper {
|
|
20
|
+
private val TAG_NAME: String = AndroidVersionHelper::class.java.name
|
|
21
|
+
|
|
22
|
+
@RequiresFeature(name = FEATURE_SENSOR_STEP_COUNTER, enforcement = "1")
|
|
23
|
+
fun isHardwareStepCounterEnabled(context: Context): Boolean {
|
|
24
|
+
Log.d(TAG_NAME, "isHardwareStepCounterEnabled: $FEATURE_SENSOR_STEP_COUNTER")
|
|
25
|
+
return context.packageManager.hasSystemFeature(FEATURE_SENSOR_STEP_COUNTER)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@RequiresFeature(name = FEATURE_SENSOR_ACCELEROMETER, enforcement = "1")
|
|
29
|
+
fun isHardwareAccelerometerEnabled(context: Context): Boolean {
|
|
30
|
+
Log.d(TAG_NAME, "isHardwareStepCounterEnabled: $FEATURE_SENSOR_ACCELEROMETER")
|
|
31
|
+
return context.packageManager.hasSystemFeature(FEATURE_SENSOR_ACCELEROMETER)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.stepcounter.utils
|
|
2
|
+
|
|
3
|
+
import com.stepcounter.utils.SensorFusionMath.dot
|
|
4
|
+
import com.stepcounter.utils.SensorFusionMath.norm
|
|
5
|
+
import com.stepcounter.utils.SensorFusionMath.normalize
|
|
6
|
+
import kotlin.math.sqrt
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This class contains all the math functions used in the sensor fusion.
|
|
10
|
+
* @property SensorFusionMath.sum
|
|
11
|
+
* @property SensorFusionMath.cross
|
|
12
|
+
* @property SensorFusionMath.norm
|
|
13
|
+
* @property SensorFusionMath.normalize
|
|
14
|
+
* @property SensorFusionMath.dot
|
|
15
|
+
* @see <a href="https://en.wikipedia.org/wiki/Euclidean_vector">Euclidean vector</a>
|
|
16
|
+
*/
|
|
17
|
+
object SensorFusionMath {
|
|
18
|
+
/**
|
|
19
|
+
* The summation function, also known as a sigma function, is commonly used in mathematics to denote
|
|
20
|
+
* the summation of a set of values. In the context of Euclidean vectors, the summation function can
|
|
21
|
+
* be used to calculate the length or magnitude of a vector.
|
|
22
|
+
* The Euclidean [norm], also known as the L2 [norm], of a vector `x = [x1, x2, ..., xn]` is defined as the
|
|
23
|
+
* square root of the sum of the squares of its components.
|
|
24
|
+
* @param vector The array to be calculated.
|
|
25
|
+
* @returns The sum of the array.
|
|
26
|
+
* @see <a href="https://en.wikipedia.org/wiki/Summation">Summation</a>
|
|
27
|
+
*/
|
|
28
|
+
fun sum(vector: FloatArray): Float {
|
|
29
|
+
var summation = 0f
|
|
30
|
+
for (v in vector) {
|
|
31
|
+
summation += v
|
|
32
|
+
}
|
|
33
|
+
return summation
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The cross product is a binary operation on two vectors in three-dimensional space that
|
|
38
|
+
* results in a perpendicular vector to the input vectors. It's used in math, physics, engineering,
|
|
39
|
+
* and computer programming and should not be confused with the [dot] product,
|
|
40
|
+
* which is a scalar product that measures the projection of one vector onto another.
|
|
41
|
+
* @param vectorA The first array.
|
|
42
|
+
* @param vectorB The second array.
|
|
43
|
+
* @see <a href="https://en.wikipedia.org/wiki/Cross_product">Cross product</a>
|
|
44
|
+
*/
|
|
45
|
+
fun cross(
|
|
46
|
+
vectorA: FloatArray,
|
|
47
|
+
vectorB: FloatArray,
|
|
48
|
+
): FloatArray {
|
|
49
|
+
val outVector = FloatArray(3)
|
|
50
|
+
outVector[0] = vectorA[1] * vectorB[2] - vectorA[2] * vectorB[1]
|
|
51
|
+
outVector[1] = vectorA[2] * vectorB[0] - vectorA[0] * vectorB[2]
|
|
52
|
+
outVector[2] = vectorA[0] * vectorB[1] - vectorA[1] * vectorB[0]
|
|
53
|
+
return outVector
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* In mathematics, a norm is a function from a real or complex [vector] space
|
|
58
|
+
* to the non-negative real numbers that behaves in certain ways like the distance
|
|
59
|
+
* from the origin: it commutes with scaling, obeys a form of the triangle inequality,
|
|
60
|
+
* and is zero only at the origin.
|
|
61
|
+
* @param vector The vector to be calculated.
|
|
62
|
+
* @returns The norm of the vector.
|
|
63
|
+
* @see <a href="https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm">Euclidean Normalization</a>
|
|
64
|
+
*/
|
|
65
|
+
fun norm(vector: FloatArray): Float {
|
|
66
|
+
var initFloat = 0f
|
|
67
|
+
for (v in vector) {
|
|
68
|
+
initFloat += v * v
|
|
69
|
+
}
|
|
70
|
+
return sqrt(initFloat)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* In mathematics, the dot product or scalar product is an algebraic operation
|
|
75
|
+
* that takes two equal-length sequences of numbers (usually coordinate vectors),
|
|
76
|
+
* and returns a single number.
|
|
77
|
+
* In Euclidean geometry, the dot product of the Cartesian coordinates of two vectors is widely used.
|
|
78
|
+
* @param a The first sequence of numbers.
|
|
79
|
+
* @param b The second sequence of numbers. Must be the same length as the first sequence.
|
|
80
|
+
* @returns The dot product(single number) of the two sequences of numbers.
|
|
81
|
+
* @see <a href="https://en.wikipedia.org/wiki/Dot_product">Dot product</a>
|
|
82
|
+
*/
|
|
83
|
+
fun dot(
|
|
84
|
+
a: FloatArray,
|
|
85
|
+
b: FloatArray,
|
|
86
|
+
): Float = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The norm function is a utility function used here to calculate the Euclidean norm of the input vector.
|
|
90
|
+
* The calculated norm value is used to normalize the input [vector] by
|
|
91
|
+
* dividing each component of the input vector by the norm value.
|
|
92
|
+
* Therefore, while both functions use the same mathematical formula for Euclidean vector normalization,
|
|
93
|
+
* they serve different purposes.
|
|
94
|
+
* The [norm] function calculates the norm of a vector, while the [normalize] function returns a new normalized vector.
|
|
95
|
+
* @param vector The input array to be normalized.
|
|
96
|
+
* @return The new normalized vector.
|
|
97
|
+
* @see SensorFusionMath.norm
|
|
98
|
+
*/
|
|
99
|
+
fun normalize(vector: FloatArray): FloatArray {
|
|
100
|
+
val normed = norm(vector)
|
|
101
|
+
if (normed == 0.0f) return vector.copyOf()
|
|
102
|
+
return FloatArray(vector.size) { i -> vector[i] / normed }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SOMotionDetecter.h
|
|
2
|
+
// MotionDetection
|
|
3
|
+
//
|
|
4
|
+
// The MIT License (MIT)
|
|
5
|
+
//
|
|
6
|
+
// Created by : Artur Mkrtchyan
|
|
7
|
+
// Copyright (c) 2014 SocialObjects Software. All rights reserved.
|
|
8
|
+
//
|
|
9
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
// in the Software without restriction, including without limitation the rights
|
|
12
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
// furnished to do so, subject to the following conditions:
|
|
15
|
+
//
|
|
16
|
+
// The above copyright notice and this permission notice shall be included in
|
|
17
|
+
// all copies or substantial portions of the Software.
|
|
18
|
+
//
|
|
19
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
// SOFTWARE
|
|
26
|
+
|
|
27
|
+
#import <Foundation/Foundation.h>
|
|
28
|
+
|
|
29
|
+
@interface SOMotionDetecter : NSObject
|
|
30
|
+
|
|
31
|
+
+ (instancetype)sharedInstance;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start accelerometer updates.
|
|
35
|
+
* @param callback Will be called every time when new step is detected
|
|
36
|
+
*/
|
|
37
|
+
- (void)startDetectionWithUpdateBlock:(void (^)(NSError *error))callback;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stop motion manager accelerometer updates
|
|
41
|
+
*/
|
|
42
|
+
- (void)stopDetection;
|
|
43
|
+
|
|
44
|
+
@end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SOMotionDetecter.m
|
|
2
|
+
// MotionDetection
|
|
3
|
+
//
|
|
4
|
+
// Created by Artur on 5/15/15.
|
|
5
|
+
// Copyright (c) 2015 Artur Mkrtchyan. All rights reserved.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "SOMotionDetecter.h"
|
|
9
|
+
#import <CoreMotion/CoreMotion.h>
|
|
10
|
+
#import <UIKit/UIKit.h>
|
|
11
|
+
|
|
12
|
+
#define kUpdateInterval 0.2f
|
|
13
|
+
@interface SOMotionDetecter ()
|
|
14
|
+
|
|
15
|
+
@property(strong, nonatomic) CMMotionManager *motionManager;
|
|
16
|
+
@property(strong, nonatomic) NSOperationQueue *queue;
|
|
17
|
+
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
@implementation SOMotionDetecter
|
|
21
|
+
|
|
22
|
+
+ (instancetype)sharedInstance {
|
|
23
|
+
static id instance;
|
|
24
|
+
static dispatch_once_t onceToken;
|
|
25
|
+
dispatch_once(&onceToken, ^{
|
|
26
|
+
instance = [self new];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return instance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (instancetype)init {
|
|
33
|
+
self = [super init];
|
|
34
|
+
|
|
35
|
+
self.motionManager = [CMMotionManager new];
|
|
36
|
+
self.motionManager.accelerometerUpdateInterval = kUpdateInterval;
|
|
37
|
+
self.motionManager.deviceMotionUpdateInterval = kUpdateInterval;
|
|
38
|
+
self.motionManager.gyroUpdateInterval = kUpdateInterval;
|
|
39
|
+
self.motionManager.magnetometerUpdateInterval = kUpdateInterval;
|
|
40
|
+
self.motionManager.showsDeviceMovementDisplay = YES;
|
|
41
|
+
self.queue = [NSOperationQueue new];
|
|
42
|
+
self.queue.maxConcurrentOperationCount = 1;
|
|
43
|
+
|
|
44
|
+
return self;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
- (void)startDetectionWithUpdateBlock:(void (^)(NSError *))callback {
|
|
48
|
+
if (self.motionManager.isAccelerometerActive) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[self.motionManager
|
|
53
|
+
startAccelerometerUpdatesToQueue:self.queue
|
|
54
|
+
withHandler:^(CMAccelerometerData *accelerometerData,
|
|
55
|
+
NSError *error) {
|
|
56
|
+
if (error) {
|
|
57
|
+
if (callback) {
|
|
58
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
59
|
+
callback(error);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
CMAcceleration acceleration =
|
|
66
|
+
accelerometerData.acceleration;
|
|
67
|
+
|
|
68
|
+
CGFloat strength = 1.2f;
|
|
69
|
+
BOOL isStep = NO;
|
|
70
|
+
if (fabs(acceleration.x) > strength ||
|
|
71
|
+
fabs(acceleration.y) > strength ||
|
|
72
|
+
fabs(acceleration.z) > strength) {
|
|
73
|
+
isStep = YES;
|
|
74
|
+
}
|
|
75
|
+
if (isStep) {
|
|
76
|
+
if (callback) {
|
|
77
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
78
|
+
callback(nil);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (void)stopDetection {
|
|
86
|
+
if (self.motionManager.isAccelerometerActive) {
|
|
87
|
+
[self.motionManager stopAccelerometerUpdates];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#import <CoreMotion/CoreMotion.h>
|
|
2
|
+
#import <React/RCTBridge.h>
|
|
3
|
+
#import <React/RCTEventDispatcher.h>
|
|
4
|
+
#import "StepCounter.h"
|
|
5
|
+
#import "SOMotionDetecter.h"
|
|
6
|
+
|
|
7
|
+
@interface StepCounter ()
|
|
8
|
+
@property (nonatomic, readonly) CMPedometer *pedometer;
|
|
9
|
+
@property (nonatomic, strong) dispatch_queue_t stateQueue;
|
|
10
|
+
|
|
11
|
+
// Session control
|
|
12
|
+
@property (nonatomic, strong) NSDate *sessionStartDate;
|
|
13
|
+
|
|
14
|
+
// Baseline + monotonic
|
|
15
|
+
@property (nonatomic) NSInteger baselineSteps; // steps recorded at session start
|
|
16
|
+
@property (nonatomic) NSInteger lastEmittedSteps; // last emitted delta (monotonic)
|
|
17
|
+
@property (nonatomic) BOOL baselineReady;
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
@implementation StepCounter
|
|
21
|
+
|
|
22
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
23
|
+
return YES;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@synthesize bridge = _bridge;
|
|
27
|
+
@synthesize callableJSModules = _callableJSModules;
|
|
28
|
+
|
|
29
|
+
RCT_EXPORT_MODULE();
|
|
30
|
+
|
|
31
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
32
|
+
return @[
|
|
33
|
+
@"StepCounter.stepCounterUpdate",
|
|
34
|
+
@"StepCounter.stepDetected",
|
|
35
|
+
@"StepCounter.errorOccurred",
|
|
36
|
+
@"StepCounter.stepsSensorInfo"
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
41
|
+
// In New Architecture, RCTEventEmitter.receiveEvent is not a registered callable JS module.
|
|
42
|
+
// In bridgeless mode (RCTHost), _bridge is nil; events must go through callableJSModules.
|
|
43
|
+
// In bridge-based New Architecture, fall back to _bridge.enqueueJSCall via RCTDeviceEventEmitter.
|
|
44
|
+
- (void)sendEventWithName:(NSString *)eventName body:(id)body {
|
|
45
|
+
NSArray *args = body ? @[eventName, body] : @[eventName];
|
|
46
|
+
if (_callableJSModules) {
|
|
47
|
+
[_callableJSModules invokeModule:@"RCTDeviceEventEmitter"
|
|
48
|
+
method:@"emit"
|
|
49
|
+
withArgs:args];
|
|
50
|
+
} else if (_bridge) {
|
|
51
|
+
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
|
|
52
|
+
method:@"emit"
|
|
53
|
+
args:args
|
|
54
|
+
completion:nil];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
#endif // RCT_NEW_ARCH_ENABLED
|
|
58
|
+
|
|
59
|
+
#pragma mark - Public API
|
|
60
|
+
|
|
61
|
+
RCT_EXPORT_METHOD(isStepCountingSupported:(RCTPromiseResolveBlock)resolve
|
|
62
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
63
|
+
resolve(@{
|
|
64
|
+
@"granted": @([self authorizationStatus]),
|
|
65
|
+
@"supported": @([CMPedometer isStepCountingAvailable]),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
[self sendEventWithName:@"StepCounter.stepsSensorInfo"
|
|
69
|
+
body:[self dictionaryAboutSensorInfo]];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
RCT_EXPORT_METHOD(queryStepCounterDataBetweenDates:(NSDate *)startDate
|
|
73
|
+
endDate:(NSDate *)endDate
|
|
74
|
+
handler:(RCTResponseSenderBlock)handler) {
|
|
75
|
+
[self.pedometer queryPedometerDataFromDate:startDate
|
|
76
|
+
toDate:endDate
|
|
77
|
+
withHandler:^(CMPedometerData *pedometerData, NSError *error) {
|
|
78
|
+
handler(@[
|
|
79
|
+
error.description ?: [NSNull null],
|
|
80
|
+
[self dictionaryFromPedometerData:pedometerData]
|
|
81
|
+
]);
|
|
82
|
+
}];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
RCT_EXPORT_METHOD(startStepCounterUpdate:(double)from) {
|
|
86
|
+
// Stop any in-progress pedometer session before starting a new one.
|
|
87
|
+
[self.pedometer stopPedometerUpdates];
|
|
88
|
+
|
|
89
|
+
// --- Timestamp guard (ms vs seconds) ---
|
|
90
|
+
// Valid Unix timestamp in seconds is ~1.7–2.0e9 (2024–2033),
|
|
91
|
+
// while milliseconds is ~1.7–2.0e12.
|
|
92
|
+
double fromSeconds = (from > 1e12) ? (from / 1000.0) : from;
|
|
93
|
+
|
|
94
|
+
// Extra sanity: if caller accidentally passes ms-but-small (unlikely) or future timestamps,
|
|
95
|
+
// clamp to "now" to avoid "future date" sessions.
|
|
96
|
+
NSDate *now = [NSDate date];
|
|
97
|
+
NSDate *requested = (fromSeconds > 0) ? [NSDate dateWithTimeIntervalSince1970:fromSeconds] : now;
|
|
98
|
+
if ([requested timeIntervalSinceDate:now] > 60.0) { // > 60s into the future
|
|
99
|
+
requested = now;
|
|
100
|
+
}
|
|
101
|
+
// Reset session state atomically before starting new handlers.
|
|
102
|
+
dispatch_sync(_stateQueue, ^{
|
|
103
|
+
self->_sessionStartDate = requested;
|
|
104
|
+
self->_baselineSteps = 0;
|
|
105
|
+
self->_lastEmittedSteps = 0;
|
|
106
|
+
self->_baselineReady = NO;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 1) Establish baseline at session start (start -> now).
|
|
110
|
+
// This makes "reset/restart" deterministic: emitted steps are delta since sessionStartDate.
|
|
111
|
+
[self.pedometer queryPedometerDataFromDate:_sessionStartDate
|
|
112
|
+
toDate:now
|
|
113
|
+
withHandler:^(CMPedometerData *data, NSError *error) {
|
|
114
|
+
dispatch_sync(self->_stateQueue, ^{
|
|
115
|
+
self->_baselineSteps = error ? 0 : data.numberOfSteps.integerValue;
|
|
116
|
+
self->_baselineReady = YES;
|
|
117
|
+
});
|
|
118
|
+
if (error) {
|
|
119
|
+
[self sendEventWithName:@"StepCounter.errorOccurred" body:error];
|
|
120
|
+
}
|
|
121
|
+
}];
|
|
122
|
+
|
|
123
|
+
// 2) Live updates from sessionStartDate.
|
|
124
|
+
[self.pedometer startPedometerUpdatesFromDate:_sessionStartDate
|
|
125
|
+
withHandler:^(CMPedometerData *data, NSError *error) {
|
|
126
|
+
if (error) {
|
|
127
|
+
[self sendEventWithName:@"StepCounter.errorOccurred" body:error];
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!data) return;
|
|
131
|
+
|
|
132
|
+
__block NSDictionary *body = nil;
|
|
133
|
+
dispatch_sync(self->_stateQueue, ^{
|
|
134
|
+
if (!self->_baselineReady) return;
|
|
135
|
+
|
|
136
|
+
// Filter: accept only updates whose startDate matches the session start.
|
|
137
|
+
// iOS can interleave "segment/window" updates with different startDate values.
|
|
138
|
+
NSTimeInterval startDiff = fabs([data.startDate timeIntervalSinceDate:self->_sessionStartDate]);
|
|
139
|
+
if (startDiff > 1.0) return;
|
|
140
|
+
|
|
141
|
+
NSInteger cumulative = data.numberOfSteps.integerValue;
|
|
142
|
+
NSInteger delta = cumulative - self->_baselineSteps;
|
|
143
|
+
|
|
144
|
+
// If OS performs corrections or timestamps shift, delta can go negative.
|
|
145
|
+
// Clamp + rebase to keep future deltas sane.
|
|
146
|
+
if (delta < 0) {
|
|
147
|
+
delta = 0;
|
|
148
|
+
self->_baselineSteps = cumulative;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Monotonic guard: never emit backwards.
|
|
152
|
+
if (delta <= self->_lastEmittedSteps) return;
|
|
153
|
+
self->_lastEmittedSteps = delta;
|
|
154
|
+
|
|
155
|
+
NSMutableDictionary *mBody = [[self dictionaryFromPedometerData:data] mutableCopy];
|
|
156
|
+
mBody[@"steps"] = @(delta);
|
|
157
|
+
mBody[@"counterType"] = @"CMPedometer";
|
|
158
|
+
body = [mBody copy];
|
|
159
|
+
});
|
|
160
|
+
if (body) {
|
|
161
|
+
[self sendEventWithName:@"StepCounter.stepCounterUpdate" body:body];
|
|
162
|
+
}
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
RCT_EXPORT_METHOD(stopStepCounterUpdate) {
|
|
167
|
+
[self.pedometer stopPedometerUpdates];
|
|
168
|
+
[[SOMotionDetecter sharedInstance] stopDetection];
|
|
169
|
+
|
|
170
|
+
dispatch_sync(_stateQueue, ^{
|
|
171
|
+
self->_sessionStartDate = nil;
|
|
172
|
+
self->_baselineSteps = 0;
|
|
173
|
+
self->_lastEmittedSteps = 0;
|
|
174
|
+
self->_baselineReady = NO;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
RCT_EXPORT_METHOD(startStepsDetection) {
|
|
179
|
+
[[SOMotionDetecter sharedInstance]
|
|
180
|
+
startDetectionWithUpdateBlock:^(NSError *error) {
|
|
181
|
+
if (error) {
|
|
182
|
+
[self sendEventWithName:@"StepCounter.errorOccurred" body:error];
|
|
183
|
+
} else {
|
|
184
|
+
[self sendEventWithName:@"StepCounter.stepDetected" body:@true];
|
|
185
|
+
}
|
|
186
|
+
}];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#pragma mark - Sensor info / mapping
|
|
190
|
+
|
|
191
|
+
- (NSDictionary *)dictionaryAboutSensorInfo {
|
|
192
|
+
return @{
|
|
193
|
+
@"name": @"CMPedometer",
|
|
194
|
+
@"granted": @([self authorizationStatus]),
|
|
195
|
+
@"stepCounting": @([CMPedometer isStepCountingAvailable]),
|
|
196
|
+
@"pace": @([CMPedometer isPaceAvailable]),
|
|
197
|
+
@"cadence": @([CMPedometer isCadenceAvailable]),
|
|
198
|
+
@"distance": @([CMPedometer isDistanceAvailable]),
|
|
199
|
+
@"floorCounting": @([CMPedometer isFloorCountingAvailable]),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
- (NSDictionary *)dictionaryFromPedometerData:(CMPedometerData *)data {
|
|
204
|
+
if (!data) {
|
|
205
|
+
return @{
|
|
206
|
+
@"counterType": @"CMPedometer",
|
|
207
|
+
@"startDate": [NSNull null],
|
|
208
|
+
@"endDate": [NSNull null],
|
|
209
|
+
@"steps": [NSNull null],
|
|
210
|
+
@"distance": [NSNull null],
|
|
211
|
+
@"floorsAscended": [NSNull null],
|
|
212
|
+
@"floorsDescended": [NSNull null],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
NSNumber *startDate = @((long long)(data.startDate.timeIntervalSince1970 * 1000.0));
|
|
217
|
+
NSNumber *endDate = @((long long)(data.endDate.timeIntervalSince1970 * 1000.0));
|
|
218
|
+
|
|
219
|
+
return @{
|
|
220
|
+
@"counterType": @"CMPedometer",
|
|
221
|
+
@"startDate": startDate ?: [NSNull null],
|
|
222
|
+
@"endDate": endDate ?: [NSNull null],
|
|
223
|
+
@"steps": data.numberOfSteps ?: [NSNull null],
|
|
224
|
+
@"distance": data.distance ?: [NSNull null],
|
|
225
|
+
@"floorsAscended": data.floorsAscended ?: [NSNull null],
|
|
226
|
+
@"floorsDescended": data.floorsDescended ?: [NSNull null],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
- (BOOL)authorizationStatus {
|
|
231
|
+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
232
|
+
#pragma clang diagnostic push
|
|
233
|
+
#pragma clang diagnostic ignored "-Wpartial-availability"
|
|
234
|
+
CMAuthorizationStatus status = [CMPedometer authorizationStatus];
|
|
235
|
+
return status == CMAuthorizationStatusAuthorized;
|
|
236
|
+
#pragma clang diagnostic pop
|
|
237
|
+
#else
|
|
238
|
+
return NO;
|
|
239
|
+
#endif
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#pragma mark - Init
|
|
243
|
+
|
|
244
|
+
- (instancetype)init {
|
|
245
|
+
self = [super init];
|
|
246
|
+
if (!self) return nil;
|
|
247
|
+
|
|
248
|
+
_pedometer = [[CMPedometer alloc] init];
|
|
249
|
+
_stateQueue = dispatch_queue_create("com.bonnmh.rnstepcounter.state", DISPATCH_QUEUE_SERIAL);
|
|
250
|
+
_baselineSteps = 0;
|
|
251
|
+
_lastEmittedSteps = 0;
|
|
252
|
+
_baselineReady = NO;
|
|
253
|
+
|
|
254
|
+
return self;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
258
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
259
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
260
|
+
return std::make_shared<facebook::react::NativeStepCounterSpecJSI>(params);
|
|
261
|
+
}
|
|
262
|
+
#endif // RCT_NEW_ARCH_ENABLED
|
|
263
|
+
|
|
264
|
+
@end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { TurboModuleRegistry } from "react-native";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `StepCountData` is an object with four properties: `distance`, `steps`, `startDate`, and `endDate`.
|
|
7
|
+
* StepCountData object - The Object that contains the step count data.
|
|
8
|
+
* counterType - The type of counter used to count the steps.
|
|
9
|
+
* steps - The number of steps taken during the time period.
|
|
10
|
+
* startDate - The start date of the data.
|
|
11
|
+
* endDate - The end date of the data.
|
|
12
|
+
* distance - The distance in meters that the user has walked or run.
|
|
13
|
+
* floorsAscended - number of floors ascended (iOS only)
|
|
14
|
+
* floorsDescended - number of floors descended (iOS only)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const NAME = "StepCounter";
|
|
18
|
+
export const VERSION = "0.3.1";
|
|
19
|
+
export const eventName = "StepCounter.stepCounterUpdate";
|
|
20
|
+
export default TurboModuleRegistry.getEnforcing("StepCounter");
|
|
21
|
+
//# sourceMappingURL=NativeStepCounter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","NAME","VERSION","eventName","getEnforcing"],"sourceRoot":"../../src","sources":["NativeStepCounter.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;;AAIpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAWA,OAAO,MAAMC,IAAI,GAAG,aAAa;AACjC,OAAO,MAAMC,OAAO,GAAG,OAAO;AAC9B,OAAO,MAAMC,SAAS,GAAG,+BAA+B;AAkCxD,eAAeH,mBAAmB,CAACI,YAAY,CAAO,aAAa,CAAC","ignoreList":[]}
|