expo-flic2 0.3.1 → 0.3.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.
package/android/build.gradle
CHANGED
|
@@ -4,14 +4,14 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'expo.modules.flic2'
|
|
7
|
-
version = '0.3.
|
|
7
|
+
version = '0.3.2'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "expo.modules.flic2"
|
|
11
11
|
|
|
12
12
|
defaultConfig {
|
|
13
13
|
versionCode 1
|
|
14
|
-
versionName "0.3.
|
|
14
|
+
versionName "0.3.2"
|
|
15
15
|
minSdkVersion 24
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -32,4 +32,5 @@ repositories {
|
|
|
32
32
|
|
|
33
33
|
dependencies {
|
|
34
34
|
implementation 'com.github.50ButtonsEach:flic2lib-android:2.0.1'
|
|
35
|
+
testImplementation 'junit:junit:4.13.2'
|
|
35
36
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package expo.modules.flic2
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Translates a Flic2 button event timestamp into an age (ms since the physical press).
|
|
5
|
+
*
|
|
6
|
+
* The Flic2 SDK provides timestamps in milliseconds since the *button's own boot*, which is
|
|
7
|
+
* unrelated to Android's clocks. To convert, we establish a correlation at onReady time:
|
|
8
|
+
* we record both the button's readyTimestamp and Android's SystemClock.elapsedRealtime()
|
|
9
|
+
* at the same moment, then use that offset for all subsequent events.
|
|
10
|
+
*/
|
|
11
|
+
object AgeCalculator {
|
|
12
|
+
/**
|
|
13
|
+
* @param nowElapsedMs SystemClock.elapsedRealtime() at the moment of processing
|
|
14
|
+
* @param androidReadyElapsedMs SystemClock.elapsedRealtime() recorded when onReady fired
|
|
15
|
+
* @param buttonReadyTimestamp button's readyTimestamp (ms since button boot) from onReady
|
|
16
|
+
* @param eventTimestamp button's event timestamp (ms since button boot)
|
|
17
|
+
* @return age in milliseconds, clamped to 0 (never negative)
|
|
18
|
+
*/
|
|
19
|
+
fun computeAgeMs(
|
|
20
|
+
nowElapsedMs: Long,
|
|
21
|
+
androidReadyElapsedMs: Long,
|
|
22
|
+
buttonReadyTimestamp: Long,
|
|
23
|
+
eventTimestamp: Long
|
|
24
|
+
): Long {
|
|
25
|
+
val estimatedAndroidEventTime = androidReadyElapsedMs + (eventTimestamp - buttonReadyTimestamp)
|
|
26
|
+
return maxOf(0L, nowElapsedMs - estimatedAndroidEventTime)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -17,6 +17,9 @@ class ExpoFlic2Module : Module() {
|
|
|
17
17
|
private var manager: Flic2Manager? = null
|
|
18
18
|
private val triggerModes = ConcurrentHashMap<String, String>()
|
|
19
19
|
private val buttonListeners = mutableMapOf<String, Flic2ButtonListener>()
|
|
20
|
+
// Correlation between button clock and Android clock, established at onReady.
|
|
21
|
+
// Maps uuid -> Pair(androidReadyElapsedMs, buttonReadyTimestampMs)
|
|
22
|
+
private val readyCorrelations = ConcurrentHashMap<String, Pair<Long, Long>>()
|
|
20
23
|
|
|
21
24
|
override fun definition() = ModuleDefinition {
|
|
22
25
|
Name("ExpoFlic2")
|
|
@@ -38,6 +41,7 @@ class ExpoFlic2Module : Module() {
|
|
|
38
41
|
}
|
|
39
42
|
buttonListeners.clear()
|
|
40
43
|
triggerModes.clear()
|
|
44
|
+
readyCorrelations.clear()
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
Function("initialize") {
|
|
@@ -115,6 +119,11 @@ class ExpoFlic2Module : Module() {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
private fun ageMs(uuid: String, eventTimestamp: Long): Long {
|
|
123
|
+
val (androidReadyMs, buttonReadyMs) = readyCorrelations[uuid] ?: return 0L
|
|
124
|
+
return AgeCalculator.computeAgeMs(SystemClock.elapsedRealtime(), androidReadyMs, buttonReadyMs, eventTimestamp)
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
private fun findButton(uuid: String): Flic2Button? {
|
|
119
128
|
return manager?.getButtons()?.find { it.uuid == uuid }
|
|
120
129
|
}
|
|
@@ -138,12 +147,12 @@ class ExpoFlic2Module : Module() {
|
|
|
138
147
|
) {
|
|
139
148
|
val mode = triggerModes[button.uuid] ?: "clickAndDoubleClickAndHold"
|
|
140
149
|
if (mode != "click" && mode != "clickAndHold") return
|
|
141
|
-
val
|
|
150
|
+
val age = ageMs(button.uuid, timestamp)
|
|
142
151
|
if (isClick) {
|
|
143
|
-
sendEvent("onFlic2Click", mapOf("uuid" to button.uuid, "queued" to wasQueued, "age" to
|
|
152
|
+
sendEvent("onFlic2Click", mapOf("uuid" to button.uuid, "queued" to wasQueued, "age" to age))
|
|
144
153
|
}
|
|
145
154
|
if (isHold && mode == "clickAndHold") {
|
|
146
|
-
sendEvent("onFlic2Hold", mapOf("uuid" to button.uuid, "queued" to wasQueued, "age" to
|
|
155
|
+
sendEvent("onFlic2Hold", mapOf("uuid" to button.uuid, "queued" to wasQueued, "age" to age))
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -158,7 +167,7 @@ class ExpoFlic2Module : Module() {
|
|
|
158
167
|
) {
|
|
159
168
|
val mode = triggerModes[button.uuid] ?: "clickAndDoubleClickAndHold"
|
|
160
169
|
if (mode == "click" || mode == "clickAndHold") return
|
|
161
|
-
val
|
|
170
|
+
val age = ageMs(button.uuid, timestamp)
|
|
162
171
|
val emitClick = isSingleClick && mode != "clickAndDoubleClick"
|
|
163
172
|
val emitDoubleClick = isDoubleClick
|
|
164
173
|
val emitHold = isHold && mode != "clickAndDoubleClick"
|
|
@@ -166,21 +175,21 @@ class ExpoFlic2Module : Module() {
|
|
|
166
175
|
sendEvent("onFlic2Click", mapOf(
|
|
167
176
|
"uuid" to button.uuid,
|
|
168
177
|
"queued" to wasQueued,
|
|
169
|
-
"age" to
|
|
178
|
+
"age" to age
|
|
170
179
|
))
|
|
171
180
|
}
|
|
172
181
|
if (emitDoubleClick) {
|
|
173
182
|
sendEvent("onFlic2DoubleClick", mapOf(
|
|
174
183
|
"uuid" to button.uuid,
|
|
175
184
|
"queued" to wasQueued,
|
|
176
|
-
"age" to
|
|
185
|
+
"age" to age
|
|
177
186
|
))
|
|
178
187
|
}
|
|
179
188
|
if (emitHold) {
|
|
180
189
|
sendEvent("onFlic2Hold", mapOf(
|
|
181
190
|
"uuid" to button.uuid,
|
|
182
191
|
"queued" to wasQueued,
|
|
183
|
-
"age" to
|
|
192
|
+
"age" to age
|
|
184
193
|
))
|
|
185
194
|
}
|
|
186
195
|
}
|
|
@@ -197,7 +206,7 @@ class ExpoFlic2Module : Module() {
|
|
|
197
206
|
"uuid" to button.uuid,
|
|
198
207
|
"isDown" to isDown,
|
|
199
208
|
"queued" to wasQueued,
|
|
200
|
-
"age" to (
|
|
209
|
+
"age" to ageMs(button.uuid, timestamp)
|
|
201
210
|
))
|
|
202
211
|
}
|
|
203
212
|
|
|
@@ -209,6 +218,7 @@ class ExpoFlic2Module : Module() {
|
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
override fun onReady(button: Flic2Button, timestamp: Long) {
|
|
221
|
+
readyCorrelations[button.uuid] = Pair(SystemClock.elapsedRealtime(), timestamp)
|
|
212
222
|
sendEvent("onFlic2Connection", mapOf(
|
|
213
223
|
"uuid" to button.uuid,
|
|
214
224
|
"state" to "ready"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package expo.modules.flic2
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class AgeCalculatorTest {
|
|
7
|
+
|
|
8
|
+
// Scenario: button has been running for 10_000ms since its own boot.
|
|
9
|
+
// Android has been running for 5_000ms since its own boot.
|
|
10
|
+
// onReady fired when button reported 10_000ms and Android was at 5_000ms.
|
|
11
|
+
|
|
12
|
+
private val androidReady = 5_000L
|
|
13
|
+
private val buttonReady = 10_000L
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `fresh press arrives with near-zero latency`() {
|
|
17
|
+
// Button pressed at button-time 10_050ms (50ms after ready).
|
|
18
|
+
// Android receives the event at android-time 5_080ms (80ms after ready).
|
|
19
|
+
// Expected age = 80 - 50 = 30ms.
|
|
20
|
+
val age = AgeCalculator.computeAgeMs(
|
|
21
|
+
nowElapsedMs = 5_080L,
|
|
22
|
+
androidReadyElapsedMs = androidReady,
|
|
23
|
+
buttonReadyTimestamp = buttonReady,
|
|
24
|
+
eventTimestamp = 10_050L
|
|
25
|
+
)
|
|
26
|
+
assertEquals(30L, age)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `queued press that happened 5 seconds before button connected`() {
|
|
31
|
+
// Button pressed at button-time 5_000ms (5_000ms before ready at 10_000ms).
|
|
32
|
+
// Android receives at android-time 5_100ms (100ms after ready).
|
|
33
|
+
// Expected age = 5_100 - (5_000 + (5_000 - 10_000)) = 5_100 - 0 = 5_100ms.
|
|
34
|
+
val age = AgeCalculator.computeAgeMs(
|
|
35
|
+
nowElapsedMs = 5_100L,
|
|
36
|
+
androidReadyElapsedMs = androidReady,
|
|
37
|
+
buttonReadyTimestamp = buttonReady,
|
|
38
|
+
eventTimestamp = 5_000L
|
|
39
|
+
)
|
|
40
|
+
assertEquals(5_100L, age)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun `age is clamped to zero when button clock is slightly ahead of android clock`() {
|
|
45
|
+
// eventTimestamp slightly ahead of ready (clock drift), should not go negative.
|
|
46
|
+
val age = AgeCalculator.computeAgeMs(
|
|
47
|
+
nowElapsedMs = 5_000L,
|
|
48
|
+
androidReadyElapsedMs = androidReady,
|
|
49
|
+
buttonReadyTimestamp = buttonReady,
|
|
50
|
+
eventTimestamp = 10_020L // 20ms into the future relative to now
|
|
51
|
+
)
|
|
52
|
+
assertEquals(0L, age)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `age is zero when event happens exactly now`() {
|
|
57
|
+
// Button pressed at button-time 10_200ms (200ms after ready).
|
|
58
|
+
// Android receives at android-time 5_200ms (200ms after ready).
|
|
59
|
+
// Perfectly synchronised clocks => age = 0.
|
|
60
|
+
val age = AgeCalculator.computeAgeMs(
|
|
61
|
+
nowElapsedMs = 5_200L,
|
|
62
|
+
androidReadyElapsedMs = androidReady,
|
|
63
|
+
buttonReadyTimestamp = buttonReady,
|
|
64
|
+
eventTimestamp = 10_200L
|
|
65
|
+
)
|
|
66
|
+
assertEquals(0L, age)
|
|
67
|
+
}
|
|
68
|
+
}
|