ilabs-flir 1.0.5 → 1.0.7

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.
@@ -1,15 +1,13 @@
1
1
  package flir.android;
2
2
 
3
+ import android.content.Context;
3
4
  import android.graphics.Bitmap;
4
- import android.os.Handler;
5
- import android.os.Looper;
6
5
  import android.util.Log;
7
6
 
8
7
  import com.flir.thermalsdk.ErrorCode;
9
8
  import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
10
9
  import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
11
10
  import com.flir.thermalsdk.image.ImageBuffer;
12
- import com.flir.thermalsdk.image.JavaImageBuffer;
13
11
  import com.flir.thermalsdk.image.Palette;
14
12
  import com.flir.thermalsdk.image.PaletteManager;
15
13
  import com.flir.thermalsdk.image.Point;
@@ -24,737 +22,562 @@ import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
24
22
  import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
25
23
  import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
26
24
  import com.flir.thermalsdk.live.remote.OnReceived;
27
- import com.flir.thermalsdk.live.remote.OnRemoteError;
28
25
  import com.flir.thermalsdk.live.streaming.Stream;
29
26
  import com.flir.thermalsdk.live.streaming.ThermalStreamer;
30
27
 
31
28
  import java.util.ArrayList;
29
+ import java.util.Collections;
32
30
  import java.util.List;
33
- import java.util.concurrent.CopyOnWriteArrayList;
31
+ import java.util.concurrent.Executor;
34
32
  import java.util.concurrent.Executors;
35
- import java.util.concurrent.ScheduledExecutorService;
36
- import java.util.concurrent.ScheduledFuture;
37
- import java.util.concurrent.TimeUnit;
38
- import java.util.concurrent.atomic.AtomicBoolean;
39
33
 
40
34
  /**
41
- * FLIR SDK Manager - Handles device discovery, connection, and streaming.
42
- * Uses the official FLIR ThermalSDK directly (bundled in AAR).
43
- *
44
- * Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
35
+ * Simplified FLIR SDK Manager - handles discovery, connection, and streaming
36
+ * No filtering - returns all discovered devices (USB, Network, Emulator)
45
37
  */
46
38
  public class FlirSdkManager {
47
39
  private static final String TAG = "FlirSdkManager";
48
- private static final String FLOW_TAG = "FLIR_FLOW";
49
40
 
50
- // Discovery timeout in milliseconds
51
- private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000;
52
- private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0;
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;
53
65
 
54
- // Emulator types
55
- public enum EmulatorType {
56
- FLIR_ONE_EDGE, // WiFi emulator
57
- FLIR_ONE // USB emulator
58
- }
59
-
60
- // Communication interfaces
61
- public enum CommInterface {
62
- USB,
63
- NETWORK,
64
- EMULATOR
65
- }
66
-
67
- // Listener interface for callbacks
66
+ /**
67
+ * Listener interface for SDK events
68
+ */
68
69
  public interface Listener {
70
+ void onDeviceFound(Identity identity);
71
+ void onDeviceListUpdated(List<Identity> devices);
72
+ void onConnected(Identity identity);
73
+ void onDisconnected();
69
74
  void onFrame(Bitmap bitmap);
70
- void onTemperature(double temp, int x, int y);
71
- void onDeviceFound(String deviceId, String deviceName, boolean isEmulator);
72
- void onDeviceListUpdated(List<DeviceInfo> devices);
73
- void onDeviceConnected(String deviceId, String deviceName, boolean isEmulator);
74
- void onDeviceDisconnected();
75
- void onDiscoveryStarted();
76
- void onDiscoveryTimeout();
77
- void onStreamStarted(String streamType);
78
- void onError(String error);
75
+ void onError(String message);
79
76
  }
80
77
 
81
- // Device info class
82
- public static class DeviceInfo {
83
- public final String deviceId;
84
- public final String deviceName;
85
- public final boolean isEmulator;
86
- public final CommInterface commInterface;
87
- public final Identity identity;
88
-
89
- DeviceInfo(String id, String name, boolean emu, CommInterface iface, Identity identity) {
90
- this.deviceId = id;
91
- this.deviceName = name;
92
- this.isEmulator = emu;
93
- this.commInterface = iface;
94
- this.identity = identity;
95
- }
78
+ // Private constructor for singleton
79
+ private FlirSdkManager(Context context) {
80
+ this.context = context.getApplicationContext();
96
81
  }
97
-
98
- private final Listener listener;
99
- private final android.content.Context appContext;
100
- private final Handler mainHandler = new Handler(Looper.getMainLooper());
101
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
102
-
103
- // SDK objects
104
- private Camera camera = null;
105
- private Stream currentStream = null;
106
- private ThermalStreamer thermalStreamer = null;
107
- private Palette currentPalette = null;
108
82
 
109
- // State tracking
110
- private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
111
- private final AtomicBoolean isConnected = new AtomicBoolean(false);
112
- private final AtomicBoolean isStreaming = new AtomicBoolean(false);
113
- private final AtomicBoolean isEmulatorMode = new AtomicBoolean(false);
114
- private final CopyOnWriteArrayList<DeviceInfo> discoveredDevices = new CopyOnWriteArrayList<>();
115
- private ScheduledFuture<?> discoveryTimeoutFuture = null;
116
- private DeviceInfo connectedDevice = null;
117
- private EmulatorType emulatorType = EmulatorType.FLIR_ONE_EDGE;
118
-
119
- // Frame state
120
- private volatile Bitmap latestFrame = null;
121
- private volatile ThermalImage currentThermalImage = null;
122
- private String currentStreamKind = null;
123
-
124
- // Step tracking for debugging
125
- private int stepCounter = 0;
126
- private long flowStartTime = 0;
127
-
128
- // SDK initialization state
129
- private static boolean sdkInitialized = false;
130
-
131
- FlirSdkManager(Listener listener, android.content.Context context) {
132
- this.listener = listener;
133
- this.appContext = context != null ? context.getApplicationContext() : null;
134
- }
135
-
136
- private void logStep(String step, String details) {
137
- stepCounter++;
138
- long elapsed = flowStartTime > 0 ? System.currentTimeMillis() - flowStartTime : 0;
139
- Log.i(FLOW_TAG, String.format("[Step %d] [+%dms] %s: %s", stepCounter, elapsed, step, details));
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;
140
91
  }
141
92
 
142
- private void resetFlowTracking() {
143
- stepCounter = 0;
144
- flowStartTime = System.currentTimeMillis();
145
- Log.i(FLOW_TAG, "========== FLIR FLOW STARTED ==========");
93
+ /**
94
+ * Set listener for SDK events
95
+ */
96
+ public void setListener(Listener listener) {
97
+ this.listener = listener;
146
98
  }
147
99
 
148
- // ==================== SDK INITIALIZATION ====================
149
-
150
- private boolean initializeSdk() {
151
- if (sdkInitialized) {
152
- Log.d(TAG, "[FLIR SDK] Already initialized");
153
- return true;
154
- }
155
-
156
- if (appContext == null) {
157
- Log.e(TAG, "[FLIR SDK] No context available");
158
- return false;
100
+ /**
101
+ * Initialize the FLIR Thermal SDK
102
+ */
103
+ public void initialize() {
104
+ if (isInitialized) {
105
+ Log.d(TAG, "Already initialized");
106
+ return;
159
107
  }
160
108
 
161
109
  try {
162
- Log.i(TAG, "[FLIR SDK] Initializing ThermalSdkAndroid...");
163
- ThermalSdkAndroid.init(appContext);
164
- sdkInitialized = true;
165
- Log.i(TAG, "[FLIR SDK] SDK Version: " + ThermalSdkAndroid.getVersion());
166
- return true;
167
- } catch (Throwable t) {
168
- Log.e(TAG, "[FLIR SDK] Initialization failed: " + t.getMessage(), t);
169
- notifyError("SDK initialization failed: " + t.getMessage());
170
- return false;
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());
171
116
  }
172
117
  }
173
118
 
174
- // ==================== PUBLIC API ====================
175
-
176
- public void setEmulatorType(EmulatorType type) {
177
- this.emulatorType = type;
178
- Log.i(TAG, "[FLIR] Emulator type set to: " + type);
179
- logStep("SET_EMULATOR_TYPE", "type=" + type);
119
+ /**
120
+ * Check if SDK is initialized
121
+ */
122
+ public boolean isInitialized() {
123
+ return isInitialized;
180
124
  }
181
125
 
182
- public void startDiscovery(boolean forceEmulator) {
183
- resetFlowTracking();
184
- logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
185
- Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
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
+ }
186
136
 
187
- // Disconnect current device first
188
- if (isConnected.get()) {
189
- logStep("DISCONNECT_PREVIOUS", "Disconnecting current device");
190
- disconnect();
137
+ if (isScanning) {
138
+ Log.d(TAG, "Already scanning");
139
+ return;
191
140
  }
192
141
 
142
+ isScanning = true;
193
143
  discoveredDevices.clear();
194
- logStep("CLEAR_DEVICES", "Cleared discovered devices list");
195
144
 
196
- if (forceEmulator) {
197
- logStep("MODE_EMULATOR", "Forcing emulator mode");
198
- isEmulatorMode.set(true);
199
- startEmulatorDiscovery();
200
- } else {
201
- logStep("MODE_FULL_DISCOVERY", "Starting full discovery, timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
202
- isEmulatorMode.set(false);
203
- startFullDiscovery();
204
- }
205
- }
206
-
207
- public void stopDiscovery() {
208
- Log.i(TAG, "[FLIR] stopDiscovery()");
209
- cancelDiscoveryTimeout();
210
- isDiscovering.set(false);
145
+ Log.d(TAG, "Starting discovery for EMULATOR, NETWORK, USB...");
211
146
 
212
147
  try {
213
- DiscoveryFactory.getInstance().stop();
214
- } catch (Throwable t) {
215
- Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
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());
216
158
  }
217
159
  }
218
160
 
219
- public void connectToDevice(String deviceId) {
220
- Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
161
+ /**
162
+ * Stop scanning for devices
163
+ */
164
+ public void stop() {
165
+ if (!isScanning) {
166
+ return;
167
+ }
221
168
 
222
- DeviceInfo target = null;
223
- for (DeviceInfo d : discoveredDevices) {
224
- if (d.deviceId.equals(deviceId)) {
225
- target = d;
226
- break;
227
- }
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);
228
177
  }
229
178
 
230
- if (target == null) {
231
- notifyError("Device not found: " + deviceId);
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");
232
196
  return;
233
197
  }
234
198
 
235
- if (isConnected.get()) {
199
+ // Disconnect if already connected
200
+ if (camera != null) {
236
201
  disconnect();
237
202
  }
238
203
 
239
- connectToIdentity(target);
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
+ });
240
223
  }
241
224
 
225
+ /**
226
+ * Disconnect from current device
227
+ */
242
228
  public void disconnect() {
243
- Log.i(TAG, "[FLIR] disconnect()");
244
-
245
- stopStreaming();
229
+ stopStream();
246
230
 
247
231
  if (camera != null) {
248
232
  try {
249
233
  camera.disconnect();
250
- } catch (Throwable t) {
251
- Log.w(TAG, "[FLIR] Camera disconnect failed: " + t.getMessage());
234
+ } catch (Exception e) {
235
+ Log.e(TAG, "Error disconnecting", e);
252
236
  }
253
237
  camera = null;
254
238
  }
255
239
 
256
- isConnected.set(false);
257
- connectedDevice = null;
240
+ if (listener != null) {
241
+ listener.onDisconnected();
242
+ }
258
243
 
259
- mainHandler.post(() -> {
260
- if (listener != null) {
261
- listener.onDeviceDisconnected();
262
- }
263
- });
244
+ Log.d(TAG, "Disconnected");
264
245
  }
265
246
 
266
- public void setStreamType(String streamType) {
267
- Log.i(TAG, "[FLIR] setStreamType: " + streamType);
268
- currentStreamKind = streamType;
269
-
270
- if (isConnected.get() && camera != null) {
271
- stopStreaming();
272
- startStreaming();
273
- }
247
+ /**
248
+ * Check if connected
249
+ */
250
+ public boolean isConnected() {
251
+ return camera != null;
274
252
  }
275
253
 
276
- public void setPalette(String paletteName) {
277
- Log.i(TAG, "[FLIR] setPalette: " + paletteName);
254
+ /**
255
+ * Start streaming from connected device
256
+ */
257
+ public void startStream() {
258
+ if (camera == null) {
259
+ notifyError("Not connected");
260
+ return;
261
+ }
278
262
 
279
- try {
280
- List<Palette> palettes = PaletteManager.getDefaultPalettes();
281
- for (Palette p : palettes) {
282
- if (p.name.equalsIgnoreCase(paletteName)) {
283
- currentPalette = p;
284
- Log.i(TAG, "[FLIR] Palette set to: " + p.name);
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");
285
269
  return;
286
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());
287
338
  }
288
- Log.w(TAG, "[FLIR] Palette not found: " + paletteName);
289
- } catch (Throwable t) {
290
- Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
291
- }
339
+ });
292
340
  }
293
341
 
294
- public void getTemperatureAt(int x, int y, Bitmap source) {
295
- Log.d(TAG, "[FLIR] getTemperatureAt(" + x + ", " + y + ")");
296
- double temp = getTemperatureAtPoint(x, y);
297
- if (!Double.isNaN(temp) && listener != null) {
298
- mainHandler.post(() -> listener.onTemperature(temp, x, y));
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;
299
353
  }
354
+
355
+ streamer = null;
356
+
357
+ // Reset frame processing state
358
+ isProcessingFrame = false;
359
+ lastFrameProcessedMs = 0;
360
+
361
+ Log.d(TAG, "Streaming stopped");
300
362
  }
301
363
 
302
364
  /**
303
- * Get temperature at a specific point from the current thermal image
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
304
370
  */
305
- public double getTemperatureAtPoint(int x, int y) {
306
- if (currentThermalImage == null) {
371
+ public double getTemperatureAt(int x, int y) {
372
+ if (streamer == null) {
307
373
  return Double.NaN;
308
374
  }
309
375
 
376
+ final double[] result = {Double.NaN};
310
377
  try {
311
- // Clamp coordinates to image bounds
312
- int imgWidth = currentThermalImage.getWidth();
313
- int imgHeight = currentThermalImage.getHeight();
314
- int clampedX = Math.max(0, Math.min(x, imgWidth - 1));
315
- int clampedY = Math.max(0, Math.min(y, imgHeight - 1));
316
-
317
- ThermalValue value = currentThermalImage.getValueAt(new Point(clampedX, clampedY));
318
- if (value != null) {
319
- return value.asCelsius().value;
320
- }
321
- } catch (Throwable t) {
322
- Log.w(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
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);
323
396
  }
324
- return Double.NaN;
325
- }
326
-
327
- public Bitmap getLatestFrame() {
328
- return latestFrame;
329
- }
330
-
331
- public List<DeviceInfo> getDiscoveredDevices() {
332
- return new ArrayList<>(discoveredDevices);
333
- }
334
-
335
- public boolean isConnected() {
336
- return isConnected.get();
337
- }
338
-
339
- public boolean isStreaming() {
340
- return isStreaming.get();
341
- }
342
-
343
- public void destroy() {
344
- Log.i(TAG, "[FLIR] destroy()");
345
- stopDiscovery();
346
- disconnect();
347
- scheduler.shutdown();
397
+
398
+ return result[0];
348
399
  }
349
400
 
350
- // ==================== DISCOVERY ====================
351
-
352
- private void startFullDiscovery() {
353
- Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
354
-
355
- if (!initializeSdk()) {
356
- Log.w(TAG, "[FLIR] SDK not available, falling back to emulator");
357
- startEmulatorDiscovery();
358
- return;
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;
359
410
  }
360
411
 
361
- isDiscovering.set(true);
362
- mainHandler.post(() -> {
363
- if (listener != null) {
364
- listener.onDiscoveryStarted();
365
- }
366
- });
367
-
412
+ final double[] result = {Double.NaN};
368
413
  try {
369
- CommunicationInterface[] interfaces = {
370
- CommunicationInterface.USB,
371
- CommunicationInterface.NETWORK,
372
- CommunicationInterface.EMULATOR
373
- };
374
-
375
- DiscoveryFactory.getInstance().scan(new DiscoveryEventListener() {
376
- @Override
377
- public void onCameraFound(DiscoveredCamera discoveredCamera) {
378
- handleCameraFound(discoveredCamera);
379
- }
380
-
381
- @Override
382
- public void onDiscoveryError(CommunicationInterface iface, ErrorCode errorCode) {
383
- Log.w(TAG, "[FLIR] Discovery error on " + iface + ": " + errorCode);
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);
384
431
  }
385
- }, interfaces);
386
-
387
- // Set timeout for device discovery
388
- scheduleDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
389
-
390
- } catch (Throwable t) {
391
- Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
392
- notifyError("Discovery failed: " + t.getMessage());
393
- startEmulatorDiscovery();
432
+ });
433
+ } catch (Exception e) {
434
+ Log.w(TAG, "Temperature query failed", e);
394
435
  }
436
+
437
+ return result[0];
395
438
  }
396
439
 
397
- private void startEmulatorDiscovery() {
398
- logStep("EMULATOR_DISCOVERY_START", "type=" + emulatorType);
399
- Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
400
-
401
- if (!initializeSdk()) {
402
- notifyError("SDK initialization failed");
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");
403
446
  return;
404
447
  }
405
448
 
406
- isDiscovering.set(true);
407
- isEmulatorMode.set(true);
408
-
409
- mainHandler.post(() -> {
410
- if (listener != null) {
411
- listener.onDiscoveryStarted();
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);
412
460
  }
413
461
  });
414
-
415
- try {
416
- DiscoveryFactory.getInstance().scan(new DiscoveryEventListener() {
417
- @Override
418
- public void onCameraFound(DiscoveredCamera discoveredCamera) {
419
- handleCameraFound(discoveredCamera);
420
- }
421
-
422
- @Override
423
- public void onDiscoveryError(CommunicationInterface iface, ErrorCode errorCode) {
424
- Log.w(TAG, "[FLIR] Emulator discovery error: " + errorCode);
425
- }
426
- }, CommunicationInterface.EMULATOR);
427
-
428
- // Short timeout for emulator
429
- scheduleDiscoveryTimeout(2000);
430
-
431
- } catch (Throwable t) {
432
- Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
433
- notifyError("Emulator discovery failed: " + t.getMessage());
434
- }
435
462
  }
436
463
 
437
- private void handleCameraFound(DiscoveredCamera discoveredCamera) {
438
- Identity identity = discoveredCamera.getIdentity();
439
- String deviceId = identity.deviceId;
440
- String deviceName = identity.toString();
441
- CommunicationInterface iface = identity.communicationInterface;
442
- boolean isEmulator = (iface == CommunicationInterface.EMULATOR);
443
-
444
- Log.i(TAG, "[FLIR] Camera found: " + deviceName + " (" + iface + ")");
445
- logStep("DEVICE_FOUND", "id=" + deviceId + ", name=" + deviceName + ", interface=" + iface);
446
-
447
- CommInterface commIface;
448
- switch (iface) {
449
- case USB:
450
- commIface = CommInterface.USB;
451
- break;
452
- case NETWORK:
453
- commIface = CommInterface.NETWORK;
454
- break;
455
- default:
456
- commIface = CommInterface.EMULATOR;
457
- }
458
-
459
- DeviceInfo deviceInfo = new DeviceInfo(deviceId, deviceName, isEmulator, commIface, identity);
460
-
461
- // Avoid duplicates
462
- boolean exists = false;
463
- for (DeviceInfo d : discoveredDevices) {
464
- if (d.deviceId.equals(deviceId)) {
465
- exists = true;
466
- break;
467
- }
468
- }
469
-
470
- if (!exists) {
471
- discoveredDevices.add(deviceInfo);
472
-
473
- mainHandler.post(() -> {
474
- if (listener != null) {
475
- listener.onDeviceFound(deviceId, deviceName, isEmulator);
476
- listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
477
- }
478
- });
479
-
480
- // Auto-connect to first device found
481
- if (discoveredDevices.size() == 1 && !isConnected.get()) {
482
- cancelDiscoveryTimeout();
483
- logStep("AUTO_CONNECT", "Connecting to first found device");
484
- connectToIdentity(deviceInfo);
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);
485
473
  }
474
+ } catch (Exception e) {
475
+ Log.e(TAG, "Error getting palettes", e);
486
476
  }
477
+ return names;
487
478
  }
488
479
 
489
- private void scheduleDiscoveryTimeout(long timeoutMs) {
490
- cancelDiscoveryTimeout();
491
-
492
- discoveryTimeoutFuture = scheduler.schedule(() -> {
493
- Log.i(TAG, "[FLIR] Discovery timeout after " + timeoutMs + "ms");
494
- logStep("DISCOVERY_TIMEOUT", "timeout=" + timeoutMs + "ms, devicesFound=" + discoveredDevices.size());
495
-
496
- isDiscovering.set(false);
497
- stopDiscovery();
498
-
499
- mainHandler.post(() -> {
500
- if (listener != null) {
501
- listener.onDiscoveryTimeout();
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;
502
487
  }
503
- });
504
-
505
- // If no devices found, try emulator
506
- if (discoveredDevices.isEmpty() && !isEmulatorMode.get()) {
507
- logStep("FALLBACK_EMULATOR", "No devices found, trying emulator");
508
- startEmulatorDiscovery();
509
488
  }
510
- }, timeoutMs, TimeUnit.MILLISECONDS);
511
- }
512
-
513
- private void cancelDiscoveryTimeout() {
514
- if (discoveryTimeoutFuture != null && !discoveryTimeoutFuture.isDone()) {
515
- discoveryTimeoutFuture.cancel(false);
516
- discoveryTimeoutFuture = null;
517
- }
518
- }
519
-
520
- // ==================== CONNECTION ====================
521
-
522
- private void connectToIdentity(DeviceInfo device) {
523
- logStep("CONNECT_START", "device=" + device.deviceName);
524
- Log.i(TAG, "[FLIR] Connecting to: " + device.deviceName);
525
-
526
- scheduler.execute(() -> {
527
- try {
528
- camera = new Camera();
529
-
530
- // Connect using Identity, error callback, and ConnectParameters
531
- // Note: connect is blocking, so we're on a background thread
532
- camera.connect(
533
- device.identity,
534
- new ConnectionStatusListener() {
535
- @Override
536
- public void onDisconnected(ErrorCode errorCode) {
537
- Log.i(TAG, "[FLIR] Disconnected: " + errorCode);
538
- logStep("DISCONNECTED", "reason=" + errorCode);
539
-
540
- isConnected.set(false);
541
- connectedDevice = null;
542
-
543
- mainHandler.post(() -> {
544
- if (listener != null) {
545
- listener.onDeviceDisconnected();
546
- }
547
- });
548
- }
549
- },
550
- new ConnectParameters()
551
- );
552
-
553
- // If we get here, connection succeeded
554
- Log.i(TAG, "[FLIR] Connected to: " + device.deviceName);
555
- logStep("CONNECTED", "device=" + device.deviceName);
556
-
557
- isConnected.set(true);
558
- connectedDevice = device;
559
-
560
- mainHandler.post(() -> {
561
- if (listener != null) {
562
- listener.onDeviceConnected(device.deviceId, device.deviceName, device.isEmulator);
563
- }
564
- });
565
-
566
- // Start streaming automatically
567
- startStreaming();
568
-
569
- } catch (Throwable t) {
570
- Log.e(TAG, "[FLIR] Connect error: " + t.getMessage(), t);
571
- isConnected.set(false);
572
- camera = null;
573
- notifyError("Connect error: " + t.getMessage());
489
+ // Return first if not found
490
+ if (!palettes.isEmpty()) {
491
+ return palettes.get(0);
574
492
  }
575
- });
493
+ } catch (Exception e) {
494
+ Log.e(TAG, "Error finding palette", e);
495
+ }
496
+ return null;
576
497
  }
577
498
 
578
- // ==================== STREAMING ====================
579
-
580
- private void startStreaming() {
581
- if (camera == null || !isConnected.get()) {
582
- Log.w(TAG, "[FLIR] Cannot start streaming - not connected");
583
- return;
584
- }
585
-
586
- logStep("STREAM_START", "streamType=" + currentStreamKind);
587
- Log.i(TAG, "[FLIR] Starting streaming...");
588
-
589
- scheduler.execute(() -> {
590
- try {
591
- // Get available streams
592
- List<Stream> streams = camera.getStreams();
593
- if (streams == null || streams.isEmpty()) {
594
- Log.e(TAG, "[FLIR] No streams available");
595
- notifyError("No streams available");
596
- return;
597
- }
598
-
599
- Log.i(TAG, "[FLIR] Available streams: " + streams.size());
600
-
601
- // Select thermal stream (prefer thermal, fallback to first)
602
- Stream thermalStream = null;
603
- for (Stream s : streams) {
604
- if (s.isThermal()) {
605
- thermalStream = s;
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;
606
513
  break;
607
514
  }
608
515
  }
609
- currentStream = thermalStream != null ? thermalStream : streams.get(0);
610
-
611
- // Create ThermalStreamer for rendering
612
- thermalStreamer = new ThermalStreamer(currentStream);
613
-
614
- // Set default palette if available
615
- if (currentPalette == null) {
616
- try {
617
- List<Palette> palettes = PaletteManager.getDefaultPalettes();
618
- for (Palette p : palettes) {
619
- if (p.name.toLowerCase().contains("iron")) {
620
- currentPalette = p;
621
- break;
622
- }
623
- }
624
- } catch (Throwable t) {
625
- Log.w(TAG, "[FLIR] Failed to get default palette: " + t.getMessage());
626
- }
516
+ if (!exists) {
517
+ discoveredDevices.add(identity);
627
518
  }
628
-
629
- // Start the stream with OnReceived and OnRemoteError callbacks
630
- currentStream.start(
631
- new OnReceived<Void>() {
632
- @Override
633
- public void onReceived(Void result) {
634
- // Process received frame on background thread
635
- scheduler.execute(() -> refreshThermalFrame());
636
- }
637
- },
638
- new OnRemoteError() {
639
- @Override
640
- public void onRemoteError(ErrorCode errorCode) {
641
- Log.e(TAG, "[FLIR] Stream error: " + errorCode);
642
- notifyError("Stream error: " + errorCode);
643
- }
644
- }
645
- );
646
-
647
- isStreaming.set(true);
648
- Log.i(TAG, "[FLIR] Stream started");
649
- logStep("STREAM_STARTED", "stream=" + currentStream);
650
-
651
- mainHandler.post(() -> {
652
- if (listener != null) {
653
- listener.onStreamStarted("thermal");
654
- }
655
- });
656
-
657
- } catch (Throwable t) {
658
- Log.e(TAG, "[FLIR] Start stream error: " + t.getMessage(), t);
659
- notifyError("Stream error: " + t.getMessage());
660
519
  }
661
- });
662
- }
663
-
664
- private void stopStreaming() {
665
- if (currentStream != null) {
666
- try {
667
- currentStream.stop();
668
- } catch (Throwable t) {
669
- Log.w(TAG, "[FLIR] Stop stream error: " + t.getMessage());
520
+
521
+ if (listener != null) {
522
+ listener.onDeviceFound(identity);
523
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
670
524
  }
671
- currentStream = null;
672
- }
673
- thermalStreamer = null;
674
- isStreaming.set(false);
675
- }
676
-
677
- /**
678
- * Refresh thermal frame using ThermalStreamer pattern.
679
- * Called when a new frame is received.
680
- */
681
- private synchronized void refreshThermalFrame() {
682
- if (thermalStreamer == null) {
683
- return;
684
525
  }
685
526
 
686
- try {
687
- // Update streamer to get latest frame
688
- thermalStreamer.update();
527
+ @Override
528
+ public void onCameraLost(Identity identity) {
529
+ Log.d(TAG, "Device lost: " + identity.deviceId);
689
530
 
690
- // Get the image buffer from streamer
691
- ImageBuffer imageBuffer = thermalStreamer.getImage();
692
- if (imageBuffer == null) {
693
- return;
531
+ synchronized (discoveredDevices) {
532
+ discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
694
533
  }
695
534
 
696
- // Access thermal image safely for temperature queries and palette
697
- thermalStreamer.withThermalImage(thermalImage -> {
698
- // Store for temperature queries
699
- currentThermalImage = thermalImage;
700
-
701
- // Apply palette if set
702
- if (currentPalette != null) {
703
- thermalImage.setPalette(currentPalette);
704
- }
705
- });
706
-
707
- // Convert to Android Bitmap
708
- Bitmap bitmap = BitmapAndroid.createBitmap(imageBuffer).getBitMap();
709
-
710
- if (bitmap != null) {
711
- latestFrame = bitmap;
712
-
713
- mainHandler.post(() -> {
714
- if (listener != null) {
715
- listener.onFrame(bitmap);
716
- }
717
- });
535
+ if (listener != null) {
536
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
718
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;
719
558
 
720
- } catch (Throwable t) {
721
- Log.w(TAG, "[FLIR] refreshThermalFrame error: " + t.getMessage());
559
+ if (listener != null) {
560
+ listener.onDisconnected();
561
+ }
722
562
  }
723
- }
563
+ };
724
564
 
725
- // ==================== PUBLIC STOP ====================
565
+ // Helper to notify errors
566
+ private void notifyError(String message) {
567
+ if (listener != null) {
568
+ listener.onError(message);
569
+ }
570
+ }
726
571
 
727
572
  /**
728
- * Stop the manager - disconnect and cleanup all resources.
573
+ * Cleanup resources
729
574
  */
730
- public void stop() {
731
- Log.i(TAG, "[FLIR] Stopping FlirSdkManager");
732
-
733
- // Stop streaming
734
- stopStreaming();
735
-
736
- // Disconnect camera
575
+ public void destroy() {
576
+ stop();
737
577
  disconnect();
738
-
739
- // Stop discovery
740
- stopDiscovery();
741
-
742
- // Clear state
743
578
  discoveredDevices.clear();
744
- currentThermalImage = null;
745
- latestFrame = null;
746
-
747
- Log.i(TAG, "[FLIR] FlirSdkManager stopped");
748
- }
749
-
750
- // ==================== HELPERS ====================
751
-
752
- private void notifyError(String error) {
753
- Log.e(TAG, "[FLIR] Error: " + error);
754
- mainHandler.post(() -> {
755
- if (listener != null) {
756
- listener.onError(error);
757
- }
758
- });
579
+ listener = null;
580
+ instance = null;
581
+ Log.d(TAG, "Destroyed");
759
582
  }
760
583
  }