ilabs-flir 2.3.7 → 2.3.10

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