ilabs-flir 2.2.1 → 2.2.3
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.
|
@@ -351,7 +351,20 @@ object FlirManager {
|
|
|
351
351
|
|
|
352
352
|
override fun onError(message: String) {
|
|
353
353
|
Log.e(TAG, "Error: $message")
|
|
354
|
-
|
|
354
|
+
|
|
355
|
+
// Parse error code if present (format: "CODE: message")
|
|
356
|
+
val parts = message.split(": ", limit = 2)
|
|
357
|
+
val errorCode = if (parts.size == 2) parts[0] else "FLIR_ERROR"
|
|
358
|
+
val errorMessage = if (parts.size == 2) parts[1] else message
|
|
359
|
+
|
|
360
|
+
// Auto-disable streaming on critical errors to allow retry
|
|
361
|
+
if (errorCode.contains("NATIVE") || errorCode.contains("INIT")) {
|
|
362
|
+
Log.w(TAG, "[Flir-BRIDGE-ERROR] Critical error detected, stopping stream")
|
|
363
|
+
isStreaming = false
|
|
364
|
+
stopStream()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
emitError(errorCode, errorMessage)
|
|
355
368
|
}
|
|
356
369
|
|
|
357
370
|
override fun onBatteryUpdated(level: Int, isCharging: Boolean) {
|
|
@@ -454,13 +467,21 @@ object FlirManager {
|
|
|
454
467
|
}
|
|
455
468
|
|
|
456
469
|
private fun emitError(message: String) {
|
|
470
|
+
emitError("FLIR_ERROR", message)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private fun emitError(errorCode: String, message: String) {
|
|
457
474
|
val ctx = reactContext ?: return
|
|
458
475
|
try {
|
|
459
476
|
val params = Arguments.createMap().apply {
|
|
477
|
+
putString("code", errorCode)
|
|
460
478
|
putString("error", message)
|
|
479
|
+
putString("message", message) // For backward compatibility
|
|
480
|
+
putBoolean("canRetry", errorCode.contains("NATIVE") || errorCode.contains("INIT"))
|
|
461
481
|
}
|
|
462
482
|
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
463
483
|
.emit("FlirError", params)
|
|
484
|
+
Log.d(TAG, "[Flir-BRIDGE-ERROR] Emitted FlirError: [$errorCode] $message")
|
|
464
485
|
} catch (e: Exception) {
|
|
465
486
|
Log.e(TAG, "Failed to emit error", e)
|
|
466
487
|
}
|
|
@@ -328,6 +328,19 @@ public class FlirSdkManager {
|
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// CRITICAL FIX: Prevent starting stream if previous stream is still active
|
|
332
|
+
// This prevents race conditions and resource conflicts
|
|
333
|
+
if (streamer != null || activeStream != null) {
|
|
334
|
+
Log.w(TAG, "[Flir-STREAMING] Stream already active, stopping first");
|
|
335
|
+
stopStream();
|
|
336
|
+
|
|
337
|
+
// Wait for cleanup to complete
|
|
338
|
+
try {
|
|
339
|
+
Thread.sleep(200);
|
|
340
|
+
} catch (InterruptedException ignored) {
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
executor.execute(() -> {
|
|
332
345
|
try {
|
|
333
346
|
// Get available streams
|
|
@@ -351,7 +364,46 @@ public class FlirSdkManager {
|
|
|
351
364
|
}
|
|
352
365
|
|
|
353
366
|
activeStream = thermalStream;
|
|
354
|
-
|
|
367
|
+
|
|
368
|
+
// CRITICAL FIX: Validate stream before creating ThermalStreamer
|
|
369
|
+
// The FLIR SDK native library can crash if stream is in invalid state
|
|
370
|
+
if (!thermalStream.isAvailable()) {
|
|
371
|
+
notifyError("Thermal stream not available. Please reconnect device.");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// CRITICAL FIX: Wrap ThermalStreamer creation in try-catch
|
|
376
|
+
// While native crashes usually bypass Java exception handling,
|
|
377
|
+
// we can catch:
|
|
378
|
+
// 1. JNI errors (UnsatisfiedLinkError, Error subclasses)
|
|
379
|
+
// 2. SDK errors before they reach native code
|
|
380
|
+
// 3. Resource initialization failures
|
|
381
|
+
// This won't prevent SIGSEGV/SIGABRT but reduces crash frequency
|
|
382
|
+
try {
|
|
383
|
+
// Small delay to ensure stream and resources are fully initialized
|
|
384
|
+
// This prevents race conditions in the native filter chain setup
|
|
385
|
+
Thread.sleep(150);
|
|
386
|
+
|
|
387
|
+
streamer = new ThermalStreamer(thermalStream);
|
|
388
|
+
Log.d(TAG, "[Flir-STREAMING] ThermalStreamer created successfully");
|
|
389
|
+
} catch (UnsatisfiedLinkError e) {
|
|
390
|
+
// JNI library loading error
|
|
391
|
+
Log.e(TAG, "[Flir-STREAMING] JNI library error creating ThermalStreamer", e);
|
|
392
|
+
notifyError("FLIR_NATIVE_ERROR", "Failed to load native library. Please restart app.");
|
|
393
|
+
return;
|
|
394
|
+
} catch (Exception e) {
|
|
395
|
+
// Java exception during initialization
|
|
396
|
+
Log.e(TAG, "[Flir-STREAMING] Failed to create ThermalStreamer", e);
|
|
397
|
+
notifyError("FLIR_INIT_ERROR", "Failed to initialize thermal camera: " + e.getMessage());
|
|
398
|
+
return;
|
|
399
|
+
} catch (Error e) {
|
|
400
|
+
// Catch native errors/crashes from FLIR SDK
|
|
401
|
+
// Note: True SIGSEGV crashes will still kill the process,
|
|
402
|
+
// but some JNI errors can be caught here
|
|
403
|
+
Log.e(TAG, "[Flir-STREAMING] Native error creating ThermalStreamer", e);
|
|
404
|
+
notifyError("FLIR_NATIVE_ERROR", "Native error from FLIR device. Please reconnect and retry.");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
355
407
|
|
|
356
408
|
// Start receiving frames using OnReceived and OnRemoteError
|
|
357
409
|
thermalStream.start(
|
|
@@ -419,7 +471,15 @@ public class FlirSdkManager {
|
|
|
419
471
|
activeStream = null;
|
|
420
472
|
}
|
|
421
473
|
|
|
422
|
-
streamer
|
|
474
|
+
// CRITICAL FIX: Properly cleanup streamer to prevent resource leaks
|
|
475
|
+
if (streamer != null) {
|
|
476
|
+
try {
|
|
477
|
+
// Give streamer time to cleanup before nulling
|
|
478
|
+
Thread.sleep(50);
|
|
479
|
+
} catch (InterruptedException ignored) {
|
|
480
|
+
}
|
|
481
|
+
streamer = null;
|
|
482
|
+
}
|
|
423
483
|
|
|
424
484
|
// Reset frame processing state
|
|
425
485
|
isProcessingFrame = false;
|
|
@@ -712,6 +772,17 @@ public class FlirSdkManager {
|
|
|
712
772
|
listener.onError(message);
|
|
713
773
|
}
|
|
714
774
|
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Notify error with error code for better handling
|
|
778
|
+
*/
|
|
779
|
+
private void notifyError(String errorCode, String message) {
|
|
780
|
+
Log.e(TAG, "[" + errorCode + "] " + message);
|
|
781
|
+
if (listener != null) {
|
|
782
|
+
// Send both code and message - listener can parse it
|
|
783
|
+
listener.onError(errorCode + ": " + message);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
715
786
|
|
|
716
787
|
/**
|
|
717
788
|
* Cleanup resources
|
|
@@ -174,25 +174,35 @@ import ThermalSDK
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
@objc public func getBatteryLevel() -> Int {
|
|
177
|
-
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
if let batt = cam.value(forKey: "battery") as? NSObject,
|
|
181
|
-
let lv = batt.value(forKey: "level") as? Int { return lv }
|
|
177
|
+
// Emulators don't have battery - return -1 immediately
|
|
178
|
+
if isEmulator {
|
|
179
|
+
return -1
|
|
182
180
|
}
|
|
183
|
-
|
|
181
|
+
|
|
182
|
+
#if FLIR_ENABLED
|
|
183
|
+
// Only try battery access on real hardware (not emulators)
|
|
184
|
+
// Note: KVC can throw NSException which Swift can't catch
|
|
185
|
+
// So we avoid using KVC entirely for battery
|
|
186
|
+
return -1 // TODO: Find safe API to access battery without KVC
|
|
187
|
+
#else
|
|
184
188
|
return -1
|
|
189
|
+
#endif
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
@objc public func isBatteryCharging() -> Bool {
|
|
188
|
-
|
|
189
|
-
if
|
|
190
|
-
|
|
191
|
-
if let batt = cam.value(forKey: "battery") as? NSObject,
|
|
192
|
-
let ch = batt.value(forKey: "charging") as? Bool { return ch }
|
|
193
|
+
// Emulators don't have battery - return false immediately
|
|
194
|
+
if isEmulator {
|
|
195
|
+
return false
|
|
193
196
|
}
|
|
194
|
-
|
|
197
|
+
|
|
198
|
+
#if FLIR_ENABLED
|
|
199
|
+
// Only try battery access on real hardware (not emulators)
|
|
200
|
+
// Note: KVC can throw NSException which Swift can't catch
|
|
201
|
+
// So we avoid using KVC entirely for battery
|
|
202
|
+
return false // TODO: Find safe API to access battery without KVC
|
|
203
|
+
#else
|
|
195
204
|
return false
|
|
205
|
+
#endif
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
@objc public func latestFrameImage() -> UIImage? {
|
|
@@ -701,7 +711,10 @@ import ThermalSDK
|
|
|
701
711
|
// MARK: - Battery Polling (like Android)
|
|
702
712
|
|
|
703
713
|
private func startBatteryPolling() {
|
|
704
|
-
|
|
714
|
+
// Don't poll battery on emulators - they don't have batteries
|
|
715
|
+
if isEmulator {
|
|
716
|
+
return
|
|
717
|
+
}
|
|
705
718
|
|
|
706
719
|
// Cancel any existing timer
|
|
707
720
|
stopBatteryPolling()
|
|
@@ -717,9 +730,6 @@ import ThermalSDK
|
|
|
717
730
|
}
|
|
718
731
|
|
|
719
732
|
private func stopBatteryPolling() {
|
|
720
|
-
if batteryPollingTimer != nil {
|
|
721
|
-
FlirLogger.log(.battery, "Stopping battery polling timer")
|
|
722
|
-
}
|
|
723
733
|
batteryPollingTimer?.invalidate()
|
|
724
734
|
batteryPollingTimer = nil
|
|
725
735
|
}
|
|
@@ -728,13 +738,11 @@ import ThermalSDK
|
|
|
728
738
|
let level = getBatteryLevel()
|
|
729
739
|
let charging = isBatteryCharging()
|
|
730
740
|
|
|
731
|
-
// Only
|
|
741
|
+
// Only emit if values changed
|
|
732
742
|
if level != lastPolledBatteryLevel || charging != lastPolledCharging {
|
|
733
743
|
lastPolledBatteryLevel = level
|
|
734
744
|
lastPolledCharging = charging
|
|
735
745
|
|
|
736
|
-
FlirLogger.logBattery(level: level, isCharging: charging)
|
|
737
|
-
|
|
738
746
|
// Emit to delegate/RN via notification
|
|
739
747
|
NotificationCenter.default.post(
|
|
740
748
|
name: Notification.Name("FlirBatteryUpdated"),
|
|
@@ -129,10 +129,24 @@ RCT_EXPORT_MODULE(FlirModule);
|
|
|
129
129
|
];
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
- (void)startObserving {
|
|
133
|
+
// Called automatically by RCTEventEmitter when first listener is added
|
|
134
|
+
// This ensures the parent class knows we have listeners
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
- (void)stopObserving {
|
|
138
|
+
// Called automatically by RCTEventEmitter when last listener is removed
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
133
142
|
_listenerCount++;
|
|
134
143
|
NSLog(@"[FlirModule] addListener: %@ (count: %ld)", eventName, (long)_listenerCount);
|
|
135
144
|
|
|
145
|
+
// CRITICAL: Call parent to register with RCTEventEmitter's internal tracking
|
|
146
|
+
// Without this, sendEventWithName will show "no listeners registered" warning
|
|
147
|
+
// and may not deliver events properly
|
|
148
|
+
[super addListener:eventName];
|
|
149
|
+
|
|
136
150
|
// When FlirDevicesFound listener is added, immediately emit current device list
|
|
137
151
|
// This handles the case where discovery happened before React Native mounted
|
|
138
152
|
if ([eventName isEqualToString:@"FlirDevicesFound"]) {
|
|
@@ -154,6 +168,9 @@ RCT_EXPORT_METHOD(removeListeners : (NSInteger)count) {
|
|
|
154
168
|
_listenerCount -= count;
|
|
155
169
|
if (_listenerCount < 0) _listenerCount = 0;
|
|
156
170
|
NSLog(@"[FlirModule] removeListeners: %ld (remaining: %ld)", (long)count, (long)_listenerCount);
|
|
171
|
+
|
|
172
|
+
// CRITICAL: Call parent to unregister with RCTEventEmitter's internal tracking
|
|
173
|
+
[super removeListeners:count];
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
+ (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
|
|
@@ -234,18 +251,26 @@ RCT_EXPORT_METHOD(getDiscoveredDevices : (RCTPromiseResolveBlock)
|
|
|
234
251
|
RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
|
|
235
252
|
RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
236
253
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
237
|
-
|
|
238
|
-
self.connectReject = reject;
|
|
254
|
+
NSLog(@"[FlirModule] connectToDevice called for: %@", deviceId);
|
|
239
255
|
|
|
240
256
|
id manager = flir_manager_shared();
|
|
241
257
|
if (manager &&
|
|
242
258
|
[manager respondsToSelector:sel_registerName("connectToDevice:")]) {
|
|
259
|
+
NSLog(@"[FlirModule] Calling FlirManager.connectToDevice");
|
|
260
|
+
|
|
261
|
+
// Store callbacks for event-driven updates (but don't block on them)
|
|
262
|
+
self.connectResolve = nil; // Don't use promise for blocking
|
|
263
|
+
self.connectReject = nil;
|
|
264
|
+
|
|
265
|
+
// Initiate connection asynchronously
|
|
243
266
|
((void (*)(id, SEL, id))objc_msgSend)(
|
|
244
267
|
manager, sel_registerName("connectToDevice:"), deviceId);
|
|
268
|
+
|
|
269
|
+
// Resolve immediately - connection status will come via events
|
|
270
|
+
resolve(@(YES));
|
|
245
271
|
} else {
|
|
272
|
+
NSLog(@"[FlirModule] FlirManager not found");
|
|
246
273
|
reject(@"ERR_NO_MANAGER", @"FlirManager not found", nil);
|
|
247
|
-
self.connectResolve = nil;
|
|
248
|
-
self.connectReject = nil;
|
|
249
274
|
}
|
|
250
275
|
});
|
|
251
276
|
}
|
|
@@ -277,16 +302,21 @@ RCT_EXPORT_METHOD(stopFlir : (RCTPromiseResolveBlock)
|
|
|
277
302
|
RCT_EXPORT_METHOD(startEmulator : (NSString *)emulatorType resolver : (
|
|
278
303
|
RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
|
|
279
304
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
280
|
-
|
|
281
|
-
|
|
305
|
+
NSLog(@"[FlirModule] startEmulator called for type: %@", emulatorType);
|
|
306
|
+
|
|
282
307
|
id manager = flir_manager_shared();
|
|
283
308
|
if (manager && [manager respondsToSelector:sel_registerName(
|
|
284
309
|
"startEmulatorWithType:")]) {
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
// Store callbacks for event-driven updates (but don't block on them)
|
|
311
|
+
self.connectResolve = nil;
|
|
312
|
+
self.connectReject = nil;
|
|
313
|
+
|
|
314
|
+
// Initiate emulator start asynchronously
|
|
288
315
|
((void (*)(id, SEL, id))objc_msgSend)(
|
|
289
316
|
manager, sel_registerName("startEmulatorWithType:"), emulatorType);
|
|
317
|
+
|
|
318
|
+
// Resolve immediately - connection status will come via events
|
|
319
|
+
resolve(@(YES));
|
|
290
320
|
} else {
|
|
291
321
|
// Fallback if selector assumption wrong/mismatch
|
|
292
322
|
reject(@"ERR_NOT_IMPL",
|
|
@@ -444,12 +474,6 @@ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
|
|
|
444
474
|
}
|
|
445
475
|
|
|
446
476
|
- (void)onDeviceConnected:(id)device {
|
|
447
|
-
if (self.connectResolve) {
|
|
448
|
-
self.connectResolve(@(YES));
|
|
449
|
-
self.connectResolve = nil;
|
|
450
|
-
self.connectReject = nil;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
477
|
// device is FlirDeviceInfo
|
|
454
478
|
NSMutableDictionary *body = [NSMutableDictionary new];
|
|
455
479
|
if ([device respondsToSelector:sel_registerName("toDictionary")]) {
|
|
@@ -497,11 +521,6 @@ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
|
|
|
497
521
|
}
|
|
498
522
|
|
|
499
523
|
- (void)onError:(NSString *)message {
|
|
500
|
-
if (self.connectReject) {
|
|
501
|
-
self.connectReject(@"ERR_FLIR", message, nil);
|
|
502
|
-
self.connectResolve = nil;
|
|
503
|
-
self.connectReject = nil;
|
|
504
|
-
}
|
|
505
524
|
NSLog(@"[FlirModule] onError - emitting FlirError: %@", message);
|
|
506
525
|
[self sendEventWithName:@"FlirError"
|
|
507
526
|
body:@{@"error" : message ?: @"Unknown error"}];
|