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.
- package/README.md +81 -1068
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +6 -0
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +30 -0
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +10 -5
- package/ios/Flir/src/FlirManager.swift +7 -0
- package/ios/Flir/src/FlirModule.m +34 -2
- package/package.json +1 -1
- package/scripts/sync-native.js +106 -0
- package/src/FlirDebugScreen.tsx +105 -0
- package/src/hooks/useFlirTemperature.ts +20 -0
- package/src/index.js +21 -7
- package/src/index.ts +3 -0
|
@@ -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
|
|
348
|
+
// camera.connect() returns. We retry for up to 10 seconds.
|
|
349
349
|
int retries = 0;
|
|
350
|
-
final int MAX_RETRIES =
|
|
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.
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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