ilabs-flir 2.2.21 → 2.2.24
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.
|
@@ -46,9 +46,8 @@ dependencies {
|
|
|
46
46
|
// FLIR SDK - Use the actual AAR files from libs folder
|
|
47
47
|
// Using 'api' to expose SDK classes to consumers and Java source files
|
|
48
48
|
// flatDir must be configured in settings.gradle or root build.gradle
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
compileOnly(files("libs/androidsdk-release.aar"))
|
|
50
|
+
compileOnly(files("libs/thermalsdk-release.aar"))
|
|
52
51
|
// Minimal compile deps to satisfy source references
|
|
53
52
|
implementation("androidx.annotation:annotation:1.5.0")
|
|
54
53
|
|
|
@@ -1,386 +1,425 @@
|
|
|
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.Point;
|
|
11
|
-
import com.flir.thermalsdk.image.ThermalValue;
|
|
12
|
-
import com.flir.thermalsdk.live.Camera;
|
|
13
|
-
import com.flir.thermalsdk.live.CommunicationInterface;
|
|
14
|
-
import com.flir.thermalsdk.live.ConnectParameters;
|
|
15
|
-
import com.flir.thermalsdk.live.Identity;
|
|
16
|
-
import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
|
|
17
|
-
import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
|
|
18
|
-
import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
|
|
19
|
-
import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
|
|
20
|
-
import com.flir.thermalsdk.live.streaming.Stream;
|
|
21
|
-
import com.flir.thermalsdk.live.streaming.ThermalStreamer;
|
|
22
|
-
|
|
23
|
-
import java.util.ArrayList;
|
|
24
|
-
import java.util.Collections;
|
|
25
|
-
import java.util.List;
|
|
26
|
-
import java.util.concurrent.Executor;
|
|
27
|
-
import java.util.concurrent.Executors;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Simplified FLIR SDK Manager - matches sample app pattern
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
|
-
public class FlirSdkManager {
|
|
34
|
-
private static final String TAG = "FlirSdkManager";
|
|
35
|
-
|
|
36
|
-
private static FlirSdkManager instance;
|
|
37
|
-
private final Context context;
|
|
38
|
-
private final Executor executor = Executors.newSingleThreadExecutor();
|
|
39
|
-
|
|
40
|
-
// State
|
|
41
|
-
private boolean isInitialized = false;
|
|
42
|
-
private boolean isScanning = false;
|
|
43
|
-
private Camera camera;
|
|
44
|
-
private ThermalStreamer streamer;
|
|
45
|
-
private Stream activeStream;
|
|
46
|
-
private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
|
|
47
|
-
private volatile Bitmap latestBitmap;
|
|
48
|
-
|
|
49
|
-
// Listener
|
|
50
|
-
private Listener listener;
|
|
51
|
-
|
|
52
|
-
public interface Listener {
|
|
53
|
-
void onDeviceFound(Identity identity);
|
|
54
|
-
|
|
55
|
-
void onDeviceListUpdated(List<Identity> devices);
|
|
56
|
-
|
|
57
|
-
void onConnected(Identity identity);
|
|
58
|
-
|
|
59
|
-
void onDisconnected();
|
|
60
|
-
|
|
61
|
-
void onFrame(Bitmap bitmap);
|
|
62
|
-
|
|
63
|
-
void onError(String message);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private FlirSdkManager(Context context) {
|
|
67
|
-
this.context = context.getApplicationContext();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public static synchronized FlirSdkManager getInstance(Context context) {
|
|
71
|
-
if (instance == null) {
|
|
72
|
-
instance = new FlirSdkManager(context);
|
|
73
|
-
}
|
|
74
|
-
return instance;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public void setListener(Listener listener) {
|
|
78
|
-
this.listener = listener;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ==================== INITIALIZE ====================
|
|
82
|
-
|
|
83
|
-
public void initialize() {
|
|
84
|
-
if (isInitialized)
|
|
85
|
-
return;
|
|
86
|
-
try {
|
|
87
|
-
ThermalSdkAndroid.init(context);
|
|
88
|
-
isInitialized = true;
|
|
89
|
-
Log.d(TAG, "SDK initialized");
|
|
90
|
-
} catch (Exception e) {
|
|
91
|
-
Log.e(TAG, "SDK init failed", e);
|
|
92
|
-
notifyError("SDK init failed: " + e.getMessage());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
public boolean isInitialized() {
|
|
97
|
-
return isInitialized;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ==================== DISCOVERY ====================
|
|
101
|
-
|
|
102
|
-
public void scan() {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Log.
|
|
175
|
-
camera =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
@Override
|
|
351
|
-
public void
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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.Point;
|
|
11
|
+
import com.flir.thermalsdk.image.ThermalValue;
|
|
12
|
+
import com.flir.thermalsdk.live.Camera;
|
|
13
|
+
import com.flir.thermalsdk.live.CommunicationInterface;
|
|
14
|
+
import com.flir.thermalsdk.live.ConnectParameters;
|
|
15
|
+
import com.flir.thermalsdk.live.Identity;
|
|
16
|
+
import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
|
|
17
|
+
import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
|
|
18
|
+
import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
|
|
19
|
+
import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
|
|
20
|
+
import com.flir.thermalsdk.live.streaming.Stream;
|
|
21
|
+
import com.flir.thermalsdk.live.streaming.ThermalStreamer;
|
|
22
|
+
|
|
23
|
+
import java.util.ArrayList;
|
|
24
|
+
import java.util.Collections;
|
|
25
|
+
import java.util.List;
|
|
26
|
+
import java.util.concurrent.Executor;
|
|
27
|
+
import java.util.concurrent.Executors;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Simplified FLIR SDK Manager - matches sample app pattern
|
|
31
|
+
* Thread-safe: All lifecycle methods run on a single background executor to prevent native race conditions.
|
|
32
|
+
*/
|
|
33
|
+
public class FlirSdkManager {
|
|
34
|
+
private static final String TAG = "FlirSdkManager";
|
|
35
|
+
|
|
36
|
+
private static FlirSdkManager instance;
|
|
37
|
+
private final Context context;
|
|
38
|
+
private final Executor executor = Executors.newSingleThreadExecutor();
|
|
39
|
+
|
|
40
|
+
// State
|
|
41
|
+
private boolean isInitialized = false;
|
|
42
|
+
private boolean isScanning = false;
|
|
43
|
+
private Camera camera;
|
|
44
|
+
private ThermalStreamer streamer;
|
|
45
|
+
private Stream activeStream;
|
|
46
|
+
private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
|
|
47
|
+
private volatile Bitmap latestBitmap;
|
|
48
|
+
|
|
49
|
+
// Listener
|
|
50
|
+
private Listener listener;
|
|
51
|
+
|
|
52
|
+
public interface Listener {
|
|
53
|
+
void onDeviceFound(Identity identity);
|
|
54
|
+
|
|
55
|
+
void onDeviceListUpdated(List<Identity> devices);
|
|
56
|
+
|
|
57
|
+
void onConnected(Identity identity);
|
|
58
|
+
|
|
59
|
+
void onDisconnected();
|
|
60
|
+
|
|
61
|
+
void onFrame(Bitmap bitmap);
|
|
62
|
+
|
|
63
|
+
void onError(String message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private FlirSdkManager(Context context) {
|
|
67
|
+
this.context = context.getApplicationContext();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public static synchronized FlirSdkManager getInstance(Context context) {
|
|
71
|
+
if (instance == null) {
|
|
72
|
+
instance = new FlirSdkManager(context);
|
|
73
|
+
}
|
|
74
|
+
return instance;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public void setListener(Listener listener) {
|
|
78
|
+
this.listener = listener;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ==================== INITIALIZE ====================
|
|
82
|
+
|
|
83
|
+
public void initialize() {
|
|
84
|
+
if (isInitialized)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
ThermalSdkAndroid.init(context);
|
|
88
|
+
isInitialized = true;
|
|
89
|
+
Log.d(TAG, "SDK initialized");
|
|
90
|
+
} catch (Exception e) {
|
|
91
|
+
Log.e(TAG, "SDK init failed", e);
|
|
92
|
+
notifyError("SDK init failed: " + e.getMessage());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public boolean isInitialized() {
|
|
97
|
+
return isInitialized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ==================== DISCOVERY ====================
|
|
101
|
+
|
|
102
|
+
public void scan() {
|
|
103
|
+
executor.execute(() -> {
|
|
104
|
+
if (!isInitialized) {
|
|
105
|
+
notifyError("SDK not initialized");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isScanning)
|
|
109
|
+
return;
|
|
110
|
+
|
|
111
|
+
isScanning = true;
|
|
112
|
+
discoveredDevices.clear();
|
|
113
|
+
Log.d(TAG, "Starting discovery...");
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
DiscoveryFactory.getInstance().scan(
|
|
117
|
+
discoveryListener,
|
|
118
|
+
CommunicationInterface.EMULATOR,
|
|
119
|
+
CommunicationInterface.USB,
|
|
120
|
+
CommunicationInterface.NETWORK,
|
|
121
|
+
CommunicationInterface.FLIR_ONE_WIRELESS);
|
|
122
|
+
} catch (Exception e) {
|
|
123
|
+
Log.e(TAG, "Scan failed", e);
|
|
124
|
+
isScanning = false;
|
|
125
|
+
notifyError("Scan failed: " + e.getMessage());
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public void stopScan() {
|
|
131
|
+
executor.execute(() -> {
|
|
132
|
+
if (!isScanning)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
DiscoveryFactory.getInstance().stop(
|
|
136
|
+
CommunicationInterface.EMULATOR,
|
|
137
|
+
CommunicationInterface.USB,
|
|
138
|
+
CommunicationInterface.NETWORK,
|
|
139
|
+
CommunicationInterface.FLIR_ONE_WIRELESS);
|
|
140
|
+
} catch (Exception e) {
|
|
141
|
+
Log.e(TAG, "Stop scan failed", e);
|
|
142
|
+
}
|
|
143
|
+
isScanning = false;
|
|
144
|
+
Log.d(TAG, "Discovery stopped");
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public List<Identity> getDiscoveredDevices() {
|
|
149
|
+
return new ArrayList<>(discoveredDevices);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ==================== CONNECTION ====================
|
|
153
|
+
|
|
154
|
+
public void connect(Identity identity) {
|
|
155
|
+
if (identity == null) {
|
|
156
|
+
notifyError("Invalid identity");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Run on background thread (matches sample app pattern)
|
|
161
|
+
executor.execute(() -> {
|
|
162
|
+
try {
|
|
163
|
+
// Disconnect if already connected
|
|
164
|
+
stopStreamInternal();
|
|
165
|
+
if (camera != null) {
|
|
166
|
+
try {
|
|
167
|
+
camera.disconnect();
|
|
168
|
+
} catch (Exception e) {
|
|
169
|
+
Log.e(TAG, "Disconnect error", e);
|
|
170
|
+
}
|
|
171
|
+
camera = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Log.d(TAG, "Connecting to: " + identity.deviceId);
|
|
175
|
+
camera = new Camera();
|
|
176
|
+
camera.connect(identity, connectionStatusListener, new ConnectParameters());
|
|
177
|
+
Log.d(TAG, "Connected to: " + identity.deviceId);
|
|
178
|
+
|
|
179
|
+
if (listener != null) {
|
|
180
|
+
listener.onConnected(identity);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Auto-start stream after connection (matches sample app)
|
|
184
|
+
startStreamInternal();
|
|
185
|
+
|
|
186
|
+
} catch (Exception e) {
|
|
187
|
+
Log.e(TAG, "Connection failed", e);
|
|
188
|
+
camera = null;
|
|
189
|
+
notifyError("Connection failed: " + e.getMessage());
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public void disconnect() {
|
|
195
|
+
executor.execute(() -> {
|
|
196
|
+
stopStreamInternal();
|
|
197
|
+
|
|
198
|
+
if (camera != null) {
|
|
199
|
+
try {
|
|
200
|
+
camera.disconnect();
|
|
201
|
+
} catch (Exception e) {
|
|
202
|
+
Log.e(TAG, "Disconnect error", e);
|
|
203
|
+
}
|
|
204
|
+
camera = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (listener != null) {
|
|
208
|
+
listener.onDisconnected();
|
|
209
|
+
}
|
|
210
|
+
Log.d(TAG, "Disconnected");
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public boolean isConnected() {
|
|
215
|
+
return camera != null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ==================== STREAMING ====================
|
|
219
|
+
|
|
220
|
+
public void startStream() {
|
|
221
|
+
executor.execute(this::startStreamInternal);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private void startStreamInternal() {
|
|
225
|
+
if (camera == null) {
|
|
226
|
+
notifyError("Not connected");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (!camera.isConnected()) {
|
|
232
|
+
Log.e(TAG, "Camera not connected, cannot start stream");
|
|
233
|
+
notifyError("Camera not connected");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
List<Stream> streams = camera.getStreams();
|
|
238
|
+
if (streams == null || streams.isEmpty()) {
|
|
239
|
+
notifyError("No streams available");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find thermal stream or use first
|
|
244
|
+
Stream thermalStream = null;
|
|
245
|
+
for (Stream stream : streams) {
|
|
246
|
+
if (stream.isThermal()) {
|
|
247
|
+
thermalStream = stream;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (thermalStream == null) {
|
|
252
|
+
thermalStream = streams.get(0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
activeStream = thermalStream;
|
|
256
|
+
streamer = new ThermalStreamer(thermalStream);
|
|
257
|
+
|
|
258
|
+
// Start stream with simple callback (matches sample app)
|
|
259
|
+
thermalStream.start(
|
|
260
|
+
unused -> {
|
|
261
|
+
executor.execute(() -> {
|
|
262
|
+
try {
|
|
263
|
+
if (streamer != null && activeStream != null) {
|
|
264
|
+
streamer.update();
|
|
265
|
+
Bitmap bitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
|
|
266
|
+
if (bitmap != null) {
|
|
267
|
+
latestBitmap = bitmap;
|
|
268
|
+
if (listener != null) {
|
|
269
|
+
listener.onFrame(bitmap);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (Exception e) {
|
|
274
|
+
Log.e(TAG, "Frame error", e);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
error -> {
|
|
279
|
+
executor.execute(() -> {
|
|
280
|
+
Log.e(TAG, "Stream error: " + error);
|
|
281
|
+
notifyError("Stream error: " + error);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
Log.d(TAG, "Streaming started");
|
|
286
|
+
|
|
287
|
+
} catch (Exception e) {
|
|
288
|
+
Log.e(TAG, "Start stream failed", e);
|
|
289
|
+
notifyError("Stream failed: " + e.getMessage());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public void stopStream() {
|
|
294
|
+
executor.execute(this::stopStreamInternal);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private void stopStreamInternal() {
|
|
298
|
+
if (activeStream != null) {
|
|
299
|
+
try {
|
|
300
|
+
activeStream.stop();
|
|
301
|
+
} catch (Exception e) {
|
|
302
|
+
Log.e(TAG, "Stop stream error", e);
|
|
303
|
+
}
|
|
304
|
+
activeStream = null;
|
|
305
|
+
}
|
|
306
|
+
streamer = null;
|
|
307
|
+
latestBitmap = null;
|
|
308
|
+
Log.d(TAG, "Streaming stopped");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public Bitmap getLatestBitmap() {
|
|
312
|
+
return latestBitmap;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ==================== TEMPERATURE ====================
|
|
316
|
+
|
|
317
|
+
public double getTemperatureAt(int x, int y) {
|
|
318
|
+
// Run on the same thread to avoid concurrent access to 'streamer'
|
|
319
|
+
final double[] result = { Double.NaN };
|
|
320
|
+
// This query remains synchronous for the caller but synchronized with the stream updates
|
|
321
|
+
synchronized (this) {
|
|
322
|
+
if (streamer == null)
|
|
323
|
+
return Double.NaN;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
streamer.withThermalImage(thermalImage -> {
|
|
327
|
+
try {
|
|
328
|
+
int w = thermalImage.getWidth();
|
|
329
|
+
int h = thermalImage.getHeight();
|
|
330
|
+
int cx = Math.max(0, Math.min(w - 1, x));
|
|
331
|
+
int cy = Math.max(0, Math.min(h - 1, y));
|
|
332
|
+
ThermalValue value = thermalImage.getValueAt(new Point(cx, cy));
|
|
333
|
+
if (value != null) {
|
|
334
|
+
result[0] = value.asCelsius().value;
|
|
335
|
+
}
|
|
336
|
+
} catch (Exception e) {
|
|
337
|
+
Log.w(TAG, "Temp query error", e);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
} catch (Exception e) {
|
|
341
|
+
Log.w(TAG, "Temp query failed", e);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return result[0];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ==================== LISTENERS ====================
|
|
348
|
+
|
|
349
|
+
private final DiscoveryEventListener discoveryListener = new DiscoveryEventListener() {
|
|
350
|
+
@Override
|
|
351
|
+
public void onCameraFound(DiscoveredCamera discoveredCamera) {
|
|
352
|
+
Identity identity = discoveredCamera.getIdentity();
|
|
353
|
+
Log.d(TAG, "Device found: " + identity.deviceId);
|
|
354
|
+
|
|
355
|
+
synchronized (discoveredDevices) {
|
|
356
|
+
boolean exists = false;
|
|
357
|
+
for (Identity d : discoveredDevices) {
|
|
358
|
+
if (d.deviceId.equals(identity.deviceId)) {
|
|
359
|
+
exists = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!exists) {
|
|
364
|
+
discoveredDevices.add(identity);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (listener != null) {
|
|
369
|
+
listener.onDeviceFound(identity);
|
|
370
|
+
listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@Override
|
|
375
|
+
public void onCameraLost(Identity identity) {
|
|
376
|
+
Log.d(TAG, "Device lost: " + identity.deviceId);
|
|
377
|
+
synchronized (discoveredDevices) {
|
|
378
|
+
discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
|
|
379
|
+
}
|
|
380
|
+
if (listener != null) {
|
|
381
|
+
listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@Override
|
|
386
|
+
public void onDiscoveryError(CommunicationInterface iface, ErrorCode error) {
|
|
387
|
+
Log.e(TAG, "Discovery error: " + iface + " - " + error);
|
|
388
|
+
notifyError("Discovery error: " + error);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@Override
|
|
392
|
+
public void onDiscoveryFinished(CommunicationInterface iface) {
|
|
393
|
+
Log.d(TAG, "Discovery finished: " + iface);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
private final ConnectionStatusListener connectionStatusListener = errorCode -> {
|
|
398
|
+
executor.execute(() -> {
|
|
399
|
+
Log.d(TAG, "Disconnected callback: " + (errorCode != null ? errorCode : "clean"));
|
|
400
|
+
camera = null;
|
|
401
|
+
if (listener != null) {
|
|
402
|
+
listener.onDisconnected();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
private void notifyError(String message) {
|
|
408
|
+
if (listener != null) {
|
|
409
|
+
listener.onError(message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ==================== CLEANUP ====================
|
|
414
|
+
|
|
415
|
+
public void destroy() {
|
|
416
|
+
stopScan();
|
|
417
|
+
disconnect();
|
|
418
|
+
executor.execute(() -> {
|
|
419
|
+
discoveredDevices.clear();
|
|
420
|
+
listener = null;
|
|
421
|
+
instance = null;
|
|
422
|
+
Log.d(TAG, "Destroyed");
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
package/app.plugin.js
CHANGED
|
@@ -48,8 +48,7 @@ const EXTERNAL_ACCESSORY_PROTOCOLS = [
|
|
|
48
48
|
|
|
49
49
|
// Bonjour services for FLIR network discovery
|
|
50
50
|
const BONJOUR_SERVICES = [
|
|
51
|
-
'_flir._tcp',
|
|
52
|
-
'_http._tcp',
|
|
51
|
+
'_flir-ircam._tcp',
|
|
53
52
|
];
|
|
54
53
|
|
|
55
54
|
// Default permission descriptions
|
|
@@ -182,7 +181,8 @@ const withFlirInfoPlist = (config, props = {}) => {
|
|
|
182
181
|
*/
|
|
183
182
|
const withFlirEntitlements = (config) => {
|
|
184
183
|
return withEntitlementsPlist(config, (config) => {
|
|
185
|
-
//
|
|
184
|
+
// Required to read current WiFi SSID for direct connections
|
|
185
|
+
config.modResults['com.apple.developer.networking.wifi-info'] = true;
|
|
186
186
|
return config;
|
|
187
187
|
});
|
|
188
188
|
};
|
|
@@ -252,6 +252,10 @@ const withFlirAndroidManifest = (config, props = {}) => {
|
|
|
252
252
|
addPermission('android.permission.INTERNET');
|
|
253
253
|
addPermission('android.permission.ACCESS_NETWORK_STATE');
|
|
254
254
|
addPermission('android.permission.ACCESS_WIFI_STATE');
|
|
255
|
+
addPermission('android.permission.CHANGE_WIFI_STATE');
|
|
256
|
+
addPermission('android.permission.CHANGE_NETWORK_STATE');
|
|
257
|
+
addPermission('android.permission.CHANGE_WIFI_MULTICAST_STATE');
|
|
258
|
+
addPermission('android.permission.NEARBY_WIFI_DEVICES');
|
|
255
259
|
addPermission('android.permission.BLUETOOTH');
|
|
256
260
|
addPermission('android.permission.BLUETOOTH_ADMIN');
|
|
257
261
|
addPermission('android.permission.BLUETOOTH_CONNECT');
|
|
@@ -111,8 +111,8 @@ import ThermalSDK
|
|
|
111
111
|
discovery?.delegate = self
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// Match sample app: discover lightning + wireless + emulator
|
|
115
|
-
discovery?.start([.lightning, .flirOneWireless, .emulator])
|
|
114
|
+
// Match sample app: discover lightning + wireless + emulator + network
|
|
115
|
+
discovery?.start([.lightning, .flirOneWireless, .emulator, .network])
|
|
116
116
|
|
|
117
117
|
emitStateChange("discovering")
|
|
118
118
|
#else
|
|
@@ -162,6 +162,18 @@ import ThermalSDK
|
|
|
162
162
|
return
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Authenticate if generic network camera
|
|
166
|
+
if identity.cameraType() == .generic {
|
|
167
|
+
var status = FLIRAuthenticationStatus.pending
|
|
168
|
+
let certName = (Bundle.main.bundleIdentifier ?? "ThermalCamera") + "-cert"
|
|
169
|
+
while status == .pending {
|
|
170
|
+
status = cam.authenticate(identity, trustedConnectionName: certName)
|
|
171
|
+
if status == .pending {
|
|
172
|
+
Thread.sleep(forTimeInterval: 0.2)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
165
177
|
// Pair and connect (matches sample app pattern)
|
|
166
178
|
try cam.pair(identity, code: 0)
|
|
167
179
|
try cam.connect()
|