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
- api(files("libs/androidsdk-release.aar"))
50
- api(files("libs/thermalsdk-release.aar"))
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
- * Simple: scan connect stream disconnect
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
- if (!isInitialized) {
104
- notifyError("SDK not initialized");
105
- return;
106
- }
107
- if (isScanning)
108
- return;
109
-
110
- isScanning = true;
111
- discoveredDevices.clear();
112
- Log.d(TAG, "Starting discovery...");
113
-
114
- try {
115
- DiscoveryFactory.getInstance().scan(
116
- discoveryListener,
117
- CommunicationInterface.EMULATOR,
118
- CommunicationInterface.USB);
119
- } catch (Exception e) {
120
- Log.e(TAG, "Scan failed", e);
121
- isScanning = false;
122
- notifyError("Scan failed: " + e.getMessage());
123
- }
124
- }
125
-
126
- public void stopScan() {
127
- if (!isScanning)
128
- return;
129
- try {
130
- DiscoveryFactory.getInstance().stop(
131
- CommunicationInterface.EMULATOR,
132
- CommunicationInterface.USB);
133
- } catch (Exception e) {
134
- Log.e(TAG, "Stop scan failed", e);
135
- }
136
- isScanning = false;
137
- Log.d(TAG, "Discovery stopped");
138
- }
139
-
140
- public List<Identity> getDiscoveredDevices() {
141
- return new ArrayList<>(discoveredDevices);
142
- }
143
-
144
- // ==================== CONNECTION ====================
145
-
146
- public void connect(Identity identity) {
147
- if (identity == null) {
148
- notifyError("Invalid identity");
149
- return;
150
- }
151
-
152
- // Disconnect if already connected
153
- if (camera != null) {
154
- disconnect();
155
- }
156
-
157
- Log.d(TAG, "Connecting to: " + identity.deviceId);
158
-
159
- // Run on background thread (matches sample app pattern)
160
- executor.execute(() -> {
161
- try {
162
- camera = new Camera();
163
- camera.connect(identity, connectionStatusListener, new ConnectParameters());
164
- Log.d(TAG, "Connected to: " + identity.deviceId);
165
-
166
- if (listener != null) {
167
- listener.onConnected(identity);
168
- }
169
-
170
- // Auto-start stream after connection (matches sample app)
171
- startStream();
172
-
173
- } catch (Exception e) {
174
- Log.e(TAG, "Connection failed", e);
175
- camera = null;
176
- notifyError("Connection failed: " + e.getMessage());
177
- }
178
- });
179
- }
180
-
181
- public void disconnect() {
182
- stopStream();
183
-
184
- if (camera != null) {
185
- try {
186
- camera.disconnect();
187
- } catch (Exception e) {
188
- Log.e(TAG, "Disconnect error", e);
189
- }
190
- camera = null;
191
- }
192
-
193
- if (listener != null) {
194
- listener.onDisconnected();
195
- }
196
- Log.d(TAG, "Disconnected");
197
- }
198
-
199
- public boolean isConnected() {
200
- return camera != null;
201
- }
202
-
203
- // ==================== STREAMING ====================
204
-
205
- public void startStream() {
206
- if (camera == null) {
207
- notifyError("Not connected");
208
- return;
209
- }
210
-
211
- executor.execute(() -> {
212
- try {
213
- List<Stream> streams = camera.getStreams();
214
- if (streams == null || streams.isEmpty()) {
215
- notifyError("No streams available");
216
- return;
217
- }
218
-
219
- // Find thermal stream or use first
220
- Stream thermalStream = null;
221
- for (Stream stream : streams) {
222
- if (stream.isThermal()) {
223
- thermalStream = stream;
224
- break;
225
- }
226
- }
227
- if (thermalStream == null) {
228
- thermalStream = streams.get(0);
229
- }
230
-
231
- activeStream = thermalStream;
232
- streamer = new ThermalStreamer(thermalStream);
233
-
234
- // Start stream with simple callback (matches sample app)
235
- thermalStream.start(
236
- unused -> {
237
- try {
238
- if (streamer != null) {
239
- streamer.update();
240
- Bitmap bitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
241
- if (bitmap != null) {
242
- latestBitmap = bitmap;
243
- if (listener != null) {
244
- listener.onFrame(bitmap);
245
- }
246
- }
247
- }
248
- } catch (Exception e) {
249
- Log.e(TAG, "Frame error", e);
250
- }
251
- },
252
- error -> {
253
- Log.e(TAG, "Stream error: " + error);
254
- notifyError("Stream error: " + error);
255
- });
256
-
257
- Log.d(TAG, "Streaming started");
258
-
259
- } catch (Exception e) {
260
- Log.e(TAG, "Start stream failed", e);
261
- notifyError("Stream failed: " + e.getMessage());
262
- }
263
- });
264
- }
265
-
266
- public void stopStream() {
267
- if (activeStream != null) {
268
- try {
269
- activeStream.stop();
270
- } catch (Exception e) {
271
- Log.e(TAG, "Stop stream error", e);
272
- }
273
- activeStream = null;
274
- }
275
- streamer = null;
276
- latestBitmap = null;
277
- Log.d(TAG, "Streaming stopped");
278
- }
279
-
280
- public Bitmap getLatestBitmap() {
281
- return latestBitmap;
282
- }
283
-
284
- // ==================== TEMPERATURE ====================
285
-
286
- public double getTemperatureAt(int x, int y) {
287
- if (streamer == null)
288
- return Double.NaN;
289
-
290
- final double[] result = { Double.NaN };
291
- try {
292
- streamer.withThermalImage(thermalImage -> {
293
- try {
294
- int w = thermalImage.getWidth();
295
- int h = thermalImage.getHeight();
296
- int cx = Math.max(0, Math.min(w - 1, x));
297
- int cy = Math.max(0, Math.min(h - 1, y));
298
- ThermalValue value = thermalImage.getValueAt(new Point(cx, cy));
299
- if (value != null) {
300
- result[0] = value.asCelsius().value;
301
- }
302
- } catch (Exception e) {
303
- Log.w(TAG, "Temp query error", e);
304
- }
305
- });
306
- } catch (Exception e) {
307
- Log.w(TAG, "Temp query failed", e);
308
- }
309
- return result[0];
310
- }
311
-
312
- // ==================== LISTENERS ====================
313
-
314
- private final DiscoveryEventListener discoveryListener = new DiscoveryEventListener() {
315
- @Override
316
- public void onCameraFound(DiscoveredCamera discoveredCamera) {
317
- Identity identity = discoveredCamera.getIdentity();
318
- Log.d(TAG, "Device found: " + identity.deviceId);
319
-
320
- synchronized (discoveredDevices) {
321
- boolean exists = false;
322
- for (Identity d : discoveredDevices) {
323
- if (d.deviceId.equals(identity.deviceId)) {
324
- exists = true;
325
- break;
326
- }
327
- }
328
- if (!exists) {
329
- discoveredDevices.add(identity);
330
- }
331
- }
332
-
333
- if (listener != null) {
334
- listener.onDeviceFound(identity);
335
- listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
336
- }
337
- }
338
-
339
- @Override
340
- public void onCameraLost(Identity identity) {
341
- Log.d(TAG, "Device lost: " + identity.deviceId);
342
- synchronized (discoveredDevices) {
343
- discoveredDevices.removeIf(d -> d.deviceId.equals(identity.deviceId));
344
- }
345
- if (listener != null) {
346
- listener.onDeviceListUpdated(new ArrayList<>(discoveredDevices));
347
- }
348
- }
349
-
350
- @Override
351
- public void onDiscoveryError(CommunicationInterface iface, ErrorCode error) {
352
- Log.e(TAG, "Discovery error: " + iface + " - " + error);
353
- notifyError("Discovery error: " + error);
354
- }
355
-
356
- @Override
357
- public void onDiscoveryFinished(CommunicationInterface iface) {
358
- Log.d(TAG, "Discovery finished: " + iface);
359
- }
360
- };
361
-
362
- private final ConnectionStatusListener connectionStatusListener = errorCode -> {
363
- Log.d(TAG, "Disconnected: " + (errorCode != null ? errorCode : "clean"));
364
- camera = null;
365
- if (listener != null) {
366
- listener.onDisconnected();
367
- }
368
- };
369
-
370
- private void notifyError(String message) {
371
- if (listener != null) {
372
- listener.onError(message);
373
- }
374
- }
375
-
376
- // ==================== CLEANUP ====================
377
-
378
- public void destroy() {
379
- stopScan();
380
- disconnect();
381
- discoveredDevices.clear();
382
- listener = null;
383
- instance = null;
384
- Log.d(TAG, "Destroyed");
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
- // Currently no special entitlements needed
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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.2.21",
3
+ "version": "2.2.24",
4
4
  "description": "FLIR Thermal SDK for React Native - iOS & Android (bundled at compile time via postinstall)",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",