ilabs-flir 2.3.12 → 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 {
@@ -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");
@@ -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.12",
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';