ilabs-flir 1.0.3 → 1.0.4
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.
- package/android/Flir/libs/flir-stubs.jar +0 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCode.java +13 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/ThermalSdkAndroid.java +16 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/androidsdk/image/BitmapAndroid.java +20 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/Camera.java +26 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/ConnectParameters.java +16 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/RemoteControl.java +16 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryEventListener.java +14 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryFactory.java +33 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/Stream.java +8 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/ThermalStreamer.java +28 -0
- package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/VisualStreamer.java +18 -0
- package/android/Flir/src/main/java/flir/android/FlirCommands.java +40 -15
- package/android/Flir/src/main/java/flir/android/FlirDownloadManager.kt +6 -5
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +265 -42
- package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +242 -194
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +1376 -755
- package/package.json +1 -1
- package/sdk-manifest.json +14 -7
- package/android/Flir/src/main/java/flir/android/CameraHandler.java +0 -224
- package/android/Flir/src/main/java/flir/android/FlirConnectionManager.java +0 -354
- package/android/Flir/src/main/java/flir/android/FlirController.kt +0 -11
- package/android/Flir/src/main/java/flir/android/FlirDiscoveryManager.java +0 -236
- package/android/Flir/src/main/java/flir/android/FrameDataHolder.java +0 -14
|
@@ -1,890 +1,1511 @@
|
|
|
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
|
|
|
8
|
+
import java.io.File;
|
|
9
|
+
import java.io.FileOutputStream;
|
|
6
10
|
import java.lang.reflect.Method;
|
|
7
11
|
import java.lang.reflect.Proxy;
|
|
12
|
+
import java.util.ArrayList;
|
|
8
13
|
import java.util.List;
|
|
9
|
-
import java.
|
|
10
|
-
import java.io.FileOutputStream;
|
|
11
|
-
import java.util.zip.ZipEntry;
|
|
12
|
-
import java.util.zip.ZipFile;
|
|
13
|
-
import dalvik.system.DexClassLoader;
|
|
14
|
+
import java.util.concurrent.CopyOnWriteArrayList;
|
|
14
15
|
import java.util.concurrent.Executors;
|
|
15
16
|
import java.util.concurrent.ScheduledExecutorService;
|
|
17
|
+
import java.util.concurrent.ScheduledFuture;
|
|
16
18
|
import java.util.concurrent.TimeUnit;
|
|
19
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
20
|
+
import java.util.zip.ZipEntry;
|
|
21
|
+
import java.util.zip.ZipFile;
|
|
22
|
+
|
|
23
|
+
import dalvik.system.DexClassLoader;
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
26
|
+
* FLIR SDK Manager - Handles device discovery, connection, and streaming using reflection.
|
|
27
|
+
* Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
|
|
28
|
+
* All SDK calls use reflection for loose binding - no compile-time dependency on FLIR SDK.
|
|
29
|
+
*
|
|
30
|
+
* Flow:
|
|
31
|
+
* isFlir=true → startDiscovery()
|
|
32
|
+
* ↓ disconnect current device (if any)
|
|
33
|
+
* ↓ (timeout: 0s if isEmu=true, 5s otherwise)
|
|
34
|
+
* ├─ Devices found → emit to RN → stream from device[0]
|
|
35
|
+
* │ ↓ RN sends deviceId
|
|
36
|
+
* │ └─ disconnect → switch to deviceId
|
|
37
|
+
* ├─ No device OR isEmu=true → emulatorDiscovery()
|
|
38
|
+
* │ ↓ Check emulator type setting
|
|
39
|
+
* │ ├─ FLIR_ONE_EDGE (default)
|
|
40
|
+
* │ └─ FLIR_ONE
|
|
41
|
+
* │ └─ stream from emulator
|
|
42
|
+
* └─ Streaming: connect → start stream → upload to texture
|
|
43
|
+
* ↓ acol changes → setPalette()
|
|
44
|
+
* ↓ touch point → getTemperatureAt(x,y)
|
|
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"; // 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
|
+
}
|
|
66
|
+
|
|
67
|
+
// Discovery timeout in milliseconds
|
|
68
|
+
private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000; // 5 seconds for real devices
|
|
69
|
+
private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0; // Immediate for emulator mode
|
|
70
|
+
|
|
71
|
+
// Emulator types
|
|
72
|
+
public enum EmulatorType {
|
|
73
|
+
FLIR_ONE_EDGE, // Default - WiFi emulator
|
|
74
|
+
FLIR_ONE // USB emulator
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Communication interfaces (mirrors SDK enum)
|
|
78
|
+
public enum CommInterface {
|
|
79
|
+
USB,
|
|
80
|
+
NETWORK,
|
|
81
|
+
EMULATOR
|
|
82
|
+
}
|
|
24
83
|
|
|
25
|
-
|
|
84
|
+
// Listener interface for callbacks
|
|
85
|
+
public interface Listener {
|
|
26
86
|
void onFrame(Bitmap bitmap);
|
|
27
|
-
|
|
28
87
|
void onTemperature(double temp, int x, int y);
|
|
29
|
-
|
|
30
|
-
void
|
|
31
|
-
|
|
32
|
-
void
|
|
33
|
-
|
|
34
|
-
void
|
|
88
|
+
void onDeviceFound(String deviceId, String deviceName, boolean isEmulator);
|
|
89
|
+
void onDeviceListUpdated(List<DeviceInfo> devices);
|
|
90
|
+
void onDeviceConnected(String deviceId, String deviceName, boolean isEmulator);
|
|
91
|
+
void onDeviceDisconnected();
|
|
92
|
+
void onDiscoveryStarted();
|
|
93
|
+
void onDiscoveryTimeout();
|
|
94
|
+
void onStreamStarted(String streamType);
|
|
95
|
+
void onError(String error);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Device info class for discovered devices
|
|
99
|
+
public static class DeviceInfo {
|
|
100
|
+
public final String deviceId;
|
|
101
|
+
public final String deviceName;
|
|
102
|
+
public final boolean isEmulator;
|
|
103
|
+
public final CommInterface commInterface;
|
|
104
|
+
public final Object identity; // SDK Identity object (kept for connection)
|
|
105
|
+
|
|
106
|
+
DeviceInfo(String id, String name, boolean emu, CommInterface iface, Object identity) {
|
|
107
|
+
this.deviceId = id;
|
|
108
|
+
this.deviceName = name;
|
|
109
|
+
this.isEmulator = emu;
|
|
110
|
+
this.commInterface = iface;
|
|
111
|
+
this.identity = identity;
|
|
112
|
+
}
|
|
35
113
|
}
|
|
36
114
|
|
|
37
|
-
private Listener listener;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
|
|
115
|
+
private final Listener listener;
|
|
116
|
+
private final android.content.Context appContext;
|
|
117
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
118
|
+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
|
|
119
|
+
|
|
120
|
+
// SDK objects (all via reflection)
|
|
43
121
|
private ClassLoader sdkClassLoader = null;
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
private
|
|
122
|
+
private Object discoveryFactory = null;
|
|
123
|
+
private Object discoveryListener = null;
|
|
124
|
+
private Object cameraObj = null;
|
|
125
|
+
private Object streamerObj = null;
|
|
126
|
+
private Object currentStream = null;
|
|
127
|
+
private Object currentPalette = null;
|
|
128
|
+
|
|
129
|
+
// State tracking
|
|
130
|
+
private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
|
|
131
|
+
private final AtomicBoolean isConnected = new AtomicBoolean(false);
|
|
132
|
+
private final AtomicBoolean isStreaming = new AtomicBoolean(false);
|
|
133
|
+
private final AtomicBoolean isEmulatorMode = new AtomicBoolean(false);
|
|
134
|
+
private final CopyOnWriteArrayList<DeviceInfo> discoveredDevices = new CopyOnWriteArrayList<>();
|
|
135
|
+
private ScheduledFuture<?> discoveryTimeoutFuture = null;
|
|
136
|
+
private DeviceInfo connectedDevice = null;
|
|
137
|
+
private EmulatorType emulatorType = EmulatorType.FLIR_ONE_EDGE;
|
|
138
|
+
|
|
139
|
+
// Frame state
|
|
47
140
|
private volatile Bitmap latestFrame = null;
|
|
141
|
+
private String sdkJarPath = null;
|
|
142
|
+
private String currentStreamKind = null;
|
|
143
|
+
|
|
144
|
+
// Frame counting for debug logging
|
|
145
|
+
private int frameCount = 0;
|
|
146
|
+
private long lastFrameLogTime = 0;
|
|
147
|
+
private int successfulBitmapCount = 0;
|
|
48
148
|
|
|
49
|
-
FlirSdkManager(Listener listener) {
|
|
149
|
+
FlirSdkManager(Listener listener, android.content.Context context) {
|
|
50
150
|
this.listener = listener;
|
|
151
|
+
this.appContext = context != null ? context.getApplicationContext() : null;
|
|
51
152
|
}
|
|
52
|
-
|
|
53
|
-
|
|
153
|
+
|
|
154
|
+
// ==================== PUBLIC API ====================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set the emulator type to use when no physical device is found
|
|
158
|
+
*/
|
|
159
|
+
public void setEmulatorType(EmulatorType type) {
|
|
160
|
+
this.emulatorType = type;
|
|
161
|
+
Log.i(TAG, "[FLIR] Emulator type set to: " + type);
|
|
162
|
+
logStep("SET_EMULATOR_TYPE", "type=" + type + " (FLIR_ONE_EDGE=WiFi, FLIR_ONE=USB)");
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
+
public void startDiscovery(boolean forceEmulator) {
|
|
170
|
+
resetFlowTracking();
|
|
171
|
+
logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
|
|
172
|
+
Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
|
|
173
|
+
|
|
174
|
+
// Always disconnect first
|
|
175
|
+
if (isConnected.get()) {
|
|
176
|
+
logStep("DISCONNECT_PREVIOUS", "Disconnecting current device before discovery");
|
|
177
|
+
disconnect();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Clear discovered devices
|
|
181
|
+
discoveredDevices.clear();
|
|
182
|
+
logStep("CLEAR_DEVICES", "Cleared discovered devices list");
|
|
183
|
+
|
|
184
|
+
if (forceEmulator) {
|
|
185
|
+
// Immediate emulator mode
|
|
186
|
+
logStep("MODE_EMULATOR", "Forcing emulator mode - skipping device discovery");
|
|
187
|
+
isEmulatorMode.set(true);
|
|
188
|
+
startEmulatorDiscovery();
|
|
189
|
+
} else {
|
|
190
|
+
// Normal discovery with timeout
|
|
191
|
+
logStep("MODE_FULL_DISCOVERY", "Starting full discovery (USB+NETWORK+EMULATOR), timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
|
|
192
|
+
isEmulatorMode.set(false);
|
|
193
|
+
startFullDiscovery();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Stop discovery scan
|
|
199
|
+
*/
|
|
200
|
+
public void stopDiscovery() {
|
|
201
|
+
Log.i(TAG, "[FLIR] stopDiscovery()");
|
|
202
|
+
cancelDiscoveryTimeout();
|
|
203
|
+
isDiscovering.set(false);
|
|
204
|
+
|
|
54
205
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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");
|
|
97
|
-
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());
|
|
206
|
+
if (discoveryFactory != null) {
|
|
207
|
+
Method stopMethod = discoveryFactory.getClass().getMethod("stop");
|
|
208
|
+
stopMethod.invoke(discoveryFactory);
|
|
114
209
|
}
|
|
115
|
-
return false;
|
|
116
210
|
} catch (Throwable t) {
|
|
117
|
-
Log.w(TAG, "[FLIR
|
|
118
|
-
return false;
|
|
211
|
+
Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
|
|
119
212
|
}
|
|
120
213
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) {
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Connect to a specific device by ID
|
|
217
|
+
*/
|
|
218
|
+
public void connectToDevice(String deviceId) {
|
|
219
|
+
Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
|
|
220
|
+
|
|
221
|
+
// Find device in discovered list
|
|
222
|
+
DeviceInfo target = null;
|
|
223
|
+
for (DeviceInfo d : discoveredDevices) {
|
|
224
|
+
if (d.deviceId.equals(deviceId)) {
|
|
225
|
+
target = d;
|
|
226
|
+
break;
|
|
144
227
|
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (target == null) {
|
|
231
|
+
notifyError("Device not found: " + deviceId);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Disconnect current if needed
|
|
236
|
+
if (isConnected.get()) {
|
|
237
|
+
disconnect();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Connect to target
|
|
241
|
+
connectToIdentity(target);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Disconnect current device/emulator
|
|
246
|
+
*/
|
|
247
|
+
public void disconnect() {
|
|
248
|
+
Log.i(TAG, "[FLIR] disconnect()");
|
|
249
|
+
|
|
250
|
+
// Stop streaming first
|
|
251
|
+
stopStreaming();
|
|
252
|
+
|
|
253
|
+
// Disconnect camera
|
|
254
|
+
if (cameraObj != null) {
|
|
145
255
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
256
|
+
Method disconnectMethod = cameraObj.getClass().getMethod("disconnect");
|
|
257
|
+
disconnectMethod.invoke(cameraObj);
|
|
258
|
+
Log.i(TAG, "[FLIR] Camera disconnected");
|
|
259
|
+
} catch (Throwable t) {
|
|
260
|
+
Log.w(TAG, "[FLIR] disconnect failed: " + t.getMessage());
|
|
148
261
|
}
|
|
149
|
-
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cameraObj = null;
|
|
265
|
+
streamerObj = null;
|
|
266
|
+
currentStream = null;
|
|
267
|
+
connectedDevice = null;
|
|
268
|
+
isConnected.set(false);
|
|
269
|
+
isStreaming.set(false);
|
|
270
|
+
|
|
271
|
+
if (listener != null) {
|
|
272
|
+
mainHandler.post(() -> listener.onDeviceDisconnected());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Set palette by name (iron, rainbow, etc.)
|
|
278
|
+
*/
|
|
279
|
+
public void setPalette(String paletteName) {
|
|
280
|
+
Log.d(TAG, "[FLIR] setPalette: " + paletteName);
|
|
281
|
+
|
|
282
|
+
if (streamerObj == null) {
|
|
283
|
+
Log.w(TAG, "[FLIR] Cannot set palette - no active streamer");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
scheduler.submit(() -> {
|
|
150
288
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
305
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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) {
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
candList.add(new File("/data/local/tmp/thermalsdk.aar"));
|
|
176
|
-
} catch (Throwable ignored) {
|
|
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());
|
|
177
315
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
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() {
|
|
324
|
+
try {
|
|
325
|
+
Class<?> paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
|
|
326
|
+
Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
|
|
327
|
+
Object palettes = getDefaultPalettes.invoke(null);
|
|
328
|
+
|
|
329
|
+
if (palettes instanceof List) {
|
|
330
|
+
// Try to find "iron" palette first, then fall back to first available
|
|
331
|
+
Object ironPalette = null;
|
|
332
|
+
Object firstPalette = null;
|
|
333
|
+
|
|
334
|
+
for (Object p : (List<?>) palettes) {
|
|
335
|
+
if (firstPalette == null) firstPalette = p;
|
|
336
|
+
|
|
191
337
|
try {
|
|
192
|
-
|
|
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);
|
|
193
354
|
} catch (Throwable ignored) {
|
|
355
|
+
Log.i(TAG, "[FLIR] Default palette initialized");
|
|
194
356
|
}
|
|
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
357
|
}
|
|
203
358
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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());
|
|
359
|
+
} catch (Throwable t) {
|
|
360
|
+
Log.w(TAG, "[FLIR] initializeDefaultPalette failed: " + t.getMessage());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get temperature at a specific point
|
|
366
|
+
*/
|
|
367
|
+
public double getTemperatureAtPoint(int x, int y) {
|
|
368
|
+
if (streamerObj == null) return Double.NaN;
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Get the thermal image from streamer
|
|
372
|
+
Method getImage = streamerObj.getClass().getMethod("getImage");
|
|
373
|
+
Object thermalImage = getImage.invoke(streamerObj);
|
|
374
|
+
|
|
375
|
+
if (thermalImage != null) {
|
|
376
|
+
// Try getValueAt(Point)
|
|
235
377
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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) {}
|
|
241
395
|
}
|
|
242
396
|
} catch (Throwable t) {
|
|
243
|
-
Log.
|
|
244
|
-
try {
|
|
245
|
-
Log.i(TAG, "[FLIR SDK] Candidate list tried on failure: ");
|
|
246
|
-
} catch (Throwable ignored) {
|
|
247
|
-
}
|
|
397
|
+
Log.d(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
|
|
248
398
|
}
|
|
249
|
-
|
|
399
|
+
|
|
400
|
+
return Double.NaN;
|
|
250
401
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get latest frame bitmap
|
|
405
|
+
*/
|
|
406
|
+
public Bitmap getLatestFrame() {
|
|
407
|
+
return latestFrame;
|
|
254
408
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if streaming is active
|
|
412
|
+
*/
|
|
413
|
+
public boolean isStreamingActive() {
|
|
414
|
+
return isStreaming.get();
|
|
258
415
|
}
|
|
259
|
-
|
|
260
|
-
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check if connected
|
|
419
|
+
*/
|
|
420
|
+
public boolean isConnected() {
|
|
421
|
+
return isConnected.get();
|
|
422
|
+
}
|
|
423
|
+
|
|
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);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Cleanup resources
|
|
440
|
+
*/
|
|
441
|
+
public void stop() {
|
|
442
|
+
Log.i(TAG, "[FLIR] stop()");
|
|
443
|
+
stopDiscovery();
|
|
444
|
+
disconnect();
|
|
445
|
+
scheduler.shutdownNow();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ==================== DISCOVERY IMPLEMENTATION ====================
|
|
449
|
+
|
|
450
|
+
private void startFullDiscovery() {
|
|
451
|
+
Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
|
|
452
|
+
|
|
453
|
+
if (!initializeSdk()) {
|
|
454
|
+
Log.w(TAG, "[FLIR] SDK not available, falling back to emulator");
|
|
455
|
+
startEmulatorDiscovery();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
isDiscovering.set(true);
|
|
460
|
+
if (listener != null) {
|
|
461
|
+
mainHandler.post(() -> listener.onDiscoveryStarted());
|
|
462
|
+
}
|
|
463
|
+
|
|
261
464
|
try {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
465
|
+
// Get DiscoveryFactory.getInstance()
|
|
466
|
+
Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
|
|
467
|
+
Method getInstance = discoveryFactoryClass.getMethod("getInstance");
|
|
468
|
+
discoveryFactory = getInstance.invoke(null);
|
|
469
|
+
|
|
470
|
+
// Get CommunicationInterface enum values
|
|
471
|
+
Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
|
|
472
|
+
Object usbInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "USB");
|
|
473
|
+
Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
|
|
474
|
+
Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
|
|
475
|
+
|
|
476
|
+
// Create discovery listener proxy
|
|
477
|
+
Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
|
|
478
|
+
discoveryListener = Proxy.newProxyInstance(
|
|
479
|
+
getEffectiveClassLoader(),
|
|
480
|
+
new Class<?>[] { listenerClass },
|
|
481
|
+
(proxy, method, args) -> handleDiscoveryCallback(method.getName(), args)
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Create interface array [USB, NETWORK, EMULATOR]
|
|
485
|
+
Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 3);
|
|
486
|
+
java.lang.reflect.Array.set(ifaceArray, 0, usbInterface);
|
|
487
|
+
java.lang.reflect.Array.set(ifaceArray, 1, networkInterface);
|
|
488
|
+
java.lang.reflect.Array.set(ifaceArray, 2, emulatorInterface);
|
|
489
|
+
|
|
490
|
+
// Start discovery scan
|
|
491
|
+
Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass, ifaceArray.getClass());
|
|
492
|
+
scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
|
|
493
|
+
|
|
494
|
+
Log.i(TAG, "[FLIR] Discovery scan started for USB, NETWORK, EMULATOR");
|
|
495
|
+
|
|
496
|
+
// Set discovery timeout
|
|
497
|
+
startDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
|
|
498
|
+
|
|
499
|
+
} catch (Throwable t) {
|
|
500
|
+
Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
|
|
501
|
+
notifyError("Discovery failed: " + t.getMessage());
|
|
502
|
+
// Fallback to emulator
|
|
503
|
+
startEmulatorDiscovery();
|
|
267
504
|
}
|
|
268
505
|
}
|
|
269
|
-
|
|
270
|
-
private
|
|
506
|
+
|
|
507
|
+
private void startEmulatorDiscovery() {
|
|
508
|
+
logStep("EMULATOR_DISCOVERY_START", "type=" + emulatorType);
|
|
509
|
+
Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
|
|
510
|
+
|
|
511
|
+
if (!initializeSdk()) {
|
|
512
|
+
logStep("SDK_INIT_FAILED", "FLIR SDK not available - check if SDK JAR is loaded");
|
|
513
|
+
notifyError("FLIR SDK not available");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
logStep("SDK_INITIALIZED", "FLIR SDK successfully initialized");
|
|
517
|
+
|
|
518
|
+
isDiscovering.set(true);
|
|
519
|
+
isEmulatorMode.set(true);
|
|
520
|
+
|
|
271
521
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
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);
|
|
522
|
+
// Get DiscoveryFactory
|
|
523
|
+
Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
|
|
279
524
|
Method getInstance = discoveryFactoryClass.getMethod("getInstance");
|
|
280
525
|
discoveryFactory = getInstance.invoke(null);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
Class<?>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|
|
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());
|
|
315
545
|
scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
546
|
+
|
|
547
|
+
logStep("EMULATOR_SCAN_STARTED", "Scanning for emulator devices...");
|
|
548
|
+
Log.i(TAG, "[FLIR] Emulator discovery started");
|
|
549
|
+
|
|
319
550
|
} catch (Throwable t) {
|
|
320
|
-
|
|
321
|
-
|
|
551
|
+
logStep("EMULATOR_DISCOVERY_ERROR", "Failed: " + t.getMessage());
|
|
552
|
+
Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
|
|
553
|
+
notifyError("Emulator discovery failed: " + t.getMessage());
|
|
322
554
|
}
|
|
323
555
|
}
|
|
324
|
-
|
|
325
|
-
private
|
|
556
|
+
|
|
557
|
+
private Object handleDiscoveryCallback(String methodName, Object[] args) {
|
|
558
|
+
switch (methodName) {
|
|
559
|
+
case "onCameraFound":
|
|
560
|
+
if (args != null && args.length > 0) {
|
|
561
|
+
handleCameraFound(args[0]);
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case "onCameraLost":
|
|
566
|
+
if (args != null && args.length > 0) {
|
|
567
|
+
handleCameraLost(args[0]);
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
|
|
571
|
+
case "onDiscoveryError":
|
|
572
|
+
if (args != null && args.length > 1) {
|
|
573
|
+
Log.w(TAG, "[FLIR] Discovery error: " + args[1]);
|
|
574
|
+
}
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private void handleCameraFound(Object discoveredCamera) {
|
|
326
581
|
try {
|
|
582
|
+
// Get identity from DiscoveredCamera
|
|
327
583
|
Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
|
|
328
584
|
Object identity = getIdentity.invoke(discoveredCamera);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!exists) {
|
|
608
|
+
discoveredDevices.add(deviceInfo);
|
|
609
|
+
|
|
610
|
+
// Notify listener
|
|
611
|
+
if (listener != null) {
|
|
612
|
+
final List<DeviceInfo> devices = new ArrayList<>(discoveredDevices);
|
|
613
|
+
mainHandler.post(() -> {
|
|
614
|
+
listener.onDeviceFound(deviceId, deviceName, isEmulator);
|
|
615
|
+
listener.onDeviceListUpdated(devices);
|
|
342
616
|
});
|
|
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) {
|
|
352
617
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
} catch (Throwable t) {
|
|
632
|
+
Log.e(TAG, "[FLIR] handleCameraFound failed: " + t.getMessage(), t);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private void handleCameraLost(Object discoveredCamera) {
|
|
637
|
+
try {
|
|
638
|
+
Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
|
|
639
|
+
Object identity = getIdentity.invoke(discoveredCamera);
|
|
640
|
+
String deviceId = extractDeviceId(identity);
|
|
641
|
+
|
|
642
|
+
Log.i(TAG, "[FLIR] Camera lost: " + deviceId);
|
|
643
|
+
|
|
644
|
+
// Remove from list
|
|
645
|
+
discoveredDevices.removeIf(d -> d.deviceId.equals(deviceId));
|
|
646
|
+
|
|
647
|
+
// If this was our connected device, disconnect
|
|
648
|
+
if (connectedDevice != null && connectedDevice.deviceId.equals(deviceId)) {
|
|
649
|
+
disconnect();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
} catch (Throwable t) {
|
|
653
|
+
Log.w(TAG, "[FLIR] handleCameraLost failed: " + t.getMessage());
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private void startDiscoveryTimeout(long timeoutMs) {
|
|
658
|
+
cancelDiscoveryTimeout();
|
|
659
|
+
|
|
660
|
+
if (timeoutMs <= 0) return;
|
|
661
|
+
|
|
662
|
+
discoveryTimeoutFuture = scheduler.schedule(() -> {
|
|
663
|
+
Log.i(TAG, "[FLIR] Discovery timeout");
|
|
664
|
+
isDiscovering.set(false);
|
|
665
|
+
|
|
666
|
+
if (listener != null) {
|
|
667
|
+
mainHandler.post(() -> listener.onDiscoveryTimeout());
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// If no physical devices found, try emulator
|
|
671
|
+
boolean hasPhysicalDevice = false;
|
|
672
|
+
for (DeviceInfo d : discoveredDevices) {
|
|
673
|
+
if (!d.isEmulator) {
|
|
674
|
+
hasPhysicalDevice = true;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (!hasPhysicalDevice) {
|
|
680
|
+
Log.i(TAG, "[FLIR] No physical devices found, starting emulator");
|
|
681
|
+
startEmulatorDiscovery();
|
|
682
|
+
} else if (!discoveredDevices.isEmpty()) {
|
|
683
|
+
// Connect to first device
|
|
684
|
+
connectToIdentity(discoveredDevices.get(0));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
}, timeoutMs, TimeUnit.MILLISECONDS);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private void cancelDiscoveryTimeout() {
|
|
691
|
+
if (discoveryTimeoutFuture != null) {
|
|
692
|
+
discoveryTimeoutFuture.cancel(false);
|
|
693
|
+
discoveryTimeoutFuture = null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ==================== CONNECTION IMPLEMENTATION ====================
|
|
698
|
+
|
|
699
|
+
private void connectToIdentity(DeviceInfo deviceInfo) {
|
|
700
|
+
logStep("CONNECT_START", "Connecting to: " + deviceInfo.deviceName + " (" + deviceInfo.commInterface + ")");
|
|
701
|
+
Log.i(TAG, "[FLIR] Connecting to: " + deviceInfo.deviceName);
|
|
702
|
+
|
|
703
|
+
scheduler.submit(() -> {
|
|
704
|
+
try {
|
|
705
|
+
// Create Camera instance
|
|
706
|
+
logStep("CREATE_CAMERA", "Creating Camera instance");
|
|
707
|
+
Class<?> cameraClass = findSdkClass("com.flir.thermalsdk.live.Camera");
|
|
708
|
+
cameraObj = cameraClass.newInstance();
|
|
709
|
+
|
|
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();
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// Try to connect with different method signatures
|
|
725
|
+
boolean connected = false;
|
|
726
|
+
|
|
727
|
+
// Try connect(Identity, ConnectionStatusListener, ConnectParameters)
|
|
358
728
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
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);
|
|
361
735
|
connected = true;
|
|
362
|
-
} catch (
|
|
736
|
+
} catch (NoSuchMethodException ignored) {}
|
|
737
|
+
|
|
738
|
+
// Try connect(Identity, ConnectionStatusListener)
|
|
739
|
+
if (!connected) {
|
|
363
740
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
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);
|
|
366
745
|
connected = true;
|
|
367
|
-
} catch (
|
|
368
|
-
Log.w(TAG, "connect: All connect attempts failed: " + ignored3.getMessage());
|
|
369
|
-
}
|
|
746
|
+
} catch (NoSuchMethodException ignored) {}
|
|
370
747
|
}
|
|
748
|
+
|
|
749
|
+
// Try connect(Identity)
|
|
750
|
+
if (!connected) {
|
|
751
|
+
try {
|
|
752
|
+
Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
|
|
753
|
+
Method connectMethod = cameraClass.getMethod("connect", identityClass);
|
|
754
|
+
connectMethod.invoke(cameraObj, deviceInfo.identity);
|
|
755
|
+
connected = true;
|
|
756
|
+
} catch (NoSuchMethodException ignored) {}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (!connected) {
|
|
760
|
+
throw new Exception("No suitable connect method found");
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
connectedDevice = deviceInfo;
|
|
764
|
+
isConnected.set(true);
|
|
765
|
+
|
|
766
|
+
logStep("CONNECTED", "Successfully connected to: " + deviceInfo.deviceName);
|
|
767
|
+
Log.i(TAG, "[FLIR] Connected to: " + deviceInfo.deviceName);
|
|
768
|
+
|
|
769
|
+
// Notify listener
|
|
770
|
+
if (listener != null) {
|
|
771
|
+
mainHandler.post(() -> listener.onDeviceConnected(
|
|
772
|
+
deviceInfo.deviceId, deviceInfo.deviceName, deviceInfo.isEmulator));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Start streaming after brief delay
|
|
776
|
+
logStep("SCHEDULE_STREAMING", "Scheduling stream start in 500ms");
|
|
777
|
+
scheduler.schedule(this::startStreaming, 500, TimeUnit.MILLISECONDS);
|
|
778
|
+
|
|
779
|
+
} catch (Throwable t) {
|
|
780
|
+
logStep("CONNECT_FAILED", "Connection failed: " + t.getMessage());
|
|
781
|
+
Log.e(TAG, "[FLIR] Connection failed: " + t.getMessage(), t);
|
|
782
|
+
notifyError("Connection failed: " + t.getMessage());
|
|
371
783
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
Log.d(TAG, "[FLIR SDK] Scheduling startSdkStreaming; cameraObj=" + cameraObj + ", identity=" + identity
|
|
387
|
-
+ ", connected=" + connected);
|
|
388
|
-
scheduler.schedule(this::startSdkStreaming, 500, TimeUnit.MILLISECONDS);
|
|
389
|
-
} catch (Throwable t) {
|
|
390
|
-
Log.w(TAG, "connectToSdkCamera failed: " + t.getMessage(), t);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
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());
|
|
391
797
|
}
|
|
392
798
|
}
|
|
393
|
-
|
|
394
|
-
|
|
799
|
+
|
|
800
|
+
// ==================== STREAMING IMPLEMENTATION ====================
|
|
801
|
+
|
|
802
|
+
private void startStreaming() {
|
|
803
|
+
if (cameraObj == null) {
|
|
804
|
+
logStep("STREAM_SKIP", "Cannot start streaming - no camera object");
|
|
805
|
+
Log.w(TAG, "[FLIR] Cannot start streaming - no camera");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
logStep("STREAM_START", "Starting streaming process...");
|
|
810
|
+
Log.i(TAG, "[FLIR] Starting streaming...");
|
|
811
|
+
|
|
395
812
|
try {
|
|
396
|
-
|
|
397
|
-
if (cameraObj == null)
|
|
398
|
-
return;
|
|
813
|
+
// Get camera streams
|
|
399
814
|
Method getStreams = cameraObj.getClass().getMethod("getStreams");
|
|
400
815
|
Object streams = getStreams.invoke(cameraObj);
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
403
828
|
Object chosenStream = null;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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) {
|
|
829
|
+
String streamType = "unknown";
|
|
830
|
+
|
|
831
|
+
for (Object s : streamList) {
|
|
832
|
+
if (s == null) continue;
|
|
833
|
+
|
|
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";
|
|
847
|
+
break;
|
|
848
|
+
} else if (chosenStream == null) {
|
|
849
|
+
chosenStream = s;
|
|
850
|
+
streamType = "visual";
|
|
445
851
|
}
|
|
852
|
+
} catch (Throwable ignored) {}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (chosenStream == null) {
|
|
856
|
+
chosenStream = streamList.get(0);
|
|
857
|
+
streamType = "default";
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
currentStream = chosenStream;
|
|
861
|
+
currentStreamKind = streamType;
|
|
862
|
+
|
|
863
|
+
logStep("STREAM_SELECTED", "Using " + streamType + " stream");
|
|
864
|
+
Log.i(TAG, "[FLIR] Using stream type: " + streamType);
|
|
865
|
+
|
|
866
|
+
// Create appropriate streamer
|
|
867
|
+
if ("thermal".equals(streamType)) {
|
|
868
|
+
logStep("CREATE_THERMAL_STREAMER", "Creating ThermalStreamer");
|
|
869
|
+
Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
|
|
870
|
+
Class<?> streamClass = findSdkClass("com.flir.thermalsdk.live.streaming.Stream");
|
|
871
|
+
streamerObj = thermalStreamerClass.getConstructor(streamClass).newInstance(chosenStream);
|
|
872
|
+
|
|
873
|
+
// Initialize default palette if not already set
|
|
874
|
+
if (currentPalette == null) {
|
|
875
|
+
logStep("INIT_PALETTE", "Initializing default 'iron' palette");
|
|
876
|
+
initializeDefaultPalette();
|
|
877
|
+
} else {
|
|
878
|
+
logStep("PALETTE_EXISTS", "Palette already set, skipping init");
|
|
446
879
|
}
|
|
447
|
-
|
|
448
|
-
|
|
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);
|
|
449
885
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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) {
|
|
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;
|
|
477
897
|
}
|
|
478
|
-
|
|
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]);
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
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));
|
|
479
926
|
}
|
|
480
|
-
|
|
927
|
+
|
|
481
928
|
} catch (Throwable t) {
|
|
482
|
-
|
|
929
|
+
logStep("STREAM_ERROR", "Streaming failed: " + t.getMessage());
|
|
930
|
+
Log.e(TAG, "[FLIR] startStreaming failed: " + t.getMessage(), t);
|
|
931
|
+
notifyError("Streaming failed: " + t.getMessage());
|
|
483
932
|
}
|
|
484
933
|
}
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
|
|
487
949
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
950
|
+
// Call streamer.update() first - this refreshes the streamer content
|
|
951
|
+
Method updateMethod = streamerObj.getClass().getMethod("update");
|
|
952
|
+
updateMethod.invoke(streamerObj);
|
|
953
|
+
|
|
954
|
+
// Get image buffer from streamer.getImage()
|
|
955
|
+
Method getImage = streamerObj.getClass().getMethod("getImage");
|
|
956
|
+
Object imageBuffer = getImage.invoke(streamerObj);
|
|
957
|
+
|
|
958
|
+
if (imageBuffer == null) {
|
|
959
|
+
if (shouldLog) logStep("FRAME_NULL_BUFFER", "imageBuffer is null at frame " + frameCount);
|
|
960
|
+
Log.d(TAG, "[FLIR] imageBuffer is null");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (frameCount == 1) {
|
|
965
|
+
logStep("FIRST_BUFFER", "Got first imageBuffer, type=" + imageBuffer.getClass().getSimpleName());
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// For thermal streamer, we MUST create the bitmap INSIDE withThermalImage callback
|
|
969
|
+
// This is critical - the palette affects the imageBuffer rendering only while inside the callback
|
|
970
|
+
if ("thermal".equals(currentStreamKind)) {
|
|
971
|
+
processThermalFrameWithCallback(imageBuffer);
|
|
972
|
+
} else {
|
|
973
|
+
// For visual stream, just convert to bitmap directly
|
|
974
|
+
Bitmap bitmap = convertToBitmap(imageBuffer);
|
|
975
|
+
if (bitmap != null) {
|
|
976
|
+
successfulBitmapCount++;
|
|
977
|
+
latestFrame = bitmap;
|
|
978
|
+
if (frameCount == 1) {
|
|
979
|
+
logStep("FIRST_BITMAP", "Visual bitmap created: " + bitmap.getWidth() + "x" + bitmap.getHeight());
|
|
980
|
+
}
|
|
981
|
+
if (listener != null) {
|
|
982
|
+
listener.onFrame(bitmap);
|
|
983
|
+
}
|
|
984
|
+
} else if (frameCount <= 5) {
|
|
985
|
+
logStep("BITMAP_NULL", "Visual convertToBitmap returned null at frame " + frameCount);
|
|
523
986
|
}
|
|
524
|
-
// if cameraObj still null - return false
|
|
525
|
-
if (cameraObj == null)
|
|
526
|
-
return false;
|
|
527
987
|
}
|
|
528
|
-
|
|
529
|
-
scheduler.submit(() -> startSdkStreaming());
|
|
530
|
-
Log.i(TAG, "[FLIR SDK] forceStartStreaming scheduled startSdkStreaming");
|
|
531
|
-
return true;
|
|
988
|
+
|
|
532
989
|
} catch (Throwable t) {
|
|
533
|
-
Log
|
|
534
|
-
|
|
990
|
+
// Log errors periodically to avoid spam
|
|
991
|
+
if (System.currentTimeMillis() % 5000 < 100) {
|
|
992
|
+
Log.d(TAG, "[FLIR] Frame processing error: " + t.getMessage());
|
|
993
|
+
}
|
|
535
994
|
}
|
|
536
995
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
996
|
+
|
|
997
|
+
/**
|
|
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.
|
|
1001
|
+
*/
|
|
1002
|
+
private void processThermalFrameWithCallback(Object imageBuffer) {
|
|
1003
|
+
if (streamerObj == null) return;
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
|
|
1007
|
+
if (!thermalStreamerClass.isInstance(streamerObj)) {
|
|
1008
|
+
// Not a ThermalStreamer, use direct conversion
|
|
1009
|
+
Bitmap bitmap = convertToBitmap(imageBuffer);
|
|
1010
|
+
if (bitmap != null) {
|
|
1011
|
+
latestFrame = bitmap;
|
|
1012
|
+
if (listener != null) listener.onFrame(bitmap);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Find withThermalImage method
|
|
1018
|
+
Method withThermalImageMethod = null;
|
|
1019
|
+
for (Method m : thermalStreamerClass.getMethods()) {
|
|
1020
|
+
if ("withThermalImage".equals(m.getName()) && m.getParameterCount() == 1) {
|
|
1021
|
+
withThermalImageMethod = m;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (withThermalImageMethod == null) {
|
|
1027
|
+
Log.w(TAG, "[FLIR] withThermalImage method not found, using fallback");
|
|
1028
|
+
// Fallback: try to set palette via getThermalImage, then convert
|
|
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
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Create Consumer proxy that does BOTH: set palette AND create bitmap
|
|
1046
|
+
// This is the key insight from LiveStreamingKotlin!
|
|
1047
|
+
final Object imgBuffer = imageBuffer;
|
|
1048
|
+
Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
|
|
1049
|
+
Object consumer = Proxy.newProxyInstance(
|
|
1050
|
+
getEffectiveClassLoader(),
|
|
1051
|
+
new Class<?>[] { consumerClass },
|
|
1052
|
+
(proxy, method, args) -> {
|
|
1053
|
+
if ("accept".equals(method.getName()) && args != null && args.length > 0) {
|
|
1054
|
+
Object thermalImage = args[0];
|
|
1055
|
+
if (thermalImage != null) {
|
|
1056
|
+
// Step 1: Set palette on thermal image
|
|
1057
|
+
if (currentPalette != null) {
|
|
1058
|
+
setPaletteOnThermalImage(thermalImage);
|
|
1059
|
+
if (frameCount == 1) {
|
|
1060
|
+
logStep("PALETTE_APPLIED", "Palette applied to ThermalImage inside callback");
|
|
561
1061
|
}
|
|
562
|
-
return;
|
|
563
1062
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Fallback: try using
|
|
568
|
-
// BitmapAndroid.createBitmap(thermalImage.getImage()).getBitMap()
|
|
569
|
-
try {
|
|
570
|
-
Object imageBase = null;
|
|
1063
|
+
|
|
1064
|
+
// Step 2: Convert to bitmap INSIDE the callback
|
|
1065
|
+
// This is critical - must happen while we have access to thermal image context
|
|
571
1066
|
try {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
listener.onFrame((Bitmap) bmp);
|
|
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());
|
|
589
1083
|
}
|
|
1084
|
+
Log.d(TAG, "[FLIR] Bitmap conversion in callback failed: " + t.getMessage());
|
|
590
1085
|
}
|
|
591
|
-
}
|
|
1086
|
+
} else if (frameCount <= 5) {
|
|
1087
|
+
logStep("THERMAL_IMAGE_NULL", "ThermalImage is null in callback");
|
|
592
1088
|
}
|
|
593
|
-
} catch (Throwable ignored) {
|
|
594
1089
|
}
|
|
1090
|
+
return null;
|
|
595
1091
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
withThermalImageMethod.invoke(streamerObj, consumer);
|
|
1095
|
+
|
|
1096
|
+
} catch (Throwable t) {
|
|
1097
|
+
Log.d(TAG, "[FLIR] processThermalFrameWithCallback failed: " + t.getMessage());
|
|
1098
|
+
// Fallback to direct conversion
|
|
1099
|
+
Bitmap bitmap = convertToBitmap(imageBuffer);
|
|
1100
|
+
if (bitmap != null) {
|
|
1101
|
+
latestFrame = bitmap;
|
|
1102
|
+
if (listener != null) listener.onFrame(bitmap);
|
|
599
1103
|
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
Bitmap getLatestFrame() {
|
|
604
|
-
return latestFrame;
|
|
1104
|
+
}
|
|
605
1105
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
|
|
609
1115
|
try {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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);
|
|
624
1136
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1137
|
+
} catch (Throwable ignored) {}
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Create Consumer proxy to call setPalette on ThermalImage
|
|
1142
|
+
Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
|
|
1143
|
+
Object consumer = Proxy.newProxyInstance(
|
|
1144
|
+
getEffectiveClassLoader(),
|
|
1145
|
+
new Class<?>[] { consumerClass },
|
|
1146
|
+
(proxy, method, args) -> {
|
|
1147
|
+
if ("accept".equals(method.getName()) && args != null && args.length > 0) {
|
|
1148
|
+
Object thermalImage = args[0];
|
|
1149
|
+
if (thermalImage != null) {
|
|
1150
|
+
setPaletteOnThermalImage(thermalImage);
|
|
633
1151
|
}
|
|
634
|
-
} catch (Throwable ignored) {
|
|
635
1152
|
}
|
|
1153
|
+
return null;
|
|
636
1154
|
}
|
|
637
|
-
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
withThermalImageMethod.invoke(streamerObj, consumer);
|
|
1158
|
+
|
|
638
1159
|
} catch (Throwable t) {
|
|
1160
|
+
Log.d(TAG, "[FLIR] applyPaletteViaThermalImage failed: " + t.getMessage());
|
|
639
1161
|
}
|
|
640
|
-
return Double.NaN;
|
|
641
1162
|
}
|
|
642
|
-
|
|
643
|
-
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Set palette on a ThermalImage object
|
|
1166
|
+
*/
|
|
1167
|
+
private void setPaletteOnThermalImage(Object thermalImage) {
|
|
1168
|
+
if (thermalImage == null || currentPalette == null) return;
|
|
1169
|
+
|
|
644
1170
|
try {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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());
|
|
651
1192
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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;
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
} catch (Throwable ignored) {
|
|
678
|
-
}
|
|
679
|
-
}
|
|
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;
|
|
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());
|
|
688
1203
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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) {}
|
|
702
1223
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
try {
|
|
713
|
-
paletteClass = findSdkClass("com.flir.thermalsdk.image.Palette");
|
|
714
|
-
} catch (Throwable ignored2) {
|
|
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) {}
|
|
715
1233
|
}
|
|
716
1234
|
}
|
|
717
|
-
if (paletteClass == null)
|
|
718
|
-
return false;
|
|
719
|
-
Method setPalette = thermalImage.getClass().getMethod("setPalette", paletteClass);
|
|
720
|
-
setPalette.invoke(thermalImage, target);
|
|
721
|
-
return true;
|
|
722
1235
|
}
|
|
723
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
Log.d(TAG, "[FLIR] BitmapAndroid.createBitmap method not found for imageBuffer type: " + imageBuffer.getClass().getName());
|
|
1239
|
+
|
|
724
1240
|
} catch (Throwable t) {
|
|
725
|
-
Log.
|
|
1241
|
+
Log.d(TAG, "[FLIR] convertToBitmap primary method failed: " + t.getMessage());
|
|
726
1242
|
}
|
|
727
|
-
|
|
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;
|
|
728
1254
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
}
|
|
1266
|
+
|
|
1267
|
+
streamerObj = null;
|
|
1268
|
+
currentStream = null;
|
|
1269
|
+
isStreaming.set(false);
|
|
732
1270
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Try loading from downloaded AAR
|
|
1288
|
+
if (attemptLoadSdkFromAar()) {
|
|
1289
|
+
initializeThermalSdk();
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return false;
|
|
736
1294
|
}
|
|
737
|
-
|
|
738
|
-
private
|
|
739
|
-
|
|
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");
|
|
740
1311
|
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
741
1314
|
try {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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) {
|
|
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;
|
|
859
1377
|
}
|
|
860
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
|
+
|
|
861
1437
|
} catch (Throwable t) {
|
|
862
|
-
Log.
|
|
1438
|
+
Log.e(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
|
|
863
1439
|
}
|
|
1440
|
+
|
|
864
1441
|
return false;
|
|
865
1442
|
}
|
|
866
|
-
|
|
867
|
-
|
|
1443
|
+
|
|
1444
|
+
private Class<?> findSdkClass(String name) throws ClassNotFoundException {
|
|
868
1445
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (stop != null)
|
|
874
|
-
stop.invoke(streamerObj);
|
|
875
|
-
} catch (Throwable ignored) {
|
|
876
|
-
}
|
|
1446
|
+
return Class.forName(name);
|
|
1447
|
+
} catch (ClassNotFoundException e) {
|
|
1448
|
+
if (sdkClassLoader != null) {
|
|
1449
|
+
return Class.forName(name, true, sdkClassLoader);
|
|
877
1450
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1451
|
+
throw e;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private ClassLoader getEffectiveClassLoader() {
|
|
1456
|
+
return sdkClassLoader != null ? sdkClassLoader : getClass().getClassLoader();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ==================== HELPERS ====================
|
|
1460
|
+
|
|
1461
|
+
private String extractDeviceId(Object identity) {
|
|
1462
|
+
try {
|
|
1463
|
+
Method getDeviceId = identity.getClass().getMethod("getDeviceId");
|
|
1464
|
+
Object result = getDeviceId.invoke(identity);
|
|
1465
|
+
return result != null ? result.toString() : "unknown";
|
|
1466
|
+
} catch (Throwable t) {
|
|
1467
|
+
return "device_" + System.currentTimeMillis();
|
|
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();
|
|
884
1479
|
}
|
|
885
|
-
}
|
|
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";
|
|
886
1486
|
} catch (Throwable t) {
|
|
887
|
-
|
|
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
|
+
private void notifyError(String error) {
|
|
1506
|
+
Log.e(TAG, "[FLIR] Error: " + error);
|
|
1507
|
+
if (listener != null) {
|
|
1508
|
+
mainHandler.post(() -> listener.onError(error));
|
|
888
1509
|
}
|
|
889
1510
|
}
|
|
890
1511
|
}
|