ilabs-flir 2.0.4 → 2.0.6

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.
Files changed (35) hide show
  1. package/Flir.podspec +139 -139
  2. package/README.md +1066 -1066
  3. package/android/Flir/build.gradle.kts +72 -72
  4. package/android/Flir/src/main/AndroidManifest.xml +45 -45
  5. package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
  6. package/android/Flir/src/main/java/flir/android/FlirFrameCache.kt +6 -6
  7. package/android/Flir/src/main/java/flir/android/FlirManager.kt +476 -476
  8. package/android/Flir/src/main/java/flir/android/FlirModule.kt +257 -257
  9. package/android/Flir/src/main/java/flir/android/FlirPackage.kt +18 -18
  10. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +74 -74
  11. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +583 -583
  12. package/android/Flir/src/main/java/flir/android/FlirStatus.kt +12 -12
  13. package/android/Flir/src/main/java/flir/android/FlirView.kt +48 -48
  14. package/android/Flir/src/main/java/flir/android/FlirViewManager.kt +13 -13
  15. package/app.plugin.js +381 -381
  16. package/expo-module.config.json +5 -5
  17. package/ios/Flir/src/Flir-Bridging-Header.h +34 -34
  18. package/ios/Flir/src/FlirEventEmitter.h +25 -25
  19. package/ios/Flir/src/FlirEventEmitter.m +63 -63
  20. package/ios/Flir/src/FlirManager.swift +599 -599
  21. package/ios/Flir/src/FlirModule.h +17 -17
  22. package/ios/Flir/src/FlirModule.m +713 -713
  23. package/ios/Flir/src/FlirPreviewView.h +13 -13
  24. package/ios/Flir/src/FlirPreviewView.m +171 -171
  25. package/ios/Flir/src/FlirState.h +68 -68
  26. package/ios/Flir/src/FlirState.m +135 -135
  27. package/ios/Flir/src/FlirViewManager.h +16 -16
  28. package/ios/Flir/src/FlirViewManager.m +27 -27
  29. package/package.json +70 -71
  30. package/react-native.config.js +14 -14
  31. package/scripts/fetch-binaries.js +103 -17
  32. package/sdk-manifest.json +50 -50
  33. package/src/index.d.ts +63 -63
  34. package/src/index.js +7 -7
  35. package/src/index.ts +6 -6
@@ -1,583 +1,583 @@
1
- package flir.android;
2
-
3
- import android.content.Context;
4
- import android.graphics.Bitmap;
5
- import android.util.Log;
6
-
7
- import com.flir.thermalsdk.ErrorCode;
8
- import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
9
- import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
10
- import com.flir.thermalsdk.image.ImageBuffer;
11
- import com.flir.thermalsdk.image.Palette;
12
- import com.flir.thermalsdk.image.PaletteManager;
13
- import com.flir.thermalsdk.image.Point;
14
- import com.flir.thermalsdk.image.ThermalImage;
15
- import com.flir.thermalsdk.image.ThermalValue;
16
- import com.flir.thermalsdk.live.Camera;
17
- import com.flir.thermalsdk.live.CommunicationInterface;
18
- import com.flir.thermalsdk.live.ConnectParameters;
19
- import com.flir.thermalsdk.live.Identity;
20
- import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
21
- import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
22
- import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
23
- import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
24
- import com.flir.thermalsdk.live.remote.OnReceived;
25
- import com.flir.thermalsdk.live.streaming.Stream;
26
- import com.flir.thermalsdk.live.streaming.ThermalStreamer;
27
-
28
- import java.util.ArrayList;
29
- import java.util.Collections;
30
- import java.util.List;
31
- import java.util.concurrent.Executor;
32
- import java.util.concurrent.Executors;
33
-
34
- /**
35
- * Simplified FLIR SDK Manager - handles discovery, connection, and streaming
36
- * No filtering - returns all discovered devices (USB, Network, Emulator)
37
- */
38
- public class FlirSdkManager {
39
- private static final String TAG = "FlirSdkManager";
40
-
41
- // Singleton instance
42
- private static FlirSdkManager instance;
43
-
44
- // Core components
45
- private final Context context;
46
- // Use bounded thread pool to prevent thread explosion during rapid frame processing
47
- private final Executor executor = Executors.newFixedThreadPool(2);
48
- // Single-threaded executor for frame processing to ensure ordered processing
49
- private final Executor frameExecutor = Executors.newSingleThreadExecutor();
50
- // Frame processing guard - skip frames if still processing previous one
51
- private volatile boolean isProcessingFrame = false;
52
- private long lastFrameProcessedMs = 0;
53
- private static final long MIN_FRAME_INTERVAL_MS = 50; // Max ~20 FPS frame processing
54
-
55
- // State
56
- private boolean isInitialized = false;
57
- private boolean isScanning = false;
58
- private Camera camera;
59
- private ThermalStreamer streamer;
60
- private Stream activeStream;
61
- private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
62
-
63
- // Listener
64
- private Listener listener;
65
-
66
- /**
67
- * Listener interface for SDK events
68
- */
69
- public interface Listener {
70
- void onDeviceFound(Identity identity);
71
- void onDeviceListUpdated(List<Identity> devices);
72
- void onConnected(Identity identity);
73
- void onDisconnected();
74
- void onFrame(Bitmap bitmap);
75
- void onError(String message);
76
- }
77
-
78
- // Private constructor for singleton
79
- private FlirSdkManager(Context context) {
80
- this.context = context.getApplicationContext();
81
- }
82
-
83
- /**
84
- * Get singleton instance
85
- */
86
- public static synchronized FlirSdkManager getInstance(Context context) {
87
- if (instance == null) {
88
- instance = new FlirSdkManager(context);
89
- }
90
- return instance;
91
- }
92
-
93
- /**
94
- * Set listener for SDK events
95
- */
96
- public void setListener(Listener listener) {
97
- this.listener = listener;
98
- }
99
-
100
- /**
101
- * Initialize the FLIR Thermal SDK
102
- */
103
- public void initialize() {
104
- if (isInitialized) {
105
- Log.d(TAG, "Already initialized");
106
- return;
107
- }
108
-
109
- try {
110
- ThermalSdkAndroid.init(context);
111
- isInitialized = true;
112
- Log.d(TAG, "SDK initialized successfully");
113
- } catch (Exception e) {
114
- Log.e(TAG, "Failed to initialize SDK", e);
115
- notifyError("SDK initialization failed: " + e.getMessage());
116
- }
117
- }
118
-
119
- /**
120
- * Check if SDK is initialized
121
- */
122
- public boolean isInitialized() {
123
- return isInitialized;
124
- }
125
-
126
- /**
127
- * Start scanning for all device types (USB, Network, Emulator)
128
- * Returns ALL devices - no filtering
129
- */
130
- public void scan() {
131
- if (!isInitialized) {
132
- Log.e(TAG, "SDK not initialized");
133
- notifyError("SDK not initialized");
134
- return;
135
- }
136
-
137
- if (isScanning) {
138
- Log.d(TAG, "Already scanning");
139
- return;
140
- }
141
-
142
- isScanning = true;
143
- discoveredDevices.clear();
144
-
145
- Log.d(TAG, "Starting discovery for EMULATOR, NETWORK, USB...");
146
-
147
- try {
148
- DiscoveryFactory.getInstance().scan(
149
- discoveryListener,
150
- CommunicationInterface.EMULATOR,
151
- CommunicationInterface.NETWORK,
152
- CommunicationInterface.USB
153
- );
154
- } catch (Exception e) {
155
- Log.e(TAG, "Failed to start scan", e);
156
- isScanning = false;
157
- notifyError("Scan failed: " + e.getMessage());
158
- }
159
- }
160
-
161
- /**
162
- * Stop scanning for devices
163
- */
164
- public void stop() {
165
- if (!isScanning) {
166
- return;
167
- }
168
-
169
- try {
170
- DiscoveryFactory.getInstance().stop(
171
- CommunicationInterface.EMULATOR,
172
- CommunicationInterface.NETWORK,
173
- CommunicationInterface.USB
174
- );
175
- } catch (Exception e) {
176
- Log.e(TAG, "Failed to stop scan", e);
177
- }
178
-
179
- isScanning = false;
180
- Log.d(TAG, "Discovery stopped");
181
- }
182
-
183
- /**
184
- * Get list of discovered devices
185
- */
186
- public List<Identity> getDiscoveredDevices() {
187
- return new ArrayList<>(discoveredDevices);
188
- }
189
-
190
- /**
191
- * Connect to a device
192
- */
193
- public void connect(Identity identity) {
194
- if (identity == null) {
195
- notifyError("Invalid identity");
196
- return;
197
- }
198
-
199
- // Disconnect if already connected
200
- if (camera != null) {
201
- disconnect();
202
- }
203
-
204
- Log.d(TAG, "Connecting to: " + identity.deviceId);
205
-
206
- // Run connection on background thread since it's blocking
207
- executor.execute(() -> {
208
- try {
209
- camera = new Camera();
210
- camera.connect(identity, connectionStatusListener, new ConnectParameters());
211
-
212
- Log.d(TAG, "Connected to camera");
213
-
214
- if (listener != null) {
215
- listener.onConnected(identity);
216
- }
217
- } catch (Exception e) {
218
- Log.e(TAG, "Connection failed", e);
219
- camera = null;
220
- notifyError("Connection failed: " + e.getMessage());
221
- }
222
- });
223
- }
224
-
225
- /**
226
- * Disconnect from current device
227
- */
228
- public void disconnect() {
229
- stopStream();
230
-
231
- if (camera != null) {
232
- try {
233
- camera.disconnect();
234
- } catch (Exception e) {
235
- Log.e(TAG, "Error disconnecting", e);
236
- }
237
- camera = null;
238
- }
239
-
240
- if (listener != null) {
241
- listener.onDisconnected();
242
- }
243
-
244
- Log.d(TAG, "Disconnected");
245
- }
246
-
247
- /**
248
- * Check if connected
249
- */
250
- public boolean isConnected() {
251
- return camera != null;
252
- }
253
-
254
- /**
255
- * Start streaming from connected device
256
- */
257
- public void startStream() {
258
- if (camera == null) {
259
- notifyError("Not connected");
260
- return;
261
- }
262
-
263
- executor.execute(() -> {
264
- try {
265
- // Get available streams
266
- List<Stream> streams = camera.getStreams();
267
- if (streams == null || streams.isEmpty()) {
268
- notifyError("No streams available");
269
- return;
270
- }
271
-
272
- // Find thermal stream
273
- Stream thermalStream = null;
274
- for (Stream stream : streams) {
275
- if (stream.isThermal()) {
276
- thermalStream = stream;
277
- break;
278
- }
279
- }
280
-
281
- if (thermalStream == null) {
282
- thermalStream = streams.get(0);
283
- }
284
-
285
- activeStream = thermalStream;
286
- streamer = new ThermalStreamer(thermalStream);
287
-
288
- // Start receiving frames using OnReceived and OnRemoteError
289
- thermalStream.start(
290
- (OnReceived<Void>) v -> {
291
- // FRAME DROP GUARD: Skip frame if still processing previous one
292
- // This prevents thread buildup and ensures smooth frame flow
293
- long now = System.currentTimeMillis();
294
- if (isProcessingFrame || (now - lastFrameProcessedMs < MIN_FRAME_INTERVAL_MS)) {
295
- // Drop frame - processing is behind or too soon since last frame
296
- return;
297
- }
298
-
299
- // Mark processing start before queuing task
300
- isProcessingFrame = true;
301
-
302
- // Use single-threaded frameExecutor to ensure ordered frame processing
303
- frameExecutor.execute(() -> {
304
- try {
305
- if (streamer != null) {
306
- streamer.update();
307
-
308
- // Get ImageBuffer and convert to Bitmap
309
- ImageBuffer imageBuffer = streamer.getImage();
310
- if (imageBuffer != null && listener != null) {
311
- BitmapAndroid bitmapAndroid = BitmapAndroid.createBitmap(imageBuffer);
312
- Bitmap bitmap = bitmapAndroid.getBitMap();
313
- if (bitmap != null) {
314
- listener.onFrame(bitmap);
315
- }
316
- }
317
- }
318
- } catch (Exception e) {
319
- Log.e(TAG, "Error processing frame", e);
320
- } finally {
321
- // Reset frame processing guard to allow next frame
322
- lastFrameProcessedMs = System.currentTimeMillis();
323
- isProcessingFrame = false;
324
- }
325
- });
326
- },
327
- error -> {
328
- Log.e(TAG, "Stream error: " + error);
329
- notifyError("Stream error: " + error);
330
- }
331
- );
332
-
333
- Log.d(TAG, "Streaming started");
334
-
335
- } catch (Exception e) {
336
- Log.e(TAG, "Failed to start stream", e);
337
- notifyError("Stream failed: " + e.getMessage());
338
- }
339
- });
340
- }
341
-
342
- /**
343
- * Stop streaming
344
- */
345
- public void stopStream() {
346
- if (activeStream != null) {
347
- try {
348
- activeStream.stop();
349
- } catch (Exception e) {
350
- Log.e(TAG, "Error stopping stream", e);
351
- }
352
- activeStream = null;
353
- }
354
-
355
- streamer = null;
356
-
357
- // Reset frame processing state
358
- isProcessingFrame = false;
359
- lastFrameProcessedMs = 0;
360
-
361
- Log.d(TAG, "Streaming stopped");
362
- }
363
-
364
- /**
365
- * Get temperature at a specific point in the image
366
- * Queries the SDK directly - simple and no locks needed
367
- * @param x X coordinate (0 to image width-1)
368
- * @param y Y coordinate (0 to image height-1)
369
- * @return Temperature in Celsius, or Double.NaN if not available
370
- */
371
- public double getTemperatureAt(int x, int y) {
372
- if (streamer == null) {
373
- return Double.NaN;
374
- }
375
-
376
- final double[] result = {Double.NaN};
377
- try {
378
- streamer.withThermalImage(thermalImage -> {
379
- try {
380
- int imgWidth = thermalImage.getWidth();
381
- int imgHeight = thermalImage.getHeight();
382
-
383
- int clampedX = Math.max(0, Math.min(imgWidth - 1, x));
384
- int clampedY = Math.max(0, Math.min(imgHeight - 1, y));
385
-
386
- ThermalValue value = thermalImage.getValueAt(new Point(clampedX, clampedY));
387
- if (value != null) {
388
- result[0] = value.asCelsius().value;
389
- }
390
- } catch (Exception e) {
391
- Log.w(TAG, "Error querying temperature", e);
392
- }
393
- });
394
- } catch (Exception e) {
395
- Log.w(TAG, "Temperature query failed", e);
396
- }
397
-
398
- return result[0];
399
- }
400
-
401
- /**
402
- * Get temperature at normalized coordinates (0.0 to 1.0)
403
- * @param normalizedX X coordinate (0.0 to 1.0)
404
- * @param normalizedY Y coordinate (0.0 to 1.0)
405
- * @return Temperature in Celsius, or Double.NaN if not available
406
- */
407
- public double getTemperatureAtNormalized(double normalizedX, double normalizedY) {
408
- if (streamer == null) {
409
- return Double.NaN;
410
- }
411
-
412
- final double[] result = {Double.NaN};
413
- try {
414
- streamer.withThermalImage(thermalImage -> {
415
- try {
416
- int width = thermalImage.getWidth();
417
- int height = thermalImage.getHeight();
418
-
419
- int x = (int) (normalizedX * (width - 1));
420
- int y = (int) (normalizedY * (height - 1));
421
-
422
- x = Math.max(0, Math.min(width - 1, x));
423
- y = Math.max(0, Math.min(height - 1, y));
424
-
425
- ThermalValue value = thermalImage.getValueAt(new Point(x, y));
426
- if (value != null) {
427
- result[0] = value.asCelsius().value;
428
- }
429
- } catch (Exception e) {
430
- Log.w(TAG, "Error querying temperature (normalized)", e);
431
- }
432
- });
433
- } catch (Exception e) {
434
- Log.w(TAG, "Temperature query failed", e);
435
- }
436
-
437
- return result[0];
438
- }
439
-
440
- /**
441
- * Set palette for thermal image rendering
442
- */
443
- public void setPalette(String paletteName) {
444
- if (streamer == null) {
445
- Log.w(TAG, "No active streamer");
446
- return;
447
- }
448
-
449
- executor.execute(() -> {
450
- try {
451
- Palette palette = findPalette(paletteName);
452
- if (palette != null) {
453
- streamer.withThermalImage(thermalImage -> {
454
- thermalImage.setPalette(palette);
455
- });
456
- Log.d(TAG, "Palette set to: " + paletteName);
457
- }
458
- } catch (Exception e) {
459
- Log.e(TAG, "Error setting palette", e);
460
- }
461
- });
462
- }
463
-
464
- /**
465
- * Get list of available palettes
466
- */
467
- public List<String> getAvailablePalettes() {
468
- List<String> names = new ArrayList<>();
469
- try {
470
- List<Palette> palettes = PaletteManager.getDefaultPalettes();
471
- for (Palette p : palettes) {
472
- names.add(p.name);
473
- }
474
- } catch (Exception e) {
475
- Log.e(TAG, "Error getting palettes", e);
476
- }
477
- return names;
478
- }
479
-
480
- // Find palette by name
481
- private Palette findPalette(String name) {
482
- try {
483
- List<Palette> palettes = PaletteManager.getDefaultPalettes();
484
- for (Palette p : palettes) {
485
- if (p.name.equalsIgnoreCase(name)) {
486
- return p;
487
- }
488
- }
489
- // Return first if not found
490
- if (!palettes.isEmpty()) {
491
- return palettes.get(0);
492
- }
493
- } catch (Exception e) {
494
- Log.e(TAG, "Error finding palette", e);
495
- }
496
- return null;
497
- }
498
-
499
- // Discovery listener - no filtering, returns all devices
500
- private final DiscoveryEventListener discoveryListener = new DiscoveryEventListener() {
501
- @Override
502
- public void onCameraFound(DiscoveredCamera discoveredCamera) {
503
- Identity identity = discoveredCamera.getIdentity();
504
- Log.d(TAG, "Device found: " + identity.deviceId +
505
- " type=" + identity.communicationInterface);
506
-
507
- // Add to list if not already present
508
- synchronized (discoveredDevices) {
509
- boolean exists = false;
510
- for (Identity d : discoveredDevices) {
511
- if (d.deviceId.equals(identity.deviceId)) {
512
- exists = true;
513
- break;
514
- }
515
- }
516
- if (!exists) {
517
- discoveredDevices.add(identity);
518
- }
519
- }
520
-
521
- if (listener != null) {
522
- listener.onDeviceFound(identity);
523
- listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
524
- }
525
- }
526
-
527
- @Override
528
- public void onCameraLost(Identity identity) {
529
- Log.d(TAG, "Device lost: " + identity.deviceId);
530
-
531
- synchronized (discoveredDevices) {
532
- discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
533
- }
534
-
535
- if (listener != null) {
536
- listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
537
- }
538
- }
539
-
540
- @Override
541
- public void onDiscoveryError(CommunicationInterface iface, ErrorCode error) {
542
- Log.e(TAG, "Discovery error: " + iface + " - " + error);
543
- notifyError("Discovery error: " + error);
544
- }
545
-
546
- @Override
547
- public void onDiscoveryFinished(CommunicationInterface iface) {
548
- Log.d(TAG, "Discovery finished for: " + iface);
549
- }
550
- };
551
-
552
- // Connection status listener
553
- private final ConnectionStatusListener connectionStatusListener = new ConnectionStatusListener() {
554
- @Override
555
- public void onDisconnected(ErrorCode error) {
556
- Log.d(TAG, "Disconnected: " + (error != null ? error : "clean"));
557
- camera = null;
558
-
559
- if (listener != null) {
560
- listener.onDisconnected();
561
- }
562
- }
563
- };
564
-
565
- // Helper to notify errors
566
- private void notifyError(String message) {
567
- if (listener != null) {
568
- listener.onError(message);
569
- }
570
- }
571
-
572
- /**
573
- * Cleanup resources
574
- */
575
- public void destroy() {
576
- stop();
577
- disconnect();
578
- discoveredDevices.clear();
579
- listener = null;
580
- instance = null;
581
- Log.d(TAG, "Destroyed");
582
- }
583
- }
1
+ package flir.android;
2
+
3
+ import android.content.Context;
4
+ import android.graphics.Bitmap;
5
+ import android.util.Log;
6
+
7
+ import com.flir.thermalsdk.ErrorCode;
8
+ import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
9
+ import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
10
+ import com.flir.thermalsdk.image.ImageBuffer;
11
+ import com.flir.thermalsdk.image.Palette;
12
+ import com.flir.thermalsdk.image.PaletteManager;
13
+ import com.flir.thermalsdk.image.Point;
14
+ import com.flir.thermalsdk.image.ThermalImage;
15
+ import com.flir.thermalsdk.image.ThermalValue;
16
+ import com.flir.thermalsdk.live.Camera;
17
+ import com.flir.thermalsdk.live.CommunicationInterface;
18
+ import com.flir.thermalsdk.live.ConnectParameters;
19
+ import com.flir.thermalsdk.live.Identity;
20
+ import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
21
+ import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
22
+ import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
23
+ import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
24
+ import com.flir.thermalsdk.live.remote.OnReceived;
25
+ import com.flir.thermalsdk.live.streaming.Stream;
26
+ import com.flir.thermalsdk.live.streaming.ThermalStreamer;
27
+
28
+ import java.util.ArrayList;
29
+ import java.util.Collections;
30
+ import java.util.List;
31
+ import java.util.concurrent.Executor;
32
+ import java.util.concurrent.Executors;
33
+
34
+ /**
35
+ * Simplified FLIR SDK Manager - handles discovery, connection, and streaming
36
+ * No filtering - returns all discovered devices (USB, Network, Emulator)
37
+ */
38
+ public class FlirSdkManager {
39
+ private static final String TAG = "FlirSdkManager";
40
+
41
+ // Singleton instance
42
+ private static FlirSdkManager instance;
43
+
44
+ // Core components
45
+ private final Context context;
46
+ // Use bounded thread pool to prevent thread explosion during rapid frame processing
47
+ private final Executor executor = Executors.newFixedThreadPool(2);
48
+ // Single-threaded executor for frame processing to ensure ordered processing
49
+ private final Executor frameExecutor = Executors.newSingleThreadExecutor();
50
+ // Frame processing guard - skip frames if still processing previous one
51
+ private volatile boolean isProcessingFrame = false;
52
+ private long lastFrameProcessedMs = 0;
53
+ private static final long MIN_FRAME_INTERVAL_MS = 50; // Max ~20 FPS frame processing
54
+
55
+ // State
56
+ private boolean isInitialized = false;
57
+ private boolean isScanning = false;
58
+ private Camera camera;
59
+ private ThermalStreamer streamer;
60
+ private Stream activeStream;
61
+ private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
62
+
63
+ // Listener
64
+ private Listener listener;
65
+
66
+ /**
67
+ * Listener interface for SDK events
68
+ */
69
+ public interface Listener {
70
+ void onDeviceFound(Identity identity);
71
+ void onDeviceListUpdated(List<Identity> devices);
72
+ void onConnected(Identity identity);
73
+ void onDisconnected();
74
+ void onFrame(Bitmap bitmap);
75
+ void onError(String message);
76
+ }
77
+
78
+ // Private constructor for singleton
79
+ private FlirSdkManager(Context context) {
80
+ this.context = context.getApplicationContext();
81
+ }
82
+
83
+ /**
84
+ * Get singleton instance
85
+ */
86
+ public static synchronized FlirSdkManager getInstance(Context context) {
87
+ if (instance == null) {
88
+ instance = new FlirSdkManager(context);
89
+ }
90
+ return instance;
91
+ }
92
+
93
+ /**
94
+ * Set listener for SDK events
95
+ */
96
+ public void setListener(Listener listener) {
97
+ this.listener = listener;
98
+ }
99
+
100
+ /**
101
+ * Initialize the FLIR Thermal SDK
102
+ */
103
+ public void initialize() {
104
+ if (isInitialized) {
105
+ Log.d(TAG, "Already initialized");
106
+ return;
107
+ }
108
+
109
+ try {
110
+ ThermalSdkAndroid.init(context);
111
+ isInitialized = true;
112
+ Log.d(TAG, "SDK initialized successfully");
113
+ } catch (Exception e) {
114
+ Log.e(TAG, "Failed to initialize SDK", e);
115
+ notifyError("SDK initialization failed: " + e.getMessage());
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if SDK is initialized
121
+ */
122
+ public boolean isInitialized() {
123
+ return isInitialized;
124
+ }
125
+
126
+ /**
127
+ * Start scanning for all device types (USB, Network, Emulator)
128
+ * Returns ALL devices - no filtering
129
+ */
130
+ public void scan() {
131
+ if (!isInitialized) {
132
+ Log.e(TAG, "SDK not initialized");
133
+ notifyError("SDK not initialized");
134
+ return;
135
+ }
136
+
137
+ if (isScanning) {
138
+ Log.d(TAG, "Already scanning");
139
+ return;
140
+ }
141
+
142
+ isScanning = true;
143
+ discoveredDevices.clear();
144
+
145
+ Log.d(TAG, "Starting discovery for EMULATOR, NETWORK, USB...");
146
+
147
+ try {
148
+ DiscoveryFactory.getInstance().scan(
149
+ discoveryListener,
150
+ CommunicationInterface.EMULATOR,
151
+ CommunicationInterface.NETWORK,
152
+ CommunicationInterface.USB
153
+ );
154
+ } catch (Exception e) {
155
+ Log.e(TAG, "Failed to start scan", e);
156
+ isScanning = false;
157
+ notifyError("Scan failed: " + e.getMessage());
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Stop scanning for devices
163
+ */
164
+ public void stop() {
165
+ if (!isScanning) {
166
+ return;
167
+ }
168
+
169
+ try {
170
+ DiscoveryFactory.getInstance().stop(
171
+ CommunicationInterface.EMULATOR,
172
+ CommunicationInterface.NETWORK,
173
+ CommunicationInterface.USB
174
+ );
175
+ } catch (Exception e) {
176
+ Log.e(TAG, "Failed to stop scan", e);
177
+ }
178
+
179
+ isScanning = false;
180
+ Log.d(TAG, "Discovery stopped");
181
+ }
182
+
183
+ /**
184
+ * Get list of discovered devices
185
+ */
186
+ public List<Identity> getDiscoveredDevices() {
187
+ return new ArrayList<>(discoveredDevices);
188
+ }
189
+
190
+ /**
191
+ * Connect to a device
192
+ */
193
+ public void connect(Identity identity) {
194
+ if (identity == null) {
195
+ notifyError("Invalid identity");
196
+ return;
197
+ }
198
+
199
+ // Disconnect if already connected
200
+ if (camera != null) {
201
+ disconnect();
202
+ }
203
+
204
+ Log.d(TAG, "Connecting to: " + identity.deviceId);
205
+
206
+ // Run connection on background thread since it's blocking
207
+ executor.execute(() -> {
208
+ try {
209
+ camera = new Camera();
210
+ camera.connect(identity, connectionStatusListener, new ConnectParameters());
211
+
212
+ Log.d(TAG, "Connected to camera");
213
+
214
+ if (listener != null) {
215
+ listener.onConnected(identity);
216
+ }
217
+ } catch (Exception e) {
218
+ Log.e(TAG, "Connection failed", e);
219
+ camera = null;
220
+ notifyError("Connection failed: " + e.getMessage());
221
+ }
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Disconnect from current device
227
+ */
228
+ public void disconnect() {
229
+ stopStream();
230
+
231
+ if (camera != null) {
232
+ try {
233
+ camera.disconnect();
234
+ } catch (Exception e) {
235
+ Log.e(TAG, "Error disconnecting", e);
236
+ }
237
+ camera = null;
238
+ }
239
+
240
+ if (listener != null) {
241
+ listener.onDisconnected();
242
+ }
243
+
244
+ Log.d(TAG, "Disconnected");
245
+ }
246
+
247
+ /**
248
+ * Check if connected
249
+ */
250
+ public boolean isConnected() {
251
+ return camera != null;
252
+ }
253
+
254
+ /**
255
+ * Start streaming from connected device
256
+ */
257
+ public void startStream() {
258
+ if (camera == null) {
259
+ notifyError("Not connected");
260
+ return;
261
+ }
262
+
263
+ executor.execute(() -> {
264
+ try {
265
+ // Get available streams
266
+ List<Stream> streams = camera.getStreams();
267
+ if (streams == null || streams.isEmpty()) {
268
+ notifyError("No streams available");
269
+ return;
270
+ }
271
+
272
+ // Find thermal stream
273
+ Stream thermalStream = null;
274
+ for (Stream stream : streams) {
275
+ if (stream.isThermal()) {
276
+ thermalStream = stream;
277
+ break;
278
+ }
279
+ }
280
+
281
+ if (thermalStream == null) {
282
+ thermalStream = streams.get(0);
283
+ }
284
+
285
+ activeStream = thermalStream;
286
+ streamer = new ThermalStreamer(thermalStream);
287
+
288
+ // Start receiving frames using OnReceived and OnRemoteError
289
+ thermalStream.start(
290
+ (OnReceived<Void>) v -> {
291
+ // FRAME DROP GUARD: Skip frame if still processing previous one
292
+ // This prevents thread buildup and ensures smooth frame flow
293
+ long now = System.currentTimeMillis();
294
+ if (isProcessingFrame || (now - lastFrameProcessedMs < MIN_FRAME_INTERVAL_MS)) {
295
+ // Drop frame - processing is behind or too soon since last frame
296
+ return;
297
+ }
298
+
299
+ // Mark processing start before queuing task
300
+ isProcessingFrame = true;
301
+
302
+ // Use single-threaded frameExecutor to ensure ordered frame processing
303
+ frameExecutor.execute(() -> {
304
+ try {
305
+ if (streamer != null) {
306
+ streamer.update();
307
+
308
+ // Get ImageBuffer and convert to Bitmap
309
+ ImageBuffer imageBuffer = streamer.getImage();
310
+ if (imageBuffer != null && listener != null) {
311
+ BitmapAndroid bitmapAndroid = BitmapAndroid.createBitmap(imageBuffer);
312
+ Bitmap bitmap = bitmapAndroid.getBitMap();
313
+ if (bitmap != null) {
314
+ listener.onFrame(bitmap);
315
+ }
316
+ }
317
+ }
318
+ } catch (Exception e) {
319
+ Log.e(TAG, "Error processing frame", e);
320
+ } finally {
321
+ // Reset frame processing guard to allow next frame
322
+ lastFrameProcessedMs = System.currentTimeMillis();
323
+ isProcessingFrame = false;
324
+ }
325
+ });
326
+ },
327
+ error -> {
328
+ Log.e(TAG, "Stream error: " + error);
329
+ notifyError("Stream error: " + error);
330
+ }
331
+ );
332
+
333
+ Log.d(TAG, "Streaming started");
334
+
335
+ } catch (Exception e) {
336
+ Log.e(TAG, "Failed to start stream", e);
337
+ notifyError("Stream failed: " + e.getMessage());
338
+ }
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Stop streaming
344
+ */
345
+ public void stopStream() {
346
+ if (activeStream != null) {
347
+ try {
348
+ activeStream.stop();
349
+ } catch (Exception e) {
350
+ Log.e(TAG, "Error stopping stream", e);
351
+ }
352
+ activeStream = null;
353
+ }
354
+
355
+ streamer = null;
356
+
357
+ // Reset frame processing state
358
+ isProcessingFrame = false;
359
+ lastFrameProcessedMs = 0;
360
+
361
+ Log.d(TAG, "Streaming stopped");
362
+ }
363
+
364
+ /**
365
+ * Get temperature at a specific point in the image
366
+ * Queries the SDK directly - simple and no locks needed
367
+ * @param x X coordinate (0 to image width-1)
368
+ * @param y Y coordinate (0 to image height-1)
369
+ * @return Temperature in Celsius, or Double.NaN if not available
370
+ */
371
+ public double getTemperatureAt(int x, int y) {
372
+ if (streamer == null) {
373
+ return Double.NaN;
374
+ }
375
+
376
+ final double[] result = {Double.NaN};
377
+ try {
378
+ streamer.withThermalImage(thermalImage -> {
379
+ try {
380
+ int imgWidth = thermalImage.getWidth();
381
+ int imgHeight = thermalImage.getHeight();
382
+
383
+ int clampedX = Math.max(0, Math.min(imgWidth - 1, x));
384
+ int clampedY = Math.max(0, Math.min(imgHeight - 1, y));
385
+
386
+ ThermalValue value = thermalImage.getValueAt(new Point(clampedX, clampedY));
387
+ if (value != null) {
388
+ result[0] = value.asCelsius().value;
389
+ }
390
+ } catch (Exception e) {
391
+ Log.w(TAG, "Error querying temperature", e);
392
+ }
393
+ });
394
+ } catch (Exception e) {
395
+ Log.w(TAG, "Temperature query failed", e);
396
+ }
397
+
398
+ return result[0];
399
+ }
400
+
401
+ /**
402
+ * Get temperature at normalized coordinates (0.0 to 1.0)
403
+ * @param normalizedX X coordinate (0.0 to 1.0)
404
+ * @param normalizedY Y coordinate (0.0 to 1.0)
405
+ * @return Temperature in Celsius, or Double.NaN if not available
406
+ */
407
+ public double getTemperatureAtNormalized(double normalizedX, double normalizedY) {
408
+ if (streamer == null) {
409
+ return Double.NaN;
410
+ }
411
+
412
+ final double[] result = {Double.NaN};
413
+ try {
414
+ streamer.withThermalImage(thermalImage -> {
415
+ try {
416
+ int width = thermalImage.getWidth();
417
+ int height = thermalImage.getHeight();
418
+
419
+ int x = (int) (normalizedX * (width - 1));
420
+ int y = (int) (normalizedY * (height - 1));
421
+
422
+ x = Math.max(0, Math.min(width - 1, x));
423
+ y = Math.max(0, Math.min(height - 1, y));
424
+
425
+ ThermalValue value = thermalImage.getValueAt(new Point(x, y));
426
+ if (value != null) {
427
+ result[0] = value.asCelsius().value;
428
+ }
429
+ } catch (Exception e) {
430
+ Log.w(TAG, "Error querying temperature (normalized)", e);
431
+ }
432
+ });
433
+ } catch (Exception e) {
434
+ Log.w(TAG, "Temperature query failed", e);
435
+ }
436
+
437
+ return result[0];
438
+ }
439
+
440
+ /**
441
+ * Set palette for thermal image rendering
442
+ */
443
+ public void setPalette(String paletteName) {
444
+ if (streamer == null) {
445
+ Log.w(TAG, "No active streamer");
446
+ return;
447
+ }
448
+
449
+ executor.execute(() -> {
450
+ try {
451
+ Palette palette = findPalette(paletteName);
452
+ if (palette != null) {
453
+ streamer.withThermalImage(thermalImage -> {
454
+ thermalImage.setPalette(palette);
455
+ });
456
+ Log.d(TAG, "Palette set to: " + paletteName);
457
+ }
458
+ } catch (Exception e) {
459
+ Log.e(TAG, "Error setting palette", e);
460
+ }
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Get list of available palettes
466
+ */
467
+ public List<String> getAvailablePalettes() {
468
+ List<String> names = new ArrayList<>();
469
+ try {
470
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
471
+ for (Palette p : palettes) {
472
+ names.add(p.name);
473
+ }
474
+ } catch (Exception e) {
475
+ Log.e(TAG, "Error getting palettes", e);
476
+ }
477
+ return names;
478
+ }
479
+
480
+ // Find palette by name
481
+ private Palette findPalette(String name) {
482
+ try {
483
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
484
+ for (Palette p : palettes) {
485
+ if (p.name.equalsIgnoreCase(name)) {
486
+ return p;
487
+ }
488
+ }
489
+ // Return first if not found
490
+ if (!palettes.isEmpty()) {
491
+ return palettes.get(0);
492
+ }
493
+ } catch (Exception e) {
494
+ Log.e(TAG, "Error finding palette", e);
495
+ }
496
+ return null;
497
+ }
498
+
499
+ // Discovery listener - no filtering, returns all devices
500
+ private final DiscoveryEventListener discoveryListener = new DiscoveryEventListener() {
501
+ @Override
502
+ public void onCameraFound(DiscoveredCamera discoveredCamera) {
503
+ Identity identity = discoveredCamera.getIdentity();
504
+ Log.d(TAG, "Device found: " + identity.deviceId +
505
+ " type=" + identity.communicationInterface);
506
+
507
+ // Add to list if not already present
508
+ synchronized (discoveredDevices) {
509
+ boolean exists = false;
510
+ for (Identity d : discoveredDevices) {
511
+ if (d.deviceId.equals(identity.deviceId)) {
512
+ exists = true;
513
+ break;
514
+ }
515
+ }
516
+ if (!exists) {
517
+ discoveredDevices.add(identity);
518
+ }
519
+ }
520
+
521
+ if (listener != null) {
522
+ listener.onDeviceFound(identity);
523
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
524
+ }
525
+ }
526
+
527
+ @Override
528
+ public void onCameraLost(Identity identity) {
529
+ Log.d(TAG, "Device lost: " + identity.deviceId);
530
+
531
+ synchronized (discoveredDevices) {
532
+ discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
533
+ }
534
+
535
+ if (listener != null) {
536
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
537
+ }
538
+ }
539
+
540
+ @Override
541
+ public void onDiscoveryError(CommunicationInterface iface, ErrorCode error) {
542
+ Log.e(TAG, "Discovery error: " + iface + " - " + error);
543
+ notifyError("Discovery error: " + error);
544
+ }
545
+
546
+ @Override
547
+ public void onDiscoveryFinished(CommunicationInterface iface) {
548
+ Log.d(TAG, "Discovery finished for: " + iface);
549
+ }
550
+ };
551
+
552
+ // Connection status listener
553
+ private final ConnectionStatusListener connectionStatusListener = new ConnectionStatusListener() {
554
+ @Override
555
+ public void onDisconnected(ErrorCode error) {
556
+ Log.d(TAG, "Disconnected: " + (error != null ? error : "clean"));
557
+ camera = null;
558
+
559
+ if (listener != null) {
560
+ listener.onDisconnected();
561
+ }
562
+ }
563
+ };
564
+
565
+ // Helper to notify errors
566
+ private void notifyError(String message) {
567
+ if (listener != null) {
568
+ listener.onError(message);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Cleanup resources
574
+ */
575
+ public void destroy() {
576
+ stop();
577
+ disconnect();
578
+ discoveredDevices.clear();
579
+ listener = null;
580
+ instance = null;
581
+ Log.d(TAG, "Destroyed");
582
+ }
583
+ }