ilabs-flir 2.3.13 → 2.4.1

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 (Self-Configuring Thermal Integration)
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 handles complex native setup, permissions, and SDK dependencies automatically.
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
- ### 🚀 Quick Start
5
+ ## 🚀 Installation
6
6
 
7
- 1. **Install**: `npm install ilabs-flir`
8
- 2. **Sync Native**: `node node_modules/ilabs-flir/scripts/sync-native.js`
9
- 3. **Build**: `cd ios && pod install && cd ..` (iOS) or `npx react-native run-android` (Android)
10
-
11
- ---
12
-
13
- ### 📺 Using the Thermal Stream (Bitmap)
7
+ ```bash
8
+ npm install ilabs-flir
9
+ ```
14
10
 
15
- The package provides two ways to work with the live thermal stream: a **Native Preview Component** (high performance) and **Raw Bitmap Access** (for custom processing).
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
- #### 1. Live Preview (Recommended)
18
- Use the `ThermalPreview` component to display the live thermal stream with zero bridge overhead.
14
+ ```bash
15
+ node node_modules/ilabs-flir/scripts/sync-native.js
16
+ ```
19
17
 
20
- ```javascript
21
- import { ThermalPreview } from 'ilabs-flir';
18
+ ---
22
19
 
23
- // Render the live thermal image
24
- <ThermalPreview style={{ width: '100%', height: 300 }} />
25
- ```
20
+ ## 📺 Usage
26
21
 
27
- #### 2. Raw Bitmap Access
28
- If you need to process the thermal image manually (e.g., with OpenCV or AI models), you can fetch the latest frame as a Base64 encoded bitmap.
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 { FlirModule } from 'ilabs-flir';
26
+ import { ThermalPreview } from 'ilabs-flir';
32
27
 
33
- const { base64, width, height } = await FlirModule.getLatestFrameBitmap();
34
- // Result: { base64: "data:image/png;base64...", width: 640, height: 480 }
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 scanning for devices (USB, WiFi/Edge, or Emulator)
47
+ // 1. Start discovery (finds USB-C, Lightning, and Edge/Wireless devices)
46
48
  await FlirModule.startDiscovery();
47
49
 
48
- // Connect to a device
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
- #### Palettes & Temperature
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
- // Apply a palette (Iron, Gray, Rainbow, etc.)
55
- await FlirModule.setPalette('Iron');
66
+ // Get the latest frame as a temporary file path
67
+ const path = await FlirModule.getLatestFramePath();
56
68
 
57
- // Get temperature at center coordinate
58
- const temperature = await FlirModule.getTemperatureAt(320, 240);
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
- ### 🏗 Architecture: Self-Configuring
65
- `ilabs-flir` is a standalone module that encapsulates all thermal "heavy lifting":
66
- - **Autonomous Sync**: Injects External Accessory protocols (iOS) and USB/Bluetooth permissions (Android) automatically.
67
- - **Bundled SDK**: Native binaries are managed via `postinstall`. No manual SDK management required.
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
- ### 📋 Requirements
72
- - **React Native**: 0.60+
73
- - **iOS**: 13.0+ (Physical device required for thermal streaming)
74
- - **Android**: SDK 24+, Kotlin 1.9.0+, Java 21
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
- ### 📄 License
79
- **Wrapper**: MIT. **FLIR SDK**: Proprietary (Requires registration at the [FLIR Developer Portal](https://www.flir.com/developer/mobile-sdk/)).
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 sdkPalettes = thermalImage.paletteManager.getDefaultPalettes()
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" {
@@ -756,7 +757,7 @@ extension FlirManager: FLIRStreamDelegate {
756
757
  // 2. Save Radiometric Snapshot if requested
757
758
  if let path = snapshotPath {
758
759
  do {
759
- try thermalImage.save(to: path)
760
+ try thermalImage.save(as: path)
760
761
  NSLog("[FlirManager] Radiometric snapshot saved to: \(path)")
761
762
  } catch {
762
763
  NSLog("[FlirManager] Failed to save radiometric snapshot: \(error)")
@@ -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
- NSDictionary *payload = @{@"level" : @(level), @"isCharging" : @(charging)};
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
- [manager
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
- sel_registerName("startDiscovery"));
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
- sel_registerName("stopDiscovery"));
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
- sel_registerName("disconnect"));
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")]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.3.13",
3
+ "version": "2.4.1",
4
4
  "description": "FLIR Thermal SDK for React Native - iOS & Android (bundled at compile time via postinstall)",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",