ilabs-flir 2.3.8 → 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.
- package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +465 -509
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +391 -375
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +177 -93
- package/ios/Flir/src/FlirManager.swift +52 -56
- package/ios/Flir/src/FlirPreviewView.m +0 -3
- package/ios/Flir/src/FlirState.m +9 -3
- package/package.json +1 -1
|
@@ -1,509 +1,465 @@
|
|
|
1
|
-
package flir.android
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.graphics.Bitmap
|
|
5
|
-
import android.util.Log
|
|
6
|
-
import com.facebook.react.bridge.Arguments
|
|
7
|
-
import com.facebook.react.bridge.ReactContext
|
|
8
|
-
import com.facebook.react.bridge.WritableArray
|
|
9
|
-
import com.facebook.react.bridge.WritableMap
|
|
10
|
-
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
|
-
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
-
import com.flir.thermalsdk.live.Identity
|
|
13
|
-
import com.flir.thermalsdk.image.Palette
|
|
14
|
-
import com.flir.thermalsdk.image.PaletteManager
|
|
15
|
-
import java.io.File
|
|
16
|
-
import java.io.FileOutputStream
|
|
17
|
-
import java.util.concurrent.atomic.AtomicLong
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Simplified FlirManager - bridge between React Native and FlirSdkManager
|
|
21
|
-
* Matches the simplified pattern: scan -> connect -> stream -> disconnect
|
|
22
|
-
*/
|
|
23
|
-
object FlirManager {
|
|
24
|
-
private const val TAG = "FlirManager"
|
|
25
|
-
|
|
26
|
-
private var sdkManager: FlirSdkManager? = null
|
|
27
|
-
private var reactContext: ReactContext? = null
|
|
28
|
-
|
|
29
|
-
// Frame rate limiting for RN events
|
|
30
|
-
private val lastEmitMs = AtomicLong(0)
|
|
31
|
-
private val minEmitIntervalMs = 100L // ~10 fps max for RN events
|
|
32
|
-
|
|
33
|
-
// Cached palette list to avoid repeated JNI calls (especially if linkage is unstable)
|
|
34
|
-
private var cachedPalettes: List<*>? = null
|
|
35
|
-
private var cachedPaletteNames: List<String>? = null
|
|
36
|
-
|
|
37
|
-
// Cached reflection for performance
|
|
38
|
-
private var coolField: java.lang.reflect.Field? = null
|
|
39
|
-
|
|
40
|
-
// State
|
|
41
|
-
private var isInitialized = false
|
|
42
|
-
private var isScanning = false
|
|
43
|
-
private var isConnected = false
|
|
44
|
-
private var isStreaming = false
|
|
45
|
-
private var connectedDeviceId: String? = null
|
|
46
|
-
private var connectedDeviceName: String? = null
|
|
47
|
-
|
|
48
|
-
// Concurrency control
|
|
49
|
-
private val shouldProcessFrames = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
50
|
-
private val isUpdatingTexture = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
51
|
-
|
|
52
|
-
// Latest bitmap
|
|
53
|
-
private var latestBitmap: Bitmap? = null
|
|
54
|
-
|
|
55
|
-
// Callbacks
|
|
56
|
-
interface TextureUpdateCallback {
|
|
57
|
-
fun onTextureUpdate(bitmap: Bitmap, textureUnit: Int)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private var textureCallback: TextureUpdateCallback? = null
|
|
61
|
-
|
|
62
|
-
fun setTextureCallback(callback: TextureUpdateCallback?) {
|
|
63
|
-
textureCallback = callback
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface TemperatureUpdateCallback {
|
|
67
|
-
fun onTemperatureUpdate(temperature: Double)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private var temperatureCallback: TemperatureUpdateCallback? = null
|
|
71
|
-
|
|
72
|
-
fun setTemperatureCallback(callback: TemperatureUpdateCallback?) {
|
|
73
|
-
temperatureCallback = callback
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
fun getLatestBitmap(): Bitmap? = latestBitmap
|
|
77
|
-
|
|
78
|
-
// Stubs for removed features
|
|
79
|
-
fun setPreferSdkRotation(prefer: Boolean) { /* No-op */ }
|
|
80
|
-
fun isPreferSdkRotation(): Boolean = false
|
|
81
|
-
fun getBatteryLevel(): Int = -1
|
|
82
|
-
fun isBatteryCharging(): Boolean = false
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
try
|
|
87
|
-
|
|
88
|
-
val
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
fun
|
|
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
|
-
fun
|
|
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
|
-
override fun
|
|
299
|
-
Log.i(TAG, "
|
|
300
|
-
isConnected =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
val safeIdx = if (paletteIdx < 0) paletteIdx + maxEff else paletteIdx
|
|
467
|
-
|
|
468
|
-
val targetPaletteName = paletteNames[safeIdx]
|
|
469
|
-
sdkManager?.setPalette(targetPaletteName)
|
|
470
|
-
|
|
471
|
-
} catch (e: Throwable) {
|
|
472
|
-
Log.w(TAG, "updateAcol reflection failed: ${e.message}")
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Generate icons for all default palettes and save to cache.
|
|
478
|
-
*/
|
|
479
|
-
fun generatePaletteIcons(context: Context): List<Map<String, String>> {
|
|
480
|
-
val results = mutableListOf<Map<String, String>>()
|
|
481
|
-
try {
|
|
482
|
-
// Standard FLIR palette list from samples
|
|
483
|
-
val paletteNames = listOf("Gray", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel")
|
|
484
|
-
|
|
485
|
-
val dir = File(context.cacheDir, "flir_palettes")
|
|
486
|
-
if (!dir.exists()) dir.mkdirs()
|
|
487
|
-
|
|
488
|
-
for (name in paletteNames) {
|
|
489
|
-
val iconFile = File(dir, "${name.lowercase()}.png")
|
|
490
|
-
// Note: We'll rely on the app to provide these icons or they will be generated by the SDK if possible.
|
|
491
|
-
// For now, we return the names and the cache path.
|
|
492
|
-
results.add(mapOf(
|
|
493
|
-
"name" to name,
|
|
494
|
-
"uri" to if (iconFile.exists()) "file://${iconFile.absolutePath}" else ""
|
|
495
|
-
))
|
|
496
|
-
}
|
|
497
|
-
} catch (t: Throwable) {
|
|
498
|
-
Log.e(TAG, "Palette icon list generation error", t)
|
|
499
|
-
}
|
|
500
|
-
return results
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
fun getPalettesWithIcons(context: Context? = null): List<Map<String, String>> {
|
|
504
|
-
val ctx = context ?: reactContext ?: return emptyList()
|
|
505
|
-
return generatePaletteIcons(ctx)
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
fun destroy() { stop() }
|
|
509
|
-
}
|
|
1
|
+
package flir.android
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.facebook.react.bridge.ReactContext
|
|
8
|
+
import com.facebook.react.bridge.WritableArray
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
import com.flir.thermalsdk.live.Identity
|
|
13
|
+
import com.flir.thermalsdk.image.Palette
|
|
14
|
+
import com.flir.thermalsdk.image.PaletteManager
|
|
15
|
+
import java.io.File
|
|
16
|
+
import java.io.FileOutputStream
|
|
17
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Simplified FlirManager - bridge between React Native and FlirSdkManager
|
|
21
|
+
* Matches the simplified pattern: scan -> connect -> stream -> disconnect
|
|
22
|
+
*/
|
|
23
|
+
object FlirManager {
|
|
24
|
+
private const val TAG = "FlirManager"
|
|
25
|
+
|
|
26
|
+
private var sdkManager: FlirSdkManager? = null
|
|
27
|
+
private var reactContext: ReactContext? = null
|
|
28
|
+
|
|
29
|
+
// Frame rate limiting for RN events
|
|
30
|
+
private val lastEmitMs = AtomicLong(0)
|
|
31
|
+
private val minEmitIntervalMs = 100L // ~10 fps max for RN events
|
|
32
|
+
|
|
33
|
+
// Cached palette list to avoid repeated JNI calls (especially if linkage is unstable)
|
|
34
|
+
private var cachedPalettes: List<*>? = null
|
|
35
|
+
private var cachedPaletteNames: List<String>? = null
|
|
36
|
+
|
|
37
|
+
// Cached reflection for performance
|
|
38
|
+
private var coolField: java.lang.reflect.Field? = null
|
|
39
|
+
|
|
40
|
+
// State
|
|
41
|
+
private var isInitialized = false
|
|
42
|
+
private var isScanning = false
|
|
43
|
+
private var isConnected = false
|
|
44
|
+
private var isStreaming = false
|
|
45
|
+
private var connectedDeviceId: String? = null
|
|
46
|
+
private var connectedDeviceName: String? = null
|
|
47
|
+
|
|
48
|
+
// Concurrency control
|
|
49
|
+
private val shouldProcessFrames = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
50
|
+
private val isUpdatingTexture = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
51
|
+
|
|
52
|
+
// Latest bitmap
|
|
53
|
+
private var latestBitmap: Bitmap? = null
|
|
54
|
+
|
|
55
|
+
// Callbacks
|
|
56
|
+
interface TextureUpdateCallback {
|
|
57
|
+
fun onTextureUpdate(bitmap: Bitmap, textureUnit: Int)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private var textureCallback: TextureUpdateCallback? = null
|
|
61
|
+
|
|
62
|
+
fun setTextureCallback(callback: TextureUpdateCallback?) {
|
|
63
|
+
textureCallback = callback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TemperatureUpdateCallback {
|
|
67
|
+
fun onTemperatureUpdate(temperature: Double)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private var temperatureCallback: TemperatureUpdateCallback? = null
|
|
71
|
+
|
|
72
|
+
fun setTemperatureCallback(callback: TemperatureUpdateCallback?) {
|
|
73
|
+
temperatureCallback = callback
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun getLatestBitmap(): Bitmap? = latestBitmap
|
|
77
|
+
|
|
78
|
+
// Stubs for removed features
|
|
79
|
+
fun setPreferSdkRotation(prefer: Boolean) { /* No-op */ }
|
|
80
|
+
fun isPreferSdkRotation(): Boolean = false
|
|
81
|
+
fun getBatteryLevel(): Int = -1
|
|
82
|
+
fun isBatteryCharging(): Boolean = false
|
|
83
|
+
|
|
84
|
+
fun setPalette(name: String) {
|
|
85
|
+
sdkManager?.setPalette(name)
|
|
86
|
+
// Also try to update the app's global Var.cool if possible
|
|
87
|
+
try {
|
|
88
|
+
val palettes = getAvailablePalettes()
|
|
89
|
+
val idx = palettes.indexOfFirst { it.equals(name, ignoreCase = true) }
|
|
90
|
+
if (idx != -1) {
|
|
91
|
+
updateAcol(idx.toFloat())
|
|
92
|
+
}
|
|
93
|
+
} catch (e: Exception) {
|
|
94
|
+
Log.e(TAG, "setPalette: Failed to update Var.cool", e)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fun getAvailablePalettes(): List<String> {
|
|
99
|
+
// Return hardcoded list to avoid PaletteManager class-loading crashes
|
|
100
|
+
// and ensure maximum performance. Matches standard FLIR and shader palettes.
|
|
101
|
+
return listOf("WhiteHot", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the FLIR SDK
|
|
106
|
+
*/
|
|
107
|
+
fun init(context: Context) {
|
|
108
|
+
if (context is ReactContext) {
|
|
109
|
+
reactContext = context
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isInitialized) return
|
|
113
|
+
|
|
114
|
+
sdkManager = FlirSdkManager.getInstance(context)
|
|
115
|
+
sdkManager?.setListener(sdkListener)
|
|
116
|
+
sdkManager?.initialize()
|
|
117
|
+
|
|
118
|
+
isInitialized = true
|
|
119
|
+
Log.i(TAG, "FlirManager initialized")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start scanning
|
|
124
|
+
*/
|
|
125
|
+
@Synchronized
|
|
126
|
+
fun startDiscovery(retry: Boolean = false) {
|
|
127
|
+
if (!isInitialized && reactContext != null) {
|
|
128
|
+
init(reactContext!!)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isScanning && !retry) return
|
|
132
|
+
|
|
133
|
+
Log.i(TAG, "Starting FlirManager discovery...")
|
|
134
|
+
isScanning = true
|
|
135
|
+
emitDeviceState("discovering")
|
|
136
|
+
sdkManager?.scan()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Start discovery with React context
|
|
141
|
+
*/
|
|
142
|
+
fun startDiscoveryAndConnect(context: ThemedReactContext, isEmuMode: Boolean = false) {
|
|
143
|
+
reactContext = context
|
|
144
|
+
startDiscovery(retry = false)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stop scanning
|
|
149
|
+
*/
|
|
150
|
+
@Synchronized
|
|
151
|
+
fun stopDiscovery() {
|
|
152
|
+
Log.i(TAG, "Stopping FlirManager discovery...")
|
|
153
|
+
sdkManager?.stopScan()
|
|
154
|
+
isScanning = false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Connect to a device
|
|
159
|
+
*/
|
|
160
|
+
@Synchronized
|
|
161
|
+
fun connectToDevice(deviceId: String?) {
|
|
162
|
+
if (deviceId == null) {
|
|
163
|
+
Log.e(TAG, "connectToDevice: deviceId is null")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
Log.i(TAG, "connectToDevice: $deviceId")
|
|
167
|
+
|
|
168
|
+
val devices = sdkManager?.discoveredDevices ?: emptyList()
|
|
169
|
+
val identity = devices.find { it.deviceId == deviceId }
|
|
170
|
+
|
|
171
|
+
if (identity != null) {
|
|
172
|
+
shouldProcessFrames.set(true)
|
|
173
|
+
sdkManager?.connect(identity)
|
|
174
|
+
} else {
|
|
175
|
+
Log.e(TAG, "Device not found: $deviceId")
|
|
176
|
+
emitError("Device not found: $deviceId")
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fun switchToDevice(deviceId: String) {
|
|
181
|
+
connectToDevice(deviceId)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Disconnect
|
|
186
|
+
*/
|
|
187
|
+
@Synchronized
|
|
188
|
+
fun disconnect() {
|
|
189
|
+
Log.i(TAG, "Disconnecting FlirManager...")
|
|
190
|
+
shouldProcessFrames.set(false)
|
|
191
|
+
sdkManager?.disconnect()
|
|
192
|
+
isConnected = false
|
|
193
|
+
isStreaming = false
|
|
194
|
+
connectedDeviceId = null
|
|
195
|
+
connectedDeviceName = null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Stop everything
|
|
200
|
+
*/
|
|
201
|
+
@Synchronized
|
|
202
|
+
fun stop() {
|
|
203
|
+
Log.i(TAG, "Stopping FlirManager completely...")
|
|
204
|
+
shouldProcessFrames.set(false)
|
|
205
|
+
|
|
206
|
+
// Clear callbacks first to prevent any more frames/updates from hitting Java/RN
|
|
207
|
+
textureCallback = null
|
|
208
|
+
temperatureCallback = null
|
|
209
|
+
|
|
210
|
+
disconnect()
|
|
211
|
+
stopDiscovery()
|
|
212
|
+
latestBitmap = null
|
|
213
|
+
Log.i(TAG, "FlirManager stopped")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Stub legacy methods
|
|
217
|
+
fun startStream() { /* handled automatically by connect */ }
|
|
218
|
+
fun stopStream() { sdkManager?.stopStream() }
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get temperature
|
|
222
|
+
*/
|
|
223
|
+
fun getTemperatureAt(x: Int, y: Int): Double? {
|
|
224
|
+
val temp = sdkManager?.getTemperatureAt(x, y)
|
|
225
|
+
return if (temp != null && !temp.isNaN()) temp else null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fun getTemperatureAtNormalized(nx: Double, ny: Double): Double? {
|
|
229
|
+
val bitmap = latestBitmap ?: return null
|
|
230
|
+
val px = (nx * bitmap.width).toInt().coerceIn(0, bitmap.width - 1)
|
|
231
|
+
val py = (ny * bitmap.height).toInt().coerceIn(0, bitmap.height - 1)
|
|
232
|
+
return getTemperatureAt(px, py)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fun getTemperatureAtPoint(x: Int, y: Int): Double? = getTemperatureAt(x, y)
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get discovered devices
|
|
239
|
+
*/
|
|
240
|
+
fun getDiscoveredDevices(): List<Identity> {
|
|
241
|
+
return sdkManager?.discoveredDevices ?: emptyList()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check states
|
|
246
|
+
*/
|
|
247
|
+
fun isConnected(): Boolean = isConnected
|
|
248
|
+
fun isStreaming(): Boolean = isStreaming
|
|
249
|
+
fun isEmulator(): Boolean = connectedDeviceName?.contains("EMULAT", ignoreCase = true) == true
|
|
250
|
+
fun isDeviceConnected(): Boolean = isConnected
|
|
251
|
+
|
|
252
|
+
fun getConnectedDeviceInfo(): String {
|
|
253
|
+
return connectedDeviceName ?: "Not connected"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Capture a high-fidelity radiometric snapshot (saves thermal data)
|
|
258
|
+
*/
|
|
259
|
+
fun captureRadiometricSnapshot(path: String, callback: FlirSdkManager.SnapshotCallback? = null) {
|
|
260
|
+
sdkManager?.captureRadiometricSnapshot(path, callback)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get latest frame as file path (for RN)
|
|
265
|
+
*/
|
|
266
|
+
fun getLatestFramePath(): String? {
|
|
267
|
+
val bitmap = latestBitmap ?: return null
|
|
268
|
+
return try {
|
|
269
|
+
val file = File.createTempFile("flir_frame_", ".jpg")
|
|
270
|
+
FileOutputStream(file).use { out ->
|
|
271
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
|
272
|
+
}
|
|
273
|
+
file.absolutePath
|
|
274
|
+
} catch (t: Throwable) {
|
|
275
|
+
null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// SDK Listener
|
|
280
|
+
private val sdkListener = object : FlirSdkManager.Listener {
|
|
281
|
+
override fun onDeviceFound(identity: Identity) {
|
|
282
|
+
// Devices updated event handles the list, but we can log unique finds
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
override fun onDeviceListUpdated(devices: List<Identity>) {
|
|
286
|
+
Log.d(TAG, "Devices found: ${devices.size}")
|
|
287
|
+
emitDevicesFound(devices)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
override fun onConnected(identity: Identity) {
|
|
291
|
+
Log.i(TAG, "Connected to: ${identity.deviceId}")
|
|
292
|
+
isConnected = true
|
|
293
|
+
connectedDeviceId = identity.deviceId
|
|
294
|
+
connectedDeviceName = identity.deviceId
|
|
295
|
+
emitDeviceState("connected")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override fun onDisconnected() {
|
|
299
|
+
Log.i(TAG, "Disconnected")
|
|
300
|
+
isConnected = false
|
|
301
|
+
isStreaming = false
|
|
302
|
+
connectedDeviceId = null
|
|
303
|
+
connectedDeviceName = null
|
|
304
|
+
emitDeviceState("disconnected")
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
override fun onFrame(bitmap: Bitmap) {
|
|
308
|
+
// IMMEDIATE STOP CHECK
|
|
309
|
+
if (!shouldProcessFrames.get()) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// THROTTLE: Limit to ~30 FPS for smoother streaming
|
|
314
|
+
val now = System.currentTimeMillis()
|
|
315
|
+
if (now - lastEmitMs.get() < 33) { // 33ms ~= 30 FPS
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
lastEmitMs.set(now)
|
|
319
|
+
|
|
320
|
+
latestBitmap = bitmap
|
|
321
|
+
|
|
322
|
+
// If this is the first frame, notify JS that we are now streaming
|
|
323
|
+
if (!isStreaming) {
|
|
324
|
+
isStreaming = true
|
|
325
|
+
emitDeviceState("streaming")
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// NON-BLOCKING TEXTURE UPDATE
|
|
329
|
+
if (textureCallback != null) {
|
|
330
|
+
// We use try-lock to ensure we don't pile up parallel calls,
|
|
331
|
+
// though usually onFrame is serial.
|
|
332
|
+
if (isUpdatingTexture.compareAndSet(false, true)) {
|
|
333
|
+
try {
|
|
334
|
+
textureCallback?.onTextureUpdate(bitmap, 0)
|
|
335
|
+
} catch (e: Exception) {
|
|
336
|
+
Log.e(TAG, "Texture update failed", e)
|
|
337
|
+
} finally {
|
|
338
|
+
isUpdatingTexture.set(false)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
override fun onError(message: String) {
|
|
345
|
+
emitError(message)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// React Native Emitters
|
|
350
|
+
|
|
351
|
+
private fun emitDeviceState(state: String) {
|
|
352
|
+
val ctx = reactContext ?: return
|
|
353
|
+
try {
|
|
354
|
+
val params = Arguments.createMap().apply {
|
|
355
|
+
putString("state", state)
|
|
356
|
+
putBoolean("isConnected", isConnected)
|
|
357
|
+
putBoolean("isStreaming", isStreaming)
|
|
358
|
+
putBoolean("isEmulator", isEmulator())
|
|
359
|
+
connectedDeviceName?.let { putString("deviceName", it) }
|
|
360
|
+
}
|
|
361
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
362
|
+
.emit("FlirDeviceConnected", params)
|
|
363
|
+
} catch (e: Exception) { }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private fun emitDevicesFound(devices: List<Identity>) {
|
|
367
|
+
val ctx = reactContext ?: return
|
|
368
|
+
try {
|
|
369
|
+
val params = Arguments.createMap()
|
|
370
|
+
val devicesArray: WritableArray = Arguments.createArray()
|
|
371
|
+
|
|
372
|
+
devices.forEach { identity ->
|
|
373
|
+
val deviceMap = Arguments.createMap().apply {
|
|
374
|
+
putString("id", identity.deviceId)
|
|
375
|
+
putString("name", identity.deviceId)
|
|
376
|
+
putString("communicationType", identity.communicationInterface.name)
|
|
377
|
+
putBoolean("isEmulator", identity.communicationInterface.name == "EMULATOR")
|
|
378
|
+
}
|
|
379
|
+
devicesArray.pushMap(deviceMap)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
params.putArray("devices", devicesArray)
|
|
383
|
+
params.putInt("count", devices.size)
|
|
384
|
+
|
|
385
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
386
|
+
.emit("FlirDevicesFound", params)
|
|
387
|
+
} catch (e: Exception) { }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private fun emitError(message: String) {
|
|
391
|
+
val ctx = reactContext ?: return
|
|
392
|
+
try {
|
|
393
|
+
val params = Arguments.createMap().apply {
|
|
394
|
+
putString("error", message)
|
|
395
|
+
putString("message", message)
|
|
396
|
+
}
|
|
397
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
398
|
+
.emit("FlirError", params)
|
|
399
|
+
} catch (e: Exception) { }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Legacy methods placeholders
|
|
403
|
+
@JvmStatic fun getInstance(): FlirManager = this
|
|
404
|
+
|
|
405
|
+
interface DiscoveryCallback {
|
|
406
|
+
fun onDeviceFound(deviceName: String)
|
|
407
|
+
fun onDiscoveryTimeout()
|
|
408
|
+
fun onEmulatorEnabled()
|
|
409
|
+
}
|
|
410
|
+
fun setDiscoveryCallback(callback: DiscoveryCallback?) { /* No-op */ }
|
|
411
|
+
fun setEmulatorMode(enabled: Boolean) { startDiscovery() }
|
|
412
|
+
fun enableEmulatorMode() = startDiscovery()
|
|
413
|
+
fun forceEmulatorMode(type: String = "FLIR_ONE_EDGE") { startDiscovery() }
|
|
414
|
+
fun setPreferredEmulatorType(type: String) { }
|
|
415
|
+
fun updateAcol(value: Float) {
|
|
416
|
+
try {
|
|
417
|
+
if (coolField == null) {
|
|
418
|
+
val varClass = Class.forName("ilabs.libs.io.data.Var")
|
|
419
|
+
coolField = varClass.getField("cool")
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
val rawIdx = value.toInt()
|
|
423
|
+
var shaderIdx = rawIdx
|
|
424
|
+
if (shaderIdx > 16) {
|
|
425
|
+
shaderIdx = shaderIdx % 16 // Shader loop
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
coolField?.set(null, shaderIdx)
|
|
429
|
+
|
|
430
|
+
// Standard FLIR palette list
|
|
431
|
+
val paletteNames = getAvailablePalettes()
|
|
432
|
+
val maxEff = paletteNames.size
|
|
433
|
+
val paletteIdx = rawIdx % maxEff
|
|
434
|
+
val safeIdx = if (paletteIdx < 0) paletteIdx + maxEff else paletteIdx
|
|
435
|
+
|
|
436
|
+
val targetPaletteName = paletteNames[safeIdx]
|
|
437
|
+
sdkManager?.setPalette(targetPaletteName)
|
|
438
|
+
|
|
439
|
+
} catch (e: Throwable) {
|
|
440
|
+
Log.w(TAG, "updateAcol reflection failed: ${e.message}")
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Generate icons for all default palettes and save to cache.
|
|
446
|
+
*/
|
|
447
|
+
fun generatePaletteIcons(context: Context): List<Map<String, String>> {
|
|
448
|
+
val results = mutableListOf<Map<String, String>>()
|
|
449
|
+
val paletteNames = getAvailablePalettes()
|
|
450
|
+
for (name in paletteNames) {
|
|
451
|
+
results.add(mapOf(
|
|
452
|
+
"name" to name,
|
|
453
|
+
"uri" to "" // No URI - rely on local assets if any
|
|
454
|
+
))
|
|
455
|
+
}
|
|
456
|
+
return results
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fun getPalettesWithIcons(context: Context? = null): List<Map<String, String>> {
|
|
460
|
+
val ctx = context ?: reactContext ?: return emptyList()
|
|
461
|
+
return generatePaletteIcons(ctx)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fun destroy() { stop() }
|
|
465
|
+
}
|