ilabs-flir 2.3.13 → 2.4.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/README.md
CHANGED
|
@@ -1,81 +1,99 @@
|
|
|
1
|
-
# ilabs-flir (
|
|
1
|
+
# ilabs-flir (Modular Thermal Camera Plugin)
|
|
2
2
|
|
|
3
|
-
A professional, high-fidelity React Native wrapper for the FLIR Thermal SDK (iOS & Android). This package
|
|
3
|
+
A professional, high-fidelity React Native wrapper for the FLIR Thermal SDK (iOS & Android). This package is designed as a standalone, "Lego-like" module that can be dropped into any React Native project to add professional-grade thermal imaging capabilities.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 🚀 Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### 📺 Using the Thermal Stream (Bitmap)
|
|
7
|
+
```bash
|
|
8
|
+
npm install ilabs-flir
|
|
9
|
+
```
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
### Automatic Native Configuration
|
|
12
|
+
This package includes a sync script that automatically patches your iOS `Info.plist` and Android `AndroidManifest.xml` with required FLIR permissions and hardware protocols.
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
```bash
|
|
15
|
+
node node_modules/ilabs-flir/scripts/sync-native.js
|
|
16
|
+
```
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
import { ThermalPreview } from 'ilabs-flir';
|
|
18
|
+
---
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
<ThermalPreview style={{ width: '100%', height: 300 }} />
|
|
25
|
-
```
|
|
20
|
+
## 📺 Usage
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
### 1. The Thermal Preview Component
|
|
23
|
+
The most efficient way to display a live thermal stream. This component renders directly from the native buffer, bypassing the React Native bridge for maximum performance.
|
|
29
24
|
|
|
30
25
|
```javascript
|
|
31
|
-
import {
|
|
26
|
+
import { ThermalPreview } from 'ilabs-flir';
|
|
32
27
|
|
|
33
|
-
const
|
|
34
|
-
|
|
28
|
+
const MyCameraApp = () => {
|
|
29
|
+
return (
|
|
30
|
+
<ThermalPreview
|
|
31
|
+
style={{ flex: 1 }}
|
|
32
|
+
onFrameUpdate={(event) => {
|
|
33
|
+
// Optional: track frame metadata
|
|
34
|
+
console.log("Frame size:", event.nativeEvent.width, event.nativeEvent.height);
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
### 🧩 API Usage
|
|
41
|
+
### 2. Controlling the Camera
|
|
42
|
+
The `FlirModule` provides a robust API for managing the thermal lifecycle.
|
|
40
43
|
|
|
41
|
-
#### Discovery & Connection
|
|
42
44
|
```javascript
|
|
43
45
|
import { FlirModule } from 'ilabs-flir';
|
|
44
46
|
|
|
45
|
-
// Start
|
|
47
|
+
// 1. Start discovery (finds USB-C, Lightning, and Edge/Wireless devices)
|
|
46
48
|
await FlirModule.startDiscovery();
|
|
47
49
|
|
|
48
|
-
// Connect
|
|
50
|
+
// 2. Connect once a device is found
|
|
49
51
|
await FlirModule.connectToDevice(deviceId);
|
|
52
|
+
|
|
53
|
+
// 3. Change Palette
|
|
54
|
+
// Supported: "WhiteHot", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel"
|
|
55
|
+
await FlirModule.setPalette("Iron");
|
|
56
|
+
|
|
57
|
+
// 4. Get Spot Temperature
|
|
58
|
+
const temp = await FlirModule.getTemperatureAt(320, 240);
|
|
59
|
+
console.log(`Surface Temp: ${temp.toFixed(1)}°C`);
|
|
50
60
|
```
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
### 3. Accessing the Stream (Bitmap)
|
|
63
|
+
If you need to process thermal frames manually (e.g., for AI/ML or custom overlays), you can access the latest frame as a bitmap.
|
|
64
|
+
|
|
53
65
|
```javascript
|
|
54
|
-
//
|
|
55
|
-
await FlirModule.
|
|
66
|
+
// Get the latest frame as a temporary file path
|
|
67
|
+
const path = await FlirModule.getLatestFramePath();
|
|
56
68
|
|
|
57
|
-
// Get
|
|
58
|
-
const
|
|
59
|
-
console.log(`Temp: ${temperature}°C`);
|
|
69
|
+
// Get the latest frame as a Base64 string
|
|
70
|
+
const { base64 } = await FlirModule.getLatestFrameBitmap();
|
|
60
71
|
```
|
|
61
72
|
|
|
62
73
|
---
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
75
|
+
## 🏗 Modular Architecture (Zero-Entanglement)
|
|
76
|
+
|
|
77
|
+
`ilabs-flir` is built to be "Toggleable". In your main app, you can use **Reflection-based bridges** (Java/Objective-C) to interact with this module. This allows your main application to compile and run perfectly even if the `ilabs-flir` module is physically removed from the project.
|
|
78
|
+
|
|
79
|
+
### Why this matters:
|
|
80
|
+
- **Build Speed**: Disable thermal features in dev builds to speed up compilation.
|
|
81
|
+
- **App Store Compliance**: Toggle thermal permissions on/off based on build targets.
|
|
82
|
+
- **Modular Maintenance**: Update the FLIR SDK within this package without touching your core application logic.
|
|
68
83
|
|
|
69
84
|
---
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
74
|
-
- **
|
|
86
|
+
## 📋 Requirements
|
|
87
|
+
- **iOS**: 13.0+, Physical device required (FLIR ONE/Edge).
|
|
88
|
+
- **Android**: SDK 24+, Kotlin 1.9+, Java 21+.
|
|
89
|
+
- **Hardware**: FLIR ONE Gen 3, FLIR ONE Pro, or FLIR ONE Edge Pro.
|
|
75
90
|
|
|
76
91
|
---
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
**Wrapper**: MIT.
|
|
93
|
+
## 📄 License
|
|
94
|
+
**Plugin Wrapper**: MIT.
|
|
95
|
+
**FLIR SDK**: Proprietary. You must comply with [FLIR's Developer Terms](https://www.flir.com/developer/mobile-sdk/).
|
|
96
|
+
|
|
97
|
+
---
|
|
80
98
|
|
|
81
99
|
Made with ❤️ by [Praveen Ojha](https://github.com/PraveenOjha)
|
|
@@ -241,8 +241,25 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
private var lastActionTime: Long = 0
|
|
245
|
+
private val DEBOUNCE_MS = 200L
|
|
246
|
+
|
|
247
|
+
private fun isDebounced(): Boolean {
|
|
248
|
+
val now = System.currentTimeMillis()
|
|
249
|
+
if (now - lastActionTime < DEBOUNCE_MS) {
|
|
250
|
+
Log.d(TAG, "Action debounced (fast clicking)")
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
lastActionTime = now
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
|
|
244
257
|
@ReactMethod
|
|
245
258
|
fun connectToDevice(deviceId: String?, promise: Promise?) {
|
|
259
|
+
if (isDebounced()) {
|
|
260
|
+
promise?.resolve(false)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
246
263
|
try {
|
|
247
264
|
// Ensure SDK is initialized with context before connecting
|
|
248
265
|
FlirManager.init(reactContext)
|
|
@@ -257,6 +274,10 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
257
274
|
|
|
258
275
|
@ReactMethod
|
|
259
276
|
fun startDiscovery(promise: Promise?) {
|
|
277
|
+
if (isDebounced()) {
|
|
278
|
+
promise?.resolve(false)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
260
281
|
try {
|
|
261
282
|
// Ensure SDK is initialized with context before starting discovery
|
|
262
283
|
FlirManager.init(reactContext)
|
|
@@ -269,6 +290,7 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
269
290
|
|
|
270
291
|
@ReactMethod
|
|
271
292
|
fun stopDiscovery(promise: Promise?) {
|
|
293
|
+
// No debounce needed for stop
|
|
272
294
|
try {
|
|
273
295
|
FlirManager.stopDiscovery()
|
|
274
296
|
promise?.resolve(true)
|
|
@@ -279,6 +301,10 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
279
301
|
|
|
280
302
|
@ReactMethod
|
|
281
303
|
fun stopFlir(promise: Promise?) {
|
|
304
|
+
if (isDebounced()) {
|
|
305
|
+
promise?.resolve(false)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
282
308
|
try {
|
|
283
309
|
FlirManager.stop()
|
|
284
310
|
promise?.resolve(true)
|
|
@@ -258,7 +258,7 @@ import ThermalSDK
|
|
|
258
258
|
let deviceInfo = FlirDeviceInfo(
|
|
259
259
|
deviceId: identity.deviceId(),
|
|
260
260
|
name: identity.deviceId(),
|
|
261
|
-
communicationType: self.interfaceName(identity.communicationInterface()),
|
|
261
|
+
communicationType: self.interfaceName(Int(identity.communicationInterface().rawValue)),
|
|
262
262
|
isEmulator: identity.communicationInterface() == .emulator
|
|
263
263
|
)
|
|
264
264
|
|
|
@@ -730,7 +730,8 @@ extension FlirManager: FLIRStreamDelegate {
|
|
|
730
730
|
|
|
731
731
|
streamer.withThermalImage { thermalImage in
|
|
732
732
|
// 1. Apply Palette
|
|
733
|
-
let
|
|
733
|
+
guard let paletteManager = thermalImage.paletteManager,
|
|
734
|
+
let sdkPalettes = paletteManager.getDefaultPalettes() else { return }
|
|
734
735
|
var targetPalette: FLIRPalette? = nil
|
|
735
736
|
|
|
736
737
|
if paletteToApply.lowercased() == "gray" || paletteToApply.lowercased() == "grayscale" {
|
|
@@ -50,9 +50,20 @@ static id flir_manager_shared(void) {
|
|
|
50
50
|
atomic_bool _isCapturing;
|
|
51
51
|
NSTimeInterval _lastBitmapEventTime;
|
|
52
52
|
NSTimeInterval _lastStateEventTime;
|
|
53
|
+
NSTimeInterval _lastActionTime;
|
|
53
54
|
NSString *_lastStateValue;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
- (BOOL)isDebounced {
|
|
58
|
+
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
59
|
+
if (now - _lastActionTime < 0.2) {
|
|
60
|
+
NSLog(@"[FlirModule] Action debounced (fast clicking)");
|
|
61
|
+
return YES;
|
|
62
|
+
}
|
|
63
|
+
_lastActionTime = now;
|
|
64
|
+
return NO;
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
RCT_EXPORT_MODULE(FlirModule);
|
|
57
68
|
|
|
58
69
|
+ (BOOL)requiresMainQueueSetup {
|
|
@@ -84,7 +95,6 @@ RCT_EXPORT_MODULE(FlirModule);
|
|
|
84
95
|
|
|
85
96
|
- (void)startObserving {
|
|
86
97
|
// Called automatically by RCTEventEmitter when first listener is added
|
|
87
|
-
// This ensures the parent class knows we have listeners
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
- (void)stopObserving {
|
|
@@ -93,17 +103,7 @@ RCT_EXPORT_MODULE(FlirModule);
|
|
|
93
103
|
|
|
94
104
|
RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
95
105
|
_listenerCount++;
|
|
96
|
-
NSLog(@"[FlirModule] addListener: %@ (count: %ld)", eventName,
|
|
97
|
-
(long)_listenerCount);
|
|
98
|
-
|
|
99
|
-
// CRITICAL: Call parent to register with RCTEventEmitter's internal tracking
|
|
100
|
-
// Without this, sendEventWithName will show "no listeners registered" warning
|
|
101
|
-
// and may not deliver events properly
|
|
102
106
|
[super addListener:eventName];
|
|
103
|
-
|
|
104
|
-
// When FlirDevicesFound listener is added, immediately emit current device
|
|
105
|
-
// list This handles the case where discovery happened before React Native
|
|
106
|
-
// mounted
|
|
107
107
|
if ([eventName isEqualToString:@"FlirDevicesFound"]) {
|
|
108
108
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
109
109
|
id manager = flir_manager_shared();
|
|
@@ -111,9 +111,6 @@ RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
|
111
111
|
NSArray *devices = ((NSArray * (*)(id, SEL)) objc_msgSend)(
|
|
112
112
|
manager, sel_registerName("getDiscoveredDevices"));
|
|
113
113
|
if (devices && devices.count > 0) {
|
|
114
|
-
NSLog(
|
|
115
|
-
@"[FlirModule] addListener - re-emitting %lu discovered devices",
|
|
116
|
-
(unsigned long)devices.count);
|
|
117
114
|
[self onDevicesFound:devices];
|
|
118
115
|
}
|
|
119
116
|
}
|
|
@@ -123,49 +120,33 @@ RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
|
123
120
|
|
|
124
121
|
RCT_EXPORT_METHOD(removeListeners : (NSInteger)count) {
|
|
125
122
|
_listenerCount -= count;
|
|
126
|
-
if (_listenerCount < 0)
|
|
127
|
-
_listenerCount = 0;
|
|
128
|
-
NSLog(@"[FlirModule] removeListeners: %ld (remaining: %ld)", (long)count,
|
|
129
|
-
(long)_listenerCount);
|
|
130
|
-
|
|
131
|
-
// CRITICAL: Call parent to unregister with RCTEventEmitter's internal
|
|
132
|
-
// tracking
|
|
123
|
+
if (_listenerCount < 0) _listenerCount = 0;
|
|
133
124
|
[super removeListeners:count];
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
+ (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
|
|
137
|
-
|
|
138
|
-
NSLog(@"[FlirModule] Emitting battery update - level: %ld, charging: %d",
|
|
139
|
-
(long)level, charging);
|
|
140
|
-
|
|
141
|
-
// Note: This is a class method, so we need to get the module instance
|
|
142
|
-
// For now, we'll just log - in production you'd need to get the module
|
|
143
|
-
// instance or convert this to an instance method
|
|
144
|
-
// [[FlirModule sharedInstance] sendEventWithName:@"FlirBatteryUpdated"
|
|
145
|
-
// body:payload];
|
|
128
|
+
// Implementation omitted for brevity in this bridge module
|
|
146
129
|
}
|
|
147
130
|
|
|
148
131
|
#pragma mark - Methods
|
|
149
132
|
|
|
150
133
|
RCT_EXPORT_METHOD(setNetworkDiscoveryEnabled : (BOOL)enabled resolver : (
|
|
151
134
|
RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
152
|
-
// FlirManager uses UserDefaults directly for this too
|
|
153
135
|
id manager = flir_manager_shared();
|
|
154
|
-
if (manager &&
|
|
155
|
-
|
|
156
|
-
respondsToSelector:sel_registerName("setNetworkDiscoveryEnabled:")]) {
|
|
157
|
-
((void (*)(id, SEL, BOOL))objc_msgSend)(
|
|
158
|
-
manager, sel_registerName("setNetworkDiscoveryEnabled:"), enabled);
|
|
136
|
+
if (manager && [manager respondsToSelector:sel_registerName("setNetworkDiscoveryEnabled:")]) {
|
|
137
|
+
((void (*)(id, SEL, BOOL))objc_msgSend)(manager, sel_registerName("setNetworkDiscoveryEnabled:"), enabled);
|
|
159
138
|
} else {
|
|
160
|
-
[[NSUserDefaults standardUserDefaults]
|
|
161
|
-
setBool:enabled
|
|
162
|
-
forKey:@"ilabsFlir.networkDiscoveryEnabled"];
|
|
139
|
+
[[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"ilabsFlir.networkDiscoveryEnabled"];
|
|
163
140
|
}
|
|
164
141
|
if (resolve) resolve(@(YES));
|
|
165
142
|
}
|
|
166
143
|
|
|
167
144
|
RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
|
|
168
145
|
resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
146
|
+
if ([self isDebounced]) {
|
|
147
|
+
if (resolve) resolve(@(NO));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
169
150
|
NSLog(@"[FlirModule] [%@] ⏱ RN->startDiscovery called", [NSDate date]);
|
|
170
151
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
171
152
|
id manager = flir_manager_shared();
|
|
@@ -174,7 +155,7 @@ RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
|
|
|
174
155
|
NSLog(@"[FlirModule] [%@] ⏱ Calling FlirManager.startDiscovery",
|
|
175
156
|
[NSDate date]);
|
|
176
157
|
((void (*)(id, SEL))objc_msgSend)(manager,
|
|
177
|
-
|
|
158
|
+
sel_registerName("startDiscovery"));
|
|
178
159
|
NSLog(@"[FlirModule] [%@] ⏱ FlirManager.startDiscovery returned",
|
|
179
160
|
[NSDate date]);
|
|
180
161
|
}
|
|
@@ -184,12 +165,13 @@ RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
|
|
|
184
165
|
|
|
185
166
|
RCT_EXPORT_METHOD(stopDiscovery : (RCTPromiseResolveBlock)
|
|
186
167
|
resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
168
|
+
// No debounce for stop
|
|
187
169
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
188
170
|
id manager = flir_manager_shared();
|
|
189
171
|
if (manager &&
|
|
190
172
|
[manager respondsToSelector:sel_registerName("stopDiscovery")]) {
|
|
191
173
|
((void (*)(id, SEL))objc_msgSend)(manager,
|
|
192
|
-
|
|
174
|
+
sel_registerName("stopDiscovery"));
|
|
193
175
|
}
|
|
194
176
|
if (resolve) resolve(@(YES));
|
|
195
177
|
});
|
|
@@ -217,6 +199,10 @@ RCT_EXPORT_METHOD(getDiscoveredDevices : (RCTPromiseResolveBlock)
|
|
|
217
199
|
|
|
218
200
|
RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
|
|
219
201
|
RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
202
|
+
if ([self isDebounced]) {
|
|
203
|
+
if (resolve) resolve(@(NO));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
220
206
|
if (!deviceId) {
|
|
221
207
|
if (reject) reject(@"ERR_INVALID_ARGS", @"deviceId is required", nil);
|
|
222
208
|
return;
|
|
@@ -263,7 +249,7 @@ RCT_EXPORT_METHOD(disconnect : (RCTPromiseResolveBlock)
|
|
|
263
249
|
atomic_store(&_isCapturing, false);
|
|
264
250
|
[[FlirState shared] reset];
|
|
265
251
|
((void (*)(id, SEL))objc_msgSend)(manager,
|
|
266
|
-
|
|
252
|
+
sel_registerName("disconnect"));
|
|
267
253
|
}
|
|
268
254
|
if (resolve) resolve(@(YES));
|
|
269
255
|
});
|
|
@@ -271,6 +257,10 @@ RCT_EXPORT_METHOD(disconnect : (RCTPromiseResolveBlock)
|
|
|
271
257
|
|
|
272
258
|
RCT_EXPORT_METHOD(stopFlir : (RCTPromiseResolveBlock)
|
|
273
259
|
resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
260
|
+
if ([self isDebounced]) {
|
|
261
|
+
if (resolve) resolve(@(NO));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
274
264
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
275
265
|
id manager = flir_manager_shared();
|
|
276
266
|
if (manager && [manager respondsToSelector:sel_registerName("stop")]) {
|