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
- emitError(message)
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
- streamer = new ThermalStreamer(thermalStream);
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 = null;
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
- discovery?.stop()
436
- isScanning = false
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
- // Handle authentication for generic cameras (network cameras)
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 status = FLIRAuthenticationStatus.pending
494
- while status == .pending {
495
- status = cam.authenticate(identity, trustedConnectionName: certName)
496
- if status == .pending {
497
- FlirLogger.log(.connection, "Waiting for camera authentication approval...")
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
- stream?.stop()
613
- stream = nil
614
- streamer = nil
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 (devices && devices.count > 0) {
159
- NSLog(@"[FlirModule] addListener - re-emitting %lu discovered devices", (unsigned long)devices.count);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
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",