ilabs-flir 2.3.12 → 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.
@@ -212,6 +212,12 @@ object FlirManager {
212
212
  latestBitmap = null
213
213
  Log.i(TAG, "FlirManager stopped")
214
214
  }
215
+
216
+ @Synchronized
217
+ fun simulateContextLoss() {
218
+ latestBitmap = null
219
+ emitDeviceState(if (isStreaming) "streaming" else "connected")
220
+ }
215
221
 
216
222
  // Stub legacy methods
217
223
  fun startStream() { /* handled automatically by connect */ }
@@ -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)
@@ -287,6 +313,36 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
287
313
  }
288
314
  }
289
315
 
316
+ @ReactMethod
317
+ fun simulateFlirContextLoss(promise: Promise?) {
318
+ try {
319
+ FlirManager.simulateContextLoss()
320
+ promise?.resolve(true)
321
+ } catch (e: Exception) {
322
+ promise?.reject("ERR_SIMULATE_LOSS", e)
323
+ }
324
+ }
325
+
326
+ @ReactMethod
327
+ fun pauseFlirForPreview(promise: Promise?) {
328
+ try {
329
+ FlirManager.stop()
330
+ promise?.resolve(true)
331
+ } catch (e: Exception) {
332
+ promise?.reject("ERR_PAUSE_FLIR", e)
333
+ }
334
+ }
335
+
336
+ @ReactMethod
337
+ fun resumeFlirAfterPreview(promise: Promise?) {
338
+ try {
339
+ FlirManager.startDiscovery(true)
340
+ promise?.resolve(true)
341
+ } catch (e: Exception) {
342
+ promise?.reject("ERR_RESUME_FLIR", e)
343
+ }
344
+ }
345
+
290
346
  @ReactMethod
291
347
  fun captureRadiometricSnapshot(path: String, promise: Promise?) {
292
348
  try {
@@ -345,9 +345,9 @@ public class FlirSdkManager {
345
345
  try {
346
346
  // RETRY MECHANISM: For Network/Wireless cameras, the connection might take
347
347
  // a few hundred milliseconds to stabilize at the native level even after
348
- // camera.connect() returns. We retry for up to 3 seconds.
348
+ // camera.connect() returns. We retry for up to 10 seconds.
349
349
  int retries = 0;
350
- final int MAX_RETRIES = 15; // 15 * 200ms = 3 seconds
350
+ final int MAX_RETRIES = 50; // 50 * 200ms = 10 seconds
351
351
  while (!camera.isConnected() && retries < MAX_RETRIES) {
352
352
  try {
353
353
  Thread.sleep(200);
@@ -359,9 +359,14 @@ public class FlirSdkManager {
359
359
  }
360
360
 
361
361
  if (!camera.isConnected()) {
362
- Log.e(TAG, "Camera failed to report connected state after " + (retries * 200) + "ms");
363
- notifyError("Camera not connected");
364
- return;
362
+ Log.w(TAG, "Camera still reports disconnected after timeout, but attempting to check streams as fallback...");
363
+ List<Stream> fallbackStreams = camera.getStreams();
364
+ if (fallbackStreams == null || fallbackStreams.isEmpty()) {
365
+ Log.e(TAG, "No streams available and camera is disconnected. Giving up.");
366
+ notifyError("Camera not connected and no streams found");
367
+ return;
368
+ }
369
+ Log.i(TAG, "Found " + fallbackStreams.size() + " streams despite disconnected status. Proceeding...");
365
370
  }
366
371
 
367
372
  Log.d(TAG, "Camera connected state confirmed after " + (retries * 200) + "ms");
@@ -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
 
@@ -606,6 +606,13 @@ import ThermalSDK
606
606
  // Placeholder for interface name mapping
607
607
  return "UNKNOWN"
608
608
  }
609
+ @objc public func simulateContextLoss() {
610
+ _latestImage = nil
611
+ DispatchQueue.main.async { [weak self] in
612
+ // Re-emit current state to trigger UI refresh
613
+ self?.emitStateChange(self?._isStreaming == true ? "streaming" : "connected")
614
+ }
615
+ }
609
616
  }
610
617
 
611
618
  #if FLIR_ENABLED
@@ -723,7 +730,8 @@ extension FlirManager: FLIRStreamDelegate {
723
730
 
724
731
  streamer.withThermalImage { thermalImage in
725
732
  // 1. Apply Palette
726
- let sdkPalettes = thermalImage.paletteManager.getDefaultPalettes()
733
+ guard let paletteManager = thermalImage.paletteManager,
734
+ let sdkPalettes = paletteManager.getDefaultPalettes() else { return }
727
735
  var targetPalette: FLIRPalette? = nil
728
736
 
729
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
- 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")]) {
@@ -429,10 +419,42 @@ RCT_EXPORT_METHOD(getConnectedDeviceInfo : (RCTPromiseResolveBlock)
429
419
  });
430
420
  }
431
421
 
432
- // Assuming integrated SDK
433
- if (resolve) resolve(@(YES));
422
+ RCT_EXPORT_METHOD(simulateFlirContextLoss : (RCTPromiseResolveBlock)
423
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
424
+ dispatch_async(dispatch_get_main_queue(), ^{
425
+ // FlirState shared reset drops the current frame which simulates context loss for Metal
426
+ [[FlirState shared] reset];
427
+ if (resolve) resolve(@(YES));
428
+ });
434
429
  }
435
430
 
431
+ RCT_EXPORT_METHOD(pauseFlirForPreview : (RCTPromiseResolveBlock)
432
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
433
+ dispatch_async(dispatch_get_main_queue(), ^{
434
+ id manager = flir_manager_shared();
435
+ if (manager && [manager respondsToSelector:sel_registerName("stop")]) {
436
+ atomic_store(&_isCapturing, false);
437
+ [[FlirState shared] reset];
438
+ ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("stop"));
439
+ }
440
+ if (resolve) resolve(@(YES));
441
+ });
442
+ }
443
+
444
+ RCT_EXPORT_METHOD(resumeFlirAfterPreview : (RCTPromiseResolveBlock)
445
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
446
+ dispatch_async(dispatch_get_main_queue(), ^{
447
+ id manager = flir_manager_shared();
448
+ if (manager && [manager respondsToSelector:sel_registerName("startDiscovery")]) {
449
+ atomic_store(&_isCapturing, true);
450
+ ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("startDiscovery"));
451
+ }
452
+ if (resolve) resolve(@(YES));
453
+ });
454
+ }
455
+
456
+
457
+
436
458
  RCT_EXPORT_METHOD(getSDKStatus : (RCTPromiseResolveBlock)
437
459
  resolve rejecter : (RCTPromiseRejectBlock)reject) {
438
460
  if (resolve) resolve(@{@"available" : @(YES), @"arch" : @"arm64", @"platform" : @"iOS"});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.3.12",
3
+ "version": "2.4.0",
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",
@@ -0,0 +1,106 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // This script applies native configuration (Info.plist, AndroidManifest)
5
+ // to the parent project. This is used in "bare" React Native projects
6
+ // that don't rely solely on Expo Prebuild.
7
+
8
+ const findProjectRoot = () => {
9
+ let curr = __dirname;
10
+ while (curr !== path.parse(curr).root) {
11
+ if (fs.existsSync(path.join(curr, 'package.json'))) {
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(curr, 'package.json'), 'utf8'));
13
+ if (pkg.name !== 'ilabs-flir') return curr;
14
+ }
15
+ curr = path.dirname(curr);
16
+ }
17
+ return null;
18
+ };
19
+
20
+ const projectRoot = findProjectRoot();
21
+ if (!projectRoot) {
22
+ console.log('[ilabs-flir] Could not find parent project root. Skipping native sync.');
23
+ process.exit(0);
24
+ }
25
+
26
+ const flirEnabled = true; // If this script is running, it's because the package is included.
27
+
28
+ console.log(`[ilabs-flir] Syncing native configuration for project at: ${projectRoot}`);
29
+
30
+ // 1. iOS: Info.plist
31
+ const findIosProjectName = () => {
32
+ const iosDir = path.join(projectRoot, 'ios');
33
+ if (!fs.existsSync(iosDir)) return null;
34
+ const dirs = fs.readdirSync(iosDir);
35
+ const xcodeProj = dirs.find(d => d.endsWith('.xcodeproj'));
36
+ return xcodeProj ? xcodeProj.replace('.xcodeproj', '') : null;
37
+ };
38
+
39
+ const iosProjectName = findIosProjectName();
40
+ if (iosProjectName) {
41
+ const infoPlistPath = path.join(projectRoot, 'ios', iosProjectName, 'Info.plist');
42
+ if (fs.existsSync(infoPlistPath)) {
43
+ let content = fs.readFileSync(infoPlistPath, 'utf8');
44
+
45
+ // Add External Accessory Protocols if missing
46
+ if (!content.includes('com.flir.rosebud.config')) {
47
+ const protocols = `
48
+ <key>UISupportedExternalAccessoryProtocols</key>
49
+ <array>
50
+ <string>com.flir.rosebud.config</string>
51
+ <string>com.flir.rosebud.frame</string>
52
+ <string>com.flir.rosebud.fileio</string>
53
+ </array>`;
54
+
55
+ if (content.includes('</dict>')) {
56
+ content = content.replace('</dict>', `${protocols}\n</dict>`);
57
+ console.log('[ilabs-flir] Added External Accessory protocols to Info.plist');
58
+ }
59
+ }
60
+
61
+ // Add Background Mode if missing
62
+ if (!content.includes('external-accessory') && content.includes('<key>UIBackgroundModes</key>')) {
63
+ content = content.replace(/<key>UIBackgroundModes<\/key>\s*<array>/, '<key>UIBackgroundModes</key>\n\t<array>\n\t\t<string>external-accessory</string>');
64
+ console.log('[ilabs-flir] Added external-accessory background mode');
65
+ } else if (!content.includes('external-accessory')) {
66
+ const bgMode = `
67
+ <key>UIBackgroundModes</key>
68
+ <array>
69
+ <string>external-accessory</string>
70
+ </array>`;
71
+ content = content.replace('</dict>', `${bgMode}\n</dict>`);
72
+ }
73
+
74
+ fs.writeFileSync(infoPlistPath, content);
75
+ }
76
+ }
77
+
78
+ // 2. Android: AndroidManifest.xml
79
+ const manifestPath = path.join(projectRoot, 'android/app/src/main/AndroidManifest.xml');
80
+ if (fs.existsSync(manifestPath)) {
81
+ let content = fs.readFileSync(manifestPath, 'utf8');
82
+
83
+ const permissions = [
84
+ 'android.permission.BLUETOOTH',
85
+ 'android.permission.BLUETOOTH_ADMIN',
86
+ 'android.permission.BLUETOOTH_CONNECT',
87
+ 'android.permission.BLUETOOTH_SCAN',
88
+ 'android.permission.ACCESS_FINE_LOCATION',
89
+ 'android.permission.INTERNET',
90
+ 'android.permission.ACCESS_NETWORK_STATE',
91
+ 'android.permission.ACCESS_WIFI_STATE',
92
+ 'android.permission.CHANGE_WIFI_STATE',
93
+ 'android.permission.CHANGE_WIFI_MULTICAST_STATE'
94
+ ];
95
+
96
+ permissions.forEach(perm => {
97
+ if (!content.includes(perm)) {
98
+ content = content.replace('</manifest>', ` <uses-permission android:name="${perm}" />\n</manifest>`);
99
+ }
100
+ });
101
+
102
+ fs.writeFileSync(manifestPath, content);
103
+ console.log('[ilabs-flir] Updated AndroidManifest.xml with required permissions');
104
+ }
105
+
106
+ console.log('[ilabs-flir] Native sync complete.');
@@ -0,0 +1,105 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View, Text, Button, StyleSheet, NativeModules, NativeEventEmitter } from 'react-native';
3
+
4
+ const { FlirModule } = NativeModules as any;
5
+ const FlirEmitter = new NativeEventEmitter(FlirModule);
6
+
7
+ export function FlirDebugScreen() {
8
+ const [battery, setBattery] = useState<number | null>(null);
9
+ const [isCharging, setIsCharging] = useState<boolean | null>(null);
10
+ const [temperature, setTemperature] = useState<number | null>(null);
11
+ const [lastEvent, setLastEvent] = useState<any>(null);
12
+
13
+ useEffect(() => {
14
+ const subscription = FlirEmitter.addListener('FlirBatteryUpdated', (evt) => {
15
+ setBattery(evt.level);
16
+ setIsCharging(evt.isCharging);
17
+ setLastEvent(evt);
18
+ });
19
+ return () => subscription.remove();
20
+ }, []);
21
+
22
+ const onSimulateContextLoss = async () => {
23
+ try {
24
+ if (FlirModule && FlirModule.simulateFlirContextLoss) {
25
+ await FlirModule.simulateFlirContextLoss();
26
+ console.log('[FLIR DEBUG] Simulated context loss');
27
+ }
28
+ } catch (e) {
29
+ console.warn('[FLIR DEBUG] simulateContextLoss error', e);
30
+ }
31
+ };
32
+
33
+ const onRequestBattery = async () => {
34
+ try {
35
+ if (FlirModule && FlirModule.getBatteryLevel) {
36
+ const lvl = await FlirModule.getBatteryLevel();
37
+ setBattery(typeof lvl === 'number' ? lvl : null);
38
+ }
39
+ try {
40
+ if (FlirModule && FlirModule.isBatteryCharging) {
41
+ const ch = await FlirModule.isBatteryCharging();
42
+ setIsCharging(Boolean(ch));
43
+ }
44
+ } catch (e) { /* ignore */ }
45
+ } catch (e) {
46
+ console.warn('getBatteryLevel error', e);
47
+ }
48
+ };
49
+
50
+ const onRequestTemperature = async () => {
51
+ try {
52
+ if (FlirModule && FlirModule.getTemperatureAt) {
53
+ const val = await FlirModule.getTemperatureAt(80, 60);
54
+ setTemperature(typeof val === 'number' ? val : null);
55
+ }
56
+ } catch (e) {
57
+ console.warn('getTemperatureAt error', e);
58
+ }
59
+ };
60
+
61
+ const onPauseFlir = async () => {
62
+ try {
63
+ if (FlirModule && FlirModule.pauseFlirForPreview) {
64
+ await FlirModule.pauseFlirForPreview();
65
+ }
66
+ } catch (e) {
67
+ console.warn('[FLIR DEBUG] pauseFlirForPreview error', e);
68
+ }
69
+ };
70
+
71
+ const onResumeFlir = async () => {
72
+ try {
73
+ if (FlirModule && FlirModule.resumeFlirAfterPreview) {
74
+ await FlirModule.resumeFlirAfterPreview();
75
+ }
76
+ } catch (e) {
77
+ console.warn('[FLIR DEBUG] resumeFlirAfterPreview error', e);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <View style={styles.container}>
83
+ <Text style={styles.title}>FLIR Debug (Package)</Text>
84
+ <Text>Battery Level: {battery ?? '--'}</Text>
85
+ <Text>Charging: {isCharging == null ? '--' : isCharging ? 'Yes' : 'No'}</Text>
86
+ <Text>Temperature at (80,60): {temperature == null ? '--' : temperature.toFixed(1) + '°C'}</Text>
87
+ <Text>Last Event: {lastEvent ? JSON.stringify(lastEvent) : '--'}</Text>
88
+ <View style={styles.row}>
89
+ <Button title="Simulate Context Loss" onPress={onSimulateContextLoss} />
90
+ <Button title="Request Battery" onPress={onRequestBattery} />
91
+ <Button title="Get Temp (80,60)" onPress={onRequestTemperature} />
92
+ </View>
93
+ <View style={[styles.row, { marginTop: 8 }] }>
94
+ <Button title="Pause FLIR (Preview Pause)" onPress={onPauseFlir} />
95
+ <Button title="Resume FLIR (Preview Resume)" onPress={onResumeFlir} />
96
+ </View>
97
+ </View>
98
+ );
99
+ }
100
+
101
+ const styles = StyleSheet.create({
102
+ container: { padding: 12 },
103
+ title: { fontSize: 18, fontWeight: 'bold', marginBottom: 8 },
104
+ row: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 12 }
105
+ });
@@ -0,0 +1,20 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { NativeModules, NativeEventEmitter } from 'react-native';
3
+
4
+ const { FlirModule } = NativeModules as any;
5
+ const FlirEmitter = new NativeEventEmitter(FlirModule);
6
+
7
+ export function useFlirTemperature() {
8
+ const [temperature, setTemperature] = useState<number | null>(null);
9
+
10
+ useEffect(() => {
11
+ const subscription = FlirEmitter.addListener('FlirTemperatureChanged', (temp: number) => {
12
+ // Convert Kelvin to Celsius if raw thermal data is detected
13
+ const normalizedTemp = (typeof temp === 'number' && temp > 200) ? temp - 273.15 : temp;
14
+ setTemperature(normalizedTemp);
15
+ });
16
+ return () => subscription.remove();
17
+ }, []);
18
+
19
+ return temperature;
20
+ }
package/src/index.js CHANGED
@@ -1,7 +1,21 @@
1
- // FlirDownload removed; use `FlirModule` for runtime access to SDK features.
2
-
3
- // Re-export existing FlirModule functionality
4
- // Note: FlirModule should be imported from the native module
5
- import { NativeModules } from 'react-native';
6
- export const FlirModule = NativeModules.FlirModule;
7
-
1
+ import { NativeModules, requireNativeComponent, Platform } from 'react-native';
2
+
3
+ export const FlirModule = NativeModules.FlirModule;
4
+
5
+ /**
6
+ * ThermalPreview Component
7
+ *
8
+ * A high-performance native component for rendering the live thermal stream.
9
+ *
10
+ * Props:
11
+ * - style: Standard React Native view styles (required: set width/height)
12
+ */
13
+ export const ThermalPreview = Platform.select({
14
+ ios: requireNativeComponent('FlirPreviewView'),
15
+ android: requireNativeComponent('FLIRCameraView'),
16
+ });
17
+
18
+ export default {
19
+ FlirModule,
20
+ ThermalPreview,
21
+ };
package/src/index.ts CHANGED
@@ -4,3 +4,6 @@
4
4
  // Note: FlirModule should be imported from the native module
5
5
  import { NativeModules } from 'react-native';
6
6
  export const FlirModule = NativeModules.FlirModule;
7
+
8
+ export * from './FlirDebugScreen';
9
+ export * from './hooks/useFlirTemperature';