ilabs-flir 2.4.7 → 2.4.9

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/Flir.podspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'Flir'
3
- s.version = '2.2.31'
3
+ s.version = '2.4.9'
4
4
  s.summary = 'FLIR Thermal SDK React Native - Bundled via postinstall'
5
5
  s.description = <<-DESC
6
6
  A React Native wrapper for the FLIR Thermal SDK, providing thermal imaging
@@ -387,6 +387,8 @@ object FlirManager {
387
387
  }
388
388
  ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
389
389
  .emit("FlirDeviceConnected", params)
390
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
391
+ .emit("FlirStateChanged", params)
390
392
  } catch (e: Exception) { }
391
393
  }
392
394
 
@@ -60,6 +60,7 @@ public class FlirSdkManager {
60
60
  private final AtomicBoolean isProcessingFrame = new AtomicBoolean(false);
61
61
  private boolean useHalfScale = false;
62
62
  private String pendingSnapshotPath = null;
63
+ private volatile List<Palette> cachedSdkPalettes = null;
63
64
 
64
65
  // Listener
65
66
  private Listener listener;
@@ -86,7 +87,37 @@ public class FlirSdkManager {
86
87
  private volatile SnapshotCallback snapshotCallback;
87
88
 
88
89
  private FlirSdkManager(Context context) {
89
- this.context = context.getApplicationContext();
90
+ // We wrap the Application context to intercept and swallow IllegalArgumentException
91
+ // during unregisterReceiver. The FLIR SDK has a bug in WifiScanner where it attempts
92
+ // to unregister a receiver that wasn't registered, which bubbles up to JNI and crashes
93
+ // the app via std::terminate.
94
+ this.context = new android.content.ContextWrapper(context.getApplicationContext()) {
95
+ @Override
96
+ public Context getApplicationContext() {
97
+ return this;
98
+ }
99
+
100
+ @Override
101
+ public android.content.Intent registerReceiver(android.content.BroadcastReceiver receiver, android.content.IntentFilter filter) {
102
+ try {
103
+ return super.registerReceiver(receiver, filter);
104
+ } catch (Exception e) {
105
+ Log.w(TAG, "Suppressed registerReceiver error: " + e.getMessage());
106
+ return null;
107
+ }
108
+ }
109
+
110
+ @Override
111
+ public void unregisterReceiver(android.content.BroadcastReceiver receiver) {
112
+ try {
113
+ super.unregisterReceiver(receiver);
114
+ } catch (IllegalArgumentException e) {
115
+ Log.w(TAG, "Suppressed SDK crash: Receiver not registered: " + e.getMessage());
116
+ } catch (Exception e) {
117
+ Log.w(TAG, "Suppressed unregisterReceiver error: " + e.getMessage());
118
+ }
119
+ }
120
+ };
90
121
  }
91
122
 
92
123
  public static synchronized FlirSdkManager getInstance(Context context) {
@@ -106,8 +137,51 @@ public class FlirSdkManager {
106
137
  if (isInitialized.compareAndSet(false, true)) {
107
138
  Log.d(TAG, "Initializing FLIR SDK (async)...");
108
139
 
140
+ // Register a main thread looper protector to completely safeguard against WifiScanner supplicant receiver crashes
141
+ new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
142
+ @Override
143
+ public void run() {
144
+ while (true) {
145
+ try {
146
+ android.os.Looper.loop();
147
+ } catch (Throwable t) {
148
+ String msg = t.getMessage();
149
+ boolean isSuppressed = false;
150
+
151
+ // Check if this is the notorious FLIR supplicant receiver crash
152
+ if (t instanceof RuntimeException && t.getCause() instanceof IllegalArgumentException) {
153
+ String causeMsg = t.getCause().getMessage();
154
+ if (causeMsg != null && (causeMsg.contains("Receiver not registered") || causeMsg.contains("WifiScanner"))) {
155
+ isSuppressed = true;
156
+ }
157
+ } else if (msg != null && (msg.contains("Receiver not registered") || msg.contains("WifiScanner") || msg.contains("supplicant.STATE_CHANGE"))) {
158
+ isSuppressed = true;
159
+ }
160
+
161
+ if (isSuppressed) {
162
+ Log.w(TAG, "🔒 [FLIR LOOPER PROTECTOR] Intercepted and swallowed broadcast receiver crash: " + t.getMessage());
163
+ } else {
164
+ // For all other unexpected exceptions, forward to the standard uncaught exception handler
165
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
166
+ if (handler != null) {
167
+ handler.uncaughtException(Thread.currentThread(), t);
168
+ }
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ });
175
+
109
176
  executor.execute(() -> {
110
177
  try {
178
+ try {
179
+ System.loadLibrary("c++_flir");
180
+ Log.d(TAG, "Successfully loaded legacy libc++_flir.so for FLIR SDK.");
181
+ } catch (UnsatisfiedLinkError e) {
182
+ Log.e(TAG, "Could not explicitly load libc++_flir.so. FLIR initialization might fail.", e);
183
+ }
184
+
111
185
  ThermalSdkAndroid.init(context);
112
186
 
113
187
  // Small delay to ensure JNI linkage is stable
@@ -436,12 +510,24 @@ public class FlirSdkManager {
436
510
  final String paletteToApply = currentPaletteName;
437
511
  final String snapshotPath = pendingSnapshotPath;
438
512
  pendingSnapshotPath = null;
439
-
440
513
  streamer.withThermalImage(thermalImage -> {
441
514
  // 1. Apply Palette
442
515
  if (paletteToApply != null) {
443
516
  try {
444
- List<Palette> sdkPalettes = PaletteManager.getDefaultPalettes();
517
+ List<Palette> sdkPalettes = cachedSdkPalettes;
518
+ if (sdkPalettes == null) {
519
+ synchronized (FlirSdkManager.this) {
520
+ sdkPalettes = cachedSdkPalettes;
521
+ if (sdkPalettes == null) {
522
+ try {
523
+ sdkPalettes = PaletteManager.getDefaultPalettes();
524
+ cachedSdkPalettes = sdkPalettes;
525
+ } catch (Throwable t) {
526
+ Log.e(TAG, "Failed to get default palettes", t);
527
+ }
528
+ }
529
+ }
530
+ }
445
531
 
446
532
  if (paletteToApply.equalsIgnoreCase("Gray") || paletteToApply.equalsIgnoreCase("grayscale")) {
447
533
  // User wants Gray - map to WhiteHot which is the SDK's standard grayscale
@@ -89,6 +89,7 @@ import ThermalSDK
89
89
  private var stream: FLIRStream?
90
90
  private var streamer: FLIRThermalStreamer?
91
91
  private var identityMap: [String: FLIRIdentity] = [:]
92
+ private var cachedSdkPalettes: [FLIRPalette]? = nil
92
93
  #endif
93
94
 
94
95
  private override init() {
@@ -203,54 +204,39 @@ import ThermalSDK
203
204
  let camType = identity.cameraType()
204
205
  NSLog("[FlirManager] Camera type: \(camType.rawValue), interface: \(iface.rawValue)")
205
206
 
206
- // ── AUTHENTICATE for network cameras ──
207
- // Official FLIR CameraConnector sample checks .generic camera type,
208
- // but FLIR One Edge Pro over network may report a different type.
209
- // Check BOTH: camera type == .generic OR interface contains .network/.flirOneWireless
210
- let needsAuth = (camType == .generic) || iface.contains(.network) || iface.contains(.flirOneWireless)
207
+ let isNetwork = (camType == .generic) || iface.contains(.network)
211
208
 
212
- if needsAuth {
213
- NSLog("[FlirManager] Network camera detected authenticating...")
214
-
215
- // Use UUID-based persistent certificate name (matches FLIR sample).
216
- // The camera has a bug where re-auth with a different name can conflict,
217
- // so we generate a UUID once and persist it in UserDefaults.
218
- let certName = self.getPersistentCertificateName()
219
- NSLog("[FlirManager] Using certificate name: \(certName)")
209
+ // ── STEP 1: INITIAL TRUST HANDSHAKE (Network Cameras Only) ──
210
+ // DiscoverySampleSwift shows that for network cameras we first authenticate
211
+ // with a user-friendly device/app name to trigger the "Trust this device" prompt.
212
+ if isNetwork {
213
+ NSLog("[FlirManager] Network camera detected initiating initial trust request...")
214
+ let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "AppFactory"
215
+ let trustName = "\(UIDevice.current.name) \(appName)"
220
216
 
221
217
  var status = FLIRAuthenticationStatus.pending
222
218
  var attempts = 0
223
- let maxAttempts = 30 // ~30 seconds timeout
219
+ let maxAttempts = 15 // 15 seconds window for initial prompt
224
220
 
225
221
  while status == .pending && attempts < maxAttempts {
226
- status = cam.authenticate(identity, trustedConnectionName: certName)
227
- NSLog("[FlirManager] Auth attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
228
-
222
+ status = cam.authenticate(identity, trustedConnectionName: trustName)
223
+ NSLog("[FlirManager] Initial trust attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
229
224
  if status == .pending {
230
- // Camera waiting for user to press "Trust" on its screen
231
225
  Thread.sleep(forTimeInterval: 1.0)
232
226
  }
233
227
  attempts += 1
234
228
  }
235
-
236
- if status != .approved {
237
- NSLog("[FlirManager] Authentication failed/timed out: \(status.rawValue)")
238
- self.camera = nil
239
- DispatchQueue.main.async {
240
- self.emitStateChange("connection_failed")
241
- self.delegate?.onError("Camera authentication failed. Check the camera screen for a trust/approve prompt.")
242
- }
243
- return
244
- }
245
- NSLog("[FlirManager] Authentication approved ✅")
246
229
  }
247
230
 
248
- // ── PAIR ──
231
+ // ── STEP 2: PAIRING (Always do this before final authentication!) ──
232
+ // Both FLIROneCameraSwift and DiscoverySampleSwift require pairing to be established
233
+ // before the camera's communication channels are authenticated and connected.
249
234
  do {
235
+ NSLog("[FlirManager] Pairing camera with code 0...")
250
236
  try cam.pair(identity, code: 0)
251
- NSLog("[FlirManager] Pair succeeded")
237
+ NSLog("[FlirManager] Pairing succeeded")
252
238
  } catch {
253
- NSLog("[FlirManager] Pair failed: \(error)")
239
+ NSLog("[FlirManager] Pairing failed: \(error)")
254
240
  self._isConnected = false
255
241
  self.camera = nil
256
242
  DispatchQueue.main.async {
@@ -260,6 +246,37 @@ import ThermalSDK
260
246
  return
261
247
  }
262
248
 
249
+ // ── STEP 3: FINAL AUTHENTICATION ──
250
+ // Use a persistent UUID certificate for network/wireless cameras to avoid trust conflicts.
251
+ // For standard classic FLIR ONE/Edge devices, use "dummy" as per FLIROneCameraSwift ViewController.swift.
252
+ let certName = isNetwork ? self.getPersistentCertificateName() : "dummy"
253
+ NSLog("[FlirManager] Performing final authentication with name: \(certName)")
254
+
255
+ var status = FLIRAuthenticationStatus.pending
256
+ var attempts = 0
257
+ let maxAttempts = 30 // ~30 seconds timeout
258
+
259
+ while status == .pending && attempts < maxAttempts {
260
+ status = cam.authenticate(identity, trustedConnectionName: certName)
261
+ NSLog("[FlirManager] Final auth attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
262
+
263
+ if status == .pending {
264
+ Thread.sleep(forTimeInterval: 1.0)
265
+ }
266
+ attempts += 1
267
+ }
268
+
269
+ if status != .approved && isNetwork {
270
+ NSLog("[FlirManager] Final authentication failed or timed out: \(status.rawValue)")
271
+ self.camera = nil
272
+ DispatchQueue.main.async {
273
+ self.emitStateChange("connection_failed")
274
+ self.delegate?.onError("Camera authentication failed. Check the camera screen for a trust/approve prompt.")
275
+ }
276
+ return
277
+ }
278
+ NSLog("[FlirManager] Authentication approved ✅")
279
+
263
280
  // ── CONNECT ──
264
281
  do {
265
282
  try cam.connect()
@@ -737,77 +754,91 @@ extension FlirManager: FLIRStreamDelegate {
737
754
  _isProcessingFrame = true
738
755
  renderQueue.async { [weak self] in
739
756
  defer { self?._isProcessingFrame = false }
740
- guard let self = self, self._isStreaming, let streamer = self.streamer else {
741
- return
742
- }
743
-
744
- objc_sync_enter(self.stateLock)
745
- let currentStreamer = self.streamer
746
- let streaming = self._isStreaming
747
- objc_sync_exit(self.stateLock)
748
-
749
- guard streaming, let streamer = currentStreamer else {
750
- return
751
- }
757
+ autoreleasepool {
758
+ guard let self = self, self._isStreaming, let streamer = self.streamer else {
759
+ return
760
+ }
761
+
762
+ objc_sync_enter(self.stateLock)
763
+ let currentStreamer = self.streamer
764
+ let streaming = self._isStreaming
765
+ objc_sync_exit(self.stateLock)
766
+
767
+ guard streaming, let streamer = currentStreamer else {
768
+ return
769
+ }
752
770
 
753
- let paletteToApply = self.currentPaletteName
754
- let snapshotPath = self.pendingSnapshotPath
755
- self.pendingSnapshotPath = nil
771
+ let paletteToApply = self.currentPaletteName
772
+ let snapshotPath = self.pendingSnapshotPath
773
+ self.pendingSnapshotPath = nil
756
774
 
757
- do {
758
- try streamer.update()
759
-
760
- streamer.withThermalImage { thermalImage in
761
- // 1. Apply Palette
762
- guard let paletteManager = thermalImage.paletteManager,
763
- let sdkPalettes = paletteManager.getDefaultPalettes() else { return }
764
- var targetPalette: FLIRPalette? = nil
775
+ do {
776
+ try streamer.update()
765
777
 
766
- if paletteToApply.lowercased() == "gray" || paletteToApply.lowercased() == "grayscale" {
767
- // Map Gray to WhiteHot (standard SDK name)
768
- targetPalette = sdkPalettes.first(where: {
769
- $0.name.lowercased() == "whitehot" || $0.name.lowercased() == "white hot"
770
- })
771
- } else {
772
- targetPalette = sdkPalettes.first(where: { $0.name.lowercased() == paletteToApply.lowercased() })
778
+ streamer.withThermalImage { thermalImage in
779
+ // 1. Apply Palette
780
+ guard let paletteManager = thermalImage.paletteManager else { return }
781
+
782
+ var sdkPalettes = self.cachedSdkPalettes
783
+ if sdkPalettes == nil {
784
+ objc_sync_enter(self.stateLock)
785
+ sdkPalettes = self.cachedSdkPalettes
786
+ if sdkPalettes == nil {
787
+ sdkPalettes = paletteManager.getDefaultPalettes() as? [FLIRPalette]
788
+ self.cachedSdkPalettes = sdkPalettes
789
+ }
790
+ objc_sync_exit(self.stateLock)
791
+ }
773
792
 
774
- // Fallback for Wheel
775
- if targetPalette == nil && paletteToApply.lowercased() == "wheel" {
776
- targetPalette = sdkPalettes.first(where: {
777
- $0.name.contains("Wheel") || $0.name.contains("ColorWheel") || $0.name.contains("Rainbow")
793
+ guard let palettes = sdkPalettes else { return }
794
+ var targetPalette: FLIRPalette? = nil
795
+
796
+ if paletteToApply.lowercased() == "gray" || paletteToApply.lowercased() == "grayscale" {
797
+ // Map Gray to WhiteHot (standard SDK name)
798
+ targetPalette = palettes.first(where: {
799
+ $0.name.lowercased() == "whitehot" || $0.name.lowercased() == "white hot"
778
800
  })
801
+ } else {
802
+ targetPalette = palettes.first(where: { $0.name.lowercased() == paletteToApply.lowercased() })
803
+
804
+ // Fallback for Wheel
805
+ if targetPalette == nil && paletteToApply.lowercased() == "wheel" {
806
+ targetPalette = palettes.first(where: {
807
+ $0.name.contains("Wheel") || $0.name.contains("ColorWheel") || $0.name.contains("Rainbow")
808
+ })
809
+ }
810
+ }
811
+
812
+ if let palette = targetPalette {
813
+ thermalImage.palette = palette
779
814
  }
780
- }
781
-
782
- if let palette = targetPalette {
783
- thermalImage.palette = palette
784
- }
785
815
 
786
- // 2. Save Radiometric Snapshot if requested
787
- if let path = snapshotPath {
788
- do {
789
- try thermalImage.save(as: path)
790
- NSLog("[FlirManager] Radiometric snapshot saved to: \(path)")
791
- } catch {
792
- NSLog("[FlirManager] Failed to save radiometric snapshot: \(error)")
816
+ // 2. Save Radiometric Snapshot if requested
817
+ if let path = snapshotPath {
818
+ do {
819
+ try thermalImage.save(as: path)
820
+ NSLog("[FlirManager] Radiometric snapshot saved to: \(path)")
821
+ } catch {
822
+ NSLog("[FlirManager] Failed to save radiometric snapshot: \(error)")
823
+ }
793
824
  }
794
- }
795
825
 
796
- // 3. Generate UIImage for display
797
- // Grab the image while the thermal image is locked to ensure settings are applied
798
- if let image = streamer.getImage() {
799
- self._latestImage = image
800
- let width = Int(image.size.width)
801
- let height = Int(image.size.height)
802
-
803
- DispatchQueue.main.async { [weak self] in
804
- self?.delegate?.onFrameReceived(image, width: width, height: height)
826
+ // 3. Generate UIImage for display
827
+ // Grab the image while the thermal image is locked to ensure settings are applied
828
+ if let image = streamer.getImage() {
829
+ self._latestImage = image
830
+ let width = Int(image.size.width)
831
+ let height = Int(image.size.height)
832
+
833
+ DispatchQueue.main.async { [weak self] in
834
+ self?.delegate?.onFrameReceived(image, width: width, height: height)
835
+ }
805
836
  }
806
837
  }
838
+ } catch {
839
+ NSLog("[FlirManager] Streamer update failed: \(error)")
840
+ return
807
841
  }
808
- } catch {
809
- NSLog("[FlirManager] Streamer update failed: \(error)")
810
- return
811
842
  }
812
843
  }
813
844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.4.7",
3
+ "version": "2.4.9",
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",