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