ilabs-flir 2.2.2 → 2.2.4
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
|
|
@@ -74,6 +74,9 @@ import ThermalSDK
|
|
|
74
74
|
// Client lifecycle for discovery/connection ownership
|
|
75
75
|
private var activeClients: Set<String> = []
|
|
76
76
|
private var shutdownWorkItem: DispatchWorkItem? = nil
|
|
77
|
+
// Discovery & stream watchdogs to detect silent failures/timeouts
|
|
78
|
+
private var discoveryTimeoutWorkItem: DispatchWorkItem? = nil
|
|
79
|
+
private var streamWatchdogWorkItem: DispatchWorkItem? = nil
|
|
77
80
|
|
|
78
81
|
// Battery polling timer (like Android)
|
|
79
82
|
private var batteryPollingTimer: Timer?
|
|
@@ -399,6 +402,27 @@ import ThermalSDK
|
|
|
399
402
|
discovery?.start(interfaces)
|
|
400
403
|
|
|
401
404
|
emitStateChange("discovering")
|
|
405
|
+
|
|
406
|
+
// Cancel any previous discovery timeout and schedule a new one so
|
|
407
|
+
// the UI doesn't remain stuck when discovery yields no devices.
|
|
408
|
+
discoveryTimeoutWorkItem?.cancel()
|
|
409
|
+
let discWork = DispatchWorkItem { [weak self] in
|
|
410
|
+
guard let self = self else { return }
|
|
411
|
+
if self.discoveredDevices.isEmpty && self.isScanning {
|
|
412
|
+
FlirLogger.logError(.discovery, "Discovery timed out with no devices")
|
|
413
|
+
self.discovery?.stop()
|
|
414
|
+
self.isScanning = false
|
|
415
|
+
DispatchQueue.main.async {
|
|
416
|
+
// Notify native listeners that discovery finished with no devices
|
|
417
|
+
self.delegate?.onDevicesFound(self.discoveredDevices)
|
|
418
|
+
// Emit a state change so JS can set noDeviceFound = true
|
|
419
|
+
self.emitStateChange("no_device_found")
|
|
420
|
+
self.delegate?.onError("Discovery timed out")
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
discoveryTimeoutWorkItem = discWork
|
|
425
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 8.0, execute: discWork)
|
|
402
426
|
#else
|
|
403
427
|
FlirLogger.logError(.discovery, "FLIR SDK not available - discovery disabled")
|
|
404
428
|
delegate?.onError("FLIR SDK not available")
|
|
@@ -432,10 +456,8 @@ import ThermalSDK
|
|
|
432
456
|
FlirLogger.log(.discovery, "Stopping discovery...")
|
|
433
457
|
|
|
434
458
|
#if FLIR_ENABLED
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
FlirLogger.log(.discovery, "Discovery stopped")
|
|
438
|
-
#endif
|
|
459
|
+
discoveryTimeoutWorkItem?.cancel()
|
|
460
|
+
discoveryTimeoutWorkItem = nil
|
|
439
461
|
}
|
|
440
462
|
|
|
441
463
|
// MARK: - Connection
|
|
@@ -486,19 +508,29 @@ import ThermalSDK
|
|
|
486
508
|
return
|
|
487
509
|
}
|
|
488
510
|
|
|
489
|
-
//
|
|
511
|
+
// Fail-fast authentication for generic/network cameras (prevent indefinite blocking)
|
|
490
512
|
if identity.cameraType() == .generic {
|
|
491
513
|
FlirLogger.log(.connection, "Generic/network camera - starting authentication...")
|
|
492
514
|
let certName = getCertificateName()
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
515
|
+
var authStatus = FLIRAuthenticationStatus.pending
|
|
516
|
+
var attempts = 0
|
|
517
|
+
let maxAttempts = 10
|
|
518
|
+
while authStatus == .pending && attempts < maxAttempts {
|
|
519
|
+
authStatus = cam.authenticate(identity, trustedConnectionName: certName)
|
|
520
|
+
if authStatus == .pending {
|
|
521
|
+
FlirLogger.log(.connection, "Authentication pending... attempt \(attempts + 1)")
|
|
498
522
|
Thread.sleep(forTimeInterval: 1.0)
|
|
523
|
+
attempts += 1
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
FlirLogger.log(.connection, "Authentication status: \(authStatus.rawValue)")
|
|
527
|
+
if authStatus == .pending {
|
|
528
|
+
FlirLogger.logError(.connection, "Authentication timed out for device: \(identity.deviceId())")
|
|
529
|
+
DispatchQueue.main.async { [weak self] in
|
|
530
|
+
self?.delegate?.onError("Authentication timed out")
|
|
499
531
|
}
|
|
532
|
+
return
|
|
500
533
|
}
|
|
501
|
-
FlirLogger.log(.connection, "Authentication status: \(status.rawValue)")
|
|
502
534
|
}
|
|
503
535
|
|
|
504
536
|
do {
|
|
@@ -512,6 +544,7 @@ import ThermalSDK
|
|
|
512
544
|
FlirLogger.log(.connection, "Step 2: Connecting to device...")
|
|
513
545
|
try cam.connect()
|
|
514
546
|
FlirLogger.log(.connection, "✅ Connected successfully to: \(identity.deviceId())")
|
|
547
|
+
|
|
515
548
|
|
|
516
549
|
// Update state
|
|
517
550
|
connectedIdentity = identity
|
|
@@ -609,10 +642,9 @@ import ThermalSDK
|
|
|
609
642
|
FlirLogger.log(.streaming, "Stopping stream...")
|
|
610
643
|
|
|
611
644
|
#if FLIR_ENABLED
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
_isStreaming = false
|
|
645
|
+
// Cancel any pending watchdog to avoid false positives after an explicit stop
|
|
646
|
+
streamWatchdogWorkItem?.cancel()
|
|
647
|
+
streamWatchdogWorkItem = nil
|
|
616
648
|
emitStateChange("connected")
|
|
617
649
|
FlirLogger.log(.streaming, "Stream stopped")
|
|
618
650
|
#endif
|
|
@@ -637,6 +669,28 @@ import ThermalSDK
|
|
|
637
669
|
_isStreaming = true
|
|
638
670
|
emitStateChange("streaming")
|
|
639
671
|
FlirLogger.log(.streaming, "✅ Stream started successfully (thermal=\(newStream.isThermal))")
|
|
672
|
+
|
|
673
|
+
// Schedule a short watchdog to detect silent no-frame scenarios.
|
|
674
|
+
streamWatchdogWorkItem?.cancel()
|
|
675
|
+
let watch = DispatchWorkItem { [weak self] in
|
|
676
|
+
guard let self = self else { return }
|
|
677
|
+
if self._latestImage == nil {
|
|
678
|
+
FlirLogger.logError(.streaming, "No frames received within watchdog period after stream start")
|
|
679
|
+
// Attempt a soft restart
|
|
680
|
+
do {
|
|
681
|
+
self.stream?.stop()
|
|
682
|
+
try self.stream?.start()
|
|
683
|
+
FlirLogger.log(.streaming, "Attempted stream restart")
|
|
684
|
+
} catch {
|
|
685
|
+
FlirLogger.logError(.streaming, "Stream restart failed", error: error)
|
|
686
|
+
DispatchQueue.main.async {
|
|
687
|
+
self.delegate?.onError("No frames received after starting stream")
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
streamWatchdogWorkItem = watch
|
|
693
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: watch)
|
|
640
694
|
} catch {
|
|
641
695
|
FlirLogger.logError(.streaming, "Stream start failed", error: error)
|
|
642
696
|
stream = nil
|
|
@@ -916,7 +970,11 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
|
|
|
916
970
|
public func discoveryError(_ error: String, netServiceError nsnetserviceserror: Int32, on iface: FLIRCommunicationInterface) {
|
|
917
971
|
FlirLogger.logError(.discovery, "Discovery error: \(error) (code=\(nsnetserviceserror)) on interface: \(iface)")
|
|
918
972
|
|
|
973
|
+
// Stop scanning and emit current device list so UI can recover
|
|
974
|
+
discovery?.stop()
|
|
975
|
+
isScanning = false
|
|
919
976
|
DispatchQueue.main.async { [weak self] in
|
|
977
|
+
self?.delegate?.onDevicesFound(self?.discoveredDevices ?? [])
|
|
920
978
|
self?.delegate?.onError("Discovery error: \(error)")
|
|
921
979
|
}
|
|
922
980
|
}
|
|
@@ -924,6 +982,18 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
|
|
|
924
982
|
public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
|
|
925
983
|
FlirLogger.log(.discovery, "Discovery finished on interface: \(iface)")
|
|
926
984
|
isScanning = false
|
|
985
|
+
// Emit final list so consumers can update even if discovery ended quietly
|
|
986
|
+
DispatchQueue.main.async { [weak self] in
|
|
987
|
+
guard let self = self else { return }
|
|
988
|
+
self.delegate?.onDevicesFound(self.discoveredDevices)
|
|
989
|
+
// If no devices were found, emit an explicit no_device_found state
|
|
990
|
+
if self.discoveredDevices.isEmpty {
|
|
991
|
+
self.emitStateChange("no_device_found")
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Cancel the timeout if discovery finished normally
|
|
995
|
+
discoveryTimeoutWorkItem?.cancel()
|
|
996
|
+
discoveryTimeoutWorkItem = nil
|
|
927
997
|
}
|
|
928
998
|
}
|
|
929
999
|
|
|
@@ -155,9 +155,14 @@ RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
|
155
155
|
if (manager) {
|
|
156
156
|
NSArray *devices = ((NSArray * (*)(id, SEL))
|
|
157
157
|
objc_msgSend)(manager, sel_registerName("getDiscoveredDevices"));
|
|
158
|
-
if
|
|
159
|
-
|
|
158
|
+
// Re-emit even if the device list is empty so JS listeners can
|
|
159
|
+
// observe a 'no devices found' state and avoid hanging while waiting.
|
|
160
|
+
if (devices) {
|
|
161
|
+
NSLog(@"[FlirModule] addListener - re-emitting discovered devices (count=%lu)", (unsigned long)devices.count);
|
|
160
162
|
[self onDevicesFound:devices];
|
|
163
|
+
} else {
|
|
164
|
+
NSLog(@"[FlirModule] addListener - no discovered devices available to re-emit");
|
|
165
|
+
[self onDevicesFound:@[]];
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
168
|
});
|