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.
- package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +465 -509
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +391 -375
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +177 -93
- package/ios/Flir/src/FlirManager.swift +52 -56
- package/ios/Flir/src/FlirPreviewView.m +0 -3
- package/ios/Flir/src/FlirState.m +9 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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 =
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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:
|
|
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 ["
|
|
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":
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
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("[
|
|
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
|
}
|