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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let
|
|
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 =
|
|
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:
|
|
227
|
-
NSLog("[FlirManager]
|
|
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
|
-
// ──
|
|
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]
|
|
237
|
+
NSLog("[FlirManager] Pairing succeeded ✅")
|
|
252
238
|
} catch {
|
|
253
|
-
NSLog("[FlirManager]
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
771
|
+
let paletteToApply = self.currentPaletteName
|
|
772
|
+
let snapshotPath = self.pendingSnapshotPath
|
|
773
|
+
self.pendingSnapshotPath = nil
|
|
756
774
|
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
767
|
-
//
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
}
|