ilabs-flir 1.0.3 → 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 (33) 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/FlirCommands.java +40 -15
  5. package/android/Flir/src/main/java/flir/android/FlirDownloadManager.kt +26 -46
  6. package/android/Flir/src/main/java/flir/android/FlirManager.kt +265 -42
  7. package/android/Flir/src/main/java/flir/android/FlirModule.kt +32 -0
  8. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +84 -195
  9. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +667 -797
  10. package/package.json +1 -1
  11. package/sdk-manifest.json +14 -7
  12. package/src/index.d.ts +21 -1
  13. package/android/Flir/libs/flir-stubs.jar +0 -0
  14. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCodeException.java +0 -14
  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/CameraType.java +0 -8
  23. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CommunicationInterface.java +0 -16
  24. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Identity.java +0 -23
  25. package/android/Flir/src/main/java/com/flir/thermalsdk/live/IpSettings.java +0 -9
  26. package/android/Flir/src/main/java/com/flir/thermalsdk/live/connectivity/ConnectionStatusListener.java +0 -7
  27. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnReceived.java +0 -5
  28. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnRemoteError.java +0 -7
  29. package/android/Flir/src/main/java/flir/android/CameraHandler.java +0 -224
  30. package/android/Flir/src/main/java/flir/android/FlirConnectionManager.java +0 -354
  31. package/android/Flir/src/main/java/flir/android/FlirController.kt +0 -11
  32. package/android/Flir/src/main/java/flir/android/FlirDiscoveryManager.java +0 -236
  33. package/android/Flir/src/main/java/flir/android/FrameDataHolder.java +0 -14
@@ -1,890 +1,760 @@
1
1
  package flir.android;
2
2
 
3
3
  import android.graphics.Bitmap;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
4
6
  import android.util.Log;
5
7
 
6
- import java.lang.reflect.Method;
7
- 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
+
31
+ import java.util.ArrayList;
8
32
  import java.util.List;
9
- import java.io.File;
10
- import java.io.FileOutputStream;
11
- import java.util.zip.ZipEntry;
12
- import java.util.zip.ZipFile;
13
- import dalvik.system.DexClassLoader;
33
+ import java.util.concurrent.CopyOnWriteArrayList;
14
34
  import java.util.concurrent.Executors;
15
35
  import java.util.concurrent.ScheduledExecutorService;
36
+ import java.util.concurrent.ScheduledFuture;
16
37
  import java.util.concurrent.TimeUnit;
38
+ import java.util.concurrent.atomic.AtomicBoolean;
17
39
 
18
40
  /**
19
- * Thin helper that wraps FLIR Atlas SDK emulator and streamer features.
20
- * All SDK-specific reflection plumbing lives here so FlirManager stays small.
41
+ * FLIR SDK Manager - Handles device discovery, connection, and streaming.
42
+ * Uses the official FLIR ThermalSDK directly (bundled in AAR).
43
+ *
44
+ * Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
21
45
  */
22
- class FlirSdkManager {
46
+ public class FlirSdkManager {
23
47
  private static final String TAG = "FlirSdkManager";
48
+ private static final String FLOW_TAG = "FLIR_FLOW";
49
+
50
+ // Discovery timeout in milliseconds
51
+ private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000;
52
+ private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0;
53
+
54
+ // Emulator types
55
+ public enum EmulatorType {
56
+ FLIR_ONE_EDGE, // WiFi emulator
57
+ FLIR_ONE // USB emulator
58
+ }
59
+
60
+ // Communication interfaces
61
+ public enum CommInterface {
62
+ USB,
63
+ NETWORK,
64
+ EMULATOR
65
+ }
24
66
 
25
- interface Listener {
67
+ // Listener interface for callbacks
68
+ public interface Listener {
26
69
  void onFrame(Bitmap bitmap);
27
-
28
70
  void onTemperature(double temp, int x, int y);
29
-
30
- void onDeviceFound(String name);
31
-
32
- void onEmulatorEnabled();
33
-
34
- void onStreamKindChanged(String kind);
71
+ void onDeviceFound(String deviceId, String deviceName, boolean isEmulator);
72
+ void onDeviceListUpdated(List<DeviceInfo> devices);
73
+ void onDeviceConnected(String deviceId, String deviceName, boolean isEmulator);
74
+ void onDeviceDisconnected();
75
+ void onDiscoveryStarted();
76
+ void onDiscoveryTimeout();
77
+ void onStreamStarted(String streamType);
78
+ void onError(String error);
79
+ }
80
+
81
+ // Device info class
82
+ public static class DeviceInfo {
83
+ public final String deviceId;
84
+ public final String deviceName;
85
+ public final boolean isEmulator;
86
+ public final CommInterface commInterface;
87
+ public final Identity identity;
88
+
89
+ DeviceInfo(String id, String name, boolean emu, CommInterface iface, Identity identity) {
90
+ this.deviceId = id;
91
+ this.deviceName = name;
92
+ this.isEmulator = emu;
93
+ this.commInterface = iface;
94
+ this.identity = identity;
95
+ }
35
96
  }
36
97
 
37
- private Listener listener;
38
-
98
+ private final Listener listener;
99
+ private final android.content.Context appContext;
100
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
101
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
102
+
39
103
  // SDK objects
40
- private Object discoveryFactory;
41
- private Object cameraObj;
42
- private Object streamerObj;
43
- private ClassLoader sdkClassLoader = null;
44
- private String sdkCurrentStreamKind = null;
45
-
46
- private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
104
+ private Camera camera = null;
105
+ private Stream currentStream = null;
106
+ private ThermalStreamer thermalStreamer = null;
107
+ private Palette currentPalette = null;
108
+
109
+ // State tracking
110
+ private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
111
+ private final AtomicBoolean isConnected = new AtomicBoolean(false);
112
+ private final AtomicBoolean isStreaming = new AtomicBoolean(false);
113
+ private final AtomicBoolean isEmulatorMode = new AtomicBoolean(false);
114
+ private final CopyOnWriteArrayList<DeviceInfo> discoveredDevices = new CopyOnWriteArrayList<>();
115
+ private ScheduledFuture<?> discoveryTimeoutFuture = null;
116
+ private DeviceInfo connectedDevice = null;
117
+ private EmulatorType emulatorType = EmulatorType.FLIR_ONE_EDGE;
118
+
119
+ // Frame state
47
120
  private volatile Bitmap latestFrame = null;
48
-
49
- FlirSdkManager(Listener listener) {
121
+ private volatile ThermalImage currentThermalImage = null;
122
+ private String currentStreamKind = null;
123
+
124
+ // Step tracking for debugging
125
+ private int stepCounter = 0;
126
+ private long flowStartTime = 0;
127
+
128
+ // SDK initialization state
129
+ private static boolean sdkInitialized = false;
130
+
131
+ FlirSdkManager(Listener listener, android.content.Context context) {
50
132
  this.listener = listener;
133
+ this.appContext = context != null ? context.getApplicationContext() : null;
51
134
  }
52
-
53
- boolean startSdkEmulator() {
54
- try {
55
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
56
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
57
- Log.d(TAG, "[FLIR SDK] Found EMULATOR interface: " + emulatorInterface);
58
-
59
- Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
60
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
61
- discoveryFactory = getInstance.invoke(null);
62
-
63
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
64
- Object discoveryListener = Proxy.newProxyInstance(
65
- listenerClass.getClassLoader(),
66
- new Class<?>[] { listenerClass },
67
- (proxy, method, args) -> {
68
- String methodName = method.getName();
69
- switch (methodName) {
70
- case "onCameraFound":
71
- Object discovered = args[0];
72
- Log.i(TAG, "[FLIR SDK] Camera found: " + discovered);
73
- connectToSdkCamera(discovered);
74
- return null;
75
-
76
- case "onCameraLost":
77
- Log.i(TAG, "[FLIR SDK] Camera lost");
78
- cameraObj = null;
79
- streamerObj = null;
80
- return null;
81
- default:
82
- return null;
83
- }
84
- });
85
-
86
- // Get NETWORK interface for FLIR ONE Edge
87
- Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
88
-
89
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
90
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
91
- // Scan for BOTH NETWORK (FLIR ONE Edge) and EMULATOR
92
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 2);
93
- java.lang.reflect.Array.set(ifaceArray, 0, networkInterface);
94
- java.lang.reflect.Array.set(ifaceArray, 1, emulatorInterface);
95
- scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
96
- Log.i(TAG, "[FLIR SDK] Started discovery scan for NETWORK and EMULATOR");
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");
97
153
  return true;
98
- } catch (ClassNotFoundException cnf) {
99
- Log.w(TAG, "[FLIR SDK] Classes not found: " + cnf.getMessage());
100
- // Try to load classes from an AAR (on-device) via DexClassLoader
101
- try {
102
- if (attemptLoadSdkFromAar()) {
103
- // If class is now available via sdkClassLoader retry using that loader
104
- Log.i(TAG,
105
- "[FLIR SDK] SDK classes found via DexClassLoader, retrying discovery using sdkClassLoader");
106
- try {
107
- return startSdkEmulatorWithClassLoader(sdkClassLoader);
108
- } catch (Throwable t) {
109
- Log.w(TAG, "[FLIR SDK] retry using sdkClassLoader failed: " + t.getMessage());
110
- }
111
- }
112
- } catch (Throwable t) {
113
- Log.w(TAG, "[FLIR SDK] DexClassLoader attempt failed: " + t.getMessage());
114
- }
154
+ }
155
+
156
+ if (appContext == null) {
157
+ Log.e(TAG, "[FLIR SDK] No context available");
115
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;
116
167
  } catch (Throwable t) {
117
- Log.w(TAG, "[FLIR SDK] startSdkEmulator failed: " + t.getMessage(), t);
168
+ Log.e(TAG, "[FLIR SDK] Initialization failed: " + t.getMessage(), t);
169
+ notifyError("SDK initialization failed: " + t.getMessage());
118
170
  return false;
119
171
  }
120
172
  }
121
-
122
- private String sdkJarPath = null;
123
-
124
- private boolean attemptLoadSdkFromAar() {
125
- // TODO: This is legacy code - context should be passed as parameter
126
- android.content.Context ctx = null; // ilabs.libs.io.data.Var.getAppContext();
173
+
174
+ // ==================== PUBLIC API ====================
175
+
176
+ public void setEmulatorType(EmulatorType type) {
177
+ this.emulatorType = type;
178
+ Log.i(TAG, "[FLIR] Emulator type set to: " + type);
179
+ logStep("SET_EMULATOR_TYPE", "type=" + type);
180
+ }
181
+
182
+ public void startDiscovery(boolean forceEmulator) {
183
+ resetFlowTracking();
184
+ logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
185
+ Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
186
+
187
+ // Disconnect current device first
188
+ if (isConnected.get()) {
189
+ logStep("DISCONNECT_PREVIOUS", "Disconnecting current device");
190
+ disconnect();
191
+ }
192
+
193
+ discoveredDevices.clear();
194
+ logStep("CLEAR_DEVICES", "Cleared discovered devices list");
195
+
196
+ if (forceEmulator) {
197
+ logStep("MODE_EMULATOR", "Forcing emulator mode");
198
+ isEmulatorMode.set(true);
199
+ startEmulatorDiscovery();
200
+ } else {
201
+ logStep("MODE_FULL_DISCOVERY", "Starting full discovery, timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
202
+ isEmulatorMode.set(false);
203
+ startFullDiscovery();
204
+ }
205
+ }
206
+
207
+ public void stopDiscovery() {
208
+ Log.i(TAG, "[FLIR] stopDiscovery()");
209
+ cancelDiscoveryTimeout();
210
+ isDiscovering.set(false);
211
+
127
212
  try {
128
- // android.content.Context ctx = ilabs.libs.io.data.Var.getAppContext();
129
- if (ctx == null) {
130
- Log.w(TAG, "[FLIR SDK] No application context available for SDK load");
131
- return false;
132
- }
133
- File filesDir = ctx.getFilesDir();
134
- // Candidate search locations (ordered by preference)
135
- java.util.List<File> candList = new java.util.ArrayList<>();
136
- // Add internal app filesDir candidates
137
- try {
138
- candList.add(new File(filesDir, "flir-sdk/thermalsdk-release.aar"));
139
- } catch (Throwable ignored) {
140
- }
141
- try {
142
- candList.add(new File(filesDir, "thermalsdk-release.aar"));
143
- } catch (Throwable ignored) {
144
- }
145
- try {
146
- candList.add(new File(filesDir, "thermalsdk.aar"));
147
- } catch (Throwable ignored) {
148
- }
149
- // Add external files dir candidates if available
150
- try {
151
- if (ctx.getExternalFilesDir(null) != null) {
152
- candList.add(new File(ctx.getExternalFilesDir(null), "thermalsdk-release.aar"));
153
- candList.add(new File(ctx.getExternalFilesDir(null), "thermalsdk.aar"));
154
- }
155
- } catch (Throwable ignored) {
156
- }
157
- // Add common sdcard and tmp locations
158
- try {
159
- candList.add(new File("/sdcard/Download/thermalsdk-release.aar"));
160
- } catch (Throwable ignored) {
161
- }
162
- try {
163
- candList.add(new File("/sdcard/thermalsdk-release.aar"));
164
- } catch (Throwable ignored) {
165
- }
166
- try {
167
- candList.add(new File("/sdcard/thermalsdk.aar"));
168
- } catch (Throwable ignored) {
169
- }
170
- try {
171
- candList.add(new File("/data/local/tmp/thermalsdk-release.aar"));
172
- } catch (Throwable ignored) {
213
+ DiscoveryFactory.getInstance().stop();
214
+ } catch (Throwable t) {
215
+ Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
216
+ }
217
+ }
218
+
219
+ public void connectToDevice(String deviceId) {
220
+ Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
221
+
222
+ DeviceInfo target = null;
223
+ for (DeviceInfo d : discoveredDevices) {
224
+ if (d.deviceId.equals(deviceId)) {
225
+ target = d;
226
+ break;
173
227
  }
228
+ }
229
+
230
+ if (target == null) {
231
+ notifyError("Device not found: " + deviceId);
232
+ return;
233
+ }
234
+
235
+ if (isConnected.get()) {
236
+ disconnect();
237
+ }
238
+
239
+ connectToIdentity(target);
240
+ }
241
+
242
+ public void disconnect() {
243
+ Log.i(TAG, "[FLIR] disconnect()");
244
+
245
+ stopStreaming();
246
+
247
+ if (camera != null) {
174
248
  try {
175
- candList.add(new File("/data/local/tmp/thermalsdk.aar"));
176
- } catch (Throwable ignored) {
177
- }
178
- File candidate = null;
179
- StringBuilder tried = new StringBuilder();
180
- Log.i(TAG,
181
- "[FLIR SDK] FilesDir=" + filesDir.getAbsolutePath() + ", ExternalFilesDir="
182
- + (ctx.getExternalFilesDir(null) != null ? ctx.getExternalFilesDir(null).getAbsolutePath()
183
- : "null"));
184
- for (File cand : candList) {
185
- try {
186
- if (cand == null) {
187
- tried.append("null,");
188
- continue;
189
- }
190
- boolean exists = false;
191
- try {
192
- exists = cand.exists();
193
- } catch (Throwable ignored) {
194
- }
195
- tried.append(cand.getAbsolutePath()).append(exists ? "(exists)," : "(no),");
196
- if (exists) {
197
- candidate = cand;
198
- break;
199
- }
200
- } catch (Throwable ignored) {
201
- tried.append("err,");
202
- }
203
- }
204
- if (candidate == null || !candidate.exists()) {
205
- Log.w(TAG, "[FLIR SDK] No SDK AAR found. Tried: " + tried.toString());
206
- return false;
249
+ camera.disconnect();
250
+ } catch (Throwable t) {
251
+ Log.w(TAG, "[FLIR] Camera disconnect failed: " + t.getMessage());
207
252
  }
208
- Log.i(TAG, "[FLIR SDK] Found SDK AAR: " + candidate.getAbsolutePath());
209
- Log.i(TAG, "[FLIR SDK] Candidate list tried: " + tried.toString());
210
- ZipFile zf = new ZipFile(candidate);
211
- ZipEntry classesEntry = zf.getEntry("classes.jar");
212
- if (classesEntry == null) {
213
- Log.w(TAG, "[FLIR SDK] classes.jar not found in AAR");
214
- zf.close();
215
- return false;
253
+ camera = null;
254
+ }
255
+
256
+ isConnected.set(false);
257
+ connectedDevice = null;
258
+
259
+ mainHandler.post(() -> {
260
+ if (listener != null) {
261
+ listener.onDeviceDisconnected();
216
262
  }
217
- File outJar = new File(filesDir, "flir-classes.jar");
218
- FileOutputStream fos = new FileOutputStream(outJar);
219
- java.io.InputStream is = zf.getInputStream(classesEntry);
220
- byte[] buf = new byte[8192];
221
- int r;
222
- while ((r = is.read(buf)) != -1)
223
- fos.write(buf, 0, r);
224
- is.close();
225
- fos.close();
226
- zf.close();
227
- File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
228
- DexClassLoader dcl = new DexClassLoader(outJar.getAbsolutePath(), dexOutDir.getAbsolutePath(), null,
229
- ctx.getClassLoader());
230
- // verify class present
231
- Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
232
- if (test != null) {
233
- sdkClassLoader = dcl;
234
- Log.i(TAG, "[FLIR SDK] DexClassLoader created and class loaded from: " + outJar.getAbsolutePath());
235
- try {
236
- Log.i(TAG, "[FLIR SDK] DexClassLoader parent loader=" + dcl.getParent() + " (loader=" + dcl + ")");
237
- } catch (Throwable ignored) {
263
+ });
264
+ }
265
+
266
+ public void setStreamType(String streamType) {
267
+ Log.i(TAG, "[FLIR] setStreamType: " + streamType);
268
+ currentStreamKind = streamType;
269
+
270
+ if (isConnected.get() && camera != null) {
271
+ stopStreaming();
272
+ startStreaming();
273
+ }
274
+ }
275
+
276
+ public void setPalette(String paletteName) {
277
+ Log.i(TAG, "[FLIR] setPalette: " + paletteName);
278
+
279
+ try {
280
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
281
+ for (Palette p : palettes) {
282
+ if (p.name.equalsIgnoreCase(paletteName)) {
283
+ currentPalette = p;
284
+ Log.i(TAG, "[FLIR] Palette set to: " + p.name);
285
+ return;
238
286
  }
239
- sdkJarPath = outJar.getAbsolutePath();
240
- return true;
241
287
  }
288
+ Log.w(TAG, "[FLIR] Palette not found: " + paletteName);
242
289
  } catch (Throwable t) {
243
- Log.w(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
244
- try {
245
- Log.i(TAG, "[FLIR SDK] Candidate list tried on failure: ");
246
- } catch (Throwable ignored) {
247
- }
290
+ Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
248
291
  }
249
- return false;
250
292
  }
251
-
252
- public boolean isSdkLoaded() {
253
- return sdkClassLoader != null;
254
- }
255
-
256
- public String getLoadedSdkJarPath() {
257
- return sdkJarPath;
258
- }
259
-
260
- private Class<?> findSdkClass(String name) throws ClassNotFoundException {
261
- try {
262
- return Class.forName(name);
263
- } catch (ClassNotFoundException e) {
264
- if (sdkClassLoader != null)
265
- return Class.forName(name, true, sdkClassLoader);
266
- throw e;
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));
267
299
  }
268
300
  }
269
-
270
- private boolean startSdkEmulatorWithClassLoader(ClassLoader loader) {
301
+
302
+ /**
303
+ * Get temperature at a specific point from the current thermal image
304
+ */
305
+ public double getTemperatureAtPoint(int x, int y) {
306
+ if (currentThermalImage == null) {
307
+ return Double.NaN;
308
+ }
309
+
271
310
  try {
272
- Class<?> commIfaceClass = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, loader);
273
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
274
- Log.d(TAG, "[FLIR SDK] (DEX) CommunicationInterface found via loader: " + loader);
275
- Log.d(TAG, "[FLIR SDK] Found EMULATOR interface (DEX): " + emulatorInterface);
276
-
277
- Class<?> discoveryFactoryClass = Class.forName("com.flir.thermalsdk.live.discovery.DiscoveryFactory", true,
278
- loader);
279
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
280
- discoveryFactory = getInstance.invoke(null);
281
- Log.d(TAG, "[FLIR SDK] (DEX) DiscoveryFactory.getInstance() returned: " + (discoveryFactory != null));
282
-
283
- Class<?> listenerClass = Class.forName("com.flir.thermalsdk.live.discovery.DiscoveryEventListener", true,
284
- loader);
285
- Object discoveryListener = Proxy.newProxyInstance(
286
- loader,
287
- new Class<?>[] { listenerClass },
288
- (proxy, method, args) -> {
289
- String methodName = method.getName();
290
- switch (methodName) {
291
- case "onCameraFound":
292
- Object discovered = args[0];
293
- Log.i(TAG, "[FLIR SDK] (DEX) Camera found: " + discovered);
294
- connectToSdkCamera(discovered);
295
- return null;
296
- case "onCameraLost":
297
- Log.i(TAG, "[FLIR SDK] (DEX) Camera lost");
298
- cameraObj = null;
299
- streamerObj = null;
300
- return null;
301
- default:
302
- return null;
303
- }
304
- });
305
-
306
- // Get NETWORK interface for FLIR ONE Edge
307
- Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
308
-
309
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
310
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
311
- // Scan for BOTH NETWORK (FLIR ONE Edge) and EMULATOR
312
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 2);
313
- java.lang.reflect.Array.set(ifaceArray, 0, networkInterface);
314
- java.lang.reflect.Array.set(ifaceArray, 1, emulatorInterface);
315
- scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
316
- Log.i(TAG, "[FLIR SDK] (DEX) scan invoked on DiscoveryFactory");
317
- Log.i(TAG, "[FLIR SDK] (DEX) Started discovery scan for NETWORK and EMULATOR");
318
- return true;
311
+ // Clamp coordinates to image bounds
312
+ int imgWidth = currentThermalImage.getWidth();
313
+ int imgHeight = currentThermalImage.getHeight();
314
+ int clampedX = Math.max(0, Math.min(x, imgWidth - 1));
315
+ int clampedY = Math.max(0, Math.min(y, imgHeight - 1));
316
+
317
+ ThermalValue value = currentThermalImage.getValueAt(new Point(clampedX, clampedY));
318
+ if (value != null) {
319
+ return value.asCelsius().value;
320
+ }
319
321
  } catch (Throwable t) {
320
- Log.w(TAG, "[FLIR SDK] startSdkEmulatorWithClassLoader failed: " + t.getMessage());
321
- return false;
322
+ Log.w(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
322
323
  }
324
+ return Double.NaN;
323
325
  }
324
-
325
- private void connectToSdkCamera(Object discoveredCamera) {
326
+
327
+ public Bitmap getLatestFrame() {
328
+ return latestFrame;
329
+ }
330
+
331
+ public List<DeviceInfo> getDiscoveredDevices() {
332
+ return new ArrayList<>(discoveredDevices);
333
+ }
334
+
335
+ public boolean isConnected() {
336
+ return isConnected.get();
337
+ }
338
+
339
+ public boolean isStreaming() {
340
+ return isStreaming.get();
341
+ }
342
+
343
+ public void destroy() {
344
+ Log.i(TAG, "[FLIR] destroy()");
345
+ stopDiscovery();
346
+ disconnect();
347
+ scheduler.shutdown();
348
+ }
349
+
350
+ // ==================== DISCOVERY ====================
351
+
352
+ private void startFullDiscovery() {
353
+ Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
354
+
355
+ if (!initializeSdk()) {
356
+ Log.w(TAG, "[FLIR] SDK not available, falling back to emulator");
357
+ startEmulatorDiscovery();
358
+ return;
359
+ }
360
+
361
+ isDiscovering.set(true);
362
+ mainHandler.post(() -> {
363
+ if (listener != null) {
364
+ listener.onDiscoveryStarted();
365
+ }
366
+ });
367
+
326
368
  try {
327
- Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
328
- Object identity = getIdentity.invoke(discoveredCamera);
329
-
330
- Class<?> cameraClass = findSdkClass("com.flir.thermalsdk.live.Camera");
331
- cameraObj = cameraClass.newInstance();
332
-
333
- Class<?> connStatusClass = findSdkClass("com.flir.thermalsdk.live.connectivity.ConnectionStatusListener");
334
- Object connListener = Proxy.newProxyInstance(connStatusClass.getClassLoader(),
335
- new Class<?>[] { connStatusClass }, (proxy, method, args) -> {
336
- if (method.getName().equals("onDisconnected")) {
337
- Log.w(TAG, "[FLIR SDK] Camera disconnected");
338
- cameraObj = null;
339
- streamerObj = null;
340
- }
341
- return null;
342
- });
343
-
344
- // Try various connect overloads to be compatible across SDK versions
345
- boolean connected = false;
346
- try {
347
- Class<?> connectParamsClass = findSdkClass("com.flir.thermalsdk.live.ConnectParameters");
348
- Object connectParams = null;
349
- try {
350
- connectParams = connectParamsClass.newInstance();
351
- } catch (Throwable ignored) {
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);
352
379
  }
353
- Method connectMethod3 = cameraClass.getMethod("connect", identity.getClass(), connStatusClass,
354
- connectParamsClass);
355
- connectMethod3.invoke(cameraObj, identity, connListener, connectParams);
356
- connected = true;
357
- } catch (Throwable ignored) {
358
- try {
359
- Method connectMethod2 = cameraClass.getMethod("connect", identity.getClass(), connStatusClass);
360
- connectMethod2.invoke(cameraObj, identity, connListener);
361
- connected = true;
362
- } catch (Throwable ignored2) {
363
- try {
364
- Method connectSimple = cameraClass.getMethod("connect", identity.getClass());
365
- connectSimple.invoke(cameraObj, identity);
366
- connected = true;
367
- } catch (Throwable ignored3) {
368
- Log.w(TAG, "connect: All connect attempts failed: " + ignored3.getMessage());
369
- }
380
+
381
+ @Override
382
+ public void onDiscoveryError(CommunicationInterface iface, ErrorCode errorCode) {
383
+ Log.w(TAG, "[FLIR] Discovery error on " + iface + ": " + errorCode);
370
384
  }
371
- }
372
-
373
- String deviceName = "EMULATOR";
374
- try {
375
- Method getName = identity.getClass().getMethod("getName");
376
- Object nm = getName.invoke(identity);
377
- if (nm != null)
378
- deviceName = nm.toString();
379
- } catch (Throwable ignored) {
380
- }
381
-
382
- if (listener != null) {
383
- listener.onDeviceFound(deviceName);
384
- listener.onEmulatorEnabled();
385
- }
386
- Log.d(TAG, "[FLIR SDK] Scheduling startSdkStreaming; cameraObj=" + cameraObj + ", identity=" + identity
387
- + ", connected=" + connected);
388
- scheduler.schedule(this::startSdkStreaming, 500, TimeUnit.MILLISECONDS);
385
+ }, interfaces);
386
+
387
+ // Set timeout for device discovery
388
+ scheduleDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
389
+
389
390
  } catch (Throwable t) {
390
- Log.w(TAG, "connectToSdkCamera failed: " + t.getMessage(), t);
391
+ Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
392
+ notifyError("Discovery failed: " + t.getMessage());
393
+ startEmulatorDiscovery();
391
394
  }
392
395
  }
393
-
394
- private void startSdkStreaming() {
396
+
397
+ private void startEmulatorDiscovery() {
398
+ logStep("EMULATOR_DISCOVERY_START", "type=" + emulatorType);
399
+ Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
400
+
401
+ if (!initializeSdk()) {
402
+ notifyError("SDK initialization failed");
403
+ return;
404
+ }
405
+
406
+ isDiscovering.set(true);
407
+ isEmulatorMode.set(true);
408
+
409
+ mainHandler.post(() -> {
410
+ if (listener != null) {
411
+ listener.onDiscoveryStarted();
412
+ }
413
+ });
414
+
395
415
  try {
396
- Log.d(TAG, "[FLIR SDK] startSdkStreaming invoked; cameraObj=" + cameraObj + ", streamerObj=" + streamerObj);
397
- if (cameraObj == null)
398
- return;
399
- Method getStreams = cameraObj.getClass().getMethod("getStreams");
400
- Object streams = getStreams.invoke(cameraObj);
401
- if (streams == null)
402
- Log.i(TAG, "[FLIR SDK] No streams returned from camera.getStreams()");
403
- Object chosenStream = null;
404
- int streamCount = 0;
405
- if (streams instanceof List) {
406
- List<?> streamList = (List<?>) streams;
407
- streamCount = streamList.size();
408
- Log.i(TAG, "[FLIR SDK] getStreams returned list with size=" + streamList.size());
409
- for (Object s : streamList) {
410
- if (s == null)
411
- continue;
412
- try {
413
- Method getName = null;
414
- try {
415
- getName = s.getClass().getMethod("getName");
416
- } catch (Throwable ignored) {
417
- }
418
- String name = null;
419
- if (getName != null)
420
- name = getName.invoke(s).toString().toLowerCase();
421
-
422
- Method getType = null;
423
- try {
424
- getType = s.getClass().getMethod("getStreamType");
425
- } catch (Throwable ignored) {
426
- }
427
- if (getType == null)
428
- try {
429
- getType = s.getClass().getMethod("getType");
430
- } catch (Throwable ignored) {
431
- }
432
- String type = null;
433
- if (getType != null)
434
- type = getType.invoke(s).toString().toLowerCase();
435
-
436
- String test = (name != null ? name : "") + "|" + (type != null ? type : "");
437
- Log.d(TAG, "[FLIR SDK] stream candidate: name='" + name + "', type='" + type + "', test='"
438
- + test + "'");
439
- if (test.contains("fusion") || test.contains("palette") || test.contains("visual")
440
- || test.contains("msx")) {
441
- chosenStream = s;
442
- break;
443
- }
444
- } catch (Throwable ignored) {
445
- }
416
+ DiscoveryFactory.getInstance().scan(new DiscoveryEventListener() {
417
+ @Override
418
+ public void onCameraFound(DiscoveredCamera discoveredCamera) {
419
+ handleCameraFound(discoveredCamera);
446
420
  }
447
- if (chosenStream == null && !streamList.isEmpty())
448
- chosenStream = streamList.get(0);
421
+
422
+ @Override
423
+ public void onDiscoveryError(CommunicationInterface iface, ErrorCode errorCode) {
424
+ Log.w(TAG, "[FLIR] Emulator discovery error: " + errorCode);
425
+ }
426
+ }, CommunicationInterface.EMULATOR);
427
+
428
+ // Short timeout for emulator
429
+ scheduleDiscoveryTimeout(2000);
430
+
431
+ } catch (Throwable t) {
432
+ Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
433
+ notifyError("Emulator discovery failed: " + t.getMessage());
434
+ }
435
+ }
436
+
437
+ private void handleCameraFound(DiscoveredCamera discoveredCamera) {
438
+ Identity identity = discoveredCamera.getIdentity();
439
+ String deviceId = identity.deviceId;
440
+ String deviceName = identity.toString();
441
+ CommunicationInterface iface = identity.communicationInterface;
442
+ boolean isEmulator = (iface == CommunicationInterface.EMULATOR);
443
+
444
+ Log.i(TAG, "[FLIR] Camera found: " + deviceName + " (" + iface + ")");
445
+ logStep("DEVICE_FOUND", "id=" + deviceId + ", name=" + deviceName + ", interface=" + iface);
446
+
447
+ CommInterface commIface;
448
+ switch (iface) {
449
+ case USB:
450
+ commIface = CommInterface.USB;
451
+ break;
452
+ case NETWORK:
453
+ commIface = CommInterface.NETWORK;
454
+ break;
455
+ default:
456
+ commIface = CommInterface.EMULATOR;
457
+ }
458
+
459
+ DeviceInfo deviceInfo = new DeviceInfo(deviceId, deviceName, isEmulator, commIface, identity);
460
+
461
+ // Avoid duplicates
462
+ boolean exists = false;
463
+ for (DeviceInfo d : discoveredDevices) {
464
+ if (d.deviceId.equals(deviceId)) {
465
+ exists = true;
466
+ break;
449
467
  }
450
- if (chosenStream == null)
451
- Log.w(TAG, "[FLIR SDK] No chosen stream found; streamCount=" + streamCount);
452
- if (chosenStream == null)
453
- return;
454
-
455
- Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
456
- java.lang.reflect.Constructor<?> ctor = thermalStreamerClass
457
- .getConstructor(findSdkClass("com.flir.thermalsdk.live.streaming.Stream"));
458
- streamerObj = ctor.newInstance(chosenStream);
459
- Log.i(TAG, "[FLIR SDK] ThermalStreamer instance created: "
460
- + (streamerObj != null ? streamerObj.getClass().getName() : "null"));
461
-
462
- boolean listenerAttached = attachStreamerListener(streamerObj);
463
- if (!listenerAttached)
464
- startFramePolling();
465
- Log.i(TAG, "[FLIR SDK] startSdkStreaming: listenerAttached=" + listenerAttached + ", streamerObj="
466
- + (streamerObj != null));
467
-
468
- // Notify stream kind change
469
- try {
470
- Method getName = chosenStream.getClass().getMethod("getName");
471
- Object nm = getName.invoke(chosenStream);
472
- if (nm != null && listener != null)
473
- listener.onStreamKindChanged(nm.toString());
474
- try {
475
- sdkCurrentStreamKind = nm != null ? nm.toString() : null;
476
- } catch (Throwable ignored) {
468
+ }
469
+
470
+ if (!exists) {
471
+ discoveredDevices.add(deviceInfo);
472
+
473
+ mainHandler.post(() -> {
474
+ if (listener != null) {
475
+ listener.onDeviceFound(deviceId, deviceName, isEmulator);
476
+ listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
477
477
  }
478
- } catch (Throwable ignored) {
478
+ });
479
+
480
+ // Auto-connect to first device found
481
+ if (discoveredDevices.size() == 1 && !isConnected.get()) {
482
+ cancelDiscoveryTimeout();
483
+ logStep("AUTO_CONNECT", "Connecting to first found device");
484
+ connectToIdentity(deviceInfo);
479
485
  }
480
-
481
- } catch (Throwable t) {
482
- Log.w(TAG, "startSdkStreaming failed: " + t.getMessage(), t);
483
486
  }
484
487
  }
485
-
486
- public boolean forceStartStreaming() {
487
- try {
488
- if (cameraObj == null) {
489
- Log.i(TAG, "[FLIR SDK] forceStartStreaming: No cameraObj, attempting discovery scan");
490
- try {
491
- // Re-run discovery to find cameras
492
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
493
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
494
- Class<?> discoveryFactoryClass = findSdkClass(
495
- "com.flir.thermalsdk.live.discovery.DiscoveryFactory");
496
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
497
- Object df = getInstance.invoke(null);
498
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
499
- Object discoveryListener = Proxy.newProxyInstance(
500
- sdkClassLoader != null ? sdkClassLoader : discoveryFactoryClass.getClassLoader(),
501
- new Class<?>[] { listenerClass },
502
- (proxy, method, args) -> {
503
- String methodName = method.getName();
504
- if ("onCameraFound".equalsIgnoreCase(methodName) && args != null && args.length > 0) {
505
- try {
506
- Object discovered = args[0];
507
- Log.i(TAG, "[FLIR SDK] forceStartStreaming: discovered camera: " + discovered);
508
- connectToSdkCamera(discovered);
509
- } catch (Throwable t) {
510
- Log.w(TAG, "[FLIR SDK] forceStartStreaming onCameraFound handler error: "
511
- + t.getMessage());
512
- }
513
- }
514
- return null;
515
- });
516
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
517
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
518
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 1);
519
- java.lang.reflect.Array.set(ifaceArray, 0, emulatorInterface);
520
- scanMethod.invoke(df, discoveryListener, ifaceArray);
521
- } catch (Throwable t) {
522
- Log.w(TAG, "[FLIR SDK] forceStartStreaming discovery scan failed: " + t.getMessage());
488
+
489
+ private void scheduleDiscoveryTimeout(long timeoutMs) {
490
+ cancelDiscoveryTimeout();
491
+
492
+ discoveryTimeoutFuture = scheduler.schedule(() -> {
493
+ Log.i(TAG, "[FLIR] Discovery timeout after " + timeoutMs + "ms");
494
+ logStep("DISCOVERY_TIMEOUT", "timeout=" + timeoutMs + "ms, devicesFound=" + discoveredDevices.size());
495
+
496
+ isDiscovering.set(false);
497
+ stopDiscovery();
498
+
499
+ mainHandler.post(() -> {
500
+ if (listener != null) {
501
+ listener.onDiscoveryTimeout();
523
502
  }
524
- // if cameraObj still null - return false
525
- if (cameraObj == null)
526
- return false;
503
+ });
504
+
505
+ // If no devices found, try emulator
506
+ if (discoveredDevices.isEmpty() && !isEmulatorMode.get()) {
507
+ logStep("FALLBACK_EMULATOR", "No devices found, trying emulator");
508
+ startEmulatorDiscovery();
527
509
  }
528
- // If cameraObj present, start streaming
529
- scheduler.submit(() -> startSdkStreaming());
530
- Log.i(TAG, "[FLIR SDK] forceStartStreaming scheduled startSdkStreaming");
531
- return true;
532
- } catch (Throwable t) {
533
- Log.w(TAG, "[FLIR SDK] forceStartStreaming failed: " + t.getMessage(), t);
534
- return false;
510
+ }, timeoutMs, TimeUnit.MILLISECONDS);
511
+ }
512
+
513
+ private void cancelDiscoveryTimeout() {
514
+ if (discoveryTimeoutFuture != null && !discoveryTimeoutFuture.isDone()) {
515
+ discoveryTimeoutFuture.cancel(false);
516
+ discoveryTimeoutFuture = null;
535
517
  }
536
518
  }
537
-
538
- private void startFramePolling() {
539
- scheduler.scheduleAtFixedRate(() -> {
519
+
520
+ // ==================== CONNECTION ====================
521
+
522
+ private void connectToIdentity(DeviceInfo device) {
523
+ logStep("CONNECT_START", "device=" + device.deviceName);
524
+ Log.i(TAG, "[FLIR] Connecting to: " + device.deviceName);
525
+
526
+ scheduler.execute(() -> {
540
527
  try {
541
- if (streamerObj == null)
542
- return;
543
- Log.d(TAG, "[FLIR SDK] startFramePolling - streamerObj class=" + streamerObj.getClass().getName());
544
- Method updateMethod = streamerObj.getClass().getMethod("update");
545
- updateMethod.invoke(streamerObj);
546
-
547
- Method getImage = streamerObj.getClass().getMethod("getImage");
548
- Object thermalImage = getImage.invoke(streamerObj);
549
- if (thermalImage != null) {
550
- try {
551
- // Prefer: direct getBitmap() if available
552
- try {
553
- Method getBitmap = thermalImage.getClass().getMethod("getBitmap");
554
- Object bmp = getBitmap.invoke(thermalImage);
555
- if (bmp instanceof Bitmap) {
556
- latestFrame = (Bitmap) bmp;
528
+ camera = new Camera();
529
+
530
+ // Connect using Identity, error callback, and ConnectParameters
531
+ // Note: connect is blocking, so we're on a background thread
532
+ camera.connect(
533
+ device.identity,
534
+ new ConnectionStatusListener() {
535
+ @Override
536
+ public void onDisconnected(ErrorCode errorCode) {
537
+ Log.i(TAG, "[FLIR] Disconnected: " + errorCode);
538
+ logStep("DISCONNECTED", "reason=" + errorCode);
539
+
540
+ isConnected.set(false);
541
+ connectedDevice = null;
542
+
543
+ mainHandler.post(() -> {
557
544
  if (listener != null) {
558
- listener.onFrame((Bitmap) bmp);
559
- Log.d(TAG, "[FLIR SDK] Frame emitted from polling: " + ((Bitmap) bmp).getWidth()
560
- + "x" + ((Bitmap) bmp).getHeight());
545
+ listener.onDeviceDisconnected();
561
546
  }
562
- return;
563
- }
564
- } catch (Throwable ignored) {
565
- }
566
-
567
- // Fallback: try using
568
- // BitmapAndroid.createBitmap(thermalImage.getImage()).getBitMap()
569
- try {
570
- Object imageBase = null;
571
- try {
572
- Method getImageFromThermal = thermalImage.getClass().getMethod("getImage");
573
- imageBase = getImageFromThermal.invoke(thermalImage);
574
- } catch (Throwable ignored) {
575
- }
576
- if (imageBase == null)
577
- imageBase = thermalImage;
578
- Class<?> bitmapAndroidClass = findSdkClass(
579
- "com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
580
- Method createBitmap = bitmapAndroidClass.getMethod("createBitmap", imageBase.getClass());
581
- Object wrapper = createBitmap.invoke(null, imageBase);
582
- if (wrapper != null) {
583
- Method getBitMapMethod = wrapper.getClass().getMethod("getBitMap");
584
- Object bmp = getBitMapMethod.invoke(wrapper);
585
- if (bmp instanceof Bitmap) {
586
- latestFrame = (Bitmap) bmp;
587
- if (listener != null)
588
- listener.onFrame((Bitmap) bmp);
589
- }
590
- }
591
- } catch (Throwable ignored) {
547
+ });
592
548
  }
593
- } catch (Throwable ignored) {
549
+ },
550
+ new ConnectParameters()
551
+ );
552
+
553
+ // If we get here, connection succeeded
554
+ Log.i(TAG, "[FLIR] Connected to: " + device.deviceName);
555
+ logStep("CONNECTED", "device=" + device.deviceName);
556
+
557
+ isConnected.set(true);
558
+ connectedDevice = device;
559
+
560
+ mainHandler.post(() -> {
561
+ if (listener != null) {
562
+ listener.onDeviceConnected(device.deviceId, device.deviceName, device.isEmulator);
594
563
  }
595
- }
564
+ });
565
+
566
+ // Start streaming automatically
567
+ startStreaming();
568
+
596
569
  } catch (Throwable t) {
597
- if (System.currentTimeMillis() % 5000 < 200)
598
- Log.d(TAG, "Frame poll error: " + 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());
599
574
  }
600
- }, 100, 100, TimeUnit.MILLISECONDS);
575
+ });
601
576
  }
602
-
603
- Bitmap getLatestFrame() {
604
- return latestFrame;
605
- }
606
-
607
- double getTemperatureAtPoint(int x, int y) {
608
- // Try to query using SDK streamer if available
609
- try {
610
- if (streamerObj != null) {
611
- Method getImage = streamerObj.getClass().getMethod("getImage");
612
- Object thermalImage = getImage.invoke(streamerObj);
613
- if (thermalImage != null) {
614
- try {
615
- Method getValueAt = thermalImage.getClass().getMethod("getValueAt",
616
- android.graphics.Point.class);
617
- android.graphics.Point p = new android.graphics.Point(x, y);
618
- Object temp = getValueAt.invoke(thermalImage, p);
619
- if (temp instanceof Double)
620
- return (Double) temp;
621
- if (temp instanceof Float)
622
- return ((Float) temp).doubleValue();
623
- } catch (NoSuchMethodException nsme) {
624
- }
625
- try {
626
- Method getValues = thermalImage.getClass().getMethod("getValues");
627
- Object values = getValues.invoke(thermalImage);
628
- if (values != null) {
629
- Method valGetAt = values.getClass().getMethod("getValueAt", int.class, int.class);
630
- Object temp = valGetAt.invoke(values, x, y);
631
- if (temp instanceof Double)
632
- return (Double) temp;
633
- }
634
- } catch (Throwable ignored) {
635
- }
636
- }
637
- }
638
- } catch (Throwable t) {
577
+
578
+ // ==================== STREAMING ====================
579
+
580
+ private void startStreaming() {
581
+ if (camera == null || !isConnected.get()) {
582
+ Log.w(TAG, "[FLIR] Cannot start streaming - not connected");
583
+ return;
639
584
  }
640
- return Double.NaN;
641
- }
642
-
643
- boolean setPalette(String paletteName) {
644
- try {
645
- if (streamerObj == null && cameraObj == null)
646
- return false;
647
- Object thermalImage = null;
648
- if (streamerObj != null) {
649
- Method getImage = streamerObj.getClass().getMethod("getImage");
650
- thermalImage = getImage.invoke(streamerObj);
651
- }
652
- if (thermalImage != null) {
653
- Class<?> paletteManagerClass = null;
654
- try {
655
- paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.palettes.PaletteManager");
656
- } catch (Throwable ignored) {
657
- try {
658
- paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
659
- } catch (Throwable ignored2) {
585
+
586
+ logStep("STREAM_START", "streamType=" + currentStreamKind);
587
+ Log.i(TAG, "[FLIR] Starting streaming...");
588
+
589
+ scheduler.execute(() -> {
590
+ try {
591
+ // Get available streams
592
+ List<Stream> streams = camera.getStreams();
593
+ if (streams == null || streams.isEmpty()) {
594
+ Log.e(TAG, "[FLIR] No streams available");
595
+ notifyError("No streams available");
596
+ return;
597
+ }
598
+
599
+ Log.i(TAG, "[FLIR] Available streams: " + streams.size());
600
+
601
+ // Select thermal stream (prefer thermal, fallback to first)
602
+ Stream thermalStream = null;
603
+ for (Stream s : streams) {
604
+ if (s.isThermal()) {
605
+ thermalStream = s;
606
+ break;
660
607
  }
661
608
  }
662
- if (paletteManagerClass == null)
663
- return false;
664
- Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
665
- Object palettesResult = getDefaultPalettes.invoke(null);
666
- Object target = null;
667
- // Handle array or List return types
668
- if (palettesResult instanceof Object[]) {
669
- for (Object p : (Object[]) palettesResult) {
670
- try {
671
- Method getName = p.getClass().getMethod("getName");
672
- String n = (String) getName.invoke(p);
673
- if (n != null && n.equalsIgnoreCase(paletteName)) {
674
- target = p;
609
+ currentStream = thermalStream != null ? thermalStream : streams.get(0);
610
+
611
+ // Create ThermalStreamer for rendering
612
+ thermalStreamer = new ThermalStreamer(currentStream);
613
+
614
+ // Set default palette if available
615
+ if (currentPalette == null) {
616
+ try {
617
+ List<Palette> palettes = PaletteManager.getDefaultPalettes();
618
+ for (Palette p : palettes) {
619
+ if (p.name.toLowerCase().contains("iron")) {
620
+ currentPalette = p;
675
621
  break;
676
622
  }
677
- } catch (Throwable ignored) {
678
623
  }
624
+ } catch (Throwable t) {
625
+ Log.w(TAG, "[FLIR] Failed to get default palette: " + t.getMessage());
679
626
  }
680
- } else if (palettesResult instanceof java.util.List) {
681
- for (Object p : (java.util.List) palettesResult) {
682
- try {
683
- Method getName = p.getClass().getMethod("getName");
684
- String n = (String) getName.invoke(p);
685
- if (n != null && n.equalsIgnoreCase(paletteName)) {
686
- target = p;
687
- break;
688
- }
689
- } catch (Throwable ignored) {
627
+ }
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());
690
636
  }
691
- }
692
- } else if (palettesResult != null && palettesResult.getClass().isArray()) {
693
- int len = java.lang.reflect.Array.getLength(palettesResult);
694
- for (int i = 0; i < len; i++) {
695
- Object p = java.lang.reflect.Array.get(palettesResult, i);
696
- try {
697
- Method getName = p.getClass().getMethod("getName");
698
- String n = (String) getName.invoke(p);
699
- if (n != null && n.equalsIgnoreCase(paletteName)) {
700
- target = p;
701
- break;
702
- }
703
- } catch (Throwable ignored) {
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);
704
643
  }
705
644
  }
706
- }
707
- if (target != null) {
708
- Class<?> paletteClass = null;
709
- try {
710
- paletteClass = findSdkClass("com.flir.thermalsdk.image.palettes.Palette");
711
- } catch (Throwable ignored) {
712
- try {
713
- paletteClass = findSdkClass("com.flir.thermalsdk.image.Palette");
714
- } catch (Throwable ignored2) {
715
- }
645
+ );
646
+
647
+ isStreaming.set(true);
648
+ Log.i(TAG, "[FLIR] Stream started");
649
+ logStep("STREAM_STARTED", "stream=" + currentStream);
650
+
651
+ mainHandler.post(() -> {
652
+ if (listener != null) {
653
+ listener.onStreamStarted("thermal");
716
654
  }
717
- if (paletteClass == null)
718
- return false;
719
- Method setPalette = thermalImage.getClass().getMethod("setPalette", paletteClass);
720
- setPalette.invoke(thermalImage, target);
721
- return true;
722
- }
655
+ });
656
+
657
+ } catch (Throwable t) {
658
+ Log.e(TAG, "[FLIR] Start stream error: " + t.getMessage(), t);
659
+ notifyError("Stream error: " + t.getMessage());
723
660
  }
724
- } catch (Throwable t) {
725
- Log.w(TAG, "setPalette failed: " + t.getMessage());
726
- }
727
- return false;
728
- }
729
-
730
- public boolean isStreamingActive() {
731
- return streamerObj != null;
732
- }
733
-
734
- public String getSdkCurrentStreamKind() {
735
- return sdkCurrentStreamKind;
661
+ });
736
662
  }
737
-
738
- private boolean attachStreamerListener(Object streamer) {
739
- if (streamer == null)
740
- return false;
741
- try {
742
- Class<?> streamerClass = streamer.getClass();
743
- String[] candidates = new String[] { "setFrameListener", "setListener", "addListener", "addFrameListener",
744
- "addStreamListener", "setOnImageAvailableListener", "addImageListener", "setDataListener",
745
- "subscribe" };
746
- for (String name : candidates) {
747
- try {
748
- for (Method m : streamerClass.getMethods()) {
749
- if (!m.getName().equalsIgnoreCase(name))
750
- continue;
751
- Class<?>[] params = m.getParameterTypes();
752
- if (params.length != 1 || !params[0].isInterface())
753
- continue;
754
- final Class<?> listenerInterface = params[0];
755
- Object proxy = Proxy.newProxyInstance(listenerInterface.getClassLoader(),
756
- new Class<?>[] { listenerInterface }, (proxyObj, method, args) -> {
757
- try {
758
- Log.d(TAG, "[FLIR SDK] Stream listener method invoked: " + method.getName());
759
- if (args != null && args.length > 0) {
760
- for (Object arg : args) {
761
- if (arg == null)
762
- continue;
763
- try {
764
- Method getFusion = arg.getClass().getMethod("getFusion");
765
- if (getFusion != null) {
766
- Object fusion = getFusion.invoke(arg);
767
- if (fusion != null) {
768
- try {
769
- Method getPhoto = fusion.getClass()
770
- .getMethod("getPhoto");
771
- Object photo = getPhoto.invoke(fusion);
772
- if (photo != null) {
773
- try {
774
- Method pb = photo.getClass()
775
- .getMethod("getBitmap");
776
- Object bmpObj = pb.invoke(photo);
777
- if (bmpObj instanceof Bitmap) {
778
- if (listener != null) {
779
- listener.onFrame((Bitmap) bmpObj);
780
- Log.d(TAG,
781
- "[FLIR SDK] Streamer listener delivered bitmap via getBitmap");
782
- }
783
- return null;
784
- }
785
- } catch (Throwable ignored) {
786
- }
787
- }
788
- } catch (Throwable ignored) {
789
- }
790
- }
791
- }
792
- } catch (Throwable ignored) {
793
- }
794
- try {
795
- Method getBitmap = arg.getClass().getMethod("getBitmap");
796
- Object bmp = getBitmap.invoke(arg);
797
- if (bmp instanceof Bitmap) {
798
- if (listener != null) {
799
- listener.onFrame((Bitmap) bmp);
800
- Log.d(TAG,
801
- "[FLIR SDK] Streamer listener delivered bitmap via direct getBitmap");
802
- }
803
- return null;
804
- }
805
- } catch (Throwable ignored) {
806
- }
807
- try {
808
- Method getImage = arg.getClass().getMethod("getImage");
809
- Object imgObj = getImage.invoke(arg);
810
- if (imgObj instanceof Bitmap) {
811
- if (listener != null) {
812
- listener.onFrame((Bitmap) imgObj);
813
- Log.d(TAG,
814
- "[FLIR SDK] Streamer listener delivered bitmap via getImage");
815
- }
816
- return null;
817
- }
818
- // fallback to BitmapAndroid.createBitmap(imgObj).getBitMap()
819
- try {
820
- Class<?> bitmapAndroidClass = findSdkClass(
821
- "com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
822
- Method createBitmap = bitmapAndroidClass
823
- .getMethod("createBitmap", imgObj.getClass());
824
- Object wrapper = createBitmap.invoke(null, imgObj);
825
- if (wrapper != null) {
826
- Method getBitMap = wrapper.getClass()
827
- .getMethod("getBitMap");
828
- Object bmpObj = getBitMap.invoke(wrapper);
829
- if (bmpObj instanceof Bitmap) {
830
- if (listener != null) {
831
- listener.onFrame((Bitmap) bmpObj);
832
- Log.d(TAG,
833
- "[FLIR SDK] Streamer listener delivered bitmap via BitmapAndroid wrapper");
834
- }
835
- return null;
836
- }
837
- }
838
- } catch (Throwable ignored2) {
839
- }
840
- } catch (Throwable ignored) {
841
- }
842
- }
843
- }
844
- } catch (Throwable t) {
845
- Log.w(TAG, "Streamer listener failed: " + t.getMessage());
846
- }
847
- return null;
848
- });
849
- try {
850
- m.invoke(streamer, proxy);
851
- } catch (Throwable e) {
852
- Log.w(TAG, "attach proxy failed: " + e.getMessage(), e);
853
- continue;
854
- }
855
- Log.i(TAG, "Attached streamer listener using: " + m.getName());
856
- return true;
857
- }
858
- } catch (Throwable ignored) {
859
- }
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());
860
670
  }
861
- } catch (Throwable t) {
862
- Log.w(TAG, "attachStreamerListener error: " + t.getMessage());
671
+ currentStream = null;
863
672
  }
864
- return false;
673
+ thermalStreamer = null;
674
+ isStreaming.set(false);
865
675
  }
866
-
867
- public void stop() {
676
+
677
+ /**
678
+ * Refresh thermal frame using ThermalStreamer pattern.
679
+ * Called when a new frame is received.
680
+ */
681
+ private synchronized void refreshThermalFrame() {
682
+ if (thermalStreamer == null) {
683
+ return;
684
+ }
685
+
868
686
  try {
869
- scheduler.shutdownNow();
870
- if (streamerObj != null) {
871
- try {
872
- Method stop = streamerObj.getClass().getMethod("stop");
873
- if (stop != null)
874
- stop.invoke(streamerObj);
875
- } catch (Throwable ignored) {
876
- }
687
+ // Update streamer to get latest frame
688
+ thermalStreamer.update();
689
+
690
+ // Get the image buffer from streamer
691
+ ImageBuffer imageBuffer = thermalStreamer.getImage();
692
+ if (imageBuffer == null) {
693
+ return;
877
694
  }
878
- if (cameraObj != null) {
879
- try {
880
- Method disconnect = cameraObj.getClass().getMethod("disconnect");
881
- if (disconnect != null)
882
- disconnect.invoke(cameraObj);
883
- } catch (Throwable ignored) {
695
+
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);
884
704
  }
705
+ });
706
+
707
+ // Convert to Android Bitmap
708
+ Bitmap bitmap = BitmapAndroid.createBitmap(imageBuffer).getBitMap();
709
+
710
+ if (bitmap != null) {
711
+ latestFrame = bitmap;
712
+
713
+ mainHandler.post(() -> {
714
+ if (listener != null) {
715
+ listener.onFrame(bitmap);
716
+ }
717
+ });
885
718
  }
719
+
886
720
  } catch (Throwable t) {
887
- Log.w(TAG, "Failed to stop SDK manager: " + t.getMessage());
721
+ Log.w(TAG, "[FLIR] refreshThermalFrame error: " + t.getMessage());
888
722
  }
889
723
  }
724
+
725
+ // ==================== PUBLIC STOP ====================
726
+
727
+ /**
728
+ * Stop the manager - disconnect and cleanup all resources.
729
+ */
730
+ public void stop() {
731
+ Log.i(TAG, "[FLIR] Stopping FlirSdkManager");
732
+
733
+ // Stop streaming
734
+ stopStreaming();
735
+
736
+ // Disconnect camera
737
+ disconnect();
738
+
739
+ // Stop discovery
740
+ stopDiscovery();
741
+
742
+ // Clear state
743
+ discoveredDevices.clear();
744
+ currentThermalImage = null;
745
+ latestFrame = null;
746
+
747
+ Log.i(TAG, "[FLIR] FlirSdkManager stopped");
748
+ }
749
+
750
+ // ==================== HELPERS ====================
751
+
752
+ private void notifyError(String error) {
753
+ Log.e(TAG, "[FLIR] Error: " + error);
754
+ mainHandler.post(() -> {
755
+ if (listener != null) {
756
+ listener.onError(error);
757
+ }
758
+ });
759
+ }
890
760
  }