ilabs-flir 2.2.5 → 2.2.7

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.
@@ -49,11 +49,7 @@ import ThermalSDK
49
49
  }
50
50
 
51
51
  /// Main FLIR Manager - Singleton that manages all FLIR camera operations
52
- #if FLIR_ENABLED
53
- @objc public class FlirManager: NSObject, FLIRDiscoveryEventDelegate, FLIRDataReceivedDelegate, FLIRStreamDelegate {
54
- #else
55
52
  @objc public class FlirManager: NSObject {
56
- #endif
57
53
  @objc public static let shared = FlirManager()
58
54
 
59
55
  // MARK: - Properties
@@ -78,9 +74,8 @@ import ThermalSDK
78
74
  // Client lifecycle for discovery/connection ownership
79
75
  private var activeClients: Set<String> = []
80
76
  private var shutdownWorkItem: DispatchWorkItem? = nil
81
- // Discovery & stream watchdogs to detect silent failures/timeouts
77
+ // Discovery timeout to prevent infinite scanning
82
78
  private var discoveryTimeoutWorkItem: DispatchWorkItem? = nil
83
- private var streamWatchdogWorkItem: DispatchWorkItem? = nil
84
79
 
85
80
  // Battery polling timer (like Android)
86
81
  private var batteryPollingTimer: Timer?
@@ -93,6 +88,7 @@ import ThermalSDK
93
88
  private var stream: FLIRStream?
94
89
  private var streamer: FLIRThermalStreamer?
95
90
  private var connectedIdentity: FLIRIdentity?
91
+ private var identityMap: [String: FLIRIdentity] = [:]
96
92
  #endif
97
93
 
98
94
  private override init() {
@@ -375,12 +371,7 @@ import ThermalSDK
375
371
 
376
372
  if discovery == nil {
377
373
  discovery = FLIRDiscovery()
378
- // Assign delegate only if we conform to the required discovery delegate protocol
379
- if let dd = self as? FLIRDiscoveryEventDelegate {
380
- discovery?.delegate = dd
381
- } else {
382
- FlirLogger.log(.discovery, "Warning: FlirManager does not conform to FLIRDiscoveryEventDelegate at runtime; delegate not assigned")
383
- }
374
+ discovery?.delegate = self
384
375
  FlirLogger.log(.discovery, "Created FLIRDiscovery instance")
385
376
  }
386
377
 
@@ -411,27 +402,26 @@ import ThermalSDK
411
402
  discovery?.start(interfaces)
412
403
 
413
404
  emitStateChange("discovering")
414
-
415
- // Cancel any previous discovery timeout and schedule a new one so
416
- // the UI doesn't remain stuck when discovery yields no devices.
405
+
406
+ // Set timeout to prevent infinite scanning (matches Android's 8-second timeout)
417
407
  discoveryTimeoutWorkItem?.cancel()
418
- let discWork = DispatchWorkItem { [weak self] in
419
- guard let self = self else { return }
420
- if self.discoveredDevices.isEmpty && self.isScanning {
421
- FlirLogger.logError(.discovery, "Discovery timed out with no devices")
422
- self.discovery?.stop()
423
- self.isScanning = false
424
- DispatchQueue.main.async {
425
- // Notify native listeners that discovery finished with no devices
426
- self.delegate?.onDevicesFound(self.discoveredDevices)
427
- // Emit a state change so JS can set noDeviceFound = true
408
+ let timeoutWork = DispatchWorkItem { [weak self] in
409
+ guard let self = self, self.isScanning else { return }
410
+ FlirLogger.log(.discovery, "⏱ Discovery timeout reached - stopping scan")
411
+ self.discovery?.stop()
412
+ self.isScanning = false
413
+
414
+ // Emit final device list and state
415
+ DispatchQueue.main.async {
416
+ self.delegate?.onDevicesFound(self.discoveredDevices)
417
+ if self.discoveredDevices.isEmpty {
428
418
  self.emitStateChange("no_device_found")
429
- self.delegate?.onError("Discovery timed out")
419
+ self.delegate?.onError("No FLIR devices found")
430
420
  }
431
421
  }
432
422
  }
433
- discoveryTimeoutWorkItem = discWork
434
- DispatchQueue.main.asyncAfter(deadline: .now() + 8.0, execute: discWork)
423
+ discoveryTimeoutWorkItem = timeoutWork
424
+ DispatchQueue.main.asyncAfter(deadline: .now() + 8.0, execute: timeoutWork)
435
425
  #else
436
426
  FlirLogger.logError(.discovery, "FLIR SDK not available - discovery disabled")
437
427
  delegate?.onError("FLIR SDK not available")
@@ -470,28 +460,38 @@ import ThermalSDK
470
460
  discovery?.stop()
471
461
  isScanning = false
472
462
  FlirLogger.log(.discovery, "Discovery stopped")
473
- #endif
463
+ #endif
464
+ }
465
+
466
+ // MARK: - Connection
467
+
468
+ @objc public func connectToDevice(_ deviceId: String) {
469
+ FlirLogger.logConnectionAttempt(deviceId: deviceId)
474
470
 
475
471
  #if FLIR_ENABLED
476
472
  // Find the identity for this device
477
473
  guard let identity = findIdentity(for: deviceId) else {
478
474
  FlirLogger.logError(.connection, "Device not found in identity map: \(deviceId)")
479
- delegate?.onError("Device not found: \(deviceId)")
475
+ DispatchQueue.main.async { [weak self] in
476
+ self?.emitStateChange("connection_failed")
477
+ self?.delegate?.onError("Device not found: \(deviceId)")
478
+ }
480
479
  return
481
480
  }
482
481
 
482
+ // Run connection on background thread with timeout monitoring
483
483
  DispatchQueue.global(qos: .userInitiated).async { [weak self] in
484
484
  self?.performConnection(identity: identity)
485
485
  }
486
486
  #else
487
487
  FlirLogger.logError(.connection, "FLIR SDK not available")
488
- delegate?.onError("FLIR SDK not available")
488
+ DispatchQueue.main.async { [weak self] in
489
+ self?.delegate?.onError("FLIR SDK not available")
490
+ }
489
491
  #endif
490
492
  }
491
493
 
492
494
  #if FLIR_ENABLED
493
- private var identityMap: [String: FLIRIdentity] = [:]
494
-
495
495
  private func findIdentity(for deviceId: String) -> FLIRIdentity? {
496
496
  return identityMap[deviceId]
497
497
  }
@@ -515,26 +515,37 @@ import ThermalSDK
515
515
  return
516
516
  }
517
517
 
518
- // Fail-fast authentication for generic/network cameras (prevent indefinite blocking)
518
+ // Handle authentication for generic cameras (network cameras)
519
519
  if identity.cameraType() == .generic {
520
520
  FlirLogger.log(.connection, "Generic/network camera - starting authentication...")
521
521
  let certName = getCertificateName()
522
- var authStatus = FLIRAuthenticationStatus.pending
522
+ var status = FLIRAuthenticationStatus.pending
523
523
  var attempts = 0
524
- let maxAttempts = 10
525
- while authStatus == .pending && attempts < maxAttempts {
526
- authStatus = cam.authenticate(identity, trustedConnectionName: certName)
527
- if authStatus == .pending {
528
- FlirLogger.log(.connection, "Authentication pending... attempt \(attempts + 1)")
524
+ let maxAttempts = 10 // 10 seconds max
525
+
526
+ while status == .pending && attempts < maxAttempts {
527
+ status = cam.authenticate(identity, trustedConnectionName: certName)
528
+ if status == .pending {
529
+ FlirLogger.log(.connection, "Waiting for camera authentication approval... (\(attempts + 1)/\(maxAttempts))")
529
530
  Thread.sleep(forTimeInterval: 1.0)
530
531
  attempts += 1
531
532
  }
532
533
  }
533
- FlirLogger.log(.connection, "Authentication status: \(authStatus.rawValue)")
534
- if authStatus == .pending {
535
- FlirLogger.logError(.connection, "Authentication timed out for device: \(identity.deviceId())")
534
+
535
+ if status == .pending {
536
+ FlirLogger.logError(.connection, "Authentication timeout after \(maxAttempts) seconds")
537
+ DispatchQueue.main.async { [weak self] in
538
+ self?.delegate?.onError("Camera authentication timeout - device may require approval")
539
+ }
540
+ return
541
+ }
542
+
543
+ FlirLogger.log(.connection, "Authentication status: \(status.rawValue)")
544
+
545
+ if status != .authenticated {
546
+ FlirLogger.logError(.connection, "Authentication failed with status: \(status.rawValue)")
536
547
  DispatchQueue.main.async { [weak self] in
537
- self?.delegate?.onError("Authentication timed out")
548
+ self?.delegate?.onError("Camera authentication failed")
538
549
  }
539
550
  return
540
551
  }
@@ -548,10 +559,10 @@ import ThermalSDK
548
559
  FlirLogger.log(.connection, "✅ Paired successfully with: \(identity.deviceId())")
549
560
 
550
561
  // Step 2: Connect (no identity parameter - uses paired identity)
562
+ // Note: This can hang on some devices - ensure we have timeout in place
551
563
  FlirLogger.log(.connection, "Step 2: Connecting to device...")
552
564
  try cam.connect()
553
565
  FlirLogger.log(.connection, "✅ Connected successfully to: \(identity.deviceId())")
554
-
555
566
 
556
567
  // Update state
557
568
  connectedIdentity = identity
@@ -603,8 +614,14 @@ import ThermalSDK
603
614
  } catch {
604
615
  FlirLogger.logError(.connection, "Connection failed", error: error)
605
616
  _isConnected = false
617
+ _isStreaming = false
618
+ connectedDeviceId = nil
619
+ connectedDeviceName = nil
606
620
  camera = nil
621
+
622
+ // CRITICAL: Always emit error state to RN so UI doesn't hang waiting
607
623
  DispatchQueue.main.async { [weak self] in
624
+ self?.emitStateChange("connection_failed")
608
625
  self?.delegate?.onError("Connection failed: \(error.localizedDescription)")
609
626
  }
610
627
  }
@@ -649,9 +666,10 @@ import ThermalSDK
649
666
  FlirLogger.log(.streaming, "Stopping stream...")
650
667
 
651
668
  #if FLIR_ENABLED
652
- // Cancel any pending watchdog to avoid false positives after an explicit stop
653
- streamWatchdogWorkItem?.cancel()
654
- streamWatchdogWorkItem = nil
669
+ stream?.stop()
670
+ stream = nil
671
+ streamer = nil
672
+ _isStreaming = false
655
673
  emitStateChange("connected")
656
674
  FlirLogger.log(.streaming, "Stream stopped")
657
675
  #endif
@@ -676,28 +694,6 @@ import ThermalSDK
676
694
  _isStreaming = true
677
695
  emitStateChange("streaming")
678
696
  FlirLogger.log(.streaming, "✅ Stream started successfully (thermal=\(newStream.isThermal))")
679
-
680
- // Schedule a short watchdog to detect silent no-frame scenarios.
681
- streamWatchdogWorkItem?.cancel()
682
- let watch = DispatchWorkItem { [weak self] in
683
- guard let self = self else { return }
684
- if self._latestImage == nil {
685
- FlirLogger.logError(.streaming, "No frames received within watchdog period after stream start")
686
- // Attempt a soft restart
687
- do {
688
- self.stream?.stop()
689
- try self.stream?.start()
690
- FlirLogger.log(.streaming, "Attempted stream restart")
691
- } catch {
692
- FlirLogger.logError(.streaming, "Stream restart failed", error: error)
693
- DispatchQueue.main.async {
694
- self.delegate?.onError("No frames received after starting stream")
695
- }
696
- }
697
- }
698
- }
699
- streamWatchdogWorkItem = watch
700
- DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: watch)
701
697
  } catch {
702
698
  FlirLogger.logError(.streaming, "Stream start failed", error: error)
703
699
  stream = nil
@@ -977,12 +973,15 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
977
973
  public func discoveryError(_ error: String, netServiceError nsnetserviceserror: Int32, on iface: FLIRCommunicationInterface) {
978
974
  FlirLogger.logError(.discovery, "Discovery error: \(error) (code=\(nsnetserviceserror)) on interface: \(iface)")
979
975
 
980
- // Stop scanning and emit current device list so UI can recover
976
+ // Stop scanning and cancel timeout on error
977
+ discoveryTimeoutWorkItem?.cancel()
978
+ discoveryTimeoutWorkItem = nil
981
979
  discovery?.stop()
982
980
  isScanning = false
981
+
982
+ // Emit current device list (could be empty) so RN/UI can recover
983
983
  DispatchQueue.main.async { [weak self] in
984
984
  guard let self = self else { return }
985
- // Re-emit device list (could be empty) so JS/UI can recover
986
985
  self.delegate?.onDevicesFound(self.discoveredDevices)
987
986
  self.delegate?.onError("Discovery error: \(error)")
988
987
  }
@@ -991,23 +990,27 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
991
990
  public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
992
991
  FlirLogger.log(.discovery, "Discovery finished on interface: \(iface)")
993
992
  isScanning = false
994
- // Emit final list so consumers can update even if discovery ended quietly
993
+
994
+ // Cancel timeout since discovery finished normally
995
+ discoveryTimeoutWorkItem?.cancel()
996
+ discoveryTimeoutWorkItem = nil
997
+
998
+ // CRITICAL: Emit final device list so RN layer doesn't hang waiting for results
995
999
  DispatchQueue.main.async { [weak self] in
996
1000
  guard let self = self else { return }
997
1001
  self.delegate?.onDevicesFound(self.discoveredDevices)
998
- // If no devices were found, emit an explicit no_device_found state
1002
+ // If no devices were found, emit explicit state so UI can show "no devices"
999
1003
  if self.discoveredDevices.isEmpty {
1000
1004
  self.emitStateChange("no_device_found")
1001
1005
  }
1002
1006
  }
1003
- // Cancel the timeout if discovery finished normally
1004
- discoveryTimeoutWorkItem?.cancel()
1005
- discoveryTimeoutWorkItem = nil
1006
1007
  }
1007
1008
  }
1009
+ #endif
1008
1010
 
1009
1011
  // MARK: - FLIRDataReceivedDelegate
1010
1012
 
1013
+ #if FLIR_ENABLED
1011
1014
  extension FlirManager: FLIRDataReceivedDelegate {
1012
1015
  public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
1013
1016
  FlirLogger.logError(.disconnect, "Camera disconnected callback", error: error)
@@ -1029,9 +1032,12 @@ extension FlirManager: FLIRDataReceivedDelegate {
1029
1032
  }
1030
1033
  }
1031
1034
  }
1035
+ #endif
1032
1036
 
1033
1037
  // MARK: - FLIRStreamDelegate
1034
1038
 
1039
+ #if FLIR_ENABLED
1040
+
1035
1041
  extension FlirManager: FLIRStreamDelegate {
1036
1042
  public func onError(_ error: Error) {
1037
1043
  FlirLogger.logError(.streaming, "Stream error", error: error)
@@ -155,14 +155,9 @@ 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
- // 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);
158
+ if (devices && devices.count > 0) {
159
+ NSLog(@"[FlirModule] addListener - re-emitting %lu discovered devices", (unsigned long)devices.count);
162
160
  [self onDevicesFound:devices];
163
- } else {
164
- NSLog(@"[FlirModule] addListener - no discovered devices available to re-emit");
165
- [self onDevicesFound:@[]];
166
161
  }
167
162
  }
168
163
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
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",