ilabs-flir 2.3.11 → 2.3.13

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 */ }
@@ -287,6 +287,36 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
287
287
  }
288
288
  }
289
289
 
290
+ @ReactMethod
291
+ fun simulateFlirContextLoss(promise: Promise?) {
292
+ try {
293
+ FlirManager.simulateContextLoss()
294
+ promise?.resolve(true)
295
+ } catch (e: Exception) {
296
+ promise?.reject("ERR_SIMULATE_LOSS", e)
297
+ }
298
+ }
299
+
300
+ @ReactMethod
301
+ fun pauseFlirForPreview(promise: Promise?) {
302
+ try {
303
+ FlirManager.stop()
304
+ promise?.resolve(true)
305
+ } catch (e: Exception) {
306
+ promise?.reject("ERR_PAUSE_FLIR", e)
307
+ }
308
+ }
309
+
310
+ @ReactMethod
311
+ fun resumeFlirAfterPreview(promise: Promise?) {
312
+ try {
313
+ FlirManager.startDiscovery(true)
314
+ promise?.resolve(true)
315
+ } catch (e: Exception) {
316
+ promise?.reject("ERR_RESUME_FLIR", e)
317
+ }
318
+ }
319
+
290
320
  @ReactMethod
291
321
  fun captureRadiometricSnapshot(path: String, promise: Promise?) {
292
322
  try {
@@ -202,8 +202,8 @@ public class FlirSdkManager {
202
202
  // camera's WiFi Access Point before calling camera.connect().
203
203
  if (identity.communicationInterface == CommunicationInterface.FLIR_ONE_WIRELESS) {
204
204
  DiscoveredCamera dc = getDiscoveredCamera(identity.deviceId);
205
- if (dc != null && dc.cameraDetails != null) {
206
- String ssid = dc.cameraDetails.ssid;
205
+ if (dc != null && dc.getCameraDetails() != null) {
206
+ String ssid = dc.getCameraDetails().ssid;
207
207
  Log.d(TAG, "Establishing WiFi connection to: " + ssid);
208
208
 
209
209
  if (!SdkWifiConnectionHelper.isConnectedToNetwork(context, ssid)) {
@@ -211,7 +211,7 @@ public class FlirSdkManager {
211
211
  final AtomicBoolean wifiDone = new AtomicBoolean(false);
212
212
  final AtomicBoolean wifiSuccess = new AtomicBoolean(false);
213
213
 
214
- SdkWifiConnectionHelper.connectToWifiWithoutCode(context, dc.cameraDetails, status -> {
214
+ SdkWifiConnectionHelper.connectToWifiWithoutCode(context, dc.getCameraDetails(), status -> {
215
215
  if (status.status == SdkWifiConnectionHelper.ConInfo.CONNECTED) {
216
216
  wifiSuccess.set(true);
217
217
  wifiDone.set(true);
@@ -257,9 +257,6 @@ public class FlirSdkManager {
257
257
  response = camera.authenticate(identity, authName, 41 * 1000);
258
258
  if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.APPROVED) {
259
259
  break;
260
- } else if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.REJECTED) {
261
- notifyError("Authentication rejected by camera");
262
- return;
263
260
  }
264
261
  } while (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.PENDING && attempts < MAX_AUTH_ATTEMPTS);
265
262
 
@@ -274,10 +271,7 @@ public class FlirSdkManager {
274
271
  stopScanInternal();
275
272
  try { Thread.sleep(300); } catch (Exception ignored) {}
276
273
  }
277
- camera = identity.camera;
278
- if (camera == null) {
279
- camera = new Camera();
280
- }
274
+ camera = new Camera();
281
275
  }
282
276
 
283
277
  Log.d(TAG, "Calling camera.connect() for " + identity.deviceId);
@@ -351,9 +345,9 @@ public class FlirSdkManager {
351
345
  try {
352
346
  // RETRY MECHANISM: For Network/Wireless cameras, the connection might take
353
347
  // a few hundred milliseconds to stabilize at the native level even after
354
- // camera.connect() returns. We retry for up to 3 seconds.
348
+ // camera.connect() returns. We retry for up to 10 seconds.
355
349
  int retries = 0;
356
- final int MAX_RETRIES = 15; // 15 * 200ms = 3 seconds
350
+ final int MAX_RETRIES = 50; // 50 * 200ms = 10 seconds
357
351
  while (!camera.isConnected() && retries < MAX_RETRIES) {
358
352
  try {
359
353
  Thread.sleep(200);
@@ -365,9 +359,14 @@ public class FlirSdkManager {
365
359
  }
366
360
 
367
361
  if (!camera.isConnected()) {
368
- Log.e(TAG, "Camera failed to report connected state after " + (retries * 200) + "ms");
369
- notifyError("Camera not connected");
370
- 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...");
371
370
  }
372
371
 
373
372
  Log.d(TAG, "Camera connected state confirmed after " + (retries * 200) + "ms");
@@ -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
@@ -429,10 +429,42 @@ RCT_EXPORT_METHOD(getConnectedDeviceInfo : (RCTPromiseResolveBlock)
429
429
  });
430
430
  }
431
431
 
432
- // Assuming integrated SDK
433
- if (resolve) resolve(@(YES));
432
+ RCT_EXPORT_METHOD(simulateFlirContextLoss : (RCTPromiseResolveBlock)
433
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
434
+ dispatch_async(dispatch_get_main_queue(), ^{
435
+ // FlirState shared reset drops the current frame which simulates context loss for Metal
436
+ [[FlirState shared] reset];
437
+ if (resolve) resolve(@(YES));
438
+ });
439
+ }
440
+
441
+ RCT_EXPORT_METHOD(pauseFlirForPreview : (RCTPromiseResolveBlock)
442
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
443
+ dispatch_async(dispatch_get_main_queue(), ^{
444
+ id manager = flir_manager_shared();
445
+ if (manager && [manager respondsToSelector:sel_registerName("stop")]) {
446
+ atomic_store(&_isCapturing, false);
447
+ [[FlirState shared] reset];
448
+ ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("stop"));
449
+ }
450
+ if (resolve) resolve(@(YES));
451
+ });
452
+ }
453
+
454
+ RCT_EXPORT_METHOD(resumeFlirAfterPreview : (RCTPromiseResolveBlock)
455
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
456
+ dispatch_async(dispatch_get_main_queue(), ^{
457
+ id manager = flir_manager_shared();
458
+ if (manager && [manager respondsToSelector:sel_registerName("startDiscovery")]) {
459
+ atomic_store(&_isCapturing, true);
460
+ ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("startDiscovery"));
461
+ }
462
+ if (resolve) resolve(@(YES));
463
+ });
434
464
  }
435
465
 
466
+
467
+
436
468
  RCT_EXPORT_METHOD(getSDKStatus : (RCTPromiseResolveBlock)
437
469
  resolve rejecter : (RCTPromiseRejectBlock)reject) {
438
470
  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.11",
3
+ "version": "2.3.13",
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';