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.
@@ -4,14 +4,14 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'expo.modules.flic2'
7
- version = '0.3.1'
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.1"
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 ageSeconds = (SystemClock.elapsedRealtime() - timestamp) / 1000
150
+ val age = ageMs(button.uuid, timestamp)
142
151
  if (isClick) {
143
- sendEvent("onFlic2Click", mapOf("uuid" to button.uuid, "queued" to wasQueued, "age" to ageSeconds))
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 ageSeconds))
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 ageSeconds = (SystemClock.elapsedRealtime() - timestamp) / 1000
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 ageSeconds
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 ageSeconds
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 ageSeconds
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 ((SystemClock.elapsedRealtime() - timestamp) / 1000)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-flic2",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Expo module for Flic2 Bluetooth buttons",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",