ilabs-flir 2.3.8 → 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,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
- fun setPalette(name: String) {
84
- sdkManager?.setPalette(name)
85
- // Also try to update the app's global Var.cool if possible
86
- try {
87
- val palettes = getAvailablePalettes()
88
- val idx = palettes.indexOf(name.lowercase())
89
- if (idx != -1) {
90
- updateAcol(idx.toFloat())
91
- }
92
- } catch (e: Exception) {}
93
- }
94
- fun getAvailablePalettes(): List<String> {
95
- return try {
96
- val palettes = PaletteManager.getDefaultPalettes().map { it.name }.toMutableList()
97
- // Move WhiteHot/Gray to the front
98
- val grayIdx = palettes.indexOfFirst {
99
- it.equals("WhiteHot", ignoreCase = true) || it.equals("Gray", ignoreCase = true) || it.contains("gray", ignoreCase = true)
100
- }
101
- if (grayIdx > 0) {
102
- val gray = palettes.removeAt(grayIdx)
103
- palettes.add(0, gray)
104
- }
105
- palettes
106
- } catch (t: Throwable) {
107
- Log.e(TAG, "Failed to get available palettes from SDK", t)
108
- listOf("grayscale", "iron", "rainbow", "arctic", "lava", "contrast", "hotcold", "medical")
109
- }
110
- }
111
-
112
- /**
113
- * Initialize the FLIR SDK
114
- */
115
- fun init(context: Context) {
116
- if (context is ReactContext) {
117
- reactContext = context
118
- }
119
-
120
- if (isInitialized) return
121
-
122
- sdkManager = FlirSdkManager.getInstance(context)
123
- sdkManager?.setListener(sdkListener)
124
- sdkManager?.initialize()
125
-
126
- isInitialized = true
127
- Log.i(TAG, "FlirManager initialized")
128
- }
129
-
130
- /**
131
- * Start scanning
132
- */
133
- @Synchronized
134
- fun startDiscovery(retry: Boolean = false) {
135
- if (!isInitialized && reactContext != null) {
136
- init(reactContext!!)
137
- }
138
-
139
- if (isScanning && !retry) return
140
-
141
- Log.i(TAG, "Starting FlirManager discovery...")
142
- isScanning = true
143
- emitDeviceState("discovering")
144
- sdkManager?.scan()
145
- }
146
-
147
- /**
148
- * Start discovery with React context
149
- */
150
- fun startDiscoveryAndConnect(context: ThemedReactContext, isEmuMode: Boolean = false) {
151
- reactContext = context
152
- startDiscovery(retry = false)
153
- }
154
-
155
- /**
156
- * Stop scanning
157
- */
158
- @Synchronized
159
- fun stopDiscovery() {
160
- Log.i(TAG, "Stopping FlirManager discovery...")
161
- sdkManager?.stopScan()
162
- isScanning = false
163
- }
164
-
165
- /**
166
- * Connect to a device
167
- */
168
- @Synchronized
169
- fun connectToDevice(deviceId: String?) {
170
- if (deviceId == null) {
171
- Log.e(TAG, "connectToDevice: deviceId is null")
172
- return
173
- }
174
- Log.i(TAG, "connectToDevice: $deviceId")
175
-
176
- val devices = sdkManager?.discoveredDevices ?: emptyList()
177
- val identity = devices.find { it.deviceId == deviceId }
178
-
179
- if (identity != null) {
180
- shouldProcessFrames.set(true)
181
- sdkManager?.connect(identity)
182
- } else {
183
- Log.e(TAG, "Device not found: $deviceId")
184
- emitError("Device not found: $deviceId")
185
- }
186
- }
187
-
188
- fun switchToDevice(deviceId: String) {
189
- connectToDevice(deviceId)
190
- }
191
-
192
- /**
193
- * Disconnect
194
- */
195
- @Synchronized
196
- fun disconnect() {
197
- Log.i(TAG, "Disconnecting FlirManager...")
198
- shouldProcessFrames.set(false)
199
- sdkManager?.disconnect()
200
- isConnected = false
201
- isStreaming = false
202
- connectedDeviceId = null
203
- connectedDeviceName = null
204
- }
205
-
206
- /**
207
- * Stop everything
208
- */
209
- @Synchronized
210
- fun stop() {
211
- Log.i(TAG, "Stopping FlirManager completely...")
212
- shouldProcessFrames.set(false)
213
-
214
- // Clear callbacks first to prevent any more frames/updates from hitting Java/RN
215
- textureCallback = null
216
- temperatureCallback = null
217
-
218
- disconnect()
219
- stopDiscovery()
220
- latestBitmap = null
221
- Log.i(TAG, "FlirManager stopped")
222
- }
223
-
224
- // Stub legacy methods
225
- fun startStream() { /* handled automatically by connect */ }
226
- fun stopStream() { sdkManager?.stopStream() }
227
-
228
- /**
229
- * Get temperature
230
- */
231
- fun getTemperatureAt(x: Int, y: Int): Double? {
232
- val temp = sdkManager?.getTemperatureAt(x, y)
233
- return if (temp != null && !temp.isNaN()) temp else null
234
- }
235
-
236
- fun getTemperatureAtNormalized(nx: Double, ny: Double): Double? {
237
- val bitmap = latestBitmap ?: return null
238
- val px = (nx * bitmap.width).toInt().coerceIn(0, bitmap.width - 1)
239
- val py = (ny * bitmap.height).toInt().coerceIn(0, bitmap.height - 1)
240
- return getTemperatureAt(px, py)
241
- }
242
-
243
- fun getTemperatureAtPoint(x: Int, y: Int): Double? = getTemperatureAt(x, y)
244
-
245
- /**
246
- * Get discovered devices
247
- */
248
- fun getDiscoveredDevices(): List<Identity> {
249
- return sdkManager?.discoveredDevices ?: emptyList()
250
- }
251
-
252
- /**
253
- * Check states
254
- */
255
- fun isConnected(): Boolean = isConnected
256
- fun isStreaming(): Boolean = isStreaming
257
- fun isEmulator(): Boolean = connectedDeviceName?.contains("EMULAT", ignoreCase = true) == true
258
- fun isDeviceConnected(): Boolean = isConnected
259
-
260
- fun getConnectedDeviceInfo(): String {
261
- return connectedDeviceName ?: "Not connected"
262
- }
263
-
264
- /**
265
- * Capture a high-fidelity radiometric snapshot (saves thermal data)
266
- */
267
- fun captureRadiometricSnapshot(path: String, callback: FlirSdkManager.SnapshotCallback? = null) {
268
- sdkManager?.captureRadiometricSnapshot(path, callback)
269
- }
270
-
271
- /**
272
- * Get latest frame as file path (for RN)
273
- */
274
- fun getLatestFramePath(): String? {
275
- val bitmap = latestBitmap ?: return null
276
- return try {
277
- val file = File.createTempFile("flir_frame_", ".jpg")
278
- FileOutputStream(file).use { out ->
279
- bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
280
- }
281
- file.absolutePath
282
- } catch (t: Throwable) {
283
- null
284
- }
285
- }
286
-
287
- // SDK Listener
288
- private val sdkListener = object : FlirSdkManager.Listener {
289
- override fun onDeviceFound(identity: Identity) {
290
- // Devices updated event handles the list, but we can log unique finds
291
- }
292
-
293
- override fun onDeviceListUpdated(devices: List<Identity>) {
294
- Log.d(TAG, "Devices found: ${devices.size}")
295
- emitDevicesFound(devices)
296
- }
297
-
298
- override fun onConnected(identity: Identity) {
299
- Log.i(TAG, "Connected to: ${identity.deviceId}")
300
- isConnected = true
301
- connectedDeviceId = identity.deviceId
302
- connectedDeviceName = identity.deviceId
303
- emitDeviceState("connected")
304
- }
305
-
306
- override fun onDisconnected() {
307
- Log.i(TAG, "Disconnected")
308
- isConnected = false
309
- isStreaming = false
310
- connectedDeviceId = null
311
- connectedDeviceName = null
312
- emitDeviceState("disconnected")
313
- }
314
-
315
- override fun onFrame(bitmap: Bitmap) {
316
- // IMMEDIATE STOP CHECK
317
- if (!shouldProcessFrames.get()) {
318
- return
319
- }
320
-
321
- // THROTTLE: Limit to ~30 FPS for smoother streaming
322
- val now = System.currentTimeMillis()
323
- if (now - lastEmitMs.get() < 33) { // 33ms ~= 30 FPS
324
- return
325
- }
326
- lastEmitMs.set(now)
327
-
328
- latestBitmap = bitmap
329
-
330
- // If this is the first frame, notify JS that we are now streaming
331
- if (!isStreaming) {
332
- isStreaming = true
333
- emitDeviceState("streaming")
334
- }
335
-
336
- // NON-BLOCKING TEXTURE UPDATE
337
- if (textureCallback != null) {
338
- // We use try-lock to ensure we don't pile up parallel calls,
339
- // though usually onFrame is serial.
340
- if (isUpdatingTexture.compareAndSet(false, true)) {
341
- try {
342
- textureCallback?.onTextureUpdate(bitmap, 0)
343
- } catch (e: Exception) {
344
- Log.e(TAG, "Texture update failed", e)
345
- } finally {
346
- isUpdatingTexture.set(false)
347
- }
348
- }
349
- }
350
-
351
- // Notify RN - disabled
352
- // emitFrameToReactNative(bitmap)
353
- }
354
-
355
- override fun onError(message: String) {
356
- emitError(message)
357
- }
358
- }
359
-
360
- // React Native Emitters
361
-
362
- private fun emitFrameToReactNative(bitmap: Bitmap) {
363
- // PERF: Disabled to reduce bridge traffic
364
- /*
365
- val now = System.currentTimeMillis()
366
- if (now - lastEmitMs.get() < minEmitIntervalMs) return
367
- lastEmitMs.set(now)
368
-
369
- val ctx = reactContext ?: return
370
- try {
371
- val params = Arguments.createMap().apply {
372
- putInt("width", bitmap.width)
373
- putInt("height", bitmap.height)
374
- putDouble("timestamp", now.toDouble())
375
- }
376
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
377
- .emit("FlirFrameReceived", params)
378
- } catch (e: Exception) { }
379
- */
380
- }
381
-
382
- private fun emitDeviceState(state: String) {
383
- val ctx = reactContext ?: return
384
- try {
385
- val params = Arguments.createMap().apply {
386
- putString("state", state)
387
- putBoolean("isConnected", isConnected)
388
- putBoolean("isStreaming", isStreaming)
389
- putBoolean("isEmulator", isEmulator())
390
- connectedDeviceName?.let { putString("deviceName", it) }
391
- }
392
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
393
- .emit("FlirDeviceConnected", params)
394
- } catch (e: Exception) { }
395
- }
396
-
397
- private fun emitDevicesFound(devices: List<Identity>) {
398
- val ctx = reactContext ?: return
399
- try {
400
- val params = Arguments.createMap()
401
- val devicesArray: WritableArray = Arguments.createArray()
402
-
403
- devices.forEach { identity ->
404
- val deviceMap = Arguments.createMap().apply {
405
- putString("id", identity.deviceId)
406
- putString("name", identity.deviceId)
407
- putString("communicationType", identity.communicationInterface.name)
408
- putBoolean("isEmulator", identity.communicationInterface.name == "EMULATOR")
409
- }
410
- devicesArray.pushMap(deviceMap)
411
- }
412
-
413
- params.putArray("devices", devicesArray)
414
- params.putInt("count", devices.size)
415
-
416
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
417
- .emit("FlirDevicesFound", params)
418
- } catch (e: Exception) { }
419
- }
420
-
421
- private fun emitError(message: String) {
422
- val ctx = reactContext ?: return
423
- try {
424
- val params = Arguments.createMap().apply {
425
- putString("error", message)
426
- putString("message", message)
427
- }
428
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
429
- .emit("FlirError", params)
430
- } catch (e: Exception) { }
431
- }
432
-
433
- // Legacy methods placeholders
434
- @JvmStatic fun getInstance(): FlirManager = this
435
-
436
- interface DiscoveryCallback {
437
- fun onDeviceFound(deviceName: String)
438
- fun onDiscoveryTimeout()
439
- fun onEmulatorEnabled()
440
- }
441
- fun setDiscoveryCallback(callback: DiscoveryCallback?) { /* No-op */ }
442
- fun setEmulatorMode(enabled: Boolean) { startDiscovery() }
443
- fun enableEmulatorMode() = startDiscovery()
444
- fun forceEmulatorMode(type: String = "FLIR_ONE_EDGE") { startDiscovery() }
445
- fun setPreferredEmulatorType(type: String) { }
446
- fun updateAcol(value: Float) {
447
- // Log.v(TAG, "updateAcol: $value")
448
- try {
449
- if (coolField == null) {
450
- val varClass = Class.forName("ilabs.libs.io.data.Var")
451
- coolField = varClass.getField("cool")
452
- }
453
-
454
- val rawIdx = value.toInt()
455
- var shaderIdx = rawIdx
456
- if (shaderIdx > 16) {
457
- shaderIdx = shaderIdx % 16 // Shader loop
458
- }
459
-
460
- coolField?.set(null, shaderIdx)
461
-
462
- // Standard FLIR palette list
463
- val paletteNames = listOf("Gray", "Iron", "Rainbow", "Arctic", "Lava", "Coldest", "Hottest", "Wheel")
464
- val maxEff = paletteNames.size
465
- val paletteIdx = rawIdx % maxEff
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
+ }