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