ilabs-flir 1.0.4 → 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.
Files changed (38) hide show
  1. package/android/Flir/build.gradle.kts +14 -27
  2. package/android/Flir/libs/androidsdk-release.aar +0 -0
  3. package/android/Flir/libs/thermalsdk-release.aar +0 -0
  4. package/android/Flir/src/main/AndroidManifest.xml +14 -0
  5. package/android/Flir/src/main/java/flir/android/FlirDownloadManager.kt +23 -44
  6. package/android/Flir/src/main/java/flir/android/FlirManager.kt +312 -313
  7. package/android/Flir/src/main/java/flir/android/FlirModule.kt +183 -0
  8. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +35 -194
  9. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +409 -1339
  10. package/package.json +1 -1
  11. package/src/index.d.ts +60 -1
  12. package/android/Flir/libs/flir-stubs.jar +0 -0
  13. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCode.java +0 -13
  14. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCodeException.java +0 -14
  15. package/android/Flir/src/main/java/com/flir/thermalsdk/ThermalSdkAndroid.java +0 -16
  16. package/android/Flir/src/main/java/com/flir/thermalsdk/androidsdk/image/BitmapAndroid.java +0 -20
  17. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ImageBuffer.java +0 -11
  18. package/android/Flir/src/main/java/com/flir/thermalsdk/image/JavaImageBuffer.java +0 -35
  19. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Palette.java +0 -15
  20. package/android/Flir/src/main/java/com/flir/thermalsdk/image/PaletteManager.java +0 -16
  21. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Point.java +0 -11
  22. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalImage.java +0 -23
  23. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalValue.java +0 -9
  24. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Camera.java +0 -26
  25. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CameraType.java +0 -8
  26. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CommunicationInterface.java +0 -16
  27. package/android/Flir/src/main/java/com/flir/thermalsdk/live/ConnectParameters.java +0 -16
  28. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Identity.java +0 -23
  29. package/android/Flir/src/main/java/com/flir/thermalsdk/live/IpSettings.java +0 -9
  30. package/android/Flir/src/main/java/com/flir/thermalsdk/live/RemoteControl.java +0 -16
  31. package/android/Flir/src/main/java/com/flir/thermalsdk/live/connectivity/ConnectionStatusListener.java +0 -7
  32. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryEventListener.java +0 -14
  33. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryFactory.java +0 -33
  34. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnReceived.java +0 -5
  35. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnRemoteError.java +0 -7
  36. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/Stream.java +0 -8
  37. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/ThermalStreamer.java +0 -28
  38. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/VisualStreamer.java +0 -18
@@ -1,1511 +1,581 @@
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 java.io.File;
9
- import java.io.FileOutputStream;
10
- import java.lang.reflect.Method;
11
- import java.lang.reflect.Proxy;
7
+ import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
8
+ import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
9
+ import com.flir.thermalsdk.image.ImageBuffer;
10
+ import com.flir.thermalsdk.image.Palette;
11
+ import com.flir.thermalsdk.image.PaletteManager;
12
+ import com.flir.thermalsdk.image.Point;
13
+ import com.flir.thermalsdk.image.ThermalValue;
14
+ import com.flir.thermalsdk.live.Camera;
15
+ import com.flir.thermalsdk.live.CommunicationInterface;
16
+ import com.flir.thermalsdk.live.ConnectParameters;
17
+ import com.flir.thermalsdk.live.Identity;
18
+ import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
19
+ import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
20
+ import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
21
+ import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
22
+ import com.flir.thermalsdk.live.remote.OnReceived;
23
+ import com.flir.thermalsdk.live.streaming.Stream;
24
+ import com.flir.thermalsdk.live.streaming.ThermalStreamer;
25
+
12
26
  import java.util.ArrayList;
27
+ import java.util.Collections;
13
28
  import java.util.List;
14
- import java.util.concurrent.CopyOnWriteArrayList;
29
+ import java.util.concurrent.Executor;
15
30
  import java.util.concurrent.Executors;
16
- import java.util.concurrent.ScheduledExecutorService;
17
- import java.util.concurrent.ScheduledFuture;
18
- import java.util.concurrent.TimeUnit;
19
- import java.util.concurrent.atomic.AtomicBoolean;
20
- import java.util.zip.ZipEntry;
21
- import java.util.zip.ZipFile;
22
-
23
- import dalvik.system.DexClassLoader;
24
31
 
25
32
  /**
26
- * FLIR SDK Manager - Handles device discovery, connection, and streaming using reflection.
27
- * Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
28
- * All SDK calls use reflection for loose binding - no compile-time dependency on FLIR SDK.
29
- *
30
- * Flow:
31
- * isFlir=true → startDiscovery()
32
- * ↓ disconnect current device (if any)
33
- * ↓ (timeout: 0s if isEmu=true, 5s otherwise)
34
- * ├─ Devices found → emit to RN → stream from device[0]
35
- * │ ↓ RN sends deviceId
36
- * │ └─ disconnect → switch to deviceId
37
- * ├─ No device OR isEmu=true → emulatorDiscovery()
38
- * │ ↓ Check emulator type setting
39
- * │ ├─ FLIR_ONE_EDGE (default)
40
- * │ └─ FLIR_ONE
41
- * │ └─ stream from emulator
42
- * └─ Streaming: connect → start stream → upload to texture
43
- * ↓ acol changes → setPalette()
44
- * ↓ touch point → getTemperatureAt(x,y)
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"; // For step-by-step flow logging
49
38
 
50
- // Step counter for tracking flow
51
- private int stepCounter = 0;
52
- private long flowStartTime = 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
- private void logStep(String step, String details) {
55
- stepCounter++;
56
- long elapsed = flowStartTime > 0 ? System.currentTimeMillis() - flowStartTime : 0;
57
- String msg = String.format("[Step %d] [+%dms] %s: %s", stepCounter, elapsed, step, details);
58
- Log.i(FLOW_TAG, msg);
64
+ /**
65
+ * Listener interface for SDK events
66
+ */
67
+ public interface Listener {
68
+ void onDeviceFound(Identity identity);
69
+ void onDeviceListUpdated(List<Identity> devices);
70
+ void onConnected(Identity identity);
71
+ void onDisconnected();
72
+ void onFrame(Bitmap bitmap);
73
+ void onError(String message);
59
74
  }
60
75
 
61
- private void resetFlowTracking() {
62
- stepCounter = 0;
63
- flowStartTime = System.currentTimeMillis();
64
- Log.i(FLOW_TAG, "========== FLIR FLOW STARTED ==========");
76
+ // Private constructor for singleton
77
+ private FlirSdkManager(Context context) {
78
+ this.context = context.getApplicationContext();
65
79
  }
66
80
 
67
- // Discovery timeout in milliseconds
68
- private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000; // 5 seconds for real devices
69
- private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0; // Immediate for emulator mode
70
-
71
- // Emulator types
72
- public enum EmulatorType {
73
- FLIR_ONE_EDGE, // Default - WiFi emulator
74
- FLIR_ONE // USB emulator
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;
75
89
  }
76
90
 
77
- // Communication interfaces (mirrors SDK enum)
78
- public enum CommInterface {
79
- USB,
80
- NETWORK,
81
- EMULATOR
82
- }
83
-
84
- // Listener interface for callbacks
85
- public interface Listener {
86
- void onFrame(Bitmap bitmap);
87
- void onTemperature(double temp, int x, int y);
88
- void onDeviceFound(String deviceId, String deviceName, boolean isEmulator);
89
- void onDeviceListUpdated(List<DeviceInfo> devices);
90
- void onDeviceConnected(String deviceId, String deviceName, boolean isEmulator);
91
- void onDeviceDisconnected();
92
- void onDiscoveryStarted();
93
- void onDiscoveryTimeout();
94
- void onStreamStarted(String streamType);
95
- void onError(String error);
91
+ /**
92
+ * Set listener for SDK events
93
+ */
94
+ public void setListener(Listener listener) {
95
+ this.listener = listener;
96
96
  }
97
97
 
98
- // Device info class for discovered devices
99
- public static class DeviceInfo {
100
- public final String deviceId;
101
- public final String deviceName;
102
- public final boolean isEmulator;
103
- public final CommInterface commInterface;
104
- public final Object identity; // SDK Identity object (kept for connection)
98
+ /**
99
+ * Initialize the FLIR Thermal SDK
100
+ */
101
+ public void initialize() {
102
+ if (isInitialized) {
103
+ Log.d(TAG, "Already initialized");
104
+ return;
105
+ }
105
106
 
106
- DeviceInfo(String id, String name, boolean emu, CommInterface iface, Object identity) {
107
- this.deviceId = id;
108
- this.deviceName = name;
109
- this.isEmulator = emu;
110
- this.commInterface = iface;
111
- this.identity = identity;
107
+ try {
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());
112
114
  }
113
115
  }
114
-
115
- private final Listener listener;
116
- private final android.content.Context appContext;
117
- private final Handler mainHandler = new Handler(Looper.getMainLooper());
118
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
119
-
120
- // SDK objects (all via reflection)
121
- private ClassLoader sdkClassLoader = null;
122
- private Object discoveryFactory = null;
123
- private Object discoveryListener = null;
124
- private Object cameraObj = null;
125
- private Object streamerObj = null;
126
- private Object currentStream = null;
127
- private Object currentPalette = null;
128
-
129
- // State tracking
130
- private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
131
- private final AtomicBoolean isConnected = new AtomicBoolean(false);
132
- private final AtomicBoolean isStreaming = new AtomicBoolean(false);
133
- private final AtomicBoolean isEmulatorMode = new AtomicBoolean(false);
134
- private final CopyOnWriteArrayList<DeviceInfo> discoveredDevices = new CopyOnWriteArrayList<>();
135
- private ScheduledFuture<?> discoveryTimeoutFuture = null;
136
- private DeviceInfo connectedDevice = null;
137
- private EmulatorType emulatorType = EmulatorType.FLIR_ONE_EDGE;
138
-
139
- // Frame state
140
- private volatile Bitmap latestFrame = null;
141
- private String sdkJarPath = null;
142
- private String currentStreamKind = null;
143
-
144
- // Frame counting for debug logging
145
- private int frameCount = 0;
146
- private long lastFrameLogTime = 0;
147
- private int successfulBitmapCount = 0;
148
-
149
- FlirSdkManager(Listener listener, android.content.Context context) {
150
- this.listener = listener;
151
- this.appContext = context != null ? context.getApplicationContext() : null;
152
- }
153
-
154
- // ==================== PUBLIC API ====================
155
116
 
156
117
  /**
157
- * Set the emulator type to use when no physical device is found
118
+ * Check if SDK is initialized
158
119
  */
159
- public void setEmulatorType(EmulatorType type) {
160
- this.emulatorType = type;
161
- Log.i(TAG, "[FLIR] Emulator type set to: " + type);
162
- logStep("SET_EMULATOR_TYPE", "type=" + type + " (FLIR_ONE_EDGE=WiFi, FLIR_ONE=USB)");
120
+ public boolean isInitialized() {
121
+ return isInitialized;
163
122
  }
164
123
 
165
124
  /**
166
- * Start device discovery. Will disconnect current device first.
167
- * @param forceEmulator If true, skip device discovery and connect to emulator immediately
125
+ * Start scanning for all device types (USB, Network, Emulator)
126
+ * Returns ALL devices - no filtering
168
127
  */
169
- public void startDiscovery(boolean forceEmulator) {
170
- resetFlowTracking();
171
- logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
172
- Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
128
+ public void scan() {
129
+ if (!isInitialized) {
130
+ Log.e(TAG, "SDK not initialized");
131
+ notifyError("SDK not initialized");
132
+ return;
133
+ }
173
134
 
174
- // Always disconnect first
175
- if (isConnected.get()) {
176
- logStep("DISCONNECT_PREVIOUS", "Disconnecting current device before discovery");
177
- disconnect();
135
+ if (isScanning) {
136
+ Log.d(TAG, "Already scanning");
137
+ return;
178
138
  }
179
139
 
180
- // Clear discovered devices
140
+ isScanning = true;
181
141
  discoveredDevices.clear();
182
- logStep("CLEAR_DEVICES", "Cleared discovered devices list");
183
142
 
184
- if (forceEmulator) {
185
- // Immediate emulator mode
186
- logStep("MODE_EMULATOR", "Forcing emulator mode - skipping device discovery");
187
- isEmulatorMode.set(true);
188
- startEmulatorDiscovery();
189
- } else {
190
- // Normal discovery with timeout
191
- logStep("MODE_FULL_DISCOVERY", "Starting full discovery (USB+NETWORK+EMULATOR), timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
192
- isEmulatorMode.set(false);
193
- startFullDiscovery();
194
- }
195
- }
196
-
197
- /**
198
- * Stop discovery scan
199
- */
200
- public void stopDiscovery() {
201
- Log.i(TAG, "[FLIR] stopDiscovery()");
202
- cancelDiscoveryTimeout();
203
- isDiscovering.set(false);
143
+ Log.d(TAG, "Starting discovery for EMULATOR, NETWORK, USB...");
204
144
 
205
145
  try {
206
- if (discoveryFactory != null) {
207
- Method stopMethod = discoveryFactory.getClass().getMethod("stop");
208
- stopMethod.invoke(discoveryFactory);
209
- }
210
- } catch (Throwable t) {
211
- 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());
212
156
  }
213
157
  }
214
158
 
215
159
  /**
216
- * Connect to a specific device by ID
160
+ * Stop scanning for devices
217
161
  */
218
- public void connectToDevice(String deviceId) {
219
- Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
220
-
221
- // Find device in discovered list
222
- DeviceInfo target = null;
223
- for (DeviceInfo d : discoveredDevices) {
224
- if (d.deviceId.equals(deviceId)) {
225
- target = d;
226
- break;
227
- }
228
- }
229
-
230
- if (target == null) {
231
- notifyError("Device not found: " + deviceId);
162
+ public void stop() {
163
+ if (!isScanning) {
232
164
  return;
233
165
  }
234
166
 
235
- // Disconnect current if needed
236
- if (isConnected.get()) {
237
- disconnect();
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);
238
175
  }
239
176
 
240
- // Connect to target
241
- connectToIdentity(target);
177
+ isScanning = false;
178
+ Log.d(TAG, "Discovery stopped");
242
179
  }
243
180
 
244
181
  /**
245
- * Disconnect current device/emulator
182
+ * Get list of discovered devices
246
183
  */
247
- public void disconnect() {
248
- Log.i(TAG, "[FLIR] disconnect()");
249
-
250
- // Stop streaming first
251
- stopStreaming();
252
-
253
- // Disconnect camera
254
- if (cameraObj != null) {
255
- try {
256
- Method disconnectMethod = cameraObj.getClass().getMethod("disconnect");
257
- disconnectMethod.invoke(cameraObj);
258
- Log.i(TAG, "[FLIR] Camera disconnected");
259
- } catch (Throwable t) {
260
- Log.w(TAG, "[FLIR] disconnect failed: " + t.getMessage());
261
- }
262
- }
263
-
264
- cameraObj = null;
265
- streamerObj = null;
266
- currentStream = null;
267
- connectedDevice = null;
268
- isConnected.set(false);
269
- isStreaming.set(false);
270
-
271
- if (listener != null) {
272
- mainHandler.post(() -> listener.onDeviceDisconnected());
273
- }
184
+ public List<Identity> getDiscoveredDevices() {
185
+ return new ArrayList<>(discoveredDevices);
274
186
  }
275
187
 
276
188
  /**
277
- * Set palette by name (iron, rainbow, etc.)
189
+ * Connect to a device
278
190
  */
279
- public void setPalette(String paletteName) {
280
- Log.d(TAG, "[FLIR] setPalette: " + paletteName);
281
-
282
- if (streamerObj == null) {
283
- Log.w(TAG, "[FLIR] Cannot set palette - no active streamer");
191
+ public void connect(Identity identity) {
192
+ if (identity == null) {
193
+ notifyError("Invalid identity");
284
194
  return;
285
195
  }
286
196
 
287
- scheduler.submit(() -> {
197
+ // Disconnect if already connected
198
+ if (camera != null) {
199
+ disconnect();
200
+ }
201
+
202
+ Log.d(TAG, "Connecting to: " + identity.deviceId);
203
+
204
+ // Run connection on background thread since it's blocking
205
+ executor.execute(() -> {
288
206
  try {
289
- // Get PaletteManager.getDefaultPalettes()
290
- Class<?> paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
291
- Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
292
- Object palettes = getDefaultPalettes.invoke(null);
207
+ camera = new Camera();
208
+ camera.connect(identity, connectionStatusListener, new ConnectParameters());
293
209
 
294
- // Find matching palette
295
- Object targetPalette = null;
296
- if (palettes instanceof List) {
297
- for (Object p : (List<?>) palettes) {
298
- Method getName = p.getClass().getMethod("getName");
299
- String name = (String) getName.invoke(p);
300
- if (name != null && name.equalsIgnoreCase(paletteName)) {
301
- targetPalette = p;
302
- break;
303
- }
304
- }
305
- }
210
+ Log.d(TAG, "Connected to camera");
306
211
 
307
- if (targetPalette != null) {
308
- currentPalette = targetPalette;
309
- Log.i(TAG, "[FLIR] Palette set to: " + paletteName);
310
- } else {
311
- Log.w(TAG, "[FLIR] Palette not found: " + paletteName);
212
+ if (listener != null) {
213
+ listener.onConnected(identity);
312
214
  }
313
- } catch (Throwable t) {
314
- Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
215
+ } catch (Exception e) {
216
+ Log.e(TAG, "Connection failed", e);
217
+ camera = null;
218
+ notifyError("Connection failed: " + e.getMessage());
315
219
  }
316
220
  });
317
221
  }
318
222
 
319
223
  /**
320
- * Initialize default "iron" palette for thermal streaming.
321
- * Called automatically when thermal streaming starts if no palette is set.
224
+ * Disconnect from current device
322
225
  */
323
- private void initializeDefaultPalette() {
324
- try {
325
- Class<?> paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
326
- Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
327
- Object palettes = getDefaultPalettes.invoke(null);
328
-
329
- if (palettes instanceof List) {
330
- // Try to find "iron" palette first, then fall back to first available
331
- Object ironPalette = null;
332
- Object firstPalette = null;
333
-
334
- for (Object p : (List<?>) palettes) {
335
- if (firstPalette == null) firstPalette = p;
336
-
337
- try {
338
- Method getName = p.getClass().getMethod("getName");
339
- String name = (String) getName.invoke(p);
340
- if (name != null && name.toLowerCase().contains("iron")) {
341
- ironPalette = p;
342
- break;
343
- }
344
- } catch (Throwable ignored) {}
345
- }
346
-
347
- currentPalette = ironPalette != null ? ironPalette : firstPalette;
348
-
349
- if (currentPalette != null) {
350
- try {
351
- Method getName = currentPalette.getClass().getMethod("getName");
352
- String name = (String) getName.invoke(currentPalette);
353
- Log.i(TAG, "[FLIR] Default palette initialized: " + name);
354
- } catch (Throwable ignored) {
355
- Log.i(TAG, "[FLIR] Default palette initialized");
356
- }
357
- }
226
+ public void disconnect() {
227
+ stopStream();
228
+
229
+ if (camera != null) {
230
+ try {
231
+ camera.disconnect();
232
+ } catch (Exception e) {
233
+ Log.e(TAG, "Error disconnecting", e);
358
234
  }
359
- } catch (Throwable t) {
360
- Log.w(TAG, "[FLIR] initializeDefaultPalette failed: " + t.getMessage());
235
+ camera = null;
361
236
  }
362
- }
363
-
364
- /**
365
- * Get temperature at a specific point
366
- */
367
- public double getTemperatureAtPoint(int x, int y) {
368
- if (streamerObj == null) return Double.NaN;
369
237
 
370
- try {
371
- // Get the thermal image from streamer
372
- Method getImage = streamerObj.getClass().getMethod("getImage");
373
- Object thermalImage = getImage.invoke(streamerObj);
374
-
375
- if (thermalImage != null) {
376
- // Try getValueAt(Point)
377
- try {
378
- Method getValueAt = thermalImage.getClass().getMethod("getValueAt", android.graphics.Point.class);
379
- android.graphics.Point p = new android.graphics.Point(x, y);
380
- Object temp = getValueAt.invoke(thermalImage, p);
381
- if (temp instanceof Double) return (Double) temp;
382
- if (temp instanceof Float) return ((Float) temp).doubleValue();
383
- } catch (NoSuchMethodException ignored) {}
384
-
385
- // Try getValues().getValueAt(x, y)
386
- try {
387
- Method getValues = thermalImage.getClass().getMethod("getValues");
388
- Object values = getValues.invoke(thermalImage);
389
- if (values != null) {
390
- Method valGetAt = values.getClass().getMethod("getValueAt", int.class, int.class);
391
- Object temp = valGetAt.invoke(values, x, y);
392
- if (temp instanceof Double) return (Double) temp;
393
- }
394
- } catch (Throwable ignored) {}
395
- }
396
- } catch (Throwable t) {
397
- Log.d(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
238
+ if (listener != null) {
239
+ listener.onDisconnected();
398
240
  }
399
241
 
400
- return Double.NaN;
401
- }
402
-
403
- /**
404
- * Get latest frame bitmap
405
- */
406
- public Bitmap getLatestFrame() {
407
- return latestFrame;
408
- }
409
-
410
- /**
411
- * Check if streaming is active
412
- */
413
- public boolean isStreamingActive() {
414
- return isStreaming.get();
242
+ Log.d(TAG, "Disconnected");
415
243
  }
416
244
 
417
245
  /**
418
246
  * Check if connected
419
247
  */
420
248
  public boolean isConnected() {
421
- return isConnected.get();
249
+ return camera != null;
422
250
  }
423
251
 
424
252
  /**
425
- * Get current stream type
253
+ * Start streaming from connected device
426
254
  */
427
- public String getCurrentStreamKind() {
428
- return currentStreamKind;
429
- }
430
-
431
- /**
432
- * Get list of discovered devices
433
- */
434
- public List<DeviceInfo> getDiscoveredDevices() {
435
- return new ArrayList<>(discoveredDevices);
436
- }
437
-
438
- /**
439
- * Cleanup resources
440
- */
441
- public void stop() {
442
- Log.i(TAG, "[FLIR] stop()");
443
- stopDiscovery();
444
- disconnect();
445
- scheduler.shutdownNow();
446
- }
447
-
448
- // ==================== DISCOVERY IMPLEMENTATION ====================
449
-
450
- private void startFullDiscovery() {
451
- Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
452
-
453
- if (!initializeSdk()) {
454
- Log.w(TAG, "[FLIR] SDK not available, falling back to emulator");
455
- startEmulatorDiscovery();
255
+ public void startStream() {
256
+ if (camera == null) {
257
+ notifyError("Not connected");
456
258
  return;
457
259
  }
458
260
 
459
- isDiscovering.set(true);
460
- if (listener != null) {
461
- mainHandler.post(() -> listener.onDiscoveryStarted());
462
- }
463
-
464
- try {
465
- // Get DiscoveryFactory.getInstance()
466
- Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
467
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
468
- discoveryFactory = getInstance.invoke(null);
469
-
470
- // Get CommunicationInterface enum values
471
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
472
- Object usbInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "USB");
473
- Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
474
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
475
-
476
- // Create discovery listener proxy
477
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
478
- discoveryListener = Proxy.newProxyInstance(
479
- getEffectiveClassLoader(),
480
- new Class<?>[] { listenerClass },
481
- (proxy, method, args) -> handleDiscoveryCallback(method.getName(), args)
482
- );
483
-
484
- // Create interface array [USB, NETWORK, EMULATOR]
485
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 3);
486
- java.lang.reflect.Array.set(ifaceArray, 0, usbInterface);
487
- java.lang.reflect.Array.set(ifaceArray, 1, networkInterface);
488
- java.lang.reflect.Array.set(ifaceArray, 2, emulatorInterface);
489
-
490
- // Start discovery scan
491
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass, ifaceArray.getClass());
492
- scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
493
-
494
- Log.i(TAG, "[FLIR] Discovery scan started for USB, NETWORK, EMULATOR");
495
-
496
- // Set discovery timeout
497
- startDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
498
-
499
- } catch (Throwable t) {
500
- Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
501
- notifyError("Discovery failed: " + t.getMessage());
502
- // Fallback to emulator
503
- startEmulatorDiscovery();
504
- }
505
- }
506
-
507
- private void startEmulatorDiscovery() {
508
- logStep("EMULATOR_DISCOVERY_START", "type=" + emulatorType);
509
- Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
510
-
511
- if (!initializeSdk()) {
512
- logStep("SDK_INIT_FAILED", "FLIR SDK not available - check if SDK JAR is loaded");
513
- notifyError("FLIR SDK not available");
514
- return;
515
- }
516
- logStep("SDK_INITIALIZED", "FLIR SDK successfully initialized");
517
-
518
- isDiscovering.set(true);
519
- isEmulatorMode.set(true);
520
-
521
- try {
522
- // Get DiscoveryFactory
523
- Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
524
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
525
- discoveryFactory = getInstance.invoke(null);
526
-
527
- // Get EMULATOR interface
528
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
529
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
530
-
531
- // Create discovery listener
532
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
533
- discoveryListener = Proxy.newProxyInstance(
534
- getEffectiveClassLoader(),
535
- new Class<?>[] { listenerClass },
536
- (proxy, method, args) -> handleDiscoveryCallback(method.getName(), args)
537
- );
538
-
539
- // Create interface array [EMULATOR]
540
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 1);
541
- java.lang.reflect.Array.set(ifaceArray, 0, emulatorInterface);
542
-
543
- // Start discovery
544
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass, ifaceArray.getClass());
545
- scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
546
-
547
- logStep("EMULATOR_SCAN_STARTED", "Scanning for emulator devices...");
548
- Log.i(TAG, "[FLIR] Emulator discovery started");
549
-
550
- } catch (Throwable t) {
551
- logStep("EMULATOR_DISCOVERY_ERROR", "Failed: " + t.getMessage());
552
- Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
553
- notifyError("Emulator discovery failed: " + t.getMessage());
554
- }
555
- }
556
-
557
- private Object handleDiscoveryCallback(String methodName, Object[] args) {
558
- switch (methodName) {
559
- case "onCameraFound":
560
- if (args != null && args.length > 0) {
561
- handleCameraFound(args[0]);
562
- }
563
- break;
564
-
565
- case "onCameraLost":
566
- if (args != null && args.length > 0) {
567
- handleCameraLost(args[0]);
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");
267
+ return;
568
268
  }
569
- break;
570
269
 
571
- case "onDiscoveryError":
572
- if (args != null && args.length > 1) {
573
- Log.w(TAG, "[FLIR] Discovery error: " + args[1]);
574
- }
575
- break;
576
- }
577
- return null;
578
- }
579
-
580
- private void handleCameraFound(Object discoveredCamera) {
581
- try {
582
- // Get identity from DiscoveredCamera
583
- Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
584
- Object identity = getIdentity.invoke(discoveredCamera);
585
-
586
- // Extract device info from identity
587
- String deviceId = extractDeviceId(identity);
588
- String deviceName = extractDeviceName(identity);
589
- CommInterface commInterface = extractCommInterface(identity);
590
- boolean isEmulator = commInterface == CommInterface.EMULATOR;
591
-
592
- logStep("DEVICE_FOUND", "name=" + deviceName + ", id=" + deviceId + ", interface=" + commInterface + ", isEmulator=" + isEmulator);
593
- Log.i(TAG, "[FLIR] Camera found: " + deviceName + " (" + commInterface + ")");
594
-
595
- // Create device info
596
- DeviceInfo deviceInfo = new DeviceInfo(deviceId, deviceName, isEmulator, commInterface, identity);
597
-
598
- // Add to list if not already present
599
- boolean exists = false;
600
- for (DeviceInfo d : discoveredDevices) {
601
- if (d.deviceId.equals(deviceId)) {
602
- exists = true;
603
- break;
270
+ // Find thermal stream
271
+ Stream thermalStream = null;
272
+ for (Stream stream : streams) {
273
+ if (stream.isThermal()) {
274
+ thermalStream = stream;
275
+ break;
276
+ }
604
277
  }
605
- }
606
-
607
- if (!exists) {
608
- discoveredDevices.add(deviceInfo);
609
278
 
610
- // Notify listener
611
- if (listener != null) {
612
- final List<DeviceInfo> devices = new ArrayList<>(discoveredDevices);
613
- mainHandler.post(() -> {
614
- listener.onDeviceFound(deviceId, deviceName, isEmulator);
615
- listener.onDeviceListUpdated(devices);
616
- });
279
+ if (thermalStream == null) {
280
+ thermalStream = streams.get(0);
617
281
  }
618
282
 
619
- // If this is first device found (not emulator in normal mode), connect to it
620
- if (!isEmulator && discoveredDevices.size() == 1 && !isEmulatorMode.get()) {
621
- cancelDiscoveryTimeout();
622
- stopDiscovery();
623
- connectToIdentity(deviceInfo);
624
- } else if (isEmulator && isEmulatorMode.get()) {
625
- // In emulator mode, connect to first emulator found
626
- stopDiscovery();
627
- connectToIdentity(deviceInfo);
628
- }
629
- }
630
-
631
- } catch (Throwable t) {
632
- Log.e(TAG, "[FLIR] handleCameraFound failed: " + t.getMessage(), t);
633
- }
634
- }
635
-
636
- private void handleCameraLost(Object discoveredCamera) {
637
- try {
638
- Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
639
- Object identity = getIdentity.invoke(discoveredCamera);
640
- String deviceId = extractDeviceId(identity);
641
-
642
- Log.i(TAG, "[FLIR] Camera lost: " + deviceId);
643
-
644
- // Remove from list
645
- discoveredDevices.removeIf(d -> d.deviceId.equals(deviceId));
646
-
647
- // If this was our connected device, disconnect
648
- if (connectedDevice != null && connectedDevice.deviceId.equals(deviceId)) {
649
- disconnect();
650
- }
651
-
652
- } catch (Throwable t) {
653
- Log.w(TAG, "[FLIR] handleCameraLost failed: " + t.getMessage());
654
- }
655
- }
656
-
657
- private void startDiscoveryTimeout(long timeoutMs) {
658
- cancelDiscoveryTimeout();
659
-
660
- if (timeoutMs <= 0) return;
661
-
662
- discoveryTimeoutFuture = scheduler.schedule(() -> {
663
- Log.i(TAG, "[FLIR] Discovery timeout");
664
- isDiscovering.set(false);
665
-
666
- if (listener != null) {
667
- mainHandler.post(() -> listener.onDiscoveryTimeout());
668
- }
669
-
670
- // If no physical devices found, try emulator
671
- boolean hasPhysicalDevice = false;
672
- for (DeviceInfo d : discoveredDevices) {
673
- if (!d.isEmulator) {
674
- hasPhysicalDevice = true;
675
- break;
676
- }
677
- }
678
-
679
- if (!hasPhysicalDevice) {
680
- Log.i(TAG, "[FLIR] No physical devices found, starting emulator");
681
- startEmulatorDiscovery();
682
- } else if (!discoveredDevices.isEmpty()) {
683
- // Connect to first device
684
- connectToIdentity(discoveredDevices.get(0));
685
- }
686
-
687
- }, timeoutMs, TimeUnit.MILLISECONDS);
688
- }
689
-
690
- private void cancelDiscoveryTimeout() {
691
- if (discoveryTimeoutFuture != null) {
692
- discoveryTimeoutFuture.cancel(false);
693
- discoveryTimeoutFuture = null;
694
- }
695
- }
696
-
697
- // ==================== CONNECTION IMPLEMENTATION ====================
698
-
699
- private void connectToIdentity(DeviceInfo deviceInfo) {
700
- logStep("CONNECT_START", "Connecting to: " + deviceInfo.deviceName + " (" + deviceInfo.commInterface + ")");
701
- Log.i(TAG, "[FLIR] Connecting to: " + deviceInfo.deviceName);
702
-
703
- scheduler.submit(() -> {
704
- try {
705
- // Create Camera instance
706
- logStep("CREATE_CAMERA", "Creating Camera instance");
707
- Class<?> cameraClass = findSdkClass("com.flir.thermalsdk.live.Camera");
708
- cameraObj = cameraClass.newInstance();
283
+ activeStream = thermalStream;
284
+ streamer = new ThermalStreamer(thermalStream);
709
285
 
710
- // Create connection status listener
711
- Class<?> connStatusClass = findSdkClass("com.flir.thermalsdk.live.connectivity.ConnectionStatusListener");
712
- Object connListener = Proxy.newProxyInstance(
713
- getEffectiveClassLoader(),
714
- new Class<?>[] { connStatusClass },
715
- (proxy, method, args) -> {
716
- if ("onDisconnected".equals(method.getName())) {
717
- Log.w(TAG, "[FLIR] Camera disconnected (callback)");
718
- handleDisconnected();
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;
719
295
  }
720
- return null;
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);
721
328
  }
722
329
  );
723
330
 
724
- // Try to connect with different method signatures
725
- boolean connected = false;
726
-
727
- // Try connect(Identity, ConnectionStatusListener, ConnectParameters)
728
- try {
729
- Class<?> connectParamsClass = findSdkClass("com.flir.thermalsdk.live.ConnectParameters");
730
- Object connectParams = connectParamsClass.newInstance();
731
- Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
732
- Method connectMethod = cameraClass.getMethod("connect",
733
- identityClass, connStatusClass, connectParamsClass);
734
- connectMethod.invoke(cameraObj, deviceInfo.identity, connListener, connectParams);
735
- connected = true;
736
- } catch (NoSuchMethodException ignored) {}
737
-
738
- // Try connect(Identity, ConnectionStatusListener)
739
- if (!connected) {
740
- try {
741
- Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
742
- Method connectMethod = cameraClass.getMethod("connect",
743
- identityClass, connStatusClass);
744
- connectMethod.invoke(cameraObj, deviceInfo.identity, connListener);
745
- connected = true;
746
- } catch (NoSuchMethodException ignored) {}
747
- }
748
-
749
- // Try connect(Identity)
750
- if (!connected) {
751
- try {
752
- Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
753
- Method connectMethod = cameraClass.getMethod("connect", identityClass);
754
- connectMethod.invoke(cameraObj, deviceInfo.identity);
755
- connected = true;
756
- } catch (NoSuchMethodException ignored) {}
757
- }
758
-
759
- if (!connected) {
760
- throw new Exception("No suitable connect method found");
761
- }
762
-
763
- connectedDevice = deviceInfo;
764
- isConnected.set(true);
331
+ Log.d(TAG, "Streaming started");
765
332
 
766
- logStep("CONNECTED", "Successfully connected to: " + deviceInfo.deviceName);
767
- Log.i(TAG, "[FLIR] Connected to: " + deviceInfo.deviceName);
768
-
769
- // Notify listener
770
- if (listener != null) {
771
- mainHandler.post(() -> listener.onDeviceConnected(
772
- deviceInfo.deviceId, deviceInfo.deviceName, deviceInfo.isEmulator));
773
- }
774
-
775
- // Start streaming after brief delay
776
- logStep("SCHEDULE_STREAMING", "Scheduling stream start in 500ms");
777
- scheduler.schedule(this::startStreaming, 500, TimeUnit.MILLISECONDS);
778
-
779
- } catch (Throwable t) {
780
- logStep("CONNECT_FAILED", "Connection failed: " + t.getMessage());
781
- Log.e(TAG, "[FLIR] Connection failed: " + t.getMessage(), t);
782
- notifyError("Connection failed: " + t.getMessage());
333
+ } catch (Exception e) {
334
+ Log.e(TAG, "Failed to start stream", e);
335
+ notifyError("Stream failed: " + e.getMessage());
783
336
  }
784
337
  });
785
338
  }
786
339
 
787
- private void handleDisconnected() {
788
- cameraObj = null;
789
- streamerObj = null;
790
- currentStream = null;
791
- connectedDevice = null;
792
- isConnected.set(false);
793
- isStreaming.set(false);
794
-
795
- if (listener != null) {
796
- mainHandler.post(() -> listener.onDeviceDisconnected());
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;
797
351
  }
352
+
353
+ streamer = null;
354
+
355
+ // Reset frame processing state
356
+ isProcessingFrame = false;
357
+ lastFrameProcessedMs = 0;
358
+
359
+ Log.d(TAG, "Streaming stopped");
798
360
  }
799
361
 
800
- // ==================== STREAMING IMPLEMENTATION ====================
801
-
802
- private void startStreaming() {
803
- if (cameraObj == null) {
804
- logStep("STREAM_SKIP", "Cannot start streaming - no camera object");
805
- Log.w(TAG, "[FLIR] Cannot start streaming - no camera");
806
- return;
362
+ /**
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
368
+ */
369
+ public double getTemperatureAt(int x, int y) {
370
+ if (streamer == null) {
371
+ return Double.NaN;
807
372
  }
808
373
 
809
- logStep("STREAM_START", "Starting streaming process...");
810
- Log.i(TAG, "[FLIR] Starting streaming...");
811
-
374
+ final double[] result = {Double.NaN};
812
375
  try {
813
- // Get camera streams
814
- Method getStreams = cameraObj.getClass().getMethod("getStreams");
815
- Object streams = getStreams.invoke(cameraObj);
816
-
817
- if (streams == null || !(streams instanceof List) || ((List<?>) streams).isEmpty()) {
818
- logStep("NO_STREAMS", "No streams available from camera");
819
- Log.w(TAG, "[FLIR] No streams available");
820
- return;
821
- }
822
-
823
- List<?> streamList = (List<?>) streams;
824
- logStep("STREAMS_FOUND", "Found " + streamList.size() + " stream(s)");
825
- Log.i(TAG, "[FLIR] Found " + streamList.size() + " stream(s)");
826
-
827
- // Prefer thermal stream, fallback to first available
828
- Object chosenStream = null;
829
- String streamType = "unknown";
830
-
831
- for (Object s : streamList) {
832
- if (s == null) continue;
833
-
376
+ streamer.withThermalImage(thermalImage -> {
834
377
  try {
835
- Method isThermal = s.getClass().getMethod("isThermal");
836
- Boolean thermal = (Boolean) isThermal.invoke(s);
378
+ int imgWidth = thermalImage.getWidth();
379
+ int imgHeight = thermalImage.getHeight();
837
380
 
838
- Method getName = null;
839
- try { getName = s.getClass().getMethod("getName"); } catch (Throwable ignored) {}
840
- String name = getName != null ? String.valueOf(getName.invoke(s)) : "unknown";
381
+ int clampedX = Math.max(0, Math.min(imgWidth - 1, x));
382
+ int clampedY = Math.max(0, Math.min(imgHeight - 1, y));
841
383
 
842
- Log.d(TAG, "[FLIR] Stream: " + name + ", thermal=" + thermal);
843
-
844
- if (thermal != null && thermal) {
845
- chosenStream = s;
846
- streamType = "thermal";
847
- break;
848
- } else if (chosenStream == null) {
849
- chosenStream = s;
850
- streamType = "visual";
851
- }
852
- } catch (Throwable ignored) {}
853
- }
854
-
855
- if (chosenStream == null) {
856
- chosenStream = streamList.get(0);
857
- streamType = "default";
858
- }
859
-
860
- currentStream = chosenStream;
861
- currentStreamKind = streamType;
862
-
863
- logStep("STREAM_SELECTED", "Using " + streamType + " stream");
864
- Log.i(TAG, "[FLIR] Using stream type: " + streamType);
865
-
866
- // Create appropriate streamer
867
- if ("thermal".equals(streamType)) {
868
- logStep("CREATE_THERMAL_STREAMER", "Creating ThermalStreamer");
869
- Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
870
- Class<?> streamClass = findSdkClass("com.flir.thermalsdk.live.streaming.Stream");
871
- streamerObj = thermalStreamerClass.getConstructor(streamClass).newInstance(chosenStream);
872
-
873
- // Initialize default palette if not already set
874
- if (currentPalette == null) {
875
- logStep("INIT_PALETTE", "Initializing default 'iron' palette");
876
- initializeDefaultPalette();
877
- } else {
878
- logStep("PALETTE_EXISTS", "Palette already set, skipping init");
879
- }
880
- } else {
881
- logStep("CREATE_VISUAL_STREAMER", "Creating VisualStreamer");
882
- Class<?> visualStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.VisualStreamer");
883
- Class<?> streamClass = findSdkClass("com.flir.thermalsdk.live.streaming.Stream");
884
- streamerObj = visualStreamerClass.getConstructor(streamClass).newInstance(chosenStream);
885
- }
886
-
887
- // Create OnReceived callback
888
- Class<?> onReceivedClass = findSdkClass("com.flir.thermalsdk.live.remote.OnReceived");
889
- Object onReceivedCallback = Proxy.newProxyInstance(
890
- getEffectiveClassLoader(),
891
- new Class<?>[] { onReceivedClass },
892
- (proxy, method, args) -> {
893
- if ("run".equals(method.getName())) {
894
- scheduler.submit(this::processFrame);
895
- }
896
- return null;
897
- }
898
- );
899
-
900
- // Create OnRemoteError callback
901
- Class<?> onErrorClass = findSdkClass("com.flir.thermalsdk.live.remote.OnRemoteError");
902
- Object onErrorCallback = Proxy.newProxyInstance(
903
- getEffectiveClassLoader(),
904
- new Class<?>[] { onErrorClass },
905
- (proxy, method, args) -> {
906
- if ("run".equals(method.getName()) && args != null && args.length > 0) {
907
- Log.e(TAG, "[FLIR] Stream error: " + args[0]);
384
+ ThermalValue value = thermalImage.getValueAt(new Point(clampedX, clampedY));
385
+ if (value != null) {
386
+ result[0] = value.asCelsius().value;
908
387
  }
909
- return null;
388
+ } catch (Exception e) {
389
+ Log.w(TAG, "Error querying temperature", e);
910
390
  }
911
- );
912
-
913
- // Start the stream
914
- logStep("STREAM_STARTING", "Invoking stream.start()");
915
- Method startMethod = chosenStream.getClass().getMethod("start", onReceivedClass, onErrorClass);
916
- startMethod.invoke(chosenStream, onReceivedCallback, onErrorCallback);
917
-
918
- isStreaming.set(true);
919
-
920
- logStep("STREAM_STARTED", "Stream started successfully - type=" + streamType + ", waiting for frames...");
921
- Log.i(TAG, "[FLIR] Streaming started (" + streamType + ")");
922
-
923
- if (listener != null) {
924
- final String type = streamType;
925
- mainHandler.post(() -> listener.onStreamStarted(type));
926
- }
927
-
928
- } catch (Throwable t) {
929
- logStep("STREAM_ERROR", "Streaming failed: " + t.getMessage());
930
- Log.e(TAG, "[FLIR] startStreaming failed: " + t.getMessage(), t);
931
- notifyError("Streaming failed: " + t.getMessage());
932
- }
933
- }
934
-
935
- private void processFrame() {
936
- if (streamerObj == null) return;
937
-
938
- frameCount++;
939
- long now = System.currentTimeMillis();
940
-
941
- // Log first frame and then every 30 frames or every 5 seconds
942
- boolean shouldLog = frameCount == 1 || frameCount % 30 == 0 || (now - lastFrameLogTime > 5000);
943
-
944
- if (shouldLog) {
945
- logStep("PROCESS_FRAME", "frame=" + frameCount + ", streamType=" + currentStreamKind + ", bitmapsSuccess=" + successfulBitmapCount);
946
- lastFrameLogTime = now;
391
+ });
392
+ } catch (Exception e) {
393
+ Log.w(TAG, "Temperature query failed", e);
947
394
  }
948
395
 
949
- try {
950
- // Call streamer.update() first - this refreshes the streamer content
951
- Method updateMethod = streamerObj.getClass().getMethod("update");
952
- updateMethod.invoke(streamerObj);
953
-
954
- // Get image buffer from streamer.getImage()
955
- Method getImage = streamerObj.getClass().getMethod("getImage");
956
- Object imageBuffer = getImage.invoke(streamerObj);
957
-
958
- if (imageBuffer == null) {
959
- if (shouldLog) logStep("FRAME_NULL_BUFFER", "imageBuffer is null at frame " + frameCount);
960
- Log.d(TAG, "[FLIR] imageBuffer is null");
961
- return;
962
- }
963
-
964
- if (frameCount == 1) {
965
- logStep("FIRST_BUFFER", "Got first imageBuffer, type=" + imageBuffer.getClass().getSimpleName());
966
- }
967
-
968
- // For thermal streamer, we MUST create the bitmap INSIDE withThermalImage callback
969
- // This is critical - the palette affects the imageBuffer rendering only while inside the callback
970
- if ("thermal".equals(currentStreamKind)) {
971
- processThermalFrameWithCallback(imageBuffer);
972
- } else {
973
- // For visual stream, just convert to bitmap directly
974
- Bitmap bitmap = convertToBitmap(imageBuffer);
975
- if (bitmap != null) {
976
- successfulBitmapCount++;
977
- latestFrame = bitmap;
978
- if (frameCount == 1) {
979
- logStep("FIRST_BITMAP", "Visual bitmap created: " + bitmap.getWidth() + "x" + bitmap.getHeight());
980
- }
981
- if (listener != null) {
982
- listener.onFrame(bitmap);
983
- }
984
- } else if (frameCount <= 5) {
985
- logStep("BITMAP_NULL", "Visual convertToBitmap returned null at frame " + frameCount);
986
- }
987
- }
988
-
989
- } catch (Throwable t) {
990
- // Log errors periodically to avoid spam
991
- if (System.currentTimeMillis() % 5000 < 100) {
992
- Log.d(TAG, "[FLIR] Frame processing error: " + t.getMessage());
993
- }
994
- }
396
+ return result[0];
995
397
  }
996
398
 
997
399
  /**
998
- * Process thermal frame using withThermalImage callback pattern.
999
- * CRITICAL: Both palette setting AND bitmap conversion must happen INSIDE the callback!
1000
- * This matches the LiveStreamingKotlin example exactly.
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
1001
404
  */
1002
- private void processThermalFrameWithCallback(Object imageBuffer) {
1003
- if (streamerObj == null) return;
405
+ public double getTemperatureAtNormalized(double normalizedX, double normalizedY) {
406
+ if (streamer == null) {
407
+ return Double.NaN;
408
+ }
1004
409
 
410
+ final double[] result = {Double.NaN};
1005
411
  try {
1006
- Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
1007
- if (!thermalStreamerClass.isInstance(streamerObj)) {
1008
- // Not a ThermalStreamer, use direct conversion
1009
- Bitmap bitmap = convertToBitmap(imageBuffer);
1010
- if (bitmap != null) {
1011
- latestFrame = bitmap;
1012
- if (listener != null) listener.onFrame(bitmap);
1013
- }
1014
- return;
1015
- }
1016
-
1017
- // Find withThermalImage method
1018
- Method withThermalImageMethod = null;
1019
- for (Method m : thermalStreamerClass.getMethods()) {
1020
- if ("withThermalImage".equals(m.getName()) && m.getParameterCount() == 1) {
1021
- withThermalImageMethod = m;
1022
- break;
1023
- }
1024
- }
1025
-
1026
- if (withThermalImageMethod == null) {
1027
- Log.w(TAG, "[FLIR] withThermalImage method not found, using fallback");
1028
- // Fallback: try to set palette via getThermalImage, then convert
412
+ streamer.withThermalImage(thermalImage -> {
1029
413
  try {
1030
- Method getThermalImage = streamerObj.getClass().getMethod("getThermalImage");
1031
- Object thermalImage = getThermalImage.invoke(streamerObj);
1032
- if (thermalImage != null && currentPalette != null) {
1033
- setPaletteOnThermalImage(thermalImage);
1034
- }
1035
- } catch (Throwable ignored) {}
1036
-
1037
- Bitmap bitmap = convertToBitmap(imageBuffer);
1038
- if (bitmap != null) {
1039
- latestFrame = bitmap;
1040
- if (listener != null) listener.onFrame(bitmap);
1041
- }
1042
- return;
1043
- }
1044
-
1045
- // Create Consumer proxy that does BOTH: set palette AND create bitmap
1046
- // This is the key insight from LiveStreamingKotlin!
1047
- final Object imgBuffer = imageBuffer;
1048
- Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
1049
- Object consumer = Proxy.newProxyInstance(
1050
- getEffectiveClassLoader(),
1051
- new Class<?>[] { consumerClass },
1052
- (proxy, method, args) -> {
1053
- if ("accept".equals(method.getName()) && args != null && args.length > 0) {
1054
- Object thermalImage = args[0];
1055
- if (thermalImage != null) {
1056
- // Step 1: Set palette on thermal image
1057
- if (currentPalette != null) {
1058
- setPaletteOnThermalImage(thermalImage);
1059
- if (frameCount == 1) {
1060
- logStep("PALETTE_APPLIED", "Palette applied to ThermalImage inside callback");
1061
- }
1062
- }
1063
-
1064
- // Step 2: Convert to bitmap INSIDE the callback
1065
- // This is critical - must happen while we have access to thermal image context
1066
- try {
1067
- Bitmap bitmap = convertToBitmap(imgBuffer);
1068
- if (bitmap != null) {
1069
- successfulBitmapCount++;
1070
- latestFrame = bitmap;
1071
- if (frameCount == 1) {
1072
- logStep("FIRST_THERMAL_BITMAP", "Thermal bitmap created INSIDE callback: " + bitmap.getWidth() + "x" + bitmap.getHeight());
1073
- }
1074
- if (listener != null) {
1075
- listener.onFrame(bitmap);
1076
- }
1077
- } else if (frameCount <= 5) {
1078
- logStep("THERMAL_BITMAP_NULL", "Thermal convertToBitmap returned null at frame " + frameCount);
1079
- }
1080
- } catch (Throwable t) {
1081
- if (frameCount <= 5) {
1082
- logStep("THERMAL_BITMAP_ERROR", "Bitmap conversion failed: " + t.getMessage());
1083
- }
1084
- Log.d(TAG, "[FLIR] Bitmap conversion in callback failed: " + t.getMessage());
1085
- }
1086
- } else if (frameCount <= 5) {
1087
- logStep("THERMAL_IMAGE_NULL", "ThermalImage is null in callback");
1088
- }
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;
1089
426
  }
1090
- return null;
427
+ } catch (Exception e) {
428
+ Log.w(TAG, "Error querying temperature (normalized)", e);
1091
429
  }
1092
- );
1093
-
1094
- withThermalImageMethod.invoke(streamerObj, consumer);
1095
-
1096
- } catch (Throwable t) {
1097
- Log.d(TAG, "[FLIR] processThermalFrameWithCallback failed: " + t.getMessage());
1098
- // Fallback to direct conversion
1099
- Bitmap bitmap = convertToBitmap(imageBuffer);
1100
- if (bitmap != null) {
1101
- latestFrame = bitmap;
1102
- if (listener != null) listener.onFrame(bitmap);
1103
- }
430
+ });
431
+ } catch (Exception e) {
432
+ Log.w(TAG, "Temperature query failed", e);
1104
433
  }
434
+
435
+ return result[0];
1105
436
  }
1106
437
 
1107
438
  /**
1108
- * Apply palette via ThermalStreamer.withThermalImage() callback pattern.
1109
- * In the SDK: thermalStreamer.withThermalImage { it.palette = selectedPalette }
1110
- * NOTE: This is now mostly unused - processThermalFrameWithCallback handles both palette and bitmap
439
+ * Set palette for thermal image rendering
1111
440
  */
1112
- private void applyPaletteViaThermalImage() {
1113
- if (streamerObj == null || currentPalette == null) return;
441
+ public void setPalette(String paletteName) {
442
+ if (streamer == null) {
443
+ Log.w(TAG, "No active streamer");
444
+ return;
445
+ }
1114
446
 
1115
- try {
1116
- Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
1117
- if (!thermalStreamerClass.isInstance(streamerObj)) return;
1118
-
1119
- // Find withThermalImage method - it takes a Consumer<ThermalImage> callback
1120
- // We'll use reflection to create a proxy for the Consumer interface
1121
- Method withThermalImageMethod = null;
1122
- for (Method m : thermalStreamerClass.getMethods()) {
1123
- if ("withThermalImage".equals(m.getName()) && m.getParameterCount() == 1) {
1124
- withThermalImageMethod = m;
1125
- break;
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);
1126
455
  }
456
+ } catch (Exception e) {
457
+ Log.e(TAG, "Error setting palette", e);
1127
458
  }
1128
-
1129
- if (withThermalImageMethod == null) {
1130
- // Fallback: Try to get ThermalImage directly
1131
- try {
1132
- Method getThermalImage = streamerObj.getClass().getMethod("getThermalImage");
1133
- Object thermalImage = getThermalImage.invoke(streamerObj);
1134
- if (thermalImage != null) {
1135
- setPaletteOnThermalImage(thermalImage);
1136
- }
1137
- } catch (Throwable ignored) {}
1138
- return;
1139
- }
1140
-
1141
- // Create Consumer proxy to call setPalette on ThermalImage
1142
- Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
1143
- Object consumer = Proxy.newProxyInstance(
1144
- getEffectiveClassLoader(),
1145
- new Class<?>[] { consumerClass },
1146
- (proxy, method, args) -> {
1147
- if ("accept".equals(method.getName()) && args != null && args.length > 0) {
1148
- Object thermalImage = args[0];
1149
- if (thermalImage != null) {
1150
- setPaletteOnThermalImage(thermalImage);
1151
- }
1152
- }
1153
- return null;
1154
- }
1155
- );
1156
-
1157
- withThermalImageMethod.invoke(streamerObj, consumer);
1158
-
1159
- } catch (Throwable t) {
1160
- Log.d(TAG, "[FLIR] applyPaletteViaThermalImage failed: " + t.getMessage());
1161
- }
459
+ });
1162
460
  }
1163
461
 
1164
462
  /**
1165
- * Set palette on a ThermalImage object
463
+ * Get list of available palettes
1166
464
  */
1167
- private void setPaletteOnThermalImage(Object thermalImage) {
1168
- if (thermalImage == null || currentPalette == null) return;
1169
-
465
+ public List<String> getAvailablePalettes() {
466
+ List<String> names = new ArrayList<>();
1170
467
  try {
1171
- Class<?> paletteClass = findSdkClass("com.flir.thermalsdk.image.Palette");
1172
- Method setPalette = thermalImage.getClass().getMethod("setPalette", paletteClass);
1173
- setPalette.invoke(thermalImage, currentPalette);
1174
- } catch (Throwable t) {
1175
- // Try with direct class
1176
- try {
1177
- Method setPalette = thermalImage.getClass().getMethod("setPalette", currentPalette.getClass());
1178
- setPalette.invoke(thermalImage, currentPalette);
1179
- } catch (Throwable ignored) {}
468
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
469
+ for (Palette p : palettes) {
470
+ names.add(p.name);
471
+ }
472
+ } catch (Exception e) {
473
+ Log.e(TAG, "Error getting palettes", e);
1180
474
  }
475
+ return names;
1181
476
  }
1182
477
 
1183
- private Bitmap convertToBitmap(Object imageBuffer) {
1184
- boolean isFirstConvert = frameCount == 1;
1185
-
478
+ // Find palette by name
479
+ private Palette findPalette(String name) {
1186
480
  try {
1187
- // Try BitmapAndroid.createBitmap(imageBuffer).bitMap (or getBitMap)
1188
- Class<?> bitmapAndroidClass = findSdkClass("com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
1189
-
1190
- if (isFirstConvert) {
1191
- logStep("BITMAP_CONVERT_START", "imageBuffer type=" + imageBuffer.getClass().getName());
1192
- }
1193
-
1194
- // Find a createBitmap method that works with our imageBuffer
1195
- for (Method m : bitmapAndroidClass.getMethods()) {
1196
- if ("createBitmap".equals(m.getName()) && m.getParameterCount() == 1) {
1197
- Class<?> paramType = m.getParameterTypes()[0];
1198
- if (paramType.isInstance(imageBuffer)) {
1199
- Object wrapper = m.invoke(null, imageBuffer);
1200
- if (wrapper != null) {
1201
- if (isFirstConvert) {
1202
- logStep("BITMAP_WRAPPER", "BitmapAndroid wrapper created: " + wrapper.getClass().getSimpleName());
1203
- }
1204
-
1205
- // Try different method names: bitMap, getBitMap, getBitmap
1206
- String[] methodNames = {"getBitMap", "bitMap", "getBitmap"};
1207
- for (String methodName : methodNames) {
1208
- try {
1209
- Method getBitMap = wrapper.getClass().getMethod(methodName);
1210
- Object bmp = getBitMap.invoke(wrapper);
1211
- if (bmp instanceof Bitmap) {
1212
- Bitmap bitmap = (Bitmap) bmp;
1213
- if (bitmap.getWidth() > 0 && bitmap.getHeight() > 0) {
1214
- if (isFirstConvert) {
1215
- logStep("BITMAP_SUCCESS", "Got bitmap via " + methodName + "(): " + bitmap.getWidth() + "x" + bitmap.getHeight());
1216
- }
1217
- return bitmap;
1218
- } else if (isFirstConvert) {
1219
- logStep("BITMAP_EMPTY", "Bitmap has zero dimensions via " + methodName);
1220
- }
1221
- }
1222
- } catch (NoSuchMethodException ignored) {}
1223
- }
1224
-
1225
- // Also try as a field access (Kotlin property)
1226
- try {
1227
- java.lang.reflect.Field field = wrapper.getClass().getField("bitMap");
1228
- Object bmp = field.get(wrapper);
1229
- if (bmp instanceof Bitmap) {
1230
- return (Bitmap) bmp;
1231
- }
1232
- } catch (Throwable ignored) {}
1233
- }
1234
- }
481
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
482
+ for (Palette p : palettes) {
483
+ if (p.name.equalsIgnoreCase(name)) {
484
+ return p;
1235
485
  }
1236
486
  }
1237
-
1238
- Log.d(TAG, "[FLIR] BitmapAndroid.createBitmap method not found for imageBuffer type: " + imageBuffer.getClass().getName());
1239
-
1240
- } catch (Throwable t) {
1241
- Log.d(TAG, "[FLIR] convertToBitmap primary method failed: " + t.getMessage());
1242
- }
1243
-
1244
- // Try alternative: direct getBitmap() on imageBuffer
1245
- try {
1246
- Method getBitmap = imageBuffer.getClass().getMethod("getBitmap");
1247
- Object bmp = getBitmap.invoke(imageBuffer);
1248
- if (bmp instanceof Bitmap) {
1249
- return (Bitmap) bmp;
1250
- }
1251
- } catch (Throwable ignored) {}
1252
-
1253
- return null;
1254
- }
1255
-
1256
- private void stopStreaming() {
1257
- if (currentStream != null) {
1258
- try {
1259
- Method stopMethod = currentStream.getClass().getMethod("stop");
1260
- stopMethod.invoke(currentStream);
1261
- Log.i(TAG, "[FLIR] Stream stopped");
1262
- } catch (Throwable t) {
1263
- Log.w(TAG, "[FLIR] stopStreaming failed: " + t.getMessage());
487
+ // Return first if not found
488
+ if (!palettes.isEmpty()) {
489
+ return palettes.get(0);
1264
490
  }
491
+ } catch (Exception e) {
492
+ Log.e(TAG, "Error finding palette", e);
1265
493
  }
1266
-
1267
- streamerObj = null;
1268
- currentStream = null;
1269
- isStreaming.set(false);
494
+ return null;
1270
495
  }
1271
496
 
1272
- // ==================== SDK LOADING ====================
1273
-
1274
- private boolean initializeSdk() {
1275
- // Try direct class loading first
1276
- try {
1277
- Class.forName("com.flir.thermalsdk.live.CommunicationInterface");
1278
- Log.i(TAG, "[FLIR SDK] Classes available on classpath");
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);
1279
504
 
1280
- // Initialize SDK
1281
- initializeThermalSdk();
1282
- return true;
1283
- } catch (ClassNotFoundException e) {
1284
- Log.w(TAG, "[FLIR SDK] Classes not on classpath, trying AAR load");
1285
- }
1286
-
1287
- // Try loading from downloaded AAR
1288
- if (attemptLoadSdkFromAar()) {
1289
- initializeThermalSdk();
1290
- return true;
1291
- }
1292
-
1293
- return false;
1294
- }
1295
-
1296
- private void initializeThermalSdk() {
1297
- try {
1298
- Class<?> sdkClass = findSdkClass("com.flir.thermalsdk.live.ThermalSdkAndroid");
1299
- Method initMethod = sdkClass.getMethod("init", android.content.Context.class);
1300
- initMethod.invoke(null, appContext);
1301
- Log.i(TAG, "[FLIR SDK] ThermalSdkAndroid.init() completed");
1302
- } catch (Throwable t) {
1303
- Log.w(TAG, "[FLIR SDK] ThermalSdkAndroid.init() failed: " + t.getMessage());
1304
- }
1305
- }
1306
-
1307
- private boolean attemptLoadSdkFromAar() {
1308
- android.content.Context ctx = appContext;
1309
- if (ctx == null) {
1310
- Log.w(TAG, "[FLIR SDK] No application context available for SDK load");
1311
- return false;
1312
- }
1313
-
1314
- try {
1315
- // First try: Load from architecture-specific DEX (new format from FlirSDKLoader)
1316
- File dexFile = FlirSDKLoader.INSTANCE.getDexPath(ctx);
1317
- if (dexFile != null && dexFile.exists()) {
1318
- Log.i(TAG, "[FLIR SDK] Found DEX file: " + dexFile.getAbsolutePath() + " (size=" + dexFile.length() + ")");
1319
-
1320
- // Get native library directory path for DexClassLoader
1321
- File nativeLibDir = FlirSDKLoader.INSTANCE.getNativeLibDir(ctx);
1322
- String nativeLibPath = nativeLibDir != null ? nativeLibDir.getAbsolutePath() : null;
1323
- Log.i(TAG, "[FLIR SDK] Native lib path: " + nativeLibPath);
1324
-
1325
- // Create DexClassLoader with native lib path
1326
- File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
1327
- DexClassLoader dcl = new DexClassLoader(
1328
- dexFile.getAbsolutePath(),
1329
- dexOutDir.getAbsolutePath(),
1330
- nativeLibPath, // This allows SDK to find .so files
1331
- ctx.getClassLoader()
1332
- );
1333
-
1334
- // Verify class loading
1335
- Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
1336
- if (test != null) {
1337
- sdkClassLoader = dcl;
1338
- sdkJarPath = dexFile.getAbsolutePath();
1339
- Log.i(TAG, "[FLIR SDK] DexClassLoader created from DEX: " + dexFile.getAbsolutePath());
1340
- return true;
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;
511
+ break;
512
+ }
1341
513
  }
1342
- }
1343
-
1344
- // Fallback: Legacy AAR loading
1345
- Log.i(TAG, "[FLIR SDK] No DEX found, trying legacy AAR locations...");
1346
-
1347
- File filesDir = ctx.getFilesDir();
1348
-
1349
- // Candidate search locations (ordered by preference)
1350
- List<File> candidates = new ArrayList<>();
1351
-
1352
- // Primary: FlirSDKLoader download directory
1353
- candidates.add(new File(filesDir, "FlirSDK/thermalsdk-release.aar"));
1354
- candidates.add(new File(filesDir, "FlirSDK/androidsdk-release.aar"));
1355
- candidates.add(new File(filesDir, "FlirSDK/thermalsdk.aar"));
1356
-
1357
- // Legacy locations
1358
- candidates.add(new File(filesDir, "flir-sdk/thermalsdk-release.aar"));
1359
- candidates.add(new File(filesDir, "thermalsdk-release.aar"));
1360
- candidates.add(new File(filesDir, "thermalsdk.aar"));
1361
-
1362
- // External storage
1363
- File extDir = ctx.getExternalFilesDir(null);
1364
- if (extDir != null) {
1365
- candidates.add(new File(extDir, "FlirSDK/thermalsdk-release.aar"));
1366
- candidates.add(new File(extDir, "thermalsdk-release.aar"));
1367
- }
1368
-
1369
- // Find first existing AAR
1370
- File aarFile = null;
1371
- StringBuilder tried = new StringBuilder();
1372
- for (File f : candidates) {
1373
- tried.append(f.getAbsolutePath()).append(f.exists() ? "(✓)," : "(✗),");
1374
- if (f.exists()) {
1375
- aarFile = f;
1376
- break;
514
+ if (!exists) {
515
+ discoveredDevices.add(identity);
1377
516
  }
1378
517
  }
1379
518
 
1380
- if (aarFile == null) {
1381
- Log.w(TAG, "[FLIR SDK] No AAR found. Tried: " + tried);
1382
- return false;
1383
- }
1384
-
1385
- Log.i(TAG, "[FLIR SDK] Found AAR: " + aarFile.getAbsolutePath());
1386
-
1387
- // Extract classes.jar from AAR to a private directory
1388
- ZipFile zf = new ZipFile(aarFile);
1389
- ZipEntry classesEntry = zf.getEntry("classes.jar");
1390
- if (classesEntry == null) {
1391
- Log.w(TAG, "[FLIR SDK] classes.jar not found in AAR");
1392
- zf.close();
1393
- return false;
519
+ if (listener != null) {
520
+ listener.onDeviceFound(identity);
521
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
1394
522
  }
523
+ }
524
+
525
+ @Override
526
+ public void onCameraLost(Identity identity) {
527
+ Log.d(TAG, "Device lost: " + identity.deviceId);
1395
528
 
1396
- // Use getDir() for a MODE_PRIVATE directory - required for DexClassLoader security
1397
- File privateDir = ctx.getDir("flir_sdk", android.content.Context.MODE_PRIVATE);
1398
- File outJar = new File(privateDir, "flir-classes.jar");
1399
-
1400
- // Delete old file if exists to ensure clean extraction
1401
- if (outJar.exists()) {
1402
- outJar.delete();
529
+ synchronized (discoveredDevices) {
530
+ discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
1403
531
  }
1404
532
 
1405
- FileOutputStream fos = new FileOutputStream(outJar);
1406
- java.io.InputStream is = zf.getInputStream(classesEntry);
1407
- byte[] buf = new byte[8192];
1408
- int r;
1409
- while ((r = is.read(buf)) != -1) fos.write(buf, 0, r);
1410
- is.close();
1411
- fos.close();
1412
- zf.close();
1413
-
1414
- // Set file to read-only (required for Android security)
1415
- outJar.setReadOnly();
1416
-
1417
- Log.i(TAG, "[FLIR SDK] Extracted classes.jar to: " + outJar.getAbsolutePath() + " (size=" + outJar.length() + ")");
1418
-
1419
- // Create DexClassLoader with private dex output directory
1420
- File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
1421
- DexClassLoader dcl = new DexClassLoader(
1422
- outJar.getAbsolutePath(),
1423
- dexOutDir.getAbsolutePath(),
1424
- null,
1425
- ctx.getClassLoader()
1426
- );
1427
-
1428
- // Verify class loading
1429
- Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
1430
- if (test != null) {
1431
- sdkClassLoader = dcl;
1432
- sdkJarPath = outJar.getAbsolutePath();
1433
- Log.i(TAG, "[FLIR SDK] DexClassLoader created from: " + outJar.getAbsolutePath());
1434
- return true;
533
+ if (listener != null) {
534
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
1435
535
  }
1436
-
1437
- } catch (Throwable t) {
1438
- Log.e(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
1439
536
  }
1440
537
 
1441
- return false;
1442
- }
1443
-
1444
- private Class<?> findSdkClass(String name) throws ClassNotFoundException {
1445
- try {
1446
- return Class.forName(name);
1447
- } catch (ClassNotFoundException e) {
1448
- if (sdkClassLoader != null) {
1449
- return Class.forName(name, true, sdkClassLoader);
1450
- }
1451
- throw e;
538
+ @Override
539
+ public void onDiscoveryError(CommunicationInterface iface, ErrorCode error) {
540
+ Log.e(TAG, "Discovery error: " + iface + " - " + error);
541
+ notifyError("Discovery error: " + error);
1452
542
  }
1453
- }
1454
-
1455
- private ClassLoader getEffectiveClassLoader() {
1456
- return sdkClassLoader != null ? sdkClassLoader : getClass().getClassLoader();
1457
- }
1458
-
1459
- // ==================== HELPERS ====================
1460
-
1461
- private String extractDeviceId(Object identity) {
1462
- try {
1463
- Method getDeviceId = identity.getClass().getMethod("getDeviceId");
1464
- Object result = getDeviceId.invoke(identity);
1465
- return result != null ? result.toString() : "unknown";
1466
- } catch (Throwable t) {
1467
- return "device_" + System.currentTimeMillis();
543
+
544
+ @Override
545
+ public void onDiscoveryFinished(CommunicationInterface iface) {
546
+ Log.d(TAG, "Discovery finished for: " + iface);
1468
547
  }
1469
- }
548
+ };
1470
549
 
1471
- private String extractDeviceName(Object identity) {
1472
- try {
1473
- // Try getName() first
1474
- try {
1475
- Method getName = identity.getClass().getMethod("getName");
1476
- Object result = getName.invoke(identity);
1477
- if (result != null && !result.toString().isEmpty()) {
1478
- return result.toString();
1479
- }
1480
- } catch (Throwable ignored) {}
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;
1481
556
 
1482
- // Try getDeviceId() as fallback
1483
- Method getDeviceId = identity.getClass().getMethod("getDeviceId");
1484
- Object result = getDeviceId.invoke(identity);
1485
- return result != null ? result.toString() : "FLIR Camera";
1486
- } catch (Throwable t) {
1487
- return "FLIR Camera";
1488
- }
1489
- }
1490
-
1491
- private CommInterface extractCommInterface(Object identity) {
1492
- try {
1493
- Method getCommInterface = identity.getClass().getMethod("getCommunicationInterface");
1494
- Object result = getCommInterface.invoke(identity);
1495
- if (result != null) {
1496
- String name = result.toString();
1497
- if (name.contains("USB")) return CommInterface.USB;
1498
- if (name.contains("NETWORK")) return CommInterface.NETWORK;
1499
- if (name.contains("EMULATOR")) return CommInterface.EMULATOR;
557
+ if (listener != null) {
558
+ listener.onDisconnected();
1500
559
  }
1501
- } catch (Throwable ignored) {}
1502
- return CommInterface.EMULATOR;
1503
- }
560
+ }
561
+ };
1504
562
 
1505
- private void notifyError(String error) {
1506
- Log.e(TAG, "[FLIR] Error: " + error);
563
+ // Helper to notify errors
564
+ private void notifyError(String message) {
1507
565
  if (listener != null) {
1508
- mainHandler.post(() -> listener.onError(error));
566
+ listener.onError(message);
1509
567
  }
1510
568
  }
569
+
570
+ /**
571
+ * Cleanup resources
572
+ */
573
+ public void destroy() {
574
+ stop();
575
+ disconnect();
576
+ discoveredDevices.clear();
577
+ listener = null;
578
+ instance = null;
579
+ Log.d(TAG, "Destroyed");
580
+ }
1511
581
  }