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.
- package/README.md +99 -1068
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +6 -0
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +56 -0
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +10 -5
- package/ios/Flir/src/FlirManager.swift +10 -2
- package/ios/Flir/src/FlirModule.m +66 -44
- 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 */ }
|
|
@@ -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
|
|
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");
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
@@ -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