ilabs-flir 2.0.4 → 2.0.6

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.
Files changed (35) hide show
  1. package/Flir.podspec +139 -139
  2. package/README.md +1066 -1066
  3. package/android/Flir/build.gradle.kts +72 -72
  4. package/android/Flir/src/main/AndroidManifest.xml +45 -45
  5. package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
  6. package/android/Flir/src/main/java/flir/android/FlirFrameCache.kt +6 -6
  7. package/android/Flir/src/main/java/flir/android/FlirManager.kt +476 -476
  8. package/android/Flir/src/main/java/flir/android/FlirModule.kt +257 -257
  9. package/android/Flir/src/main/java/flir/android/FlirPackage.kt +18 -18
  10. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +74 -74
  11. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +583 -583
  12. package/android/Flir/src/main/java/flir/android/FlirStatus.kt +12 -12
  13. package/android/Flir/src/main/java/flir/android/FlirView.kt +48 -48
  14. package/android/Flir/src/main/java/flir/android/FlirViewManager.kt +13 -13
  15. package/app.plugin.js +381 -381
  16. package/expo-module.config.json +5 -5
  17. package/ios/Flir/src/Flir-Bridging-Header.h +34 -34
  18. package/ios/Flir/src/FlirEventEmitter.h +25 -25
  19. package/ios/Flir/src/FlirEventEmitter.m +63 -63
  20. package/ios/Flir/src/FlirManager.swift +599 -599
  21. package/ios/Flir/src/FlirModule.h +17 -17
  22. package/ios/Flir/src/FlirModule.m +713 -713
  23. package/ios/Flir/src/FlirPreviewView.h +13 -13
  24. package/ios/Flir/src/FlirPreviewView.m +171 -171
  25. package/ios/Flir/src/FlirState.h +68 -68
  26. package/ios/Flir/src/FlirState.m +135 -135
  27. package/ios/Flir/src/FlirViewManager.h +16 -16
  28. package/ios/Flir/src/FlirViewManager.m +27 -27
  29. package/package.json +70 -71
  30. package/react-native.config.js +14 -14
  31. package/scripts/fetch-binaries.js +103 -17
  32. package/sdk-manifest.json +50 -50
  33. package/src/index.d.ts +63 -63
  34. package/src/index.js +7 -7
  35. package/src/index.ts +6 -6
@@ -1,476 +1,476 @@
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.ReactApplicationContext
8
- import com.facebook.react.bridge.ReactContext
9
- import com.facebook.react.bridge.WritableArray
10
- import com.facebook.react.bridge.WritableMap
11
- import com.facebook.react.modules.core.DeviceEventManagerModule
12
- import com.facebook.react.uimanager.ThemedReactContext
13
- import com.flir.thermalsdk.live.Identity
14
- import java.io.File
15
- import java.io.FileOutputStream
16
- import java.util.concurrent.atomic.AtomicLong
17
-
18
- /**
19
- * Simplified FlirManager - bridge between React Native and FlirSdkManager
20
- * No filtering - returns ALL discovered devices (USB, Network, Emulator)
21
- * Let React Native handle any filtering logic
22
- */
23
- object FlirManager {
24
- private const val TAG = "FlirManager"
25
-
26
- private var sdkManager: FlirSdkManager? = null
27
- private var reactContext: ReactContext? = null
28
- private var appContext: Context? = null
29
-
30
- // Frame rate limiting
31
- private val lastEmitMs = AtomicLong(0)
32
- private val minEmitIntervalMs = 100L // ~10 fps max for RN events
33
-
34
- // State
35
- private var isInitialized = false
36
- private var isScanning = false
37
- private var isConnected = false
38
- private var isStreaming = false
39
- private var connectedDeviceId: String? = null
40
- private var connectedDeviceName: String? = null
41
-
42
- // Latest bitmap for texture updates
43
- private var latestBitmap: Bitmap? = null
44
-
45
- // Callbacks
46
- interface TextureUpdateCallback {
47
- fun onTextureUpdate(bitmap: Bitmap, textureUnit: Int)
48
- }
49
-
50
- interface TemperatureCallback {
51
- fun onTemperatureData(temperature: Double, x: Int, y: Int)
52
- }
53
-
54
- private var textureCallback: TextureUpdateCallback? = null
55
- private var temperatureCallback: TemperatureCallback? = null
56
-
57
- fun setTextureCallback(callback: TextureUpdateCallback?) {
58
- textureCallback = callback
59
- }
60
-
61
- fun setTemperatureCallback(callback: TemperatureCallback?) {
62
- temperatureCallback = callback
63
- }
64
-
65
- fun getLatestBitmap(): Bitmap? = latestBitmap
66
-
67
- /**
68
- * Initialize the FLIR SDK
69
- */
70
- fun init(context: Context) {
71
- // Store react context for event emission if it's a React context
72
- // Always update if we get a valid ReactContext (in case previous was stale)
73
- if (context is ReactContext) {
74
- Log.d(TAG, "Storing ReactContext for event emission: ${context.javaClass.simpleName}")
75
- reactContext = context
76
- } else {
77
- Log.d(TAG, "Context is not ReactContext: ${context.javaClass.simpleName}")
78
- }
79
-
80
- if (isInitialized) {
81
- Log.d(TAG, "Already initialized")
82
- return
83
- }
84
-
85
- appContext = context.applicationContext
86
-
87
- sdkManager = FlirSdkManager.getInstance(context)
88
- sdkManager?.setListener(sdkListener)
89
- sdkManager?.initialize()
90
-
91
- isInitialized = true
92
- Log.i(TAG, "FlirManager initialized")
93
- }
94
-
95
- /**
96
- * Start scanning for devices (USB, Network, Emulator - ALL types)
97
- */
98
- fun startDiscovery(retry: Boolean = false) {
99
- Log.i(TAG, "startDiscovery(retry=$retry)")
100
-
101
- if (!isInitialized && appContext != null) {
102
- init(appContext!!)
103
- }
104
-
105
- if (isScanning && !retry) {
106
- Log.d(TAG, "Already scanning")
107
- return
108
- }
109
-
110
- isScanning = true
111
- emitDeviceState("discovering")
112
- sdkManager?.scan()
113
- }
114
-
115
- /**
116
- * Start discovery with React context
117
- */
118
- fun startDiscoveryAndConnect(context: ThemedReactContext, isEmuMode: Boolean = false) {
119
- reactContext = context
120
- startDiscovery(retry = false)
121
- }
122
-
123
- /**
124
- * Stop scanning
125
- */
126
- fun stopDiscovery() {
127
- Log.i(TAG, "stopDiscovery")
128
- sdkManager?.stop()
129
- isScanning = false
130
- }
131
-
132
- /**
133
- * Connect to a device by ID
134
- */
135
- fun connectToDevice(deviceId: String) {
136
- Log.i(TAG, "connectToDevice: $deviceId")
137
-
138
- val devices = sdkManager?.discoveredDevices ?: emptyList()
139
- val identity = devices.find { it.deviceId == deviceId }
140
-
141
- if (identity != null) {
142
- sdkManager?.connect(identity)
143
- } else {
144
- Log.e(TAG, "Device not found: $deviceId")
145
- emitError("Device not found: $deviceId")
146
- }
147
- }
148
-
149
- /**
150
- * Switch to a different device
151
- */
152
- fun switchToDevice(deviceId: String) {
153
- if (deviceId == connectedDeviceId) {
154
- Log.d(TAG, "Already connected to: $deviceId")
155
- return
156
- }
157
-
158
- // Disconnect current and connect new
159
- if (isConnected) {
160
- sdkManager?.disconnect()
161
- }
162
- connectToDevice(deviceId)
163
- }
164
-
165
- /**
166
- * Start streaming from connected device
167
- */
168
- fun startStream() {
169
- Log.i(TAG, "startStream")
170
- sdkManager?.startStream()
171
- }
172
-
173
- /**
174
- * Stop streaming
175
- */
176
- fun stopStream() {
177
- Log.i(TAG, "stopStream")
178
- sdkManager?.stopStream()
179
- isStreaming = false
180
- }
181
-
182
- /**
183
- * Disconnect from current device
184
- */
185
- fun disconnect() {
186
- Log.i(TAG, "disconnect")
187
- sdkManager?.disconnect()
188
- isConnected = false
189
- isStreaming = false
190
- connectedDeviceId = null
191
- connectedDeviceName = null
192
- }
193
-
194
- /**
195
- * Stop everything
196
- */
197
- fun stop() {
198
- Log.i(TAG, "stop")
199
- stopStream()
200
- disconnect()
201
- stopDiscovery()
202
- latestBitmap = null
203
- }
204
-
205
- /**
206
- * Get temperature at point in image coordinates
207
- */
208
- fun getTemperatureAt(x: Int, y: Int): Double? {
209
- return sdkManager?.getTemperatureAt(x, y)?.takeIf { !it.isNaN() }
210
- }
211
-
212
- /**
213
- * Get temperature at normalized coordinates (0.0 to 1.0)
214
- */
215
- fun getTemperatureAtNormalized(normalizedX: Double, normalizedY: Double): Double? {
216
- return sdkManager?.getTemperatureAtNormalized(normalizedX, normalizedY)?.takeIf { !it.isNaN() }
217
- }
218
-
219
- /**
220
- * Alias for getTemperatureAt
221
- */
222
- fun getTemperatureAtPoint(x: Int, y: Int): Double? = getTemperatureAt(x, y)
223
-
224
- /**
225
- * Set palette
226
- */
227
- fun setPalette(name: String) {
228
- Log.d(TAG, "setPalette: $name")
229
- sdkManager?.setPalette(name)
230
- }
231
-
232
- /**
233
- * Get available palettes
234
- */
235
- fun getAvailablePalettes(): List<String> {
236
- return sdkManager?.availablePalettes ?: emptyList()
237
- }
238
-
239
- /**
240
- * Get list of discovered devices
241
- */
242
- fun getDiscoveredDevices(): List<Identity> {
243
- return sdkManager?.discoveredDevices ?: emptyList()
244
- }
245
-
246
- /**
247
- * Check states
248
- */
249
- fun isConnected(): Boolean = isConnected
250
- fun isStreaming(): Boolean = isStreaming
251
- fun isEmulator(): Boolean = connectedDeviceName?.contains("EMULAT", ignoreCase = true) == true
252
- fun isDeviceConnected(): Boolean = isConnected
253
-
254
- /**
255
- * Get connected device info
256
- */
257
- fun getConnectedDeviceInfo(): String {
258
- return connectedDeviceName ?: "Not connected"
259
- }
260
-
261
- /**
262
- * Get latest frame as file path (for RN)
263
- */
264
- fun getLatestFramePath(): String? {
265
- val bitmap = latestBitmap ?: return null
266
- return try {
267
- val file = File.createTempFile("flir_frame_", ".jpg")
268
- FileOutputStream(file).use { out ->
269
- bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
270
- }
271
- file.absolutePath
272
- } catch (t: Throwable) {
273
- Log.e(TAG, "Failed to save frame", t)
274
- null
275
- }
276
- }
277
-
278
- // SDK Listener
279
- private val sdkListener = object : FlirSdkManager.Listener {
280
- override fun onDeviceFound(identity: Identity) {
281
- Log.i(TAG, "Device found: ${identity.deviceId}")
282
- }
283
-
284
- override fun onDeviceListUpdated(devices: List<Identity>) {
285
- Log.i(TAG, "Devices updated: ${devices.size} found")
286
- devices.forEach {
287
- Log.d(TAG, " - ${it.deviceId} (${it.communicationInterface})")
288
- }
289
- emitDevicesFound(devices)
290
- }
291
-
292
- override fun onConnected(identity: Identity?) {
293
- Log.i(TAG, "Connected to: ${identity?.deviceId}")
294
- isConnected = true
295
- connectedDeviceId = identity?.deviceId
296
- connectedDeviceName = identity?.deviceId
297
- emitDeviceState("connected")
298
-
299
- // Auto-start streaming when connected
300
- startStream()
301
- }
302
-
303
- override fun onDisconnected() {
304
- Log.i(TAG, "Disconnected")
305
- isConnected = false
306
- isStreaming = false
307
- connectedDeviceId = null
308
- connectedDeviceName = null
309
- emitDeviceState("disconnected")
310
- }
311
-
312
- override fun onFrame(bitmap: Bitmap) {
313
- if (bitmap.isRecycled || bitmap.width <= 0 || bitmap.height <= 0) {
314
- return
315
- }
316
-
317
- latestBitmap = bitmap
318
- isStreaming = true
319
-
320
- // Notify texture callback (for GL rendering)
321
- if (textureCallback != null) {
322
- textureCallback?.onTextureUpdate(bitmap, 0)
323
- } else {
324
- // Log only occasionally to avoid spam
325
- if (System.currentTimeMillis() % 5000 < 100) {
326
- Log.w(TAG, "⚠️ Frame received but textureCallback is null - texture won't update!")
327
- }
328
- }
329
-
330
- // Rate-limited RN event
331
- emitFrameToReactNative(bitmap)
332
- }
333
-
334
- override fun onError(message: String) {
335
- Log.e(TAG, "Error: $message")
336
- emitError(message)
337
- }
338
- }
339
-
340
- // React Native event emitters
341
- private fun emitFrameToReactNative(bitmap: Bitmap) {
342
- val now = System.currentTimeMillis()
343
- if (now - lastEmitMs.get() < minEmitIntervalMs) return
344
- lastEmitMs.set(now)
345
-
346
- val ctx = reactContext ?: return
347
- try {
348
- val params = Arguments.createMap().apply {
349
- putInt("width", bitmap.width)
350
- putInt("height", bitmap.height)
351
- putDouble("timestamp", now.toDouble())
352
- }
353
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
354
- .emit("FlirFrameReceived", params)
355
- } catch (e: Exception) {
356
- // Ignore
357
- }
358
- }
359
-
360
- private fun emitDeviceState(state: String) {
361
- val ctx = reactContext
362
- if (ctx == null) {
363
- Log.e(TAG, "Cannot emit FlirDeviceConnected($state) - reactContext is null!")
364
- return
365
- }
366
- Log.d(TAG, "Emitting FlirDeviceConnected: $state")
367
- try {
368
- val params = Arguments.createMap().apply {
369
- putString("state", state)
370
- putBoolean("isConnected", isConnected)
371
- putBoolean("isStreaming", isStreaming)
372
- putBoolean("isEmulator", isEmulator())
373
- connectedDeviceName?.let { putString("deviceName", it) }
374
- connectedDeviceId?.let { putString("deviceId", it) }
375
- }
376
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
377
- .emit("FlirDeviceConnected", params)
378
- } catch (e: Exception) {
379
- Log.e(TAG, "Failed to emit device state", e)
380
- }
381
- }
382
-
383
- private fun emitDevicesFound(devices: List<Identity>) {
384
- val ctx = reactContext
385
- if (ctx == null) {
386
- Log.e(TAG, "Cannot emit FlirDevicesFound - reactContext is null!")
387
- return
388
- }
389
- Log.d(TAG, "Emitting FlirDevicesFound with ${devices.size} devices")
390
- try {
391
- val params = Arguments.createMap()
392
- val devicesArray: WritableArray = Arguments.createArray()
393
-
394
- devices.forEach { identity ->
395
- val deviceMap: WritableMap = Arguments.createMap().apply {
396
- putString("id", identity.deviceId)
397
- putString("name", identity.deviceId)
398
- putString("communicationType", identity.communicationInterface.name)
399
- putBoolean("isEmulator", identity.communicationInterface.name == "EMULATOR")
400
- }
401
- devicesArray.pushMap(deviceMap)
402
- }
403
-
404
- params.putArray("devices", devicesArray)
405
- params.putInt("count", devices.size)
406
-
407
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
408
- .emit("FlirDevicesFound", params)
409
- Log.d(TAG, "Successfully emitted FlirDevicesFound")
410
- } catch (e: Exception) {
411
- Log.e(TAG, "Failed to emit devices found", e)
412
- }
413
- }
414
-
415
- private fun emitError(message: String) {
416
- val ctx = reactContext ?: return
417
- try {
418
- val params = Arguments.createMap().apply {
419
- putString("error", message)
420
- }
421
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
422
- .emit("FlirError", params)
423
- } catch (e: Exception) {
424
- Log.e(TAG, "Failed to emit error", e)
425
- }
426
- }
427
-
428
- // Legacy compatibility
429
- @JvmStatic
430
- fun getInstance(): FlirManager = this
431
-
432
- interface DiscoveryCallback {
433
- fun onDeviceFound(deviceName: String)
434
- fun onDiscoveryTimeout()
435
- fun onEmulatorEnabled()
436
- }
437
-
438
- private var discoveryCallback: DiscoveryCallback? = null
439
-
440
- fun setDiscoveryCallback(callback: DiscoveryCallback?) {
441
- discoveryCallback = callback
442
- }
443
-
444
- // Legacy methods - no-ops or simple forwards
445
- fun setEmulatorMode(enabled: Boolean) {
446
- Log.d(TAG, "setEmulatorMode($enabled) - legacy, use startDiscovery() instead")
447
- if (enabled) {
448
- startDiscovery(retry = true)
449
- }
450
- }
451
-
452
- fun enableEmulatorMode() = setEmulatorMode(true)
453
-
454
- fun forceEmulatorMode(type: String = "FLIR_ONE_EDGE") {
455
- Log.d(TAG, "forceEmulatorMode($type) - legacy, use startDiscovery() instead")
456
- startDiscovery(retry = true)
457
- }
458
-
459
- fun setPreferredEmulatorType(type: String) {
460
- Log.d(TAG, "setPreferredEmulatorType($type) - legacy, no longer used")
461
- }
462
-
463
- fun updateAcol(value: Float) {
464
- // No-op - not used in simplified version
465
- }
466
-
467
- /**
468
- * Cleanup
469
- */
470
- fun destroy() {
471
- stop()
472
- sdkManager?.destroy()
473
- sdkManager = null
474
- isInitialized = false
475
- }
476
- }
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.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.bridge.WritableArray
10
+ import com.facebook.react.bridge.WritableMap
11
+ import com.facebook.react.modules.core.DeviceEventManagerModule
12
+ import com.facebook.react.uimanager.ThemedReactContext
13
+ import com.flir.thermalsdk.live.Identity
14
+ import java.io.File
15
+ import java.io.FileOutputStream
16
+ import java.util.concurrent.atomic.AtomicLong
17
+
18
+ /**
19
+ * Simplified FlirManager - bridge between React Native and FlirSdkManager
20
+ * No filtering - returns ALL discovered devices (USB, Network, Emulator)
21
+ * Let React Native handle any filtering logic
22
+ */
23
+ object FlirManager {
24
+ private const val TAG = "FlirManager"
25
+
26
+ private var sdkManager: FlirSdkManager? = null
27
+ private var reactContext: ReactContext? = null
28
+ private var appContext: Context? = null
29
+
30
+ // Frame rate limiting
31
+ private val lastEmitMs = AtomicLong(0)
32
+ private val minEmitIntervalMs = 100L // ~10 fps max for RN events
33
+
34
+ // State
35
+ private var isInitialized = false
36
+ private var isScanning = false
37
+ private var isConnected = false
38
+ private var isStreaming = false
39
+ private var connectedDeviceId: String? = null
40
+ private var connectedDeviceName: String? = null
41
+
42
+ // Latest bitmap for texture updates
43
+ private var latestBitmap: Bitmap? = null
44
+
45
+ // Callbacks
46
+ interface TextureUpdateCallback {
47
+ fun onTextureUpdate(bitmap: Bitmap, textureUnit: Int)
48
+ }
49
+
50
+ interface TemperatureCallback {
51
+ fun onTemperatureData(temperature: Double, x: Int, y: Int)
52
+ }
53
+
54
+ private var textureCallback: TextureUpdateCallback? = null
55
+ private var temperatureCallback: TemperatureCallback? = null
56
+
57
+ fun setTextureCallback(callback: TextureUpdateCallback?) {
58
+ textureCallback = callback
59
+ }
60
+
61
+ fun setTemperatureCallback(callback: TemperatureCallback?) {
62
+ temperatureCallback = callback
63
+ }
64
+
65
+ fun getLatestBitmap(): Bitmap? = latestBitmap
66
+
67
+ /**
68
+ * Initialize the FLIR SDK
69
+ */
70
+ fun init(context: Context) {
71
+ // Store react context for event emission if it's a React context
72
+ // Always update if we get a valid ReactContext (in case previous was stale)
73
+ if (context is ReactContext) {
74
+ Log.d(TAG, "Storing ReactContext for event emission: ${context.javaClass.simpleName}")
75
+ reactContext = context
76
+ } else {
77
+ Log.d(TAG, "Context is not ReactContext: ${context.javaClass.simpleName}")
78
+ }
79
+
80
+ if (isInitialized) {
81
+ Log.d(TAG, "Already initialized")
82
+ return
83
+ }
84
+
85
+ appContext = context.applicationContext
86
+
87
+ sdkManager = FlirSdkManager.getInstance(context)
88
+ sdkManager?.setListener(sdkListener)
89
+ sdkManager?.initialize()
90
+
91
+ isInitialized = true
92
+ Log.i(TAG, "FlirManager initialized")
93
+ }
94
+
95
+ /**
96
+ * Start scanning for devices (USB, Network, Emulator - ALL types)
97
+ */
98
+ fun startDiscovery(retry: Boolean = false) {
99
+ Log.i(TAG, "startDiscovery(retry=$retry)")
100
+
101
+ if (!isInitialized && appContext != null) {
102
+ init(appContext!!)
103
+ }
104
+
105
+ if (isScanning && !retry) {
106
+ Log.d(TAG, "Already scanning")
107
+ return
108
+ }
109
+
110
+ isScanning = true
111
+ emitDeviceState("discovering")
112
+ sdkManager?.scan()
113
+ }
114
+
115
+ /**
116
+ * Start discovery with React context
117
+ */
118
+ fun startDiscoveryAndConnect(context: ThemedReactContext, isEmuMode: Boolean = false) {
119
+ reactContext = context
120
+ startDiscovery(retry = false)
121
+ }
122
+
123
+ /**
124
+ * Stop scanning
125
+ */
126
+ fun stopDiscovery() {
127
+ Log.i(TAG, "stopDiscovery")
128
+ sdkManager?.stop()
129
+ isScanning = false
130
+ }
131
+
132
+ /**
133
+ * Connect to a device by ID
134
+ */
135
+ fun connectToDevice(deviceId: String) {
136
+ Log.i(TAG, "connectToDevice: $deviceId")
137
+
138
+ val devices = sdkManager?.discoveredDevices ?: emptyList()
139
+ val identity = devices.find { it.deviceId == deviceId }
140
+
141
+ if (identity != null) {
142
+ sdkManager?.connect(identity)
143
+ } else {
144
+ Log.e(TAG, "Device not found: $deviceId")
145
+ emitError("Device not found: $deviceId")
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Switch to a different device
151
+ */
152
+ fun switchToDevice(deviceId: String) {
153
+ if (deviceId == connectedDeviceId) {
154
+ Log.d(TAG, "Already connected to: $deviceId")
155
+ return
156
+ }
157
+
158
+ // Disconnect current and connect new
159
+ if (isConnected) {
160
+ sdkManager?.disconnect()
161
+ }
162
+ connectToDevice(deviceId)
163
+ }
164
+
165
+ /**
166
+ * Start streaming from connected device
167
+ */
168
+ fun startStream() {
169
+ Log.i(TAG, "startStream")
170
+ sdkManager?.startStream()
171
+ }
172
+
173
+ /**
174
+ * Stop streaming
175
+ */
176
+ fun stopStream() {
177
+ Log.i(TAG, "stopStream")
178
+ sdkManager?.stopStream()
179
+ isStreaming = false
180
+ }
181
+
182
+ /**
183
+ * Disconnect from current device
184
+ */
185
+ fun disconnect() {
186
+ Log.i(TAG, "disconnect")
187
+ sdkManager?.disconnect()
188
+ isConnected = false
189
+ isStreaming = false
190
+ connectedDeviceId = null
191
+ connectedDeviceName = null
192
+ }
193
+
194
+ /**
195
+ * Stop everything
196
+ */
197
+ fun stop() {
198
+ Log.i(TAG, "stop")
199
+ stopStream()
200
+ disconnect()
201
+ stopDiscovery()
202
+ latestBitmap = null
203
+ }
204
+
205
+ /**
206
+ * Get temperature at point in image coordinates
207
+ */
208
+ fun getTemperatureAt(x: Int, y: Int): Double? {
209
+ return sdkManager?.getTemperatureAt(x, y)?.takeIf { !it.isNaN() }
210
+ }
211
+
212
+ /**
213
+ * Get temperature at normalized coordinates (0.0 to 1.0)
214
+ */
215
+ fun getTemperatureAtNormalized(normalizedX: Double, normalizedY: Double): Double? {
216
+ return sdkManager?.getTemperatureAtNormalized(normalizedX, normalizedY)?.takeIf { !it.isNaN() }
217
+ }
218
+
219
+ /**
220
+ * Alias for getTemperatureAt
221
+ */
222
+ fun getTemperatureAtPoint(x: Int, y: Int): Double? = getTemperatureAt(x, y)
223
+
224
+ /**
225
+ * Set palette
226
+ */
227
+ fun setPalette(name: String) {
228
+ Log.d(TAG, "setPalette: $name")
229
+ sdkManager?.setPalette(name)
230
+ }
231
+
232
+ /**
233
+ * Get available palettes
234
+ */
235
+ fun getAvailablePalettes(): List<String> {
236
+ return sdkManager?.availablePalettes ?: emptyList()
237
+ }
238
+
239
+ /**
240
+ * Get list of discovered devices
241
+ */
242
+ fun getDiscoveredDevices(): List<Identity> {
243
+ return sdkManager?.discoveredDevices ?: emptyList()
244
+ }
245
+
246
+ /**
247
+ * Check states
248
+ */
249
+ fun isConnected(): Boolean = isConnected
250
+ fun isStreaming(): Boolean = isStreaming
251
+ fun isEmulator(): Boolean = connectedDeviceName?.contains("EMULAT", ignoreCase = true) == true
252
+ fun isDeviceConnected(): Boolean = isConnected
253
+
254
+ /**
255
+ * Get connected device info
256
+ */
257
+ fun getConnectedDeviceInfo(): String {
258
+ return connectedDeviceName ?: "Not connected"
259
+ }
260
+
261
+ /**
262
+ * Get latest frame as file path (for RN)
263
+ */
264
+ fun getLatestFramePath(): String? {
265
+ val bitmap = latestBitmap ?: return null
266
+ return try {
267
+ val file = File.createTempFile("flir_frame_", ".jpg")
268
+ FileOutputStream(file).use { out ->
269
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
270
+ }
271
+ file.absolutePath
272
+ } catch (t: Throwable) {
273
+ Log.e(TAG, "Failed to save frame", t)
274
+ null
275
+ }
276
+ }
277
+
278
+ // SDK Listener
279
+ private val sdkListener = object : FlirSdkManager.Listener {
280
+ override fun onDeviceFound(identity: Identity) {
281
+ Log.i(TAG, "Device found: ${identity.deviceId}")
282
+ }
283
+
284
+ override fun onDeviceListUpdated(devices: List<Identity>) {
285
+ Log.i(TAG, "Devices updated: ${devices.size} found")
286
+ devices.forEach {
287
+ Log.d(TAG, " - ${it.deviceId} (${it.communicationInterface})")
288
+ }
289
+ emitDevicesFound(devices)
290
+ }
291
+
292
+ override fun onConnected(identity: Identity?) {
293
+ Log.i(TAG, "Connected to: ${identity?.deviceId}")
294
+ isConnected = true
295
+ connectedDeviceId = identity?.deviceId
296
+ connectedDeviceName = identity?.deviceId
297
+ emitDeviceState("connected")
298
+
299
+ // Auto-start streaming when connected
300
+ startStream()
301
+ }
302
+
303
+ override fun onDisconnected() {
304
+ Log.i(TAG, "Disconnected")
305
+ isConnected = false
306
+ isStreaming = false
307
+ connectedDeviceId = null
308
+ connectedDeviceName = null
309
+ emitDeviceState("disconnected")
310
+ }
311
+
312
+ override fun onFrame(bitmap: Bitmap) {
313
+ if (bitmap.isRecycled || bitmap.width <= 0 || bitmap.height <= 0) {
314
+ return
315
+ }
316
+
317
+ latestBitmap = bitmap
318
+ isStreaming = true
319
+
320
+ // Notify texture callback (for GL rendering)
321
+ if (textureCallback != null) {
322
+ textureCallback?.onTextureUpdate(bitmap, 0)
323
+ } else {
324
+ // Log only occasionally to avoid spam
325
+ if (System.currentTimeMillis() % 5000 < 100) {
326
+ Log.w(TAG, "⚠️ Frame received but textureCallback is null - texture won't update!")
327
+ }
328
+ }
329
+
330
+ // Rate-limited RN event
331
+ emitFrameToReactNative(bitmap)
332
+ }
333
+
334
+ override fun onError(message: String) {
335
+ Log.e(TAG, "Error: $message")
336
+ emitError(message)
337
+ }
338
+ }
339
+
340
+ // React Native event emitters
341
+ private fun emitFrameToReactNative(bitmap: Bitmap) {
342
+ val now = System.currentTimeMillis()
343
+ if (now - lastEmitMs.get() < minEmitIntervalMs) return
344
+ lastEmitMs.set(now)
345
+
346
+ val ctx = reactContext ?: return
347
+ try {
348
+ val params = Arguments.createMap().apply {
349
+ putInt("width", bitmap.width)
350
+ putInt("height", bitmap.height)
351
+ putDouble("timestamp", now.toDouble())
352
+ }
353
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
354
+ .emit("FlirFrameReceived", params)
355
+ } catch (e: Exception) {
356
+ // Ignore
357
+ }
358
+ }
359
+
360
+ private fun emitDeviceState(state: String) {
361
+ val ctx = reactContext
362
+ if (ctx == null) {
363
+ Log.e(TAG, "Cannot emit FlirDeviceConnected($state) - reactContext is null!")
364
+ return
365
+ }
366
+ Log.d(TAG, "Emitting FlirDeviceConnected: $state")
367
+ try {
368
+ val params = Arguments.createMap().apply {
369
+ putString("state", state)
370
+ putBoolean("isConnected", isConnected)
371
+ putBoolean("isStreaming", isStreaming)
372
+ putBoolean("isEmulator", isEmulator())
373
+ connectedDeviceName?.let { putString("deviceName", it) }
374
+ connectedDeviceId?.let { putString("deviceId", it) }
375
+ }
376
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
377
+ .emit("FlirDeviceConnected", params)
378
+ } catch (e: Exception) {
379
+ Log.e(TAG, "Failed to emit device state", e)
380
+ }
381
+ }
382
+
383
+ private fun emitDevicesFound(devices: List<Identity>) {
384
+ val ctx = reactContext
385
+ if (ctx == null) {
386
+ Log.e(TAG, "Cannot emit FlirDevicesFound - reactContext is null!")
387
+ return
388
+ }
389
+ Log.d(TAG, "Emitting FlirDevicesFound with ${devices.size} devices")
390
+ try {
391
+ val params = Arguments.createMap()
392
+ val devicesArray: WritableArray = Arguments.createArray()
393
+
394
+ devices.forEach { identity ->
395
+ val deviceMap: WritableMap = Arguments.createMap().apply {
396
+ putString("id", identity.deviceId)
397
+ putString("name", identity.deviceId)
398
+ putString("communicationType", identity.communicationInterface.name)
399
+ putBoolean("isEmulator", identity.communicationInterface.name == "EMULATOR")
400
+ }
401
+ devicesArray.pushMap(deviceMap)
402
+ }
403
+
404
+ params.putArray("devices", devicesArray)
405
+ params.putInt("count", devices.size)
406
+
407
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
408
+ .emit("FlirDevicesFound", params)
409
+ Log.d(TAG, "Successfully emitted FlirDevicesFound")
410
+ } catch (e: Exception) {
411
+ Log.e(TAG, "Failed to emit devices found", e)
412
+ }
413
+ }
414
+
415
+ private fun emitError(message: String) {
416
+ val ctx = reactContext ?: return
417
+ try {
418
+ val params = Arguments.createMap().apply {
419
+ putString("error", message)
420
+ }
421
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
422
+ .emit("FlirError", params)
423
+ } catch (e: Exception) {
424
+ Log.e(TAG, "Failed to emit error", e)
425
+ }
426
+ }
427
+
428
+ // Legacy compatibility
429
+ @JvmStatic
430
+ fun getInstance(): FlirManager = this
431
+
432
+ interface DiscoveryCallback {
433
+ fun onDeviceFound(deviceName: String)
434
+ fun onDiscoveryTimeout()
435
+ fun onEmulatorEnabled()
436
+ }
437
+
438
+ private var discoveryCallback: DiscoveryCallback? = null
439
+
440
+ fun setDiscoveryCallback(callback: DiscoveryCallback?) {
441
+ discoveryCallback = callback
442
+ }
443
+
444
+ // Legacy methods - no-ops or simple forwards
445
+ fun setEmulatorMode(enabled: Boolean) {
446
+ Log.d(TAG, "setEmulatorMode($enabled) - legacy, use startDiscovery() instead")
447
+ if (enabled) {
448
+ startDiscovery(retry = true)
449
+ }
450
+ }
451
+
452
+ fun enableEmulatorMode() = setEmulatorMode(true)
453
+
454
+ fun forceEmulatorMode(type: String = "FLIR_ONE_EDGE") {
455
+ Log.d(TAG, "forceEmulatorMode($type) - legacy, use startDiscovery() instead")
456
+ startDiscovery(retry = true)
457
+ }
458
+
459
+ fun setPreferredEmulatorType(type: String) {
460
+ Log.d(TAG, "setPreferredEmulatorType($type) - legacy, no longer used")
461
+ }
462
+
463
+ fun updateAcol(value: Float) {
464
+ // No-op - not used in simplified version
465
+ }
466
+
467
+ /**
468
+ * Cleanup
469
+ */
470
+ fun destroy() {
471
+ stop()
472
+ sdkManager?.destroy()
473
+ sdkManager = null
474
+ isInitialized = false
475
+ }
476
+ }