ilabs-flir 2.3.8 → 2.3.11

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.
@@ -8,6 +8,7 @@ import android.util.Log;
8
8
  import com.flir.thermalsdk.ErrorCode;
9
9
  import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
10
10
  import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
11
+ import com.flir.thermalsdk.image.ImageBuffer;
11
12
  import com.flir.thermalsdk.image.Palette;
12
13
  import com.flir.thermalsdk.image.PaletteManager;
13
14
  import com.flir.thermalsdk.image.Point;
@@ -22,12 +23,15 @@ import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
22
23
  import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
23
24
  import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
24
25
  import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
26
+ import com.flir.thermalsdk.androidsdk.live.connectivity.SdkWifiConnectionHelper;
25
27
  import com.flir.thermalsdk.live.streaming.Stream;
26
28
  import com.flir.thermalsdk.live.streaming.ThermalStreamer;
27
29
 
28
30
  import java.util.ArrayList;
29
31
  import java.util.Collections;
32
+ import java.util.HashMap;
30
33
  import java.util.List;
34
+ import java.util.Map;
31
35
  import java.util.concurrent.Executor;
32
36
  import java.util.concurrent.Executors;
33
37
  import java.util.concurrent.atomic.AtomicBoolean;
@@ -50,6 +54,7 @@ public class FlirSdkManager {
50
54
  private ThermalStreamer streamer;
51
55
  private Stream activeStream;
52
56
  private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
57
+ private final Map<String, DiscoveredCamera> discoveredCameras = Collections.synchronizedMap(new HashMap<>());
53
58
  private volatile Bitmap latestBitmap;
54
59
  private volatile String currentPaletteName = "WhiteHot";
55
60
  private final AtomicBoolean isProcessingFrame = new AtomicBoolean(false);
@@ -151,25 +156,19 @@ public class FlirSdkManager {
151
156
 
152
157
  public void stopScan() {
153
158
  if (!isScanning) return;
154
-
155
- // Use a temporary flag to prevent concurrent stop calls
156
159
  isScanning = false;
157
-
158
- executor.execute(() -> {
159
- try {
160
- Log.d(TAG, "Stopping discovery...");
161
- DiscoveryFactory.getInstance().stop(
162
- CommunicationInterface.EMULATOR,
163
- CommunicationInterface.USB,
164
- CommunicationInterface.NETWORK,
165
- CommunicationInterface.FLIR_ONE_WIRELESS);
166
- Log.d(TAG, "Discovery stopped successfully");
167
- } catch (Exception e) {
168
- // This is where the 'Receiver not registered' usually happens in SDK internals.
169
- // We catch it silently as it means the SDK already cleaned up or is in a weird state.
170
- Log.w(TAG, "Stop scan warning (internal SDK): " + e.getMessage());
171
- }
172
- });
160
+ executor.execute(this::stopScanInternal);
161
+ }
162
+
163
+ private void stopScanInternal() {
164
+ try {
165
+ Log.d(TAG, "Stopping discovery...");
166
+ // Use zero-arg stop() as seen in official samples to stop all scanners
167
+ DiscoveryFactory.getInstance().stop();
168
+ Log.d(TAG, "Discovery stopped successfully");
169
+ } catch (Exception e) {
170
+ Log.w(TAG, "Stop scan warning (internal SDK): " + e.getMessage());
171
+ }
173
172
  }
174
173
 
175
174
  public List<Identity> getDiscoveredDevices() {
@@ -198,62 +197,92 @@ public class FlirSdkManager {
198
197
  camera = null;
199
198
  }
200
199
 
201
- Log.d(TAG, "Connecting to: " + identity.deviceId);
202
- camera = new Camera();
203
-
204
- // ── Authenticate for NETWORK/WIRELESS cameras (required by FLIR SDK) ──
205
- // Matches the official NetworkCamera sample app pattern.
206
- // The FLIR One Edge Pro is a network/wireless camera and will reject
207
- // connections without prior authentication + trust approval.
208
- if (identity.communicationInterface == CommunicationInterface.NETWORK ||
209
- identity.communicationInterface == CommunicationInterface.FLIR_ONE_WIRELESS) {
210
- Log.d(TAG, "Network/Wireless camera detected — authenticating...");
211
-
212
- // Use a persistent application name (workaround for camera bug
213
- // where re-auth with a different name conflicts). Same pattern
214
- // as CameraAuthName in the NetworkCamera sample.
215
- SharedPreferences prefs = context.getSharedPreferences(
216
- "flir_auth", Context.MODE_PRIVATE);
217
- String authName = prefs.getString("auth_name", null);
218
- if (authName == null) {
219
- authName = context.getPackageName() + "-" +
220
- (System.currentTimeMillis() % 10000);
221
- prefs.edit().putString("auth_name", authName).apply();
200
+ // ── FLIR ONE WIRELESS (WiFi) Connection ──
201
+ // Matches the official FlirOneWireless sample. We must connect to the
202
+ // camera's WiFi Access Point before calling camera.connect().
203
+ if (identity.communicationInterface == CommunicationInterface.FLIR_ONE_WIRELESS) {
204
+ DiscoveredCamera dc = getDiscoveredCamera(identity.deviceId);
205
+ if (dc != null && dc.cameraDetails != null) {
206
+ String ssid = dc.cameraDetails.ssid;
207
+ Log.d(TAG, "Establishing WiFi connection to: " + ssid);
208
+
209
+ if (!SdkWifiConnectionHelper.isConnectedToNetwork(context, ssid)) {
210
+ // This is a blocking-style wrapper for simplicity in the executor thread
211
+ final AtomicBoolean wifiDone = new AtomicBoolean(false);
212
+ final AtomicBoolean wifiSuccess = new AtomicBoolean(false);
213
+
214
+ SdkWifiConnectionHelper.connectToWifiWithoutCode(context, dc.cameraDetails, status -> {
215
+ if (status.status == SdkWifiConnectionHelper.ConInfo.CONNECTED) {
216
+ wifiSuccess.set(true);
217
+ wifiDone.set(true);
218
+ } else if (status.status == SdkWifiConnectionHelper.ConInfo.ERROR) {
219
+ wifiSuccess.set(false);
220
+ wifiDone.set(true);
221
+ }
222
+ });
223
+
224
+ // Wait for WiFi connection (max 15 seconds)
225
+ int waitLoops = 0;
226
+ while (!wifiDone.get() && waitLoops < 30) {
227
+ try { Thread.sleep(500); } catch (Exception ignored) {}
228
+ waitLoops++;
229
+ }
230
+
231
+ if (!wifiSuccess.get()) {
232
+ notifyError("Failed to connect to camera WiFi: " + ssid);
233
+ return;
234
+ }
235
+ }
222
236
  }
237
+ }
223
238
 
239
+ // ── NETWORK Authentication (Required for A/T-series, NOT Wireless) ──
240
+ if (identity.communicationInterface == CommunicationInterface.NETWORK) {
241
+ Log.d(TAG, "Authenticating with network camera: " + identity.deviceId);
242
+
243
+ if (isScanning) {
244
+ isScanning = false;
245
+ stopScanInternal();
246
+ try { Thread.sleep(500); } catch (Exception ignored) {}
247
+ }
248
+
249
+ camera = new Camera();
250
+ String authName = "ThermalCameraFx";
224
251
  AuthenticationResponse response;
225
252
  int attempts = 0;
226
- final int MAX_AUTH_ATTEMPTS = 30; // 30 seconds max wait
253
+ final int MAX_AUTH_ATTEMPTS = 3;
254
+
227
255
  do {
228
- response = camera.authenticate(identity, authName,
229
- 41 * 1000); // 41-second timeout per attempt
230
- Log.d(TAG, "Auth attempt " + (attempts + 1) +
231
- " status: " + response.authenticationStatus);
232
-
233
- if (response.authenticationStatus ==
234
- AuthenticationResponse.AuthenticationStatus.PENDING) {
235
- // Camera is waiting for user to press "Trust" on its screen
236
- Thread.sleep(1000);
237
- }
238
256
  attempts++;
239
- } while (response.authenticationStatus ==
240
- AuthenticationResponse.AuthenticationStatus.PENDING
241
- && attempts < MAX_AUTH_ATTEMPTS);
242
-
243
- if (response.authenticationStatus !=
244
- AuthenticationResponse.AuthenticationStatus.APPROVED) {
245
- Log.e(TAG, "Authentication rejected/timed out: " +
246
- response.authenticationStatus);
247
- camera = null;
248
- notifyError("Camera authentication failed. " +
249
- "Check the camera screen for a trust prompt.");
257
+ response = camera.authenticate(identity, authName, 41 * 1000);
258
+ if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.APPROVED) {
259
+ break;
260
+ } else if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.REJECTED) {
261
+ notifyError("Authentication rejected by camera");
262
+ return;
263
+ }
264
+ } while (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.PENDING && attempts < MAX_AUTH_ATTEMPTS);
265
+
266
+ if (response.authenticationStatus != AuthenticationResponse.AuthenticationStatus.APPROVED) {
267
+ notifyError("Authentication failed: " + response.authenticationStatus.name());
250
268
  return;
251
269
  }
252
- Log.d(TAG, "Authentication approved");
270
+ } else {
271
+ // For all other types, stop scan if still running
272
+ if (isScanning) {
273
+ isScanning = false;
274
+ stopScanInternal();
275
+ try { Thread.sleep(300); } catch (Exception ignored) {}
276
+ }
277
+ camera = identity.camera;
278
+ if (camera == null) {
279
+ camera = new Camera();
280
+ }
253
281
  }
254
282
 
283
+ Log.d(TAG, "Calling camera.connect() for " + identity.deviceId);
255
284
  camera.connect(identity, connectionStatusListener, new ConnectParameters());
256
- Log.d(TAG, "Connected to: " + identity.deviceId);
285
+ Log.d(TAG, "camera.connect() returned for " + identity.deviceId + ". isConnected=" + camera.isConnected());
257
286
 
258
287
  if (listener != null) {
259
288
  listener.onConnected(identity);
@@ -263,7 +292,7 @@ public class FlirSdkManager {
263
292
  startStreamInternal();
264
293
 
265
294
  } catch (Exception e) {
266
- Log.e(TAG, "Connection failed", e);
295
+ Log.e(TAG, "Connection failed for " + identity.deviceId, e);
267
296
  camera = null;
268
297
  notifyError("Connection failed: " + e.getMessage());
269
298
  }
@@ -294,6 +323,10 @@ public class FlirSdkManager {
294
323
  return camera != null;
295
324
  }
296
325
 
326
+ private DiscoveredCamera getDiscoveredCamera(String deviceId) {
327
+ return discoveredCameras.get(deviceId);
328
+ }
329
+
297
330
  // ==================== STREAMING ====================
298
331
 
299
332
  public void startStream() {
@@ -316,18 +349,50 @@ public class FlirSdkManager {
316
349
  }
317
350
 
318
351
  try {
352
+ // RETRY MECHANISM: For Network/Wireless cameras, the connection might take
353
+ // a few hundred milliseconds to stabilize at the native level even after
354
+ // camera.connect() returns. We retry for up to 3 seconds.
355
+ int retries = 0;
356
+ final int MAX_RETRIES = 15; // 15 * 200ms = 3 seconds
357
+ while (!camera.isConnected() && retries < MAX_RETRIES) {
358
+ try {
359
+ Thread.sleep(200);
360
+ } catch (InterruptedException ignored) {}
361
+ retries++;
362
+ if (retries % 5 == 0) {
363
+ Log.d(TAG, "Waiting for camera connection state to stabilize... (attempt " + retries + ")");
364
+ }
365
+ }
366
+
319
367
  if (!camera.isConnected()) {
320
- Log.e(TAG, "Camera not connected, cannot start stream");
368
+ Log.e(TAG, "Camera failed to report connected state after " + (retries * 200) + "ms");
321
369
  notifyError("Camera not connected");
322
370
  return;
323
371
  }
324
372
 
325
- List<Stream> streams = camera.getStreams();
373
+ Log.d(TAG, "Camera connected state confirmed after " + (retries * 200) + "ms");
374
+
375
+ // RETRY MECHANISM for getStreams: Sometimes streams are not immediately available
376
+ List<Stream> streams = null;
377
+ retries = 0;
378
+ while (retries < 10) { // 10 * 200ms = 2 seconds
379
+ streams = camera.getStreams();
380
+ if (streams != null && !streams.isEmpty()) {
381
+ break;
382
+ }
383
+ try {
384
+ Thread.sleep(200);
385
+ } catch (InterruptedException ignored) {}
386
+ retries++;
387
+ }
388
+
326
389
  if (streams == null || streams.isEmpty()) {
327
390
  notifyError("No streams available");
328
391
  return;
329
392
  }
330
393
 
394
+ Log.d(TAG, "Streams found: " + streams.size() + " (after " + (retries * 200) + "ms)");
395
+
331
396
  // Find thermal stream or use first
332
397
  Stream thermalStream = null;
333
398
  for (Stream stream : streams) {
@@ -362,32 +427,37 @@ public class FlirSdkManager {
362
427
  // 1. Apply Palette
363
428
  if (paletteToApply != null) {
364
429
  try {
365
- Palette palette = null;
366
- // Standard FLIR palette list from samples
367
- String[] paletteNames = {"Gray", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel"};
430
+ List<Palette> sdkPalettes = PaletteManager.getDefaultPalettes();
368
431
 
369
- for (String name : paletteNames) {
370
- if (name.equalsIgnoreCase(paletteToApply)) {
371
- try {
372
- // We still need to find the Palette object from the SDK if possible
373
- // But we try to do it safely
374
- List<Palette> sdkPalettes = PaletteManager.getDefaultPalettes();
375
- for (Palette p : sdkPalettes) {
376
- if (p.name.equalsIgnoreCase(name)) {
377
- palette = p;
378
- break;
379
- }
432
+ if (paletteToApply.equalsIgnoreCase("Gray") || paletteToApply.equalsIgnoreCase("grayscale")) {
433
+ // User wants Gray - map to WhiteHot which is the SDK's standard grayscale
434
+ for (Palette p : sdkPalettes) {
435
+ if (p.name.equalsIgnoreCase("WhiteHot") || p.name.equalsIgnoreCase("White hot")) {
436
+ thermalImage.setPalette(p);
437
+ break;
438
+ }
439
+ }
440
+ } else {
441
+ Palette palette = null;
442
+ for (Palette p : sdkPalettes) {
443
+ if (p.name.equalsIgnoreCase(paletteToApply)) {
444
+ palette = p;
445
+ break;
446
+ }
447
+ }
448
+
449
+ if (palette != null) {
450
+ thermalImage.setPalette(palette);
451
+ } else if (paletteToApply.equalsIgnoreCase("Wheel")) {
452
+ // Fallback for Wheel if not found - some SDKs use different names
453
+ for (Palette p : sdkPalettes) {
454
+ if (p.name.contains("Wheel") || p.name.contains("ColorWheel") || p.name.contains("Rainbow")) {
455
+ thermalImage.setPalette(p);
456
+ break;
380
457
  }
381
- } catch (Throwable t) {
382
- Log.w(TAG, "Dynamic palette lookup failed, using fallback mechanism");
383
458
  }
384
- break;
385
459
  }
386
460
  }
387
-
388
- if (palette != null) {
389
- thermalImage.setPalette(palette);
390
- }
391
461
  } catch (Throwable t) {
392
462
  Log.e(TAG, "Failed to apply palette: " + paletteToApply, t);
393
463
  }
@@ -396,8 +466,9 @@ public class FlirSdkManager {
396
466
  // 2. Save Radiometric Snapshot if requested
397
467
  if (snapshotPath != null) {
398
468
  try {
469
+ Log.i(TAG, "[SNAPSHOT] Attempting to save radiometric snapshot: " + snapshotPath);
399
470
  thermalImage.saveAs(snapshotPath);
400
- Log.i(TAG, "Radiometric snapshot saved to: " + snapshotPath);
471
+ Log.i(TAG, "[SNAPSHOT] ✅ Success: Radiometric snapshot saved");
401
472
  if (snapshotCallback != null) {
402
473
  snapshotCallback.onSnapshotSaved(snapshotPath);
403
474
  }
@@ -410,12 +481,22 @@ public class FlirSdkManager {
410
481
  }
411
482
 
412
483
  // 3. Generate Bitmap for display
413
- Bitmap bitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
414
- if (bitmap != null) {
415
- latestBitmap = bitmap;
416
- if (listener != null) {
417
- listener.onFrame(bitmap);
484
+ // We use streamer.getImage() to get the rendered image with palette applied.
485
+ try {
486
+ Bitmap newBitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
487
+ if (newBitmap != null) {
488
+ Bitmap oldBitmap = latestBitmap;
489
+ latestBitmap = newBitmap;
490
+ if (listener != null) {
491
+ listener.onFrame(newBitmap);
492
+ }
493
+ // Recycle old bitmap to prevent memory leak
494
+ if (oldBitmap != null && oldBitmap != newBitmap) {
495
+ oldBitmap.recycle();
496
+ }
418
497
  }
498
+ } catch (Exception e) {
499
+ Log.e(TAG, "Bitmap creation failed", e);
419
500
  }
420
501
  });
421
502
  }
@@ -514,6 +595,8 @@ public class FlirSdkManager {
514
595
  Identity identity = discoveredCamera.getIdentity();
515
596
  Log.d(TAG, "Device found: " + identity.deviceId);
516
597
 
598
+ discoveredCameras.put(identity.deviceId, discoveredCamera);
599
+
517
600
  synchronized (discoveredDevices) {
518
601
  boolean exists = false;
519
602
  for (Identity d : discoveredDevices) {
@@ -536,6 +619,7 @@ public class FlirSdkManager {
536
619
  @Override
537
620
  public void onCameraLost(Identity identity) {
538
621
  Log.d(TAG, "Device lost: " + identity.deviceId);
622
+ discoveredCameras.remove(identity.deviceId);
539
623
  synchronized (discoveredDevices) {
540
624
  discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
541
625
  }
@@ -162,7 +162,13 @@ import ThermalSDK
162
162
  disconnect()
163
163
  }
164
164
 
165
- // Connect on background thread (matches sample app)
165
+ // Stop discovery before connecting to free up SDK resources
166
+ // This prevents resource contention in the native SDK layer.
167
+ if discovery != nil {
168
+ discovery?.stop()
169
+ }
170
+
171
+ // Connect on background thread (matches sample app pattern)
166
172
  DispatchQueue.global().async { [weak self] in
167
173
  guard let self = self else { return }
168
174
 
@@ -191,7 +197,8 @@ import ThermalSDK
191
197
  NSLog("[FlirManager] Network camera detected — authenticating...")
192
198
 
193
199
  // Use UUID-based persistent certificate name (matches FLIR sample).
194
- // The camera has a bug where re-auth with a different name conflicts.
200
+ // The camera has a bug where re-auth with a different name can conflict,
201
+ // so we generate a UUID once and persist it in UserDefaults.
195
202
  let certName = self.getPersistentCertificateName()
196
203
  NSLog("[FlirManager] Using certificate name: \(certName)")
197
204
 
@@ -435,7 +442,7 @@ import ThermalSDK
435
442
  // Compiler says it is NOT optional here, so direct assignment.
436
443
  let value = spot.getValue()
437
444
  result = value.value
438
-
445
+
439
446
  try? measurements.remove(spot)
440
447
  }
441
448
  }
@@ -445,8 +452,6 @@ import ThermalSDK
445
452
  #endif
446
453
  }
447
454
 
448
-
449
-
450
455
  @objc public func getTemperatureAtNormalized(_ nx: Double, y: Double) -> Double {
451
456
  #if FLIR_ENABLED
452
457
  guard let streamer = streamer, _isStreaming else { return Double.nan }
@@ -458,10 +463,10 @@ import ThermalSDK
458
463
 
459
464
  // Map normalized (0.0 - 1.0) to actual sensor pixels
460
465
  let cx = max(0, min(Int(w) - 1, Int(nx * w)))
461
- let cy = max(0, min(Int(h) - 1, Int(y * h)))
466
+ let cy_fixed = max(0, min(Int(h) - 1, Int(y * h)))
462
467
 
463
468
  if let measurements = thermalImage.measurements,
464
- let spot = try? measurements.addSpot(CGPoint(x: cx, y: cy)) {
469
+ let spot = try? measurements.addSpot(CGPoint(x: cx, y: cy_fixed)) {
465
470
  result = spot.getValue().value
466
471
  try? measurements.remove(spot)
467
472
  }
@@ -503,8 +508,6 @@ import ThermalSDK
503
508
 
504
509
  // MARK: - Battery (stub - not needed per user)
505
510
 
506
- // MARK: - Battery (stub - not needed per user)
507
-
508
511
  @objc public func getBatteryLevel() -> Int { return -1 }
509
512
  @objc public func isBatteryCharging() -> Bool { return false }
510
513
 
@@ -531,21 +534,16 @@ import ThermalSDK
531
534
  }
532
535
 
533
536
  @objc public func getAvailablePalettes() -> [String] {
534
- return ["Gray", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel"]
537
+ return ["WhiteHot", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel"]
535
538
  }
536
539
 
537
540
  @objc public func generatePaletteIcons() -> [[String: String]] {
538
541
  let paletteNames = getAvailablePalettes()
539
- let fileManager = FileManager.default
540
- let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
541
- let paletteDir = cacheDir.appendingPathComponent("flir_palettes")
542
-
543
542
  var results: [[String: String]] = []
544
543
  for name in paletteNames {
545
- let iconURL = paletteDir.appendingPathComponent("\(name.lowercased()).png")
546
544
  results.append([
547
545
  "name": name,
548
- "uri": fileManager.fileExists(atPath: iconURL.path) ? iconURL.absoluteString : ""
546
+ "uri": "" // No URI - rely on local assets if any
549
547
  ])
550
548
  }
551
549
  return results
@@ -604,20 +602,15 @@ import ThermalSDK
604
602
  return newName
605
603
  }
606
604
 
607
- #if FLIR_ENABLED
608
- private func interfaceName(_ iface: FLIRCommunicationInterface) -> String {
609
- if iface.contains(.lightning) { return "LIGHTNING" }
610
- if iface.contains(.network) { return "NETWORK" }
611
- if iface.contains(.flirOneWireless) { return "WIRELESS" }
612
- if iface.contains(.emulator) { return "EMULATOR" }
605
+ private func interfaceName(_ iface: Int) -> String {
606
+ // Placeholder for interface name mapping
613
607
  return "UNKNOWN"
614
608
  }
615
- #endif
616
609
  }
617
610
 
611
+ #if FLIR_ENABLED
618
612
  // MARK: - Discovery Delegate
619
613
 
620
- #if FLIR_ENABLED
621
614
  extension FlirManager: FLIRDiscoveryEventDelegate {
622
615
  public func cameraDiscovered(_ camera: FLIRDiscoveredCamera) {
623
616
  let identity = camera.identity
@@ -632,7 +625,7 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
632
625
  let deviceInfo = FlirDeviceInfo(
633
626
  deviceId: deviceId,
634
627
  name: camera.displayName ?? deviceId,
635
- communicationType: interfaceName(identity.communicationInterface()),
628
+ communicationType: interfaceName(Int(identity.communicationInterface().rawValue)),
636
629
  isEmulator: identity.communicationInterface() == .emulator
637
630
  )
638
631
 
@@ -653,7 +646,7 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
653
646
  }
654
647
 
655
648
  public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
656
- NSLog("[FlirManager] Discovery finished: \(iface)")
649
+ NSLog("[FlirManager] Discovery finished: \(iface.rawValue)")
657
650
  }
658
651
 
659
652
  public func cameraLost(_ cameraIdentity: FLIRIdentity) {
@@ -669,11 +662,9 @@ extension FlirManager: FLIRDiscoveryEventDelegate {
669
662
  }
670
663
  }
671
664
  }
672
- #endif
673
665
 
674
666
  // MARK: - Camera Delegate
675
667
 
676
- #if FLIR_ENABLED
677
668
  extension FlirManager: FLIRDataReceivedDelegate {
678
669
  public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
679
670
  NSLog("[FlirManager] Camera disconnected: \(error?.localizedDescription ?? "clean")")
@@ -690,11 +681,9 @@ extension FlirManager: FLIRDataReceivedDelegate {
690
681
  }
691
682
  }
692
683
  }
693
- #endif
694
684
 
695
685
  // MARK: - Stream Delegate
696
686
 
697
- #if FLIR_ENABLED
698
687
  extension FlirManager: FLIRStreamDelegate {
699
688
  public func onError(_ error: Error) {
700
689
  NSLog("[FlirManager] Stream error: \(error)")
@@ -702,13 +691,10 @@ extension FlirManager: FLIRStreamDelegate {
702
691
  }
703
692
 
704
693
  public func onImageReceived() {
705
- NSLog("[FLIR-TRACE 1️⃣] onImageReceived called on SDK thread")
706
-
707
694
  // Process frame on dedicated render queue (matches sample app pattern)
708
695
  // This prevents blocking the SDK callback thread and main thread
709
696
  // Guard to skip frame if already processing (prevents backpressure/latency)
710
697
  guard !_isProcessingFrame else {
711
- NSLog("[FLIR-TRACE ⏩] Skipping frame (already processing)")
712
698
  return
713
699
  }
714
700
 
@@ -716,12 +702,9 @@ extension FlirManager: FLIRStreamDelegate {
716
702
  renderQueue.async { [weak self] in
717
703
  defer { self?._isProcessingFrame = false }
718
704
  guard let self = self, self._isStreaming, let streamer = self.streamer else {
719
- NSLog("[FLIR-TRACE ❌] No self, streamer or not streaming in renderQueue")
720
705
  return
721
706
  }
722
707
 
723
- NSLog("[FLIR-TRACE 2️⃣] Processing on renderQueue")
724
-
725
708
  objc_sync_enter(self.stateLock)
726
709
  let currentStreamer = self.streamer
727
710
  let streaming = self._isStreaming
@@ -740,7 +723,26 @@ extension FlirManager: FLIRStreamDelegate {
740
723
 
741
724
  streamer.withThermalImage { thermalImage in
742
725
  // 1. Apply Palette
743
- if let palette = thermalImage.paletteManager.getDefaultPalettes().first(where: { $0.name.lowercased() == paletteToApply.lowercased() }) {
726
+ let sdkPalettes = thermalImage.paletteManager.getDefaultPalettes()
727
+ var targetPalette: FLIRPalette? = nil
728
+
729
+ if paletteToApply.lowercased() == "gray" || paletteToApply.lowercased() == "grayscale" {
730
+ // Map Gray to WhiteHot (standard SDK name)
731
+ targetPalette = sdkPalettes.first(where: {
732
+ $0.name.lowercased() == "whitehot" || $0.name.lowercased() == "white hot"
733
+ })
734
+ } else {
735
+ targetPalette = sdkPalettes.first(where: { $0.name.lowercased() == paletteToApply.lowercased() })
736
+
737
+ // Fallback for Wheel
738
+ if targetPalette == nil && paletteToApply.lowercased() == "wheel" {
739
+ targetPalette = sdkPalettes.first(where: {
740
+ $0.name.contains("Wheel") || $0.name.contains("ColorWheel") || $0.name.contains("Rainbow")
741
+ })
742
+ }
743
+ }
744
+
745
+ if let palette = targetPalette {
744
746
  thermalImage.palette = palette
745
747
  }
746
748
 
@@ -753,29 +755,23 @@ extension FlirManager: FLIRStreamDelegate {
753
755
  NSLog("[FlirManager] Failed to save radiometric snapshot: \(error)")
754
756
  }
755
757
  }
758
+
759
+ // 3. Generate UIImage for display
760
+ // Grab the image while the thermal image is locked to ensure settings are applied
761
+ if let image = streamer.getImage() {
762
+ self._latestImage = image
763
+ let width = Int(image.size.width)
764
+ let height = Int(image.size.height)
765
+
766
+ DispatchQueue.main.async { [weak self] in
767
+ self?.delegate?.onFrameReceived(image, width: width, height: height)
768
+ }
769
+ }
756
770
  }
757
-
758
- NSLog("[FLIR-TRACE 3️⃣] Streamer updated successfully")
759
771
  } catch {
760
- NSLog("[FLIR-TRACE ❌] Streamer update failed: \(error)")
772
+ NSLog("[FlirManager] Streamer update failed: \(error)")
761
773
  return
762
774
  }
763
-
764
- guard let image = streamer.getImage() else {
765
- NSLog("[FLIR-TRACE ❌] streamer.getImage() returned nil")
766
- return
767
- }
768
-
769
- NSLog("[FLIR-TRACE 4️⃣] Got image from streamer: \(image.size.width)x\(image.size.height)")
770
-
771
- self._latestImage = image
772
- let width = Int(image.size.width)
773
- let height = Int(image.size.height)
774
-
775
- DispatchQueue.main.async { [weak self] in
776
- NSLog("[FLIR-TRACE 5️⃣] Dispatching to delegate.onFrameReceived on main thread")
777
- self?.delegate?.onFrameReceived(image, width: width, height: height)
778
- }
779
775
  }
780
776
  }
781
777
  }
@@ -72,9 +72,6 @@
72
72
  }
73
73
 
74
74
  - (void)updateWithImage:(UIImage *)image {
75
- if (!image)
76
- return;
77
-
78
75
  dispatch_async(dispatch_get_main_queue(), ^{
79
76
  self.imageView.image = image;
80
77
  });