expo-flic2 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.
- package/.claude/settings.local.json +2 -1
- package/CLAUDE.md +29 -0
- package/README.md +407 -15
- package/android/build.gradle +3 -1
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# expo-flic2
|
|
2
|
+
|
|
3
|
+
## Versioning
|
|
4
|
+
|
|
5
|
+
When releasing a new version, update the version string in **three places**:
|
|
6
|
+
|
|
7
|
+
### 1. `package.json` (source of truth)
|
|
8
|
+
```json
|
|
9
|
+
"version": "X.Y.Z"
|
|
10
|
+
```
|
|
11
|
+
This also drives the **iOS** version — `ios/ExpoFlic2.podspec` reads `package['version']` directly, so no separate iOS change is needed.
|
|
12
|
+
|
|
13
|
+
### 2. `android/build.gradle` (two places)
|
|
14
|
+
```groovy
|
|
15
|
+
version = 'X.Y.Z' // top-level, used by Gradle/Maven
|
|
16
|
+
...
|
|
17
|
+
defaultConfig {
|
|
18
|
+
versionName "X.Y.Z" // required by expo-module-gradle-plugin
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Summary table
|
|
23
|
+
|
|
24
|
+
| File | Field | Notes |
|
|
25
|
+
|------|-------|-------|
|
|
26
|
+
| `package.json` | `"version"` | Drives npm publish and iOS podspec |
|
|
27
|
+
| `android/build.gradle` | `version = '...'` | Gradle/Maven artifact version |
|
|
28
|
+
| `android/build.gradle` | `defaultConfig.versionName` | Required by expo-module-gradle-plugin |
|
|
29
|
+
| `ios/ExpoFlic2.podspec` | `s.version` | **Auto-read from package.json — do not edit** |
|
package/README.md
CHANGED
|
@@ -1,35 +1,427 @@
|
|
|
1
1
|
# expo-flic2
|
|
2
2
|
|
|
3
|
-
Expo module for Flic2 Bluetooth buttons
|
|
3
|
+
Expo module for integrating [Flic2](https://flic.io/) Bluetooth buttons into your React Native / Expo app.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Supports **iOS** and **Android**. Provides a typed API for scanning, connecting, and reacting to button events (click, double-click, hold, and press/release).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/flic2/)
|
|
7
|
+
## Installation
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```sh
|
|
10
|
+
npm install expo-flic2
|
|
11
|
+
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
### iOS
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Run `npx pod-install` after installing.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Add Bluetooth usage descriptions to your `app.json` (or `app.config.js`) — the config plugin handles this automatically when using Expo managed workflow:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"expo": {
|
|
22
|
+
"plugins": ["expo-flic2"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
For bare React Native projects, add to `ios/MyApp/Info.plist`:
|
|
19
28
|
|
|
29
|
+
```xml
|
|
30
|
+
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
31
|
+
<string>This app uses Bluetooth to connect to Flic2 buttons.</string>
|
|
32
|
+
<key>NSBluetoothPeripheralUsageDescription</key>
|
|
33
|
+
<string>This app uses Bluetooth to connect to Flic2 buttons.</string>
|
|
20
34
|
```
|
|
21
|
-
|
|
35
|
+
|
|
36
|
+
### Android
|
|
37
|
+
|
|
38
|
+
The module requires the following permissions in `AndroidManifest.xml`:
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
42
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
43
|
+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
44
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
45
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The config plugin adds these automatically for managed Expo projects.
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { useEffect } from "react";
|
|
54
|
+
import {
|
|
55
|
+
initialize,
|
|
56
|
+
startScan,
|
|
57
|
+
stopScan,
|
|
58
|
+
addOnScanListener,
|
|
59
|
+
addOnClickListener,
|
|
60
|
+
addOnConnectionListener,
|
|
61
|
+
connectButton,
|
|
62
|
+
} from "expo-flic2";
|
|
63
|
+
|
|
64
|
+
export default function App() {
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// 1. Initialize the Flic2 Bluetooth manager
|
|
67
|
+
initialize();
|
|
68
|
+
|
|
69
|
+
// 2. Listen for button events
|
|
70
|
+
const clickSub = addOnClickListener(({ uuid, queued, age }) => {
|
|
71
|
+
console.log(`Button ${uuid} clicked (age: ${age}ms)`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const connSub = addOnConnectionListener(({ uuid, state, error }) => {
|
|
75
|
+
console.log(`Button ${uuid} connection state: ${state}`);
|
|
76
|
+
if (error) console.error("Connection error:", error);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 3. Scan for nearby buttons
|
|
80
|
+
const scanSub = addOnScanListener(({ isScanning, button, error }) => {
|
|
81
|
+
if (error) {
|
|
82
|
+
console.error("Scan error:", error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (button) {
|
|
86
|
+
console.log("Discovered button:", button.name, button.uuid);
|
|
87
|
+
// Connect as soon as it's found
|
|
88
|
+
connectButton(button.uuid);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
startScan();
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
clickSub.remove();
|
|
96
|
+
connSub.remove();
|
|
97
|
+
scanSub.remove();
|
|
98
|
+
stopScan();
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
return <YourAppUI />;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### Initialization
|
|
109
|
+
|
|
110
|
+
#### `initialize()`
|
|
111
|
+
|
|
112
|
+
Initialize the Flic2 Bluetooth manager. Call this once before using any other API, typically on app startup.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { initialize } from "expo-flic2";
|
|
116
|
+
|
|
117
|
+
initialize();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Scanning
|
|
121
|
+
|
|
122
|
+
#### `startScan()`
|
|
123
|
+
|
|
124
|
+
Start scanning for nearby Flic2 buttons. Fires `onFlic2Scan` events as buttons are discovered.
|
|
125
|
+
|
|
126
|
+
#### `stopScan()`
|
|
127
|
+
|
|
128
|
+
Stop the active scan.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { startScan, stopScan, addOnScanListener } from "expo-flic2";
|
|
132
|
+
|
|
133
|
+
const sub = addOnScanListener(({ isScanning, button, error, scanEvent }) => {
|
|
134
|
+
if (button) {
|
|
135
|
+
console.log("Found:", button.name, button.serialNumber);
|
|
136
|
+
}
|
|
137
|
+
if (!isScanning) {
|
|
138
|
+
console.log("Scan stopped");
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
startScan();
|
|
143
|
+
|
|
144
|
+
// Later...
|
|
145
|
+
stopScan();
|
|
146
|
+
sub.remove();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Button Management
|
|
150
|
+
|
|
151
|
+
#### `getButtons(): Flic2Button[]`
|
|
152
|
+
|
|
153
|
+
Returns all known (previously paired or discovered) buttons.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { getButtons } from "expo-flic2";
|
|
157
|
+
|
|
158
|
+
const buttons = getButtons();
|
|
159
|
+
buttons.forEach((btn) => {
|
|
160
|
+
console.log(btn.name, btn.connectionState, btn.batteryLevel);
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `connectButton(uuid: string)`
|
|
165
|
+
|
|
166
|
+
Connect to a button by its UUID.
|
|
167
|
+
|
|
168
|
+
#### `disconnectButton(uuid: string)`
|
|
169
|
+
|
|
170
|
+
Disconnect from a button without removing it from the known list.
|
|
171
|
+
|
|
172
|
+
#### `forgetButton(uuid: string)`
|
|
173
|
+
|
|
174
|
+
Disconnect and remove a button from the known list.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { connectButton, disconnectButton, forgetButton } from "expo-flic2";
|
|
178
|
+
|
|
179
|
+
connectButton("some-uuid");
|
|
180
|
+
disconnectButton("some-uuid");
|
|
181
|
+
forgetButton("some-uuid"); // removes it entirely
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `setButtonTriggerMode(uuid: string, mode: Flic2TriggerMode)`
|
|
185
|
+
|
|
186
|
+
Configure which gesture events a button fires. Use this to reduce latency — for example, if you only need single clicks, set `Click` mode so the button doesn't wait to rule out a double-click.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { setButtonTriggerMode, Flic2TriggerMode } from "expo-flic2";
|
|
190
|
+
|
|
191
|
+
// Only fire click events
|
|
192
|
+
setButtonTriggerMode(uuid, Flic2TriggerMode.Click);
|
|
193
|
+
|
|
194
|
+
// Fire click and double-click events
|
|
195
|
+
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndDoubleClick);
|
|
196
|
+
|
|
197
|
+
// Fire click and hold events
|
|
198
|
+
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndHold);
|
|
199
|
+
|
|
200
|
+
// Fire all event types
|
|
201
|
+
setButtonTriggerMode(uuid, Flic2TriggerMode.ClickAndDoubleClickAndHold);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Event Listeners
|
|
205
|
+
|
|
206
|
+
All listeners return an `EventSubscription` — call `.remove()` to unsubscribe.
|
|
207
|
+
|
|
208
|
+
#### `addOnClickListener(listener)`
|
|
209
|
+
|
|
210
|
+
Fires when a button is single-clicked.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const sub = addOnClickListener(({ uuid, queued, age }) => {
|
|
214
|
+
// queued: true if the event was stored locally while disconnected
|
|
215
|
+
// age: how many ms ago the event occurred
|
|
216
|
+
console.log(`Clicked: ${uuid}`);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### `addOnDoubleClickListener(listener)`
|
|
221
|
+
|
|
222
|
+
Fires when a button is double-clicked. Requires `Flic2TriggerMode.ClickAndDoubleClick` or `ClickAndDoubleClickAndHold`.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const sub = addOnDoubleClickListener(({ uuid, queued, age }) => {
|
|
226
|
+
console.log(`Double-clicked: ${uuid}`);
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### `addOnHoldListener(listener)`
|
|
231
|
+
|
|
232
|
+
Fires when a button is held. Requires `Flic2TriggerMode.ClickAndHold` or `ClickAndDoubleClickAndHold`.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const sub = addOnHoldListener(({ uuid, queued, age }) => {
|
|
236
|
+
console.log(`Held: ${uuid}`);
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### `addOnUpOrDownListener(listener)`
|
|
241
|
+
|
|
242
|
+
Fires on every press and release, regardless of trigger mode.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const sub = addOnUpOrDownListener(({ uuid, isDown, queued, age }) => {
|
|
246
|
+
console.log(`Button ${uuid} ${isDown ? "pressed" : "released"}`);
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `addOnConnectionListener(listener)`
|
|
251
|
+
|
|
252
|
+
Fires when a button's connection state changes.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { addOnConnectionListener, Flic2ConnectionState } from "expo-flic2";
|
|
256
|
+
|
|
257
|
+
const sub = addOnConnectionListener(({ uuid, state, error }) => {
|
|
258
|
+
if (state === Flic2ConnectionState.Ready) {
|
|
259
|
+
console.log(`${uuid} is ready to use`);
|
|
260
|
+
} else if (state === Flic2ConnectionState.Disconnected && error) {
|
|
261
|
+
console.error(`${uuid} disconnected with error: ${error}`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `addOnScanListener(listener)`
|
|
267
|
+
|
|
268
|
+
Fires during scanning with discovery updates.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const sub = addOnScanListener(({ isScanning, button, error, scanEvent }) => {
|
|
272
|
+
// scanEvent (iOS only): "discovered" | "connected" | "verified" | "verificationFailed"
|
|
273
|
+
if (button) console.log("Discovered:", button.name);
|
|
274
|
+
if (error) console.error("Scan error:", error);
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### `addOnBatteryListener(listener)`
|
|
279
|
+
|
|
280
|
+
Fires when a button reports a battery level update.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const sub = addOnBatteryListener(({ uuid, level }) => {
|
|
284
|
+
console.log(`Battery for ${uuid}: ${level}%`);
|
|
285
|
+
if (level < 20) {
|
|
286
|
+
alert("Flic2 battery is low!");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
22
289
|
```
|
|
23
290
|
|
|
24
|
-
|
|
291
|
+
#### `addOnManagerStateListener(listener)`
|
|
25
292
|
|
|
293
|
+
Fires when the device's Bluetooth state changes.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const sub = addOnManagerStateListener(({ state }) => {
|
|
297
|
+
if (state === "poweredOff") {
|
|
298
|
+
alert("Please enable Bluetooth to use Flic2 buttons.");
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
26
302
|
|
|
303
|
+
## Types
|
|
27
304
|
|
|
305
|
+
```typescript
|
|
306
|
+
type Flic2Button = {
|
|
307
|
+
uuid: string;
|
|
308
|
+
bluetoothAddress: string;
|
|
309
|
+
serialNumber: string;
|
|
310
|
+
name: string;
|
|
311
|
+
connectionState: Flic2ConnectionState;
|
|
312
|
+
firmwareVersion: number;
|
|
313
|
+
batteryLevel: number;
|
|
314
|
+
pressCount: number;
|
|
315
|
+
triggerMode: Flic2TriggerMode;
|
|
316
|
+
isReady: boolean;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
enum Flic2ConnectionState {
|
|
320
|
+
Disconnected = "disconnected",
|
|
321
|
+
Connecting = "connecting",
|
|
322
|
+
Connected = "connected",
|
|
323
|
+
Ready = "ready",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
enum Flic2TriggerMode {
|
|
327
|
+
Click = "click",
|
|
328
|
+
ClickAndHold = "clickAndHold",
|
|
329
|
+
ClickAndDoubleClick = "clickAndDoubleClick",
|
|
330
|
+
ClickAndDoubleClickAndHold = "clickAndDoubleClickAndHold",
|
|
331
|
+
}
|
|
332
|
+
```
|
|
28
333
|
|
|
29
|
-
|
|
334
|
+
## Complete Example: Button Controller Hook
|
|
30
335
|
|
|
31
|
-
|
|
336
|
+
```typescript
|
|
337
|
+
import { useEffect, useState } from "react";
|
|
338
|
+
import {
|
|
339
|
+
initialize,
|
|
340
|
+
startScan,
|
|
341
|
+
stopScan,
|
|
342
|
+
getButtons,
|
|
343
|
+
connectButton,
|
|
344
|
+
setButtonTriggerMode,
|
|
345
|
+
addOnManagerStateListener,
|
|
346
|
+
addOnScanListener,
|
|
347
|
+
addOnConnectionListener,
|
|
348
|
+
addOnClickListener,
|
|
349
|
+
addOnDoubleClickListener,
|
|
350
|
+
addOnHoldListener,
|
|
351
|
+
Flic2Button,
|
|
352
|
+
Flic2TriggerMode,
|
|
353
|
+
Flic2ConnectionState,
|
|
354
|
+
} from "expo-flic2";
|
|
355
|
+
|
|
356
|
+
export function useFlic2() {
|
|
357
|
+
const [buttons, setButtons] = useState<Flic2Button[]>([]);
|
|
358
|
+
const [isScanning, setIsScanning] = useState(false);
|
|
359
|
+
const [bluetoothReady, setBluetoothReady] = useState(false);
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
initialize();
|
|
363
|
+
|
|
364
|
+
const managerSub = addOnManagerStateListener(({ state }) => {
|
|
365
|
+
setBluetoothReady(state === "poweredOn");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const scanSub = addOnScanListener(({ isScanning, button }) => {
|
|
369
|
+
setIsScanning(isScanning);
|
|
370
|
+
if (button) {
|
|
371
|
+
connectButton(button.uuid);
|
|
372
|
+
setButtonTriggerMode(button.uuid, Flic2TriggerMode.ClickAndDoubleClickAndHold);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const connSub = addOnConnectionListener(({ state }) => {
|
|
377
|
+
if (state === Flic2ConnectionState.Ready) {
|
|
378
|
+
setButtons(getButtons());
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return () => {
|
|
383
|
+
managerSub.remove();
|
|
384
|
+
scanSub.remove();
|
|
385
|
+
connSub.remove();
|
|
386
|
+
};
|
|
387
|
+
}, []);
|
|
388
|
+
|
|
389
|
+
const scan = () => {
|
|
390
|
+
setIsScanning(true);
|
|
391
|
+
startScan();
|
|
392
|
+
// Auto-stop after 10 seconds
|
|
393
|
+
setTimeout(() => stopScan(), 10_000);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return { buttons, isScanning, bluetoothReady, scan };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Usage in a component:
|
|
400
|
+
function FlicController() {
|
|
401
|
+
const { buttons, isScanning, bluetoothReady, scan } = useFlic2();
|
|
402
|
+
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
const clickSub = addOnClickListener(({ uuid }) => {
|
|
405
|
+
console.log("Click from", uuid);
|
|
406
|
+
});
|
|
407
|
+
const doubleSub = addOnDoubleClickListener(({ uuid }) => {
|
|
408
|
+
console.log("Double-click from", uuid);
|
|
409
|
+
});
|
|
410
|
+
const holdSub = addOnHoldListener(({ uuid }) => {
|
|
411
|
+
console.log("Hold from", uuid);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return () => {
|
|
415
|
+
clickSub.remove();
|
|
416
|
+
doubleSub.remove();
|
|
417
|
+
holdSub.remove();
|
|
418
|
+
};
|
|
419
|
+
}, []);
|
|
420
|
+
|
|
421
|
+
// ...
|
|
422
|
+
}
|
|
423
|
+
```
|
|
32
424
|
|
|
33
|
-
|
|
425
|
+
## Contributing
|
|
34
426
|
|
|
35
|
-
Contributions are
|
|
427
|
+
Contributions are welcome! Please refer to the [contributing guide](https://github.com/expo/expo#contributing).
|
package/android/build.gradle
CHANGED