ilabs-flir 1.0.4 → 1.0.5

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 (36) 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/java/flir/android/FlirDownloadManager.kt +23 -44
  5. package/android/Flir/src/main/java/flir/android/FlirModule.kt +32 -0
  6. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +35 -194
  7. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +410 -1161
  8. package/package.json +1 -1
  9. package/src/index.d.ts +21 -1
  10. package/android/Flir/libs/flir-stubs.jar +0 -0
  11. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCode.java +0 -13
  12. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCodeException.java +0 -14
  13. package/android/Flir/src/main/java/com/flir/thermalsdk/ThermalSdkAndroid.java +0 -16
  14. package/android/Flir/src/main/java/com/flir/thermalsdk/androidsdk/image/BitmapAndroid.java +0 -20
  15. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ImageBuffer.java +0 -11
  16. package/android/Flir/src/main/java/com/flir/thermalsdk/image/JavaImageBuffer.java +0 -35
  17. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Palette.java +0 -15
  18. package/android/Flir/src/main/java/com/flir/thermalsdk/image/PaletteManager.java +0 -16
  19. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Point.java +0 -11
  20. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalImage.java +0 -23
  21. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalValue.java +0 -9
  22. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Camera.java +0 -26
  23. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CameraType.java +0 -8
  24. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CommunicationInterface.java +0 -16
  25. package/android/Flir/src/main/java/com/flir/thermalsdk/live/ConnectParameters.java +0 -16
  26. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Identity.java +0 -23
  27. package/android/Flir/src/main/java/com/flir/thermalsdk/live/IpSettings.java +0 -9
  28. package/android/Flir/src/main/java/com/flir/thermalsdk/live/RemoteControl.java +0 -16
  29. package/android/Flir/src/main/java/com/flir/thermalsdk/live/connectivity/ConnectionStatusListener.java +0 -7
  30. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryEventListener.java +0 -14
  31. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryFactory.java +0 -33
  32. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnReceived.java +0 -5
  33. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnRemoteError.java +0 -7
  34. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/Stream.java +0 -8
  35. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/ThermalStreamer.java +0 -28
  36. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/VisualStreamer.java +0 -18
@@ -5,10 +5,29 @@ import android.os.Handler;
5
5
  import android.os.Looper;
6
6
  import android.util.Log;
7
7
 
8
- import java.io.File;
9
- import java.io.FileOutputStream;
10
- import java.lang.reflect.Method;
11
- import java.lang.reflect.Proxy;
8
+ import com.flir.thermalsdk.ErrorCode;
9
+ import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
10
+ import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
11
+ import com.flir.thermalsdk.image.ImageBuffer;
12
+ import com.flir.thermalsdk.image.JavaImageBuffer;
13
+ import com.flir.thermalsdk.image.Palette;
14
+ import com.flir.thermalsdk.image.PaletteManager;
15
+ import com.flir.thermalsdk.image.Point;
16
+ import com.flir.thermalsdk.image.ThermalImage;
17
+ import com.flir.thermalsdk.image.ThermalValue;
18
+ import com.flir.thermalsdk.live.Camera;
19
+ import com.flir.thermalsdk.live.CommunicationInterface;
20
+ import com.flir.thermalsdk.live.ConnectParameters;
21
+ import com.flir.thermalsdk.live.Identity;
22
+ import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
23
+ import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
24
+ import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
25
+ import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
26
+ import com.flir.thermalsdk.live.remote.OnReceived;
27
+ import com.flir.thermalsdk.live.remote.OnRemoteError;
28
+ import com.flir.thermalsdk.live.streaming.Stream;
29
+ import com.flir.thermalsdk.live.streaming.ThermalStreamer;
30
+
12
31
  import java.util.ArrayList;
13
32
  import java.util.List;
14
33
  import java.util.concurrent.CopyOnWriteArrayList;
@@ -17,64 +36,28 @@ import java.util.concurrent.ScheduledExecutorService;
17
36
  import java.util.concurrent.ScheduledFuture;
18
37
  import java.util.concurrent.TimeUnit;
19
38
  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
39
 
25
40
  /**
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.
41
+ * FLIR SDK Manager - Handles device discovery, connection, and streaming.
42
+ * Uses the official FLIR ThermalSDK directly (bundled in AAR).
29
43
  *
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)
44
+ * Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
45
45
  */
46
46
  public class FlirSdkManager {
47
47
  private static final String TAG = "FlirSdkManager";
48
- private static final String FLOW_TAG = "FLIR_FLOW"; // For step-by-step flow logging
49
-
50
- // Step counter for tracking flow
51
- private int stepCounter = 0;
52
- private long flowStartTime = 0;
53
-
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);
59
- }
60
-
61
- private void resetFlowTracking() {
62
- stepCounter = 0;
63
- flowStartTime = System.currentTimeMillis();
64
- Log.i(FLOW_TAG, "========== FLIR FLOW STARTED ==========");
65
- }
48
+ private static final String FLOW_TAG = "FLIR_FLOW";
66
49
 
67
50
  // 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
51
+ private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000;
52
+ private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0;
70
53
 
71
54
  // Emulator types
72
55
  public enum EmulatorType {
73
- FLIR_ONE_EDGE, // Default - WiFi emulator
56
+ FLIR_ONE_EDGE, // WiFi emulator
74
57
  FLIR_ONE // USB emulator
75
58
  }
76
59
 
77
- // Communication interfaces (mirrors SDK enum)
60
+ // Communication interfaces
78
61
  public enum CommInterface {
79
62
  USB,
80
63
  NETWORK,
@@ -95,15 +78,15 @@ public class FlirSdkManager {
95
78
  void onError(String error);
96
79
  }
97
80
 
98
- // Device info class for discovered devices
81
+ // Device info class
99
82
  public static class DeviceInfo {
100
83
  public final String deviceId;
101
84
  public final String deviceName;
102
85
  public final boolean isEmulator;
103
86
  public final CommInterface commInterface;
104
- public final Object identity; // SDK Identity object (kept for connection)
87
+ public final Identity identity;
105
88
 
106
- DeviceInfo(String id, String name, boolean emu, CommInterface iface, Object identity) {
89
+ DeviceInfo(String id, String name, boolean emu, CommInterface iface, Identity identity) {
107
90
  this.deviceId = id;
108
91
  this.deviceName = name;
109
92
  this.isEmulator = emu;
@@ -117,14 +100,11 @@ public class FlirSdkManager {
117
100
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
118
101
  private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
119
102
 
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;
103
+ // SDK objects
104
+ private Camera camera = null;
105
+ private Stream currentStream = null;
106
+ private ThermalStreamer thermalStreamer = null;
107
+ private Palette currentPalette = null;
128
108
 
129
109
  // State tracking
130
110
  private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
@@ -138,87 +118,107 @@ public class FlirSdkManager {
138
118
 
139
119
  // Frame state
140
120
  private volatile Bitmap latestFrame = null;
141
- private String sdkJarPath = null;
121
+ private volatile ThermalImage currentThermalImage = null;
142
122
  private String currentStreamKind = null;
143
123
 
144
- // Frame counting for debug logging
145
- private int frameCount = 0;
146
- private long lastFrameLogTime = 0;
147
- private int successfulBitmapCount = 0;
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;
148
130
 
149
131
  FlirSdkManager(Listener listener, android.content.Context context) {
150
132
  this.listener = listener;
151
133
  this.appContext = context != null ? context.getApplicationContext() : null;
152
134
  }
153
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));
140
+ }
141
+
142
+ private void resetFlowTracking() {
143
+ stepCounter = 0;
144
+ flowStartTime = System.currentTimeMillis();
145
+ Log.i(FLOW_TAG, "========== FLIR FLOW STARTED ==========");
146
+ }
147
+
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;
159
+ }
160
+
161
+ 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;
171
+ }
172
+ }
173
+
154
174
  // ==================== PUBLIC API ====================
155
175
 
156
- /**
157
- * Set the emulator type to use when no physical device is found
158
- */
159
176
  public void setEmulatorType(EmulatorType type) {
160
177
  this.emulatorType = type;
161
178
  Log.i(TAG, "[FLIR] Emulator type set to: " + type);
162
- logStep("SET_EMULATOR_TYPE", "type=" + type + " (FLIR_ONE_EDGE=WiFi, FLIR_ONE=USB)");
179
+ logStep("SET_EMULATOR_TYPE", "type=" + type);
163
180
  }
164
181
 
165
- /**
166
- * Start device discovery. Will disconnect current device first.
167
- * @param forceEmulator If true, skip device discovery and connect to emulator immediately
168
- */
169
182
  public void startDiscovery(boolean forceEmulator) {
170
183
  resetFlowTracking();
171
184
  logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
172
185
  Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
173
186
 
174
- // Always disconnect first
187
+ // Disconnect current device first
175
188
  if (isConnected.get()) {
176
- logStep("DISCONNECT_PREVIOUS", "Disconnecting current device before discovery");
189
+ logStep("DISCONNECT_PREVIOUS", "Disconnecting current device");
177
190
  disconnect();
178
191
  }
179
192
 
180
- // Clear discovered devices
181
193
  discoveredDevices.clear();
182
194
  logStep("CLEAR_DEVICES", "Cleared discovered devices list");
183
195
 
184
196
  if (forceEmulator) {
185
- // Immediate emulator mode
186
- logStep("MODE_EMULATOR", "Forcing emulator mode - skipping device discovery");
197
+ logStep("MODE_EMULATOR", "Forcing emulator mode");
187
198
  isEmulatorMode.set(true);
188
199
  startEmulatorDiscovery();
189
200
  } else {
190
- // Normal discovery with timeout
191
- logStep("MODE_FULL_DISCOVERY", "Starting full discovery (USB+NETWORK+EMULATOR), timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
201
+ logStep("MODE_FULL_DISCOVERY", "Starting full discovery, timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
192
202
  isEmulatorMode.set(false);
193
203
  startFullDiscovery();
194
204
  }
195
205
  }
196
206
 
197
- /**
198
- * Stop discovery scan
199
- */
200
207
  public void stopDiscovery() {
201
208
  Log.i(TAG, "[FLIR] stopDiscovery()");
202
209
  cancelDiscoveryTimeout();
203
210
  isDiscovering.set(false);
204
211
 
205
212
  try {
206
- if (discoveryFactory != null) {
207
- Method stopMethod = discoveryFactory.getClass().getMethod("stop");
208
- stopMethod.invoke(discoveryFactory);
209
- }
213
+ DiscoveryFactory.getInstance().stop();
210
214
  } catch (Throwable t) {
211
215
  Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
212
216
  }
213
217
  }
214
218
 
215
- /**
216
- * Connect to a specific device by ID
217
- */
218
219
  public void connectToDevice(String deviceId) {
219
220
  Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
220
221
 
221
- // Find device in discovered list
222
222
  DeviceInfo target = null;
223
223
  for (DeviceInfo d : discoveredDevices) {
224
224
  if (d.deviceId.equals(deviceId)) {
@@ -232,220 +232,122 @@ public class FlirSdkManager {
232
232
  return;
233
233
  }
234
234
 
235
- // Disconnect current if needed
236
235
  if (isConnected.get()) {
237
236
  disconnect();
238
237
  }
239
238
 
240
- // Connect to target
241
239
  connectToIdentity(target);
242
240
  }
243
241
 
244
- /**
245
- * Disconnect current device/emulator
246
- */
247
242
  public void disconnect() {
248
243
  Log.i(TAG, "[FLIR] disconnect()");
249
244
 
250
- // Stop streaming first
251
245
  stopStreaming();
252
246
 
253
- // Disconnect camera
254
- if (cameraObj != null) {
247
+ if (camera != null) {
255
248
  try {
256
- Method disconnectMethod = cameraObj.getClass().getMethod("disconnect");
257
- disconnectMethod.invoke(cameraObj);
258
- Log.i(TAG, "[FLIR] Camera disconnected");
249
+ camera.disconnect();
259
250
  } catch (Throwable t) {
260
- Log.w(TAG, "[FLIR] disconnect failed: " + t.getMessage());
251
+ Log.w(TAG, "[FLIR] Camera disconnect failed: " + t.getMessage());
261
252
  }
253
+ camera = null;
262
254
  }
263
255
 
264
- cameraObj = null;
265
- streamerObj = null;
266
- currentStream = null;
267
- connectedDevice = null;
268
256
  isConnected.set(false);
269
- isStreaming.set(false);
257
+ connectedDevice = null;
270
258
 
271
- if (listener != null) {
272
- mainHandler.post(() -> listener.onDeviceDisconnected());
273
- }
259
+ mainHandler.post(() -> {
260
+ if (listener != null) {
261
+ listener.onDeviceDisconnected();
262
+ }
263
+ });
274
264
  }
275
265
 
276
- /**
277
- * Set palette by name (iron, rainbow, etc.)
278
- */
279
- public void setPalette(String paletteName) {
280
- Log.d(TAG, "[FLIR] setPalette: " + paletteName);
266
+ public void setStreamType(String streamType) {
267
+ Log.i(TAG, "[FLIR] setStreamType: " + streamType);
268
+ currentStreamKind = streamType;
281
269
 
282
- if (streamerObj == null) {
283
- Log.w(TAG, "[FLIR] Cannot set palette - no active streamer");
284
- return;
270
+ if (isConnected.get() && camera != null) {
271
+ stopStreaming();
272
+ startStreaming();
285
273
  }
286
-
287
- scheduler.submit(() -> {
288
- 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);
293
-
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
- }
306
-
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);
312
- }
313
- } catch (Throwable t) {
314
- Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
315
- }
316
- });
317
274
  }
318
275
 
319
- /**
320
- * Initialize default "iron" palette for thermal streaming.
321
- * Called automatically when thermal streaming starts if no palette is set.
322
- */
323
- private void initializeDefaultPalette() {
276
+ public void setPalette(String paletteName) {
277
+ Log.i(TAG, "[FLIR] setPalette: " + paletteName);
278
+
324
279
  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
- }
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);
285
+ return;
357
286
  }
358
287
  }
288
+ Log.w(TAG, "[FLIR] Palette not found: " + paletteName);
359
289
  } catch (Throwable t) {
360
- Log.w(TAG, "[FLIR] initializeDefaultPalette failed: " + t.getMessage());
290
+ Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
291
+ }
292
+ }
293
+
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));
361
299
  }
362
300
  }
363
301
 
364
302
  /**
365
- * Get temperature at a specific point
303
+ * Get temperature at a specific point from the current thermal image
366
304
  */
367
305
  public double getTemperatureAtPoint(int x, int y) {
368
- if (streamerObj == null) return Double.NaN;
306
+ if (currentThermalImage == null) {
307
+ return Double.NaN;
308
+ }
369
309
 
370
310
  try {
371
- // Get the thermal image from streamer
372
- Method getImage = streamerObj.getClass().getMethod("getImage");
373
- Object thermalImage = getImage.invoke(streamerObj);
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));
374
316
 
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) {}
317
+ ThermalValue value = currentThermalImage.getValueAt(new Point(clampedX, clampedY));
318
+ if (value != null) {
319
+ return value.asCelsius().value;
395
320
  }
396
321
  } catch (Throwable t) {
397
- Log.d(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
322
+ Log.w(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
398
323
  }
399
-
400
324
  return Double.NaN;
401
325
  }
402
326
 
403
- /**
404
- * Get latest frame bitmap
405
- */
406
327
  public Bitmap getLatestFrame() {
407
328
  return latestFrame;
408
329
  }
409
330
 
410
- /**
411
- * Check if streaming is active
412
- */
413
- public boolean isStreamingActive() {
414
- return isStreaming.get();
331
+ public List<DeviceInfo> getDiscoveredDevices() {
332
+ return new ArrayList<>(discoveredDevices);
415
333
  }
416
334
 
417
- /**
418
- * Check if connected
419
- */
420
335
  public boolean isConnected() {
421
336
  return isConnected.get();
422
337
  }
423
338
 
424
- /**
425
- * Get current stream type
426
- */
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);
339
+ public boolean isStreaming() {
340
+ return isStreaming.get();
436
341
  }
437
342
 
438
- /**
439
- * Cleanup resources
440
- */
441
- public void stop() {
442
- Log.i(TAG, "[FLIR] stop()");
343
+ public void destroy() {
344
+ Log.i(TAG, "[FLIR] destroy()");
443
345
  stopDiscovery();
444
346
  disconnect();
445
- scheduler.shutdownNow();
347
+ scheduler.shutdown();
446
348
  }
447
349
 
448
- // ==================== DISCOVERY IMPLEMENTATION ====================
350
+ // ==================== DISCOVERY ====================
449
351
 
450
352
  private void startFullDiscovery() {
451
353
  Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
@@ -457,49 +359,37 @@ public class FlirSdkManager {
457
359
  }
458
360
 
459
361
  isDiscovering.set(true);
460
- if (listener != null) {
461
- mainHandler.post(() -> listener.onDiscoveryStarted());
462
- }
362
+ mainHandler.post(() -> {
363
+ if (listener != null) {
364
+ listener.onDiscoveryStarted();
365
+ }
366
+ });
463
367
 
464
368
  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");
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);
384
+ }
385
+ }, interfaces);
495
386
 
496
- // Set discovery timeout
497
- startDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
387
+ // Set timeout for device discovery
388
+ scheduleDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
498
389
 
499
390
  } catch (Throwable t) {
500
391
  Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
501
392
  notifyError("Discovery failed: " + t.getMessage());
502
- // Fallback to emulator
503
393
  startEmulatorDiscovery();
504
394
  }
505
395
  }
@@ -509,1003 +399,362 @@ public class FlirSdkManager {
509
399
  Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
510
400
 
511
401
  if (!initializeSdk()) {
512
- logStep("SDK_INIT_FAILED", "FLIR SDK not available - check if SDK JAR is loaded");
513
- notifyError("FLIR SDK not available");
402
+ notifyError("SDK initialization failed");
514
403
  return;
515
404
  }
516
- logStep("SDK_INITIALIZED", "FLIR SDK successfully initialized");
517
405
 
518
406
  isDiscovering.set(true);
519
407
  isEmulatorMode.set(true);
520
408
 
409
+ mainHandler.post(() -> {
410
+ if (listener != null) {
411
+ listener.onDiscoveryStarted();
412
+ }
413
+ });
414
+
521
415
  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);
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);
546
427
 
547
- logStep("EMULATOR_SCAN_STARTED", "Scanning for emulator devices...");
548
- Log.i(TAG, "[FLIR] Emulator discovery started");
428
+ // Short timeout for emulator
429
+ scheduleDiscoveryTimeout(2000);
549
430
 
550
431
  } catch (Throwable t) {
551
- logStep("EMULATOR_DISCOVERY_ERROR", "Failed: " + t.getMessage());
552
432
  Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
553
433
  notifyError("Emulator discovery failed: " + t.getMessage());
554
434
  }
555
435
  }
556
436
 
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]);
568
- }
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;
569
451
  break;
570
-
571
- case "onDiscoveryError":
572
- if (args != null && args.length > 1) {
573
- Log.w(TAG, "[FLIR] Discovery error: " + args[1]);
574
- }
452
+ case NETWORK:
453
+ commIface = CommInterface.NETWORK;
575
454
  break;
455
+ default:
456
+ commIface = CommInterface.EMULATOR;
576
457
  }
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;
604
- }
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;
605
467
  }
468
+ }
469
+
470
+ if (!exists) {
471
+ discoveredDevices.add(deviceInfo);
606
472
 
607
- if (!exists) {
608
- discoveredDevices.add(deviceInfo);
609
-
610
- // Notify listener
473
+ mainHandler.post(() -> {
611
474
  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
- });
617
- }
618
-
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);
475
+ listener.onDeviceFound(deviceId, deviceName, isEmulator);
476
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
628
477
  }
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);
478
+ });
643
479
 
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();
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);
650
485
  }
651
-
652
- } catch (Throwable t) {
653
- Log.w(TAG, "[FLIR] handleCameraLost failed: " + t.getMessage());
654
486
  }
655
487
  }
656
488
 
657
- private void startDiscoveryTimeout(long timeoutMs) {
489
+ private void scheduleDiscoveryTimeout(long timeoutMs) {
658
490
  cancelDiscoveryTimeout();
659
491
 
660
- if (timeoutMs <= 0) return;
661
-
662
492
  discoveryTimeoutFuture = scheduler.schedule(() -> {
663
- Log.i(TAG, "[FLIR] Discovery timeout");
664
- isDiscovering.set(false);
493
+ Log.i(TAG, "[FLIR] Discovery timeout after " + timeoutMs + "ms");
494
+ logStep("DISCOVERY_TIMEOUT", "timeout=" + timeoutMs + "ms, devicesFound=" + discoveredDevices.size());
665
495
 
666
- if (listener != null) {
667
- mainHandler.post(() -> listener.onDiscoveryTimeout());
668
- }
496
+ isDiscovering.set(false);
497
+ stopDiscovery();
669
498
 
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;
499
+ mainHandler.post(() -> {
500
+ if (listener != null) {
501
+ listener.onDiscoveryTimeout();
676
502
  }
677
- }
503
+ });
678
504
 
679
- if (!hasPhysicalDevice) {
680
- Log.i(TAG, "[FLIR] No physical devices found, starting emulator");
505
+ // If no devices found, try emulator
506
+ if (discoveredDevices.isEmpty() && !isEmulatorMode.get()) {
507
+ logStep("FALLBACK_EMULATOR", "No devices found, trying emulator");
681
508
  startEmulatorDiscovery();
682
- } else if (!discoveredDevices.isEmpty()) {
683
- // Connect to first device
684
- connectToIdentity(discoveredDevices.get(0));
685
509
  }
686
-
687
510
  }, timeoutMs, TimeUnit.MILLISECONDS);
688
511
  }
689
512
 
690
513
  private void cancelDiscoveryTimeout() {
691
- if (discoveryTimeoutFuture != null) {
514
+ if (discoveryTimeoutFuture != null && !discoveryTimeoutFuture.isDone()) {
692
515
  discoveryTimeoutFuture.cancel(false);
693
516
  discoveryTimeoutFuture = null;
694
517
  }
695
518
  }
696
519
 
697
- // ==================== CONNECTION IMPLEMENTATION ====================
520
+ // ==================== CONNECTION ====================
698
521
 
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);
522
+ private void connectToIdentity(DeviceInfo device) {
523
+ logStep("CONNECT_START", "device=" + device.deviceName);
524
+ Log.i(TAG, "[FLIR] Connecting to: " + device.deviceName);
702
525
 
703
- scheduler.submit(() -> {
526
+ scheduler.execute(() -> {
704
527
  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();
528
+ camera = new Camera();
709
529
 
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();
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
+ });
719
548
  }
720
- return null;
721
- }
549
+ },
550
+ new ConnectParameters()
722
551
  );
723
552
 
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) {}
553
+ // If we get here, connection succeeded
554
+ Log.i(TAG, "[FLIR] Connected to: " + device.deviceName);
555
+ logStep("CONNECTED", "device=" + device.deviceName);
737
556
 
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
557
  isConnected.set(true);
558
+ connectedDevice = device;
765
559
 
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
- }
560
+ mainHandler.post(() -> {
561
+ if (listener != null) {
562
+ listener.onDeviceConnected(device.deviceId, device.deviceName, device.isEmulator);
563
+ }
564
+ });
774
565
 
775
- // Start streaming after brief delay
776
- logStep("SCHEDULE_STREAMING", "Scheduling stream start in 500ms");
777
- scheduler.schedule(this::startStreaming, 500, TimeUnit.MILLISECONDS);
566
+ // Start streaming automatically
567
+ startStreaming();
778
568
 
779
569
  } 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());
570
+ Log.e(TAG, "[FLIR] Connect error: " + t.getMessage(), t);
571
+ isConnected.set(false);
572
+ camera = null;
573
+ notifyError("Connect error: " + t.getMessage());
783
574
  }
784
575
  });
785
576
  }
786
577
 
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());
797
- }
798
- }
799
-
800
- // ==================== STREAMING IMPLEMENTATION ====================
578
+ // ==================== STREAMING ====================
801
579
 
802
580
  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");
581
+ if (camera == null || !isConnected.get()) {
582
+ Log.w(TAG, "[FLIR] Cannot start streaming - not connected");
806
583
  return;
807
584
  }
808
585
 
809
- logStep("STREAM_START", "Starting streaming process...");
586
+ logStep("STREAM_START", "streamType=" + currentStreamKind);
810
587
  Log.i(TAG, "[FLIR] Starting streaming...");
811
588
 
812
- 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;
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());
833
600
 
834
- try {
835
- Method isThermal = s.getClass().getMethod("isThermal");
836
- Boolean thermal = (Boolean) isThermal.invoke(s);
837
-
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";
841
-
842
- Log.d(TAG, "[FLIR] Stream: " + name + ", thermal=" + thermal);
843
-
844
- if (thermal != null && thermal) {
845
- chosenStream = s;
846
- streamType = "thermal";
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;
847
606
  break;
848
- } else if (chosenStream == null) {
849
- chosenStream = s;
850
- streamType = "visual";
851
607
  }
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);
608
+ }
609
+ currentStream = thermalStream != null ? thermalStream : streams.get(0);
872
610
 
873
- // Initialize default palette if not already set
611
+ // Create ThermalStreamer for rendering
612
+ thermalStreamer = new ThermalStreamer(currentStream);
613
+
614
+ // Set default palette if available
874
615
  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]);
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());
908
626
  }
909
- return null;
910
627
  }
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;
947
- }
948
-
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());
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
+ }
980
644
  }
645
+ );
646
+
647
+ isStreaming.set(true);
648
+ Log.i(TAG, "[FLIR] Stream started");
649
+ logStep("STREAM_STARTED", "stream=" + currentStream);
650
+
651
+ mainHandler.post(() -> {
981
652
  if (listener != null) {
982
- listener.onFrame(bitmap);
653
+ listener.onStreamStarted("thermal");
983
654
  }
984
- } else if (frameCount <= 5) {
985
- logStep("BITMAP_NULL", "Visual convertToBitmap returned null at frame " + frameCount);
986
- }
655
+ });
656
+
657
+ } catch (Throwable t) {
658
+ Log.e(TAG, "[FLIR] Start stream error: " + t.getMessage(), t);
659
+ notifyError("Stream error: " + t.getMessage());
987
660
  }
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());
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());
993
670
  }
671
+ currentStream = null;
994
672
  }
673
+ thermalStreamer = null;
674
+ isStreaming.set(false);
995
675
  }
996
676
 
997
677
  /**
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.
678
+ * Refresh thermal frame using ThermalStreamer pattern.
679
+ * Called when a new frame is received.
1001
680
  */
1002
- private void processThermalFrameWithCallback(Object imageBuffer) {
1003
- if (streamerObj == null) return;
681
+ private synchronized void refreshThermalFrame() {
682
+ if (thermalStreamer == null) {
683
+ return;
684
+ }
1004
685
 
1005
686
  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
- }
687
+ // Update streamer to get latest frame
688
+ thermalStreamer.update();
1016
689
 
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
1029
- 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
- }
690
+ // Get the image buffer from streamer
691
+ ImageBuffer imageBuffer = thermalStreamer.getImage();
692
+ if (imageBuffer == null) {
1042
693
  return;
1043
694
  }
1044
695
 
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
- }
1089
- }
1090
- return null;
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);
1091
704
  }
1092
- );
705
+ });
1093
706
 
1094
- withThermalImageMethod.invoke(streamerObj, consumer);
707
+ // Convert to Android Bitmap
708
+ Bitmap bitmap = BitmapAndroid.createBitmap(imageBuffer).getBitMap();
1095
709
 
1096
- } catch (Throwable t) {
1097
- Log.d(TAG, "[FLIR] processThermalFrameWithCallback failed: " + t.getMessage());
1098
- // Fallback to direct conversion
1099
- Bitmap bitmap = convertToBitmap(imageBuffer);
1100
710
  if (bitmap != null) {
1101
711
  latestFrame = bitmap;
1102
- if (listener != null) listener.onFrame(bitmap);
1103
- }
1104
- }
1105
- }
1106
-
1107
- /**
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
1111
- */
1112
- private void applyPaletteViaThermalImage() {
1113
- if (streamerObj == null || currentPalette == null) return;
1114
-
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;
1126
- }
1127
- }
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);
712
+
713
+ mainHandler.post(() -> {
714
+ if (listener != null) {
715
+ listener.onFrame(bitmap);
1136
716
  }
1137
- } catch (Throwable ignored) {}
1138
- return;
717
+ });
1139
718
  }
1140
719
 
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
720
  } catch (Throwable t) {
1160
- Log.d(TAG, "[FLIR] applyPaletteViaThermalImage failed: " + t.getMessage());
721
+ Log.w(TAG, "[FLIR] refreshThermalFrame error: " + t.getMessage());
1161
722
  }
1162
723
  }
1163
724
 
725
+ // ==================== PUBLIC STOP ====================
726
+
1164
727
  /**
1165
- * Set palette on a ThermalImage object
728
+ * Stop the manager - disconnect and cleanup all resources.
1166
729
  */
1167
- private void setPaletteOnThermalImage(Object thermalImage) {
1168
- if (thermalImage == null || currentPalette == null) return;
1169
-
1170
- 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) {}
1180
- }
1181
- }
1182
-
1183
- private Bitmap convertToBitmap(Object imageBuffer) {
1184
- boolean isFirstConvert = frameCount == 1;
1185
-
1186
- 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
- }
1235
- }
1236
- }
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());
1264
- }
1265
- }
730
+ public void stop() {
731
+ Log.i(TAG, "[FLIR] Stopping FlirSdkManager");
1266
732
 
1267
- streamerObj = null;
1268
- currentStream = null;
1269
- isStreaming.set(false);
1270
- }
1271
-
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");
1279
-
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
- }
733
+ // Stop streaming
734
+ stopStreaming();
1286
735
 
1287
- // Try loading from downloaded AAR
1288
- if (attemptLoadSdkFromAar()) {
1289
- initializeThermalSdk();
1290
- return true;
1291
- }
736
+ // Disconnect camera
737
+ disconnect();
1292
738
 
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
- }
739
+ // Stop discovery
740
+ stopDiscovery();
1313
741
 
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;
1341
- }
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;
1377
- }
1378
- }
1379
-
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;
1394
- }
1395
-
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();
1403
- }
1404
-
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;
1435
- }
1436
-
1437
- } catch (Throwable t) {
1438
- Log.e(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
1439
- }
742
+ // Clear state
743
+ discoveredDevices.clear();
744
+ currentThermalImage = null;
745
+ latestFrame = null;
1440
746
 
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;
1452
- }
1453
- }
1454
-
1455
- private ClassLoader getEffectiveClassLoader() {
1456
- return sdkClassLoader != null ? sdkClassLoader : getClass().getClassLoader();
747
+ Log.i(TAG, "[FLIR] FlirSdkManager stopped");
1457
748
  }
1458
749
 
1459
750
  // ==================== HELPERS ====================
1460
751
 
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();
1468
- }
1469
- }
1470
-
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) {}
1481
-
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;
1500
- }
1501
- } catch (Throwable ignored) {}
1502
- return CommInterface.EMULATOR;
1503
- }
1504
-
1505
752
  private void notifyError(String error) {
1506
753
  Log.e(TAG, "[FLIR] Error: " + error);
1507
- if (listener != null) {
1508
- mainHandler.post(() -> listener.onError(error));
1509
- }
754
+ mainHandler.post(() -> {
755
+ if (listener != null) {
756
+ listener.onError(error);
757
+ }
758
+ });
1510
759
  }
1511
760
  }