ilabs-flir 2.3.10 → 2.3.11
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.
|
@@ -23,12 +23,15 @@ import com.flir.thermalsdk.live.connectivity.ConnectionStatusListener;
|
|
|
23
23
|
import com.flir.thermalsdk.live.discovery.DiscoveredCamera;
|
|
24
24
|
import com.flir.thermalsdk.live.discovery.DiscoveryEventListener;
|
|
25
25
|
import com.flir.thermalsdk.live.discovery.DiscoveryFactory;
|
|
26
|
+
import com.flir.thermalsdk.androidsdk.live.connectivity.SdkWifiConnectionHelper;
|
|
26
27
|
import com.flir.thermalsdk.live.streaming.Stream;
|
|
27
28
|
import com.flir.thermalsdk.live.streaming.ThermalStreamer;
|
|
28
29
|
|
|
29
30
|
import java.util.ArrayList;
|
|
30
31
|
import java.util.Collections;
|
|
32
|
+
import java.util.HashMap;
|
|
31
33
|
import java.util.List;
|
|
34
|
+
import java.util.Map;
|
|
32
35
|
import java.util.concurrent.Executor;
|
|
33
36
|
import java.util.concurrent.Executors;
|
|
34
37
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
@@ -51,6 +54,7 @@ public class FlirSdkManager {
|
|
|
51
54
|
private ThermalStreamer streamer;
|
|
52
55
|
private Stream activeStream;
|
|
53
56
|
private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
|
|
57
|
+
private final Map<String, DiscoveredCamera> discoveredCameras = Collections.synchronizedMap(new HashMap<>());
|
|
54
58
|
private volatile Bitmap latestBitmap;
|
|
55
59
|
private volatile String currentPaletteName = "WhiteHot";
|
|
56
60
|
private final AtomicBoolean isProcessingFrame = new AtomicBoolean(false);
|
|
@@ -152,25 +156,19 @@ public class FlirSdkManager {
|
|
|
152
156
|
|
|
153
157
|
public void stopScan() {
|
|
154
158
|
if (!isScanning) return;
|
|
155
|
-
|
|
156
|
-
// Use a temporary flag to prevent concurrent stop calls
|
|
157
159
|
isScanning = false;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// We catch it silently as it means the SDK already cleaned up or is in a weird state.
|
|
171
|
-
Log.w(TAG, "Stop scan warning (internal SDK): " + e.getMessage());
|
|
172
|
-
}
|
|
173
|
-
});
|
|
160
|
+
executor.execute(this::stopScanInternal);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private void stopScanInternal() {
|
|
164
|
+
try {
|
|
165
|
+
Log.d(TAG, "Stopping discovery...");
|
|
166
|
+
// Use zero-arg stop() as seen in official samples to stop all scanners
|
|
167
|
+
DiscoveryFactory.getInstance().stop();
|
|
168
|
+
Log.d(TAG, "Discovery stopped successfully");
|
|
169
|
+
} catch (Exception e) {
|
|
170
|
+
Log.w(TAG, "Stop scan warning (internal SDK): " + e.getMessage());
|
|
171
|
+
}
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
public List<Identity> getDiscoveredDevices() {
|
|
@@ -199,62 +197,92 @@ public class FlirSdkManager {
|
|
|
199
197
|
camera = null;
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
200
|
+
// ── FLIR ONE WIRELESS (WiFi) Connection ──
|
|
201
|
+
// Matches the official FlirOneWireless sample. We must connect to the
|
|
202
|
+
// camera's WiFi Access Point before calling camera.connect().
|
|
203
|
+
if (identity.communicationInterface == CommunicationInterface.FLIR_ONE_WIRELESS) {
|
|
204
|
+
DiscoveredCamera dc = getDiscoveredCamera(identity.deviceId);
|
|
205
|
+
if (dc != null && dc.cameraDetails != null) {
|
|
206
|
+
String ssid = dc.cameraDetails.ssid;
|
|
207
|
+
Log.d(TAG, "Establishing WiFi connection to: " + ssid);
|
|
208
|
+
|
|
209
|
+
if (!SdkWifiConnectionHelper.isConnectedToNetwork(context, ssid)) {
|
|
210
|
+
// This is a blocking-style wrapper for simplicity in the executor thread
|
|
211
|
+
final AtomicBoolean wifiDone = new AtomicBoolean(false);
|
|
212
|
+
final AtomicBoolean wifiSuccess = new AtomicBoolean(false);
|
|
213
|
+
|
|
214
|
+
SdkWifiConnectionHelper.connectToWifiWithoutCode(context, dc.cameraDetails, status -> {
|
|
215
|
+
if (status.status == SdkWifiConnectionHelper.ConInfo.CONNECTED) {
|
|
216
|
+
wifiSuccess.set(true);
|
|
217
|
+
wifiDone.set(true);
|
|
218
|
+
} else if (status.status == SdkWifiConnectionHelper.ConInfo.ERROR) {
|
|
219
|
+
wifiSuccess.set(false);
|
|
220
|
+
wifiDone.set(true);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Wait for WiFi connection (max 15 seconds)
|
|
225
|
+
int waitLoops = 0;
|
|
226
|
+
while (!wifiDone.get() && waitLoops < 30) {
|
|
227
|
+
try { Thread.sleep(500); } catch (Exception ignored) {}
|
|
228
|
+
waitLoops++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!wifiSuccess.get()) {
|
|
232
|
+
notifyError("Failed to connect to camera WiFi: " + ssid);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
223
236
|
}
|
|
237
|
+
}
|
|
224
238
|
|
|
239
|
+
// ── NETWORK Authentication (Required for A/T-series, NOT Wireless) ──
|
|
240
|
+
if (identity.communicationInterface == CommunicationInterface.NETWORK) {
|
|
241
|
+
Log.d(TAG, "Authenticating with network camera: " + identity.deviceId);
|
|
242
|
+
|
|
243
|
+
if (isScanning) {
|
|
244
|
+
isScanning = false;
|
|
245
|
+
stopScanInternal();
|
|
246
|
+
try { Thread.sleep(500); } catch (Exception ignored) {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
camera = new Camera();
|
|
250
|
+
String authName = "ThermalCameraFx";
|
|
225
251
|
AuthenticationResponse response;
|
|
226
252
|
int attempts = 0;
|
|
227
|
-
final int MAX_AUTH_ATTEMPTS =
|
|
253
|
+
final int MAX_AUTH_ATTEMPTS = 3;
|
|
254
|
+
|
|
228
255
|
do {
|
|
229
|
-
response = camera.authenticate(identity, authName,
|
|
230
|
-
41 * 1000); // 41-second timeout per attempt
|
|
231
|
-
Log.d(TAG, "Auth attempt " + (attempts + 1) +
|
|
232
|
-
" status: " + response.authenticationStatus);
|
|
233
|
-
|
|
234
|
-
if (response.authenticationStatus ==
|
|
235
|
-
AuthenticationResponse.AuthenticationStatus.PENDING) {
|
|
236
|
-
// Camera is waiting for user to press "Trust" on its screen
|
|
237
|
-
Thread.sleep(1000);
|
|
238
|
-
}
|
|
239
256
|
attempts++;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
response = camera.authenticate(identity, authName, 41 * 1000);
|
|
258
|
+
if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.APPROVED) {
|
|
259
|
+
break;
|
|
260
|
+
} else if (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.REJECTED) {
|
|
261
|
+
notifyError("Authentication rejected by camera");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} while (response.authenticationStatus == AuthenticationResponse.AuthenticationStatus.PENDING && attempts < MAX_AUTH_ATTEMPTS);
|
|
265
|
+
|
|
266
|
+
if (response.authenticationStatus != AuthenticationResponse.AuthenticationStatus.APPROVED) {
|
|
267
|
+
notifyError("Authentication failed: " + response.authenticationStatus.name());
|
|
251
268
|
return;
|
|
252
269
|
}
|
|
253
|
-
|
|
270
|
+
} else {
|
|
271
|
+
// For all other types, stop scan if still running
|
|
272
|
+
if (isScanning) {
|
|
273
|
+
isScanning = false;
|
|
274
|
+
stopScanInternal();
|
|
275
|
+
try { Thread.sleep(300); } catch (Exception ignored) {}
|
|
276
|
+
}
|
|
277
|
+
camera = identity.camera;
|
|
278
|
+
if (camera == null) {
|
|
279
|
+
camera = new Camera();
|
|
280
|
+
}
|
|
254
281
|
}
|
|
255
282
|
|
|
283
|
+
Log.d(TAG, "Calling camera.connect() for " + identity.deviceId);
|
|
256
284
|
camera.connect(identity, connectionStatusListener, new ConnectParameters());
|
|
257
|
-
Log.d(TAG, "
|
|
285
|
+
Log.d(TAG, "camera.connect() returned for " + identity.deviceId + ". isConnected=" + camera.isConnected());
|
|
258
286
|
|
|
259
287
|
if (listener != null) {
|
|
260
288
|
listener.onConnected(identity);
|
|
@@ -264,7 +292,7 @@ public class FlirSdkManager {
|
|
|
264
292
|
startStreamInternal();
|
|
265
293
|
|
|
266
294
|
} catch (Exception e) {
|
|
267
|
-
Log.e(TAG, "Connection failed", e);
|
|
295
|
+
Log.e(TAG, "Connection failed for " + identity.deviceId, e);
|
|
268
296
|
camera = null;
|
|
269
297
|
notifyError("Connection failed: " + e.getMessage());
|
|
270
298
|
}
|
|
@@ -295,6 +323,10 @@ public class FlirSdkManager {
|
|
|
295
323
|
return camera != null;
|
|
296
324
|
}
|
|
297
325
|
|
|
326
|
+
private DiscoveredCamera getDiscoveredCamera(String deviceId) {
|
|
327
|
+
return discoveredCameras.get(deviceId);
|
|
328
|
+
}
|
|
329
|
+
|
|
298
330
|
// ==================== STREAMING ====================
|
|
299
331
|
|
|
300
332
|
public void startStream() {
|
|
@@ -317,18 +349,50 @@ public class FlirSdkManager {
|
|
|
317
349
|
}
|
|
318
350
|
|
|
319
351
|
try {
|
|
352
|
+
// RETRY MECHANISM: For Network/Wireless cameras, the connection might take
|
|
353
|
+
// a few hundred milliseconds to stabilize at the native level even after
|
|
354
|
+
// camera.connect() returns. We retry for up to 3 seconds.
|
|
355
|
+
int retries = 0;
|
|
356
|
+
final int MAX_RETRIES = 15; // 15 * 200ms = 3 seconds
|
|
357
|
+
while (!camera.isConnected() && retries < MAX_RETRIES) {
|
|
358
|
+
try {
|
|
359
|
+
Thread.sleep(200);
|
|
360
|
+
} catch (InterruptedException ignored) {}
|
|
361
|
+
retries++;
|
|
362
|
+
if (retries % 5 == 0) {
|
|
363
|
+
Log.d(TAG, "Waiting for camera connection state to stabilize... (attempt " + retries + ")");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
320
367
|
if (!camera.isConnected()) {
|
|
321
|
-
Log.e(TAG, "Camera
|
|
368
|
+
Log.e(TAG, "Camera failed to report connected state after " + (retries * 200) + "ms");
|
|
322
369
|
notifyError("Camera not connected");
|
|
323
370
|
return;
|
|
324
371
|
}
|
|
325
372
|
|
|
326
|
-
|
|
373
|
+
Log.d(TAG, "Camera connected state confirmed after " + (retries * 200) + "ms");
|
|
374
|
+
|
|
375
|
+
// RETRY MECHANISM for getStreams: Sometimes streams are not immediately available
|
|
376
|
+
List<Stream> streams = null;
|
|
377
|
+
retries = 0;
|
|
378
|
+
while (retries < 10) { // 10 * 200ms = 2 seconds
|
|
379
|
+
streams = camera.getStreams();
|
|
380
|
+
if (streams != null && !streams.isEmpty()) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
Thread.sleep(200);
|
|
385
|
+
} catch (InterruptedException ignored) {}
|
|
386
|
+
retries++;
|
|
387
|
+
}
|
|
388
|
+
|
|
327
389
|
if (streams == null || streams.isEmpty()) {
|
|
328
390
|
notifyError("No streams available");
|
|
329
391
|
return;
|
|
330
392
|
}
|
|
331
393
|
|
|
394
|
+
Log.d(TAG, "Streams found: " + streams.size() + " (after " + (retries * 200) + "ms)");
|
|
395
|
+
|
|
332
396
|
// Find thermal stream or use first
|
|
333
397
|
Stream thermalStream = null;
|
|
334
398
|
for (Stream stream : streams) {
|
|
@@ -531,6 +595,8 @@ public class FlirSdkManager {
|
|
|
531
595
|
Identity identity = discoveredCamera.getIdentity();
|
|
532
596
|
Log.d(TAG, "Device found: " + identity.deviceId);
|
|
533
597
|
|
|
598
|
+
discoveredCameras.put(identity.deviceId, discoveredCamera);
|
|
599
|
+
|
|
534
600
|
synchronized (discoveredDevices) {
|
|
535
601
|
boolean exists = false;
|
|
536
602
|
for (Identity d : discoveredDevices) {
|
|
@@ -553,6 +619,7 @@ public class FlirSdkManager {
|
|
|
553
619
|
@Override
|
|
554
620
|
public void onCameraLost(Identity identity) {
|
|
555
621
|
Log.d(TAG, "Device lost: " + identity.deviceId);
|
|
622
|
+
discoveredCameras.remove(identity.deviceId);
|
|
556
623
|
synchronized (discoveredDevices) {
|
|
557
624
|
discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
|
|
558
625
|
}
|
|
@@ -162,7 +162,13 @@ import ThermalSDK
|
|
|
162
162
|
disconnect()
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
165
|
+
// Stop discovery before connecting to free up SDK resources
|
|
166
|
+
// This prevents resource contention in the native SDK layer.
|
|
167
|
+
if discovery != nil {
|
|
168
|
+
discovery?.stop()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Connect on background thread (matches sample app pattern)
|
|
166
172
|
DispatchQueue.global().async { [weak self] in
|
|
167
173
|
guard let self = self else { return }
|
|
168
174
|
|