ilabs-flir 2.4.6 → 2.4.8
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 +1 -1
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +2 -19
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +14 -13
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +96 -10
- package/ios/Flir/src/FlirManager.swift +48 -32
- package/package.json +1 -1
package/Flir.podspec
CHANGED
|
@@ -86,16 +86,6 @@ object FlirManager {
|
|
|
86
86
|
|
|
87
87
|
fun setPalette(name: String) {
|
|
88
88
|
sdkManager?.setPalette(name)
|
|
89
|
-
// Also try to update the app's global Var.cool if possible
|
|
90
|
-
try {
|
|
91
|
-
val palettes = getAvailablePalettes()
|
|
92
|
-
val idx = palettes.indexOfFirst { it.equals(name, ignoreCase = true) }
|
|
93
|
-
if (idx != -1) {
|
|
94
|
-
updateAcol(idx.toFloat())
|
|
95
|
-
}
|
|
96
|
-
} catch (e: Exception) {
|
|
97
|
-
Log.e(TAG, "setPalette: Failed to update Var.cool", e)
|
|
98
|
-
}
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
fun getAvailablePalettes(): List<String> {
|
|
@@ -397,6 +387,8 @@ object FlirManager {
|
|
|
397
387
|
}
|
|
398
388
|
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
399
389
|
.emit("FlirDeviceConnected", params)
|
|
390
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
391
|
+
.emit("FlirStateChanged", params)
|
|
400
392
|
} catch (e: Exception) { }
|
|
401
393
|
}
|
|
402
394
|
|
|
@@ -463,15 +455,6 @@ object FlirManager {
|
|
|
463
455
|
}
|
|
464
456
|
|
|
465
457
|
coolField?.set(null, shaderIdx)
|
|
466
|
-
|
|
467
|
-
// Standard FLIR palette list
|
|
468
|
-
val paletteNames = getAvailablePalettes()
|
|
469
|
-
val maxEff = paletteNames.size
|
|
470
|
-
val paletteIdx = rawIdx % maxEff
|
|
471
|
-
val safeIdx = if (paletteIdx < 0) paletteIdx + maxEff else paletteIdx
|
|
472
|
-
|
|
473
|
-
val targetPaletteName = paletteNames[safeIdx]
|
|
474
|
-
sdkManager?.setPalette(targetPaletteName)
|
|
475
458
|
|
|
476
459
|
} catch (e: Throwable) {
|
|
477
460
|
Log.w(TAG, "updateAcol reflection failed: ${e.message}")
|
|
@@ -34,7 +34,7 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
34
34
|
// Simple placeholder conversion: converts an ARGB color to a pseudo-temperature value.
|
|
35
35
|
// Replace with SDK call when integrating thermalsdk APIs.
|
|
36
36
|
@ReactMethod
|
|
37
|
-
fun getTemperatureFromColor(color: Int, promise:
|
|
37
|
+
fun getTemperatureFromColor(color: Int, promise: Promise?) {
|
|
38
38
|
try {
|
|
39
39
|
val r = (color shr 16) and 0xFF
|
|
40
40
|
val g = (color shr 8) and 0xFF
|
|
@@ -305,17 +305,18 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
305
305
|
|
|
306
306
|
@ReactMethod
|
|
307
307
|
fun stopFlir(promise: Promise?) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
308
|
+
if (isDebounced()) {
|
|
309
|
+
promise?.resolve(false)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
FlirManager.stop()
|
|
314
|
+
promise?.resolve(true)
|
|
315
|
+
} catch (e: Exception) {
|
|
316
|
+
promise?.reject("ERR_FLIR_STOP", e)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
319
320
|
|
|
320
321
|
@ReactMethod
|
|
321
322
|
fun simulateFlirContextLoss(promise: Promise?) {
|
|
@@ -440,7 +441,7 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
|
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
@ReactMethod
|
|
443
|
-
fun getDebugInfo(promise:
|
|
444
|
+
fun getDebugInfo(promise: Promise?) {
|
|
444
445
|
try {
|
|
445
446
|
val result = com.facebook.react.bridge.Arguments.createMap()
|
|
446
447
|
|
|
@@ -86,7 +86,37 @@ public class FlirSdkManager {
|
|
|
86
86
|
private volatile SnapshotCallback snapshotCallback;
|
|
87
87
|
|
|
88
88
|
private FlirSdkManager(Context context) {
|
|
89
|
-
|
|
89
|
+
// We wrap the Application context to intercept and swallow IllegalArgumentException
|
|
90
|
+
// during unregisterReceiver. The FLIR SDK has a bug in WifiScanner where it attempts
|
|
91
|
+
// to unregister a receiver that wasn't registered, which bubbles up to JNI and crashes
|
|
92
|
+
// the app via std::terminate.
|
|
93
|
+
this.context = new android.content.ContextWrapper(context.getApplicationContext()) {
|
|
94
|
+
@Override
|
|
95
|
+
public Context getApplicationContext() {
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Override
|
|
100
|
+
public android.content.Intent registerReceiver(android.content.BroadcastReceiver receiver, android.content.IntentFilter filter) {
|
|
101
|
+
try {
|
|
102
|
+
return super.registerReceiver(receiver, filter);
|
|
103
|
+
} catch (Exception e) {
|
|
104
|
+
Log.w(TAG, "Suppressed registerReceiver error: " + e.getMessage());
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Override
|
|
110
|
+
public void unregisterReceiver(android.content.BroadcastReceiver receiver) {
|
|
111
|
+
try {
|
|
112
|
+
super.unregisterReceiver(receiver);
|
|
113
|
+
} catch (IllegalArgumentException e) {
|
|
114
|
+
Log.w(TAG, "Suppressed SDK crash: Receiver not registered: " + e.getMessage());
|
|
115
|
+
} catch (Exception e) {
|
|
116
|
+
Log.w(TAG, "Suppressed unregisterReceiver error: " + e.getMessage());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
public static synchronized FlirSdkManager getInstance(Context context) {
|
|
@@ -106,8 +136,51 @@ public class FlirSdkManager {
|
|
|
106
136
|
if (isInitialized.compareAndSet(false, true)) {
|
|
107
137
|
Log.d(TAG, "Initializing FLIR SDK (async)...");
|
|
108
138
|
|
|
139
|
+
// Register a main thread looper protector to completely safeguard against WifiScanner supplicant receiver crashes
|
|
140
|
+
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
|
141
|
+
@Override
|
|
142
|
+
public void run() {
|
|
143
|
+
while (true) {
|
|
144
|
+
try {
|
|
145
|
+
android.os.Looper.loop();
|
|
146
|
+
} catch (Throwable t) {
|
|
147
|
+
String msg = t.getMessage();
|
|
148
|
+
boolean isSuppressed = false;
|
|
149
|
+
|
|
150
|
+
// Check if this is the notorious FLIR supplicant receiver crash
|
|
151
|
+
if (t instanceof RuntimeException && t.getCause() instanceof IllegalArgumentException) {
|
|
152
|
+
String causeMsg = t.getCause().getMessage();
|
|
153
|
+
if (causeMsg != null && (causeMsg.contains("Receiver not registered") || causeMsg.contains("WifiScanner"))) {
|
|
154
|
+
isSuppressed = true;
|
|
155
|
+
}
|
|
156
|
+
} else if (msg != null && (msg.contains("Receiver not registered") || msg.contains("WifiScanner") || msg.contains("supplicant.STATE_CHANGE"))) {
|
|
157
|
+
isSuppressed = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (isSuppressed) {
|
|
161
|
+
Log.w(TAG, "🔒 [FLIR LOOPER PROTECTOR] Intercepted and swallowed broadcast receiver crash: " + t.getMessage());
|
|
162
|
+
} else {
|
|
163
|
+
// For all other unexpected exceptions, forward to the standard uncaught exception handler
|
|
164
|
+
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
|
|
165
|
+
if (handler != null) {
|
|
166
|
+
handler.uncaughtException(Thread.currentThread(), t);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
109
175
|
executor.execute(() -> {
|
|
110
176
|
try {
|
|
177
|
+
try {
|
|
178
|
+
System.loadLibrary("c++_flir");
|
|
179
|
+
Log.d(TAG, "Successfully loaded legacy libc++_flir.so for FLIR SDK.");
|
|
180
|
+
} catch (UnsatisfiedLinkError e) {
|
|
181
|
+
Log.e(TAG, "Could not explicitly load libc++_flir.so. FLIR initialization might fail.", e);
|
|
182
|
+
}
|
|
183
|
+
|
|
111
184
|
ThermalSdkAndroid.init(context);
|
|
112
185
|
|
|
113
186
|
// Small delay to ensure JNI linkage is stable
|
|
@@ -134,12 +207,17 @@ public class FlirSdkManager {
|
|
|
134
207
|
notifyError("SDK not initialized");
|
|
135
208
|
return;
|
|
136
209
|
}
|
|
137
|
-
if (isScanning)
|
|
138
|
-
|
|
210
|
+
if (isScanning) {
|
|
211
|
+
Log.d(TAG, "Discovery already running, ensuring clean state...");
|
|
212
|
+
try {
|
|
213
|
+
DiscoveryFactory.getInstance().stop();
|
|
214
|
+
} catch (Throwable ignored) {}
|
|
215
|
+
isScanning = false;
|
|
216
|
+
}
|
|
139
217
|
|
|
140
218
|
isScanning = true;
|
|
141
219
|
discoveredDevices.clear();
|
|
142
|
-
Log.d(TAG, "Starting discovery...");
|
|
220
|
+
Log.d(TAG, "Starting discovery for all interfaces...");
|
|
143
221
|
|
|
144
222
|
try {
|
|
145
223
|
DiscoveryFactory.getInstance().scan(
|
|
@@ -157,19 +235,23 @@ public class FlirSdkManager {
|
|
|
157
235
|
}
|
|
158
236
|
|
|
159
237
|
public void stopScan() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
238
|
+
executor.execute(() -> {
|
|
239
|
+
if (!isScanning) return;
|
|
240
|
+
isScanning = false;
|
|
241
|
+
stopScanInternal();
|
|
242
|
+
});
|
|
163
243
|
}
|
|
164
244
|
|
|
165
245
|
private void stopScanInternal() {
|
|
166
246
|
try {
|
|
167
|
-
Log.d(TAG, "Stopping discovery...");
|
|
247
|
+
Log.d(TAG, "Stopping all discovery scanners...");
|
|
168
248
|
// Use zero-arg stop() as seen in official samples to stop all scanners
|
|
169
249
|
DiscoveryFactory.getInstance().stop();
|
|
170
250
|
Log.d(TAG, "Discovery stopped successfully");
|
|
171
|
-
} catch (
|
|
172
|
-
|
|
251
|
+
} catch (Throwable t) {
|
|
252
|
+
// This catches the notorious 'Receiver not registered' IllegalArgumentException
|
|
253
|
+
// and any other JNI-bubbled exceptions during teardown.
|
|
254
|
+
Log.w(TAG, "Stop scan suppressed (SDK internal race): " + t.getMessage());
|
|
173
255
|
}
|
|
174
256
|
}
|
|
175
257
|
|
|
@@ -297,6 +379,10 @@ public class FlirSdkManager {
|
|
|
297
379
|
|
|
298
380
|
public void disconnect() {
|
|
299
381
|
executor.execute(() -> {
|
|
382
|
+
if (isScanning) {
|
|
383
|
+
isScanning = false;
|
|
384
|
+
stopScanInternal();
|
|
385
|
+
}
|
|
300
386
|
stopStreamInternal();
|
|
301
387
|
|
|
302
388
|
if (camera != null) {
|
|
@@ -203,54 +203,39 @@ import ThermalSDK
|
|
|
203
203
|
let camType = identity.cameraType()
|
|
204
204
|
NSLog("[FlirManager] Camera type: \(camType.rawValue), interface: \(iface.rawValue)")
|
|
205
205
|
|
|
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)
|
|
206
|
+
let isNetwork = (camType == .generic) || iface.contains(.network)
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let
|
|
219
|
-
NSLog("[FlirManager] Using certificate name: \(certName)")
|
|
208
|
+
// ── STEP 1: INITIAL TRUST HANDSHAKE (Network Cameras Only) ──
|
|
209
|
+
// DiscoverySampleSwift shows that for network cameras we first authenticate
|
|
210
|
+
// with a user-friendly device/app name to trigger the "Trust this device" prompt.
|
|
211
|
+
if isNetwork {
|
|
212
|
+
NSLog("[FlirManager] Network camera detected — initiating initial trust request...")
|
|
213
|
+
let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "AppFactory"
|
|
214
|
+
let trustName = "\(UIDevice.current.name) \(appName)"
|
|
220
215
|
|
|
221
216
|
var status = FLIRAuthenticationStatus.pending
|
|
222
217
|
var attempts = 0
|
|
223
|
-
let maxAttempts =
|
|
218
|
+
let maxAttempts = 15 // 15 seconds window for initial prompt
|
|
224
219
|
|
|
225
220
|
while status == .pending && attempts < maxAttempts {
|
|
226
|
-
status = cam.authenticate(identity, trustedConnectionName:
|
|
227
|
-
NSLog("[FlirManager]
|
|
228
|
-
|
|
221
|
+
status = cam.authenticate(identity, trustedConnectionName: trustName)
|
|
222
|
+
NSLog("[FlirManager] Initial trust attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
|
|
229
223
|
if status == .pending {
|
|
230
|
-
// Camera waiting for user to press "Trust" on its screen
|
|
231
224
|
Thread.sleep(forTimeInterval: 1.0)
|
|
232
225
|
}
|
|
233
226
|
attempts += 1
|
|
234
227
|
}
|
|
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
228
|
}
|
|
247
229
|
|
|
248
|
-
// ──
|
|
230
|
+
// ── STEP 2: PAIRING (Always do this before final authentication!) ──
|
|
231
|
+
// Both FLIROneCameraSwift and DiscoverySampleSwift require pairing to be established
|
|
232
|
+
// before the camera's communication channels are authenticated and connected.
|
|
249
233
|
do {
|
|
234
|
+
NSLog("[FlirManager] Pairing camera with code 0...")
|
|
250
235
|
try cam.pair(identity, code: 0)
|
|
251
|
-
NSLog("[FlirManager]
|
|
236
|
+
NSLog("[FlirManager] Pairing succeeded ✅")
|
|
252
237
|
} catch {
|
|
253
|
-
NSLog("[FlirManager]
|
|
238
|
+
NSLog("[FlirManager] Pairing failed: \(error)")
|
|
254
239
|
self._isConnected = false
|
|
255
240
|
self.camera = nil
|
|
256
241
|
DispatchQueue.main.async {
|
|
@@ -260,6 +245,37 @@ import ThermalSDK
|
|
|
260
245
|
return
|
|
261
246
|
}
|
|
262
247
|
|
|
248
|
+
// ── STEP 3: FINAL AUTHENTICATION ──
|
|
249
|
+
// Use a persistent UUID certificate for network/wireless cameras to avoid trust conflicts.
|
|
250
|
+
// For standard classic FLIR ONE/Edge devices, use "dummy" as per FLIROneCameraSwift ViewController.swift.
|
|
251
|
+
let certName = isNetwork ? self.getPersistentCertificateName() : "dummy"
|
|
252
|
+
NSLog("[FlirManager] Performing final authentication with name: \(certName)")
|
|
253
|
+
|
|
254
|
+
var status = FLIRAuthenticationStatus.pending
|
|
255
|
+
var attempts = 0
|
|
256
|
+
let maxAttempts = 30 // ~30 seconds timeout
|
|
257
|
+
|
|
258
|
+
while status == .pending && attempts < maxAttempts {
|
|
259
|
+
status = cam.authenticate(identity, trustedConnectionName: certName)
|
|
260
|
+
NSLog("[FlirManager] Final auth attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
|
|
261
|
+
|
|
262
|
+
if status == .pending {
|
|
263
|
+
Thread.sleep(forTimeInterval: 1.0)
|
|
264
|
+
}
|
|
265
|
+
attempts += 1
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if status != .approved && isNetwork {
|
|
269
|
+
NSLog("[FlirManager] Final authentication failed or timed out: \(status.rawValue)")
|
|
270
|
+
self.camera = nil
|
|
271
|
+
DispatchQueue.main.async {
|
|
272
|
+
self.emitStateChange("connection_failed")
|
|
273
|
+
self.delegate?.onError("Camera authentication failed. Check the camera screen for a trust/approve prompt.")
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
NSLog("[FlirManager] Authentication approved ✅")
|
|
278
|
+
|
|
263
279
|
// ── CONNECT ──
|
|
264
280
|
do {
|
|
265
281
|
try cam.connect()
|