ilabs-flir 2.2.32 → 2.3.1

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.
@@ -71,7 +71,19 @@ object FlirManager {
71
71
  fun isPreferSdkRotation(): Boolean = false
72
72
  fun getBatteryLevel(): Int = -1
73
73
  fun isBatteryCharging(): Boolean = false
74
- fun setPalette(name: String) { /* No-op */ }
74
+ fun setPalette(name: String) {
75
+ sdkManager?.setPalette(name)
76
+ // Also try to update the app's global Var.cool if possible
77
+ try {
78
+ val palettes = listOf("iron", "rainbow", "grayscale", "arctic", "lava", "contrast", "hotcold", "medical")
79
+ val idx = palettes.indexOf(name.lowercase())
80
+ if (idx != -1) {
81
+ // If we found the index, we use the raw value.
82
+ // But if the name itself is passed as a number or from a loop, handle it.
83
+ updateAcol(idx.toFloat())
84
+ }
85
+ } catch (e: Exception) {}
86
+ }
75
87
  fun getAvailablePalettes(): List<String> = emptyList()
76
88
 
77
89
  /**
@@ -95,6 +107,7 @@ object FlirManager {
95
107
  /**
96
108
  * Start scanning
97
109
  */
110
+ @Synchronized
98
111
  fun startDiscovery(retry: Boolean = false) {
99
112
  if (!isInitialized && reactContext != null) {
100
113
  init(reactContext!!)
@@ -102,6 +115,7 @@ object FlirManager {
102
115
 
103
116
  if (isScanning && !retry) return
104
117
 
118
+ Log.i(TAG, "Starting FlirManager discovery...")
105
119
  isScanning = true
106
120
  emitDeviceState("discovering")
107
121
  sdkManager?.scan()
@@ -118,7 +132,9 @@ object FlirManager {
118
132
  /**
119
133
  * Stop scanning
120
134
  */
135
+ @Synchronized
121
136
  fun stopDiscovery() {
137
+ Log.i(TAG, "Stopping FlirManager discovery...")
122
138
  sdkManager?.stopScan()
123
139
  isScanning = false
124
140
  }
@@ -126,7 +142,12 @@ object FlirManager {
126
142
  /**
127
143
  * Connect to a device
128
144
  */
129
- fun connectToDevice(deviceId: String) {
145
+ @Synchronized
146
+ fun connectToDevice(deviceId: String?) {
147
+ if (deviceId == null) {
148
+ Log.e(TAG, "connectToDevice: deviceId is null")
149
+ return
150
+ }
130
151
  Log.i(TAG, "connectToDevice: $deviceId")
131
152
 
132
153
  val devices = sdkManager?.discoveredDevices ?: emptyList()
@@ -148,7 +169,9 @@ object FlirManager {
148
169
  /**
149
170
  * Disconnect
150
171
  */
172
+ @Synchronized
151
173
  fun disconnect() {
174
+ Log.i(TAG, "Disconnecting FlirManager...")
152
175
  shouldProcessFrames.set(false)
153
176
  sdkManager?.disconnect()
154
177
  isConnected = false
@@ -160,11 +183,19 @@ object FlirManager {
160
183
  /**
161
184
  * Stop everything
162
185
  */
186
+ @Synchronized
163
187
  fun stop() {
188
+ Log.i(TAG, "Stopping FlirManager completely...")
164
189
  shouldProcessFrames.set(false)
190
+
191
+ // Clear callbacks first to prevent any more frames/updates from hitting Java/RN
192
+ textureCallback = null
193
+ temperatureCallback = null
194
+
165
195
  disconnect()
166
196
  stopDiscovery()
167
197
  latestBitmap = null
198
+ Log.i(TAG, "FlirManager stopped")
168
199
  }
169
200
 
170
201
  // Stub legacy methods
@@ -208,6 +239,13 @@ object FlirManager {
208
239
  }
209
240
 
210
241
  /**
242
+ * Capture a high-fidelity radiometric snapshot (saves thermal data)
243
+ */
244
+ fun captureRadiometricSnapshot(path: String) {
245
+ sdkManager?.captureRadiometricSnapshot(path)
246
+ }
247
+
248
+ /**
211
249
  * Get latest frame as file path (for RN)
212
250
  */
213
251
  fun getLatestFramePath(): String? {
@@ -382,6 +420,33 @@ object FlirManager {
382
420
  fun enableEmulatorMode() = startDiscovery()
383
421
  fun forceEmulatorMode(type: String = "FLIR_ONE_EDGE") { startDiscovery() }
384
422
  fun setPreferredEmulatorType(type: String) { }
385
- fun updateAcol(value: Float) { }
423
+ fun updateAcol(value: Float) {
424
+ Log.d(TAG, "updateAcol: $value")
425
+ // Use reflection to update ilabs.libs.io.data.Var.cool to avoid circular dependency
426
+ try {
427
+ val varClass = Class.forName("ilabs.libs.io.data.Var")
428
+ val coolField = varClass.getField("cool")
429
+
430
+ // Modular logic for palettes: if index > 7, wrap around (17 mod 8)
431
+ // But we keep shader variants (14, 15, 16) as-is if within that range
432
+ val rawIdx = value.toInt()
433
+ var shaderIdx = rawIdx
434
+ if (shaderIdx > 16) {
435
+ shaderIdx = shaderIdx % 16 // Shader loop
436
+ }
437
+
438
+ coolField.set(null, shaderIdx)
439
+ Log.d(TAG, "Updated Var.cool to $shaderIdx via reflection (raw=$rawIdx)")
440
+
441
+ // If we are in FLIR mode, also notify the SDK palette with modular loop
442
+ val palettes = listOf("iron", "rainbow", "grayscale", "arctic", "lava", "contrast", "hotcold", "medical")
443
+ val paletteIdx = rawIdx % palettes.size
444
+ val safeIdx = if (paletteIdx < 0) paletteIdx + palettes.size else paletteIdx
445
+ sdkManager?.setPalette(palettes[safeIdx])
446
+
447
+ } catch (e: Exception) {
448
+ Log.w(TAG, "Could not update Var.cool via reflection: ${e.message}")
449
+ }
450
+ }
386
451
  fun destroy() { stop() }
387
452
  }
@@ -34,7 +34,7 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
34
34
  // Simple placeholder conversion: converts an ARGB color to a pseudo-temperature value.
35
35
  // Replace with SDK call when integrating thermalsdk APIs.
36
36
  @ReactMethod
37
- fun getTemperatureFromColor(color: Int, promise: Promise) {
37
+ fun getTemperatureFromColor(color: Int, promise: Promise?) {
38
38
  try {
39
39
  val r = (color shr 16) and 0xFF
40
40
  val g = (color shr 8) and 0xFF
@@ -42,81 +42,81 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
42
42
  // Luminance-like value scaled to a plausible temperature range (0°C - 400°C)
43
43
  val lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
44
44
  val temp = 0.0 + (lum / 255.0) * 400.0
45
- promise.resolve(temp)
45
+ promise?.resolve(temp)
46
46
  } catch (e: Exception) {
47
- promise.reject("ERR_FLIR_CONVERT", e)
47
+ promise?.reject("ERR_FLIR_CONVERT", e)
48
48
  }
49
49
  }
50
50
 
51
51
  @ReactMethod
52
- fun getLatestFramePath(promise: Promise) {
52
+ fun getLatestFramePath(promise: Promise?) {
53
53
  try {
54
54
  val path = FlirFrameCache.latestFramePath
55
- if (path != null) promise.resolve(path) else promise.resolve(null)
55
+ if (path != null) promise?.resolve(path) else promise?.resolve(null)
56
56
  } catch (e: Exception) {
57
- promise.reject("ERR_FLIR_PATH", e)
57
+ promise?.reject("ERR_FLIR_PATH", e)
58
58
  }
59
59
  }
60
60
 
61
61
  @ReactMethod
62
- fun getTemperatureAt(x: Int, y: Int, promise: Promise) {
62
+ fun getTemperatureAt(x: Int, y: Int, promise: Promise?) {
63
63
  try {
64
64
  val temp = FlirManager.getTemperatureAt(x, y)
65
- if (temp != null) promise.resolve(temp) else promise.reject("ERR_NO_DATA", "No temperature data available")
65
+ if (temp != null) promise?.resolve(temp) else promise?.reject("ERR_NO_DATA", "No temperature data available")
66
66
  } catch (e: Exception) {
67
- promise.reject("ERR_FLIR_SAMPLE", e)
67
+ promise?.reject("ERR_FLIR_SAMPLE", e)
68
68
  }
69
69
  }
70
70
 
71
71
  @ReactMethod
72
- fun getTemperatureAtNormalized(nx: Double, ny: Double, promise: Promise) {
72
+ fun getTemperatureAtNormalized(nx: Double, ny: Double, promise: Promise?) {
73
73
  try {
74
74
  val temp = FlirManager.getTemperatureAtNormalized(nx, ny)
75
- if (temp != null) promise.resolve(temp) else promise.resolve(null)
75
+ if (temp != null) promise?.resolve(temp) else promise?.resolve(null)
76
76
  } catch (e: Exception) {
77
- promise.reject("ERR_FLIR_TEMP_NORM", e)
77
+ promise?.reject("ERR_FLIR_TEMP_NORM", e)
78
78
  }
79
79
  }
80
80
 
81
81
  @ReactMethod
82
- fun isEmulator(promise: Promise) {
82
+ fun isEmulator(promise: Promise?) {
83
83
  try {
84
- promise.resolve(FlirManager.isEmulator())
84
+ promise?.resolve(FlirManager.isEmulator())
85
85
  } catch (e: Exception) {
86
- promise.reject("ERR_FLIR_EMULATOR_CHECK", e)
86
+ promise?.reject("ERR_FLIR_EMULATOR_CHECK", e)
87
87
  }
88
88
  }
89
89
 
90
90
  @ReactMethod
91
- fun isDeviceConnected(promise: Promise) {
91
+ fun isDeviceConnected(promise: Promise?) {
92
92
  try {
93
- promise.resolve(FlirManager.isDeviceConnected())
93
+ promise?.resolve(FlirManager.isDeviceConnected())
94
94
  } catch (e: Exception) {
95
- promise.reject("ERR_FLIR_DEVICE_CHECK", e)
95
+ promise?.reject("ERR_FLIR_DEVICE_CHECK", e)
96
96
  }
97
97
  }
98
98
 
99
99
  @ReactMethod
100
- fun getConnectedDeviceInfo(promise: Promise) {
100
+ fun getConnectedDeviceInfo(promise: Promise?) {
101
101
  try {
102
- promise.resolve(FlirManager.getConnectedDeviceInfo())
102
+ promise?.resolve(FlirManager.getConnectedDeviceInfo())
103
103
  } catch (e: Exception) {
104
- promise.reject("ERR_FLIR_DEVICE_INFO", e)
104
+ promise?.reject("ERR_FLIR_DEVICE_INFO", e)
105
105
  }
106
106
  }
107
107
 
108
108
  @ReactMethod
109
- fun isSDKDownloaded(promise: Promise) {
109
+ fun isSDKDownloaded(promise: Promise?) {
110
110
  try {
111
111
  val available = FlirSDKLoader.isSDKAvailable(reactContext)
112
- promise.resolve(available)
112
+ promise?.resolve(available)
113
113
  } catch (e: Exception) {
114
- promise.reject("ERR_FLIR_SDK_CHECK", e)
114
+ promise?.reject("ERR_FLIR_SDK_CHECK", e)
115
115
  }
116
116
  }
117
117
 
118
118
  @ReactMethod
119
- fun getSDKStatus(promise: Promise) {
119
+ fun getSDKStatus(promise: Promise?) {
120
120
  try {
121
121
  val available = FlirSDKLoader.isSDKAvailable(reactContext)
122
122
  val arch = FlirSDKLoader.getDeviceArch()
@@ -131,14 +131,14 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
131
131
  result.putBoolean("dexExists", dexPath?.exists() == true)
132
132
  result.putBoolean("nativeLibsExist", nativeLibDir?.exists() == true)
133
133
 
134
- promise.resolve(result)
134
+ promise?.resolve(result)
135
135
  } catch (e: Exception) {
136
- promise.reject("ERR_FLIR_SDK_STATUS", e)
136
+ promise?.reject("ERR_FLIR_SDK_STATUS", e)
137
137
  }
138
138
  }
139
139
 
140
140
  @ReactMethod
141
- fun getDiscoveredDevices(promise: Promise) {
141
+ fun getDiscoveredDevices(promise: Promise?) {
142
142
  try {
143
143
  val devices = FlirManager.getDiscoveredDevices()
144
144
  val result = com.facebook.react.bridge.Arguments.createArray()
@@ -152,129 +152,153 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
152
152
  result.pushMap(deviceMap)
153
153
  }
154
154
 
155
- promise.resolve(result)
155
+ promise?.resolve(result)
156
156
  } catch (e: Exception) {
157
- promise.reject("ERR_FLIR_DEVICES", e)
157
+ promise?.reject("ERR_FLIR_DEVICES", e)
158
158
  }
159
159
  }
160
160
 
161
161
  @ReactMethod
162
- fun setPreferSdkRotation(prefer: Boolean, promise: Promise) {
162
+ fun setPreferSdkRotation(prefer: Boolean, promise: Promise?) {
163
163
  try {
164
164
  FlirManager.setPreferSdkRotation(prefer)
165
- promise.resolve(true)
165
+ promise?.resolve(true)
166
166
  } catch (e: Exception) {
167
- promise.reject("ERR_FLIR_SET_ROTATION_PREF", e)
167
+ promise?.reject("ERR_FLIR_SET_ROTATION_PREF", e)
168
168
  }
169
169
  }
170
170
 
171
171
  @ReactMethod
172
- fun isPreferSdkRotation(promise: Promise) {
172
+ fun isPreferSdkRotation(promise: Promise?) {
173
173
  try {
174
174
  val v = FlirManager.isPreferSdkRotation()
175
- promise.resolve(v)
175
+ promise?.resolve(v)
176
176
  } catch (e: Exception) {
177
- promise.reject("ERR_FLIR_GET_ROTATION_PREF", e)
177
+ promise?.reject("ERR_FLIR_GET_ROTATION_PREF", e)
178
178
  }
179
179
  }
180
180
 
181
181
  @ReactMethod
182
- fun getBatteryLevel(promise: Promise) {
182
+ fun getBatteryLevel(promise: Promise?) {
183
183
  try {
184
184
  val level = FlirManager.getBatteryLevel()
185
- promise.resolve(level)
185
+ promise?.resolve(level)
186
186
  } catch (e: Exception) {
187
- promise.reject("ERR_FLIR_GET_BATTERY", e)
187
+ promise?.reject("ERR_FLIR_GET_BATTERY", e)
188
188
  }
189
189
  }
190
190
 
191
191
  @ReactMethod
192
- fun isBatteryCharging(promise: Promise) {
192
+ fun isBatteryCharging(promise: Promise?) {
193
193
  try {
194
194
  val v = FlirManager.isBatteryCharging()
195
- promise.resolve(v)
195
+ promise?.resolve(v)
196
196
  } catch (e: Exception) {
197
- promise.reject("ERR_FLIR_CHARGING", e)
197
+ promise?.reject("ERR_FLIR_CHARGING", e)
198
198
  }
199
199
  }
200
200
 
201
201
  @ReactMethod
202
- fun startEmulator(emulatorType: String, promise: Promise) {
202
+ fun startEmulator(emulatorType: String?, promise: Promise?) {
203
203
  try {
204
204
  // Ensure SDK is initialized with context before starting discovery
205
205
  FlirManager.init(reactContext)
206
206
  // With simplified API, just start discovery - emulators are discovered like any device
207
207
  FlirManager.startDiscovery(true)
208
- promise.resolve(true)
208
+ promise?.resolve(true)
209
209
  } catch (e: Exception) {
210
- promise.reject("ERR_FLIR_EMULATOR", e)
210
+ promise?.reject("ERR_FLIR_EMULATOR", e)
211
211
  }
212
212
  }
213
213
 
214
214
  @ReactMethod
215
- fun connectToDevice(deviceId: String, promise: Promise) {
215
+ fun connectToDevice(deviceId: String?, promise: Promise?) {
216
216
  try {
217
217
  // Ensure SDK is initialized with context before connecting
218
218
  FlirManager.init(reactContext)
219
- FlirManager.connectToDevice(deviceId)
220
- promise.resolve(true)
219
+ if (deviceId != null) {
220
+ FlirManager.connectToDevice(deviceId)
221
+ }
222
+ promise?.resolve(true)
221
223
  } catch (e: Exception) {
222
- promise.reject("ERR_FLIR_CONNECT", e)
224
+ promise?.reject("ERR_FLIR_CONNECT", e)
223
225
  }
224
226
  }
225
227
 
226
228
  @ReactMethod
227
- fun startDiscovery(promise: Promise) {
229
+ fun startDiscovery(promise: Promise?) {
228
230
  try {
229
231
  // Ensure SDK is initialized with context before starting discovery
230
232
  FlirManager.init(reactContext)
231
233
  FlirManager.startDiscovery(true)
232
- promise.resolve(true)
234
+ promise?.resolve(true)
233
235
  } catch (e: Exception) {
234
- promise.reject("ERR_FLIR_DISCOVERY", e)
236
+ promise?.reject("ERR_FLIR_DISCOVERY", e)
235
237
  }
236
238
  }
237
239
 
238
240
  @ReactMethod
239
- fun stopDiscovery(promise: Promise) {
241
+ fun stopDiscovery(promise: Promise?) {
240
242
  try {
241
243
  FlirManager.stopDiscovery()
242
- promise.resolve(true)
244
+ promise?.resolve(true)
243
245
  } catch (e: Exception) {
244
- promise.reject("ERR_FLIR_STOP_DISCOVERY", e)
246
+ promise?.reject("ERR_FLIR_STOP_DISCOVERY", e)
245
247
  }
246
248
  }
247
249
 
248
250
  @ReactMethod
249
- fun stopFlir(promise: Promise) {
251
+ fun stopFlir(promise: Promise?) {
250
252
  try {
251
253
  FlirManager.stop()
252
- promise.resolve(true)
254
+ promise?.resolve(true)
255
+ } catch (e: Exception) {
256
+ promise?.reject("ERR_FLIR_STOP", e)
257
+ }
258
+ }
259
+
260
+ @ReactMethod
261
+ fun updateAcol(value: Float, promise: Promise?) {
262
+ try {
263
+ FlirManager.updateAcol(value)
264
+ promise?.resolve(true)
265
+ } catch (e: Exception) {
266
+ promise?.reject("ERR_FLIR_ACOL", e)
267
+ }
268
+ }
269
+
270
+ @ReactMethod
271
+ fun setPalette(name: String?, promise: Promise?) {
272
+ try {
273
+ if (name != null) {
274
+ FlirManager.setPalette(name)
275
+ }
276
+ promise?.resolve(true)
253
277
  } catch (e: Exception) {
254
- promise.reject("ERR_FLIR_STOP", e)
278
+ promise?.reject("ERR_FLIR_PALETTE", e)
255
279
  }
256
280
  }
257
281
 
258
282
  @ReactMethod
259
- fun initializeSDK(promise: Promise) {
283
+ fun initializeSDK(promise: Promise?) {
260
284
  try {
261
285
  FlirManager.init(reactContext)
262
286
 
263
287
  val result = com.facebook.react.bridge.Arguments.createMap()
264
288
  result.putBoolean("initialized", true)
265
289
  result.putString("message", "SDK initialized successfully")
266
- promise.resolve(result)
290
+ promise?.resolve(result)
267
291
  } catch (e: Exception) {
268
292
  val result = com.facebook.react.bridge.Arguments.createMap()
269
293
  result.putBoolean("initialized", false)
270
294
  result.putString("error", e.message ?: "Unknown error")
271
295
  result.putString("errorType", e.javaClass.simpleName)
272
- promise.resolve(result)
296
+ promise?.resolve(result)
273
297
  }
274
298
  }
275
299
 
276
300
  @ReactMethod
277
- fun getDebugInfo(promise: Promise) {
301
+ fun getDebugInfo(promise: Promise?) {
278
302
  try {
279
303
  val result = com.facebook.react.bridge.Arguments.createMap()
280
304
 
@@ -299,9 +323,9 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
299
323
  result.putBoolean("isStreaming", FlirManager.isStreaming())
300
324
  result.putString("connectedDevice", FlirManager.getConnectedDeviceInfo())
301
325
 
302
- promise.resolve(result)
326
+ promise?.resolve(result)
303
327
  } catch (e: Exception) {
304
- promise.reject("ERR_DEBUG_INFO", e)
328
+ promise?.reject("ERR_DEBUG_INFO", e)
305
329
  }
306
330
  }
307
331
  }
@@ -8,7 +8,10 @@ import android.util.Log;
8
8
  import com.flir.thermalsdk.ErrorCode;
9
9
  import com.flir.thermalsdk.androidsdk.ThermalSdkAndroid;
10
10
  import com.flir.thermalsdk.androidsdk.image.BitmapAndroid;
11
+ import com.flir.thermalsdk.image.Palette;
12
+ import com.flir.thermalsdk.image.PaletteManager;
11
13
  import com.flir.thermalsdk.image.Point;
14
+ import com.flir.thermalsdk.image.ThermalImage;
12
15
  import com.flir.thermalsdk.image.ThermalValue;
13
16
  import com.flir.thermalsdk.live.AuthenticationResponse;
14
17
  import com.flir.thermalsdk.live.Camera;
@@ -48,8 +51,10 @@ public class FlirSdkManager {
48
51
  private Stream activeStream;
49
52
  private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
50
53
  private volatile Bitmap latestBitmap;
54
+ private volatile String currentPaletteName = "iron";
51
55
  private final AtomicBoolean isProcessingFrame = new AtomicBoolean(false);
52
56
  private boolean useHalfScale = false;
57
+ private String pendingSnapshotPath = null;
53
58
 
54
59
  // Listener
55
60
  private Listener listener;
@@ -337,14 +342,42 @@ public class FlirSdkManager {
337
342
  if (streamer != null && activeStream != null) {
338
343
  streamer.update();
339
344
 
340
- // createBitmap is safe to do here or in executor
341
- Bitmap bitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
342
- if (bitmap != null) {
343
- latestBitmap = bitmap;
344
- if (listener != null) {
345
- listener.onFrame(bitmap);
345
+ final String paletteToApply = currentPaletteName;
346
+ final String snapshotPath = pendingSnapshotPath;
347
+ pendingSnapshotPath = null;
348
+
349
+ streamer.withThermalImage(thermalImage -> {
350
+ // 1. Apply Palette
351
+ if (paletteToApply != null) {
352
+ Palette palette =
353
+ PaletteManager.getDefaultPalettes().stream()
354
+ .filter(p -> p.name.equalsIgnoreCase(paletteToApply))
355
+ .findFirst()
356
+ .orElse(null);
357
+ if (palette != null) {
358
+ thermalImage.setPalette(palette);
359
+ }
346
360
  }
347
- }
361
+
362
+ // 2. Save Radiometric Snapshot if requested
363
+ if (snapshotPath != null) {
364
+ try {
365
+ thermalImage.saveAs(snapshotPath);
366
+ Log.i(TAG, "Radiometric snapshot saved to: " + snapshotPath);
367
+ } catch (java.io.IOException e) {
368
+ Log.e(TAG, "Failed to save radiometric snapshot", e);
369
+ }
370
+ }
371
+
372
+ // 3. Generate Bitmap for display
373
+ Bitmap bitmap = BitmapAndroid.createBitmap(thermalImage.getImage()).getBitMap();
374
+ if (bitmap != null) {
375
+ latestBitmap = bitmap;
376
+ if (listener != null) {
377
+ listener.onFrame(bitmap);
378
+ }
379
+ }
380
+ });
348
381
  }
349
382
  } catch (Exception e) {
350
383
  Log.e(TAG, "Frame processing error", e);
@@ -423,6 +456,16 @@ public class FlirSdkManager {
423
456
  }
424
457
 
425
458
  // ==================== LISTENERS ====================
459
+
460
+ public void setPalette(String paletteName) {
461
+ this.currentPaletteName = paletteName;
462
+ Log.d(TAG, "Requested palette: " + paletteName);
463
+ }
464
+
465
+ public void captureRadiometricSnapshot(String path) {
466
+ this.pendingSnapshotPath = path;
467
+ Log.d(TAG, "Pending radiometric snapshot: " + path);
468
+ }
426
469
 
427
470
  private final DiscoveryEventListener discoveryListener = new DiscoveryEventListener() {
428
471
  @Override
@@ -63,6 +63,13 @@ import ThermalSDK
63
63
  private var connectedDeviceId: String?
64
64
  private var connectedDeviceName: String?
65
65
 
66
+ // Internal synchronization
67
+ private let stateLock = NSObject()
68
+
69
+ // Palette and Snapshot state
70
+ private var currentPaletteName: String = "iron"
71
+ private var pendingSnapshotPath: String?
72
+
66
73
  // Dedicated render queue for frame processing (matches sample app pattern)
67
74
  private let renderQueue = DispatchQueue(label: "com.flir.render")
68
75
 
@@ -122,6 +129,7 @@ import ThermalSDK
122
129
  }
123
130
 
124
131
  @objc public func stopDiscovery() {
132
+ objc_sync_enter(stateLock); defer { objc_sync_exit(stateLock) }
125
133
  NSLog("[FlirManager] stopDiscovery")
126
134
 
127
135
  #if FLIR_ENABLED
@@ -133,6 +141,7 @@ import ThermalSDK
133
141
  // MARK: - Connection
134
142
 
135
143
  @objc public func connectToDevice(_ deviceId: String) {
144
+ objc_sync_enter(stateLock); defer { objc_sync_exit(stateLock) }
136
145
  NSLog("[FlirManager] connectToDevice: \(deviceId)")
137
146
 
138
147
  #if FLIR_ENABLED
@@ -270,10 +279,11 @@ import ThermalSDK
270
279
  }
271
280
 
272
281
  @objc public func disconnect() {
282
+ objc_sync_enter(stateLock); defer { objc_sync_exit(stateLock) }
273
283
  NSLog("[FlirManager] disconnect")
274
284
 
275
285
  #if FLIR_ENABLED
276
- stopStream()
286
+ stopStreamInternalSync()
277
287
  camera?.disconnect()
278
288
  camera = nil
279
289
  _isConnected = false
@@ -289,9 +299,36 @@ import ThermalSDK
289
299
  }
290
300
 
291
301
  @objc public func stop() {
292
- stopStream()
293
- disconnect()
294
- stopDiscovery()
302
+ objc_sync_enter(stateLock); defer { objc_sync_exit(stateLock) }
303
+ stopStreamInternalSync()
304
+ disconnectInternalSync()
305
+ stopDiscoveryInternalSync()
306
+ }
307
+
308
+ private func stopDiscoveryInternalSync() {
309
+ NSLog("[FlirManager] stopDiscovery")
310
+ #if FLIR_ENABLED
311
+ discovery?.stop()
312
+ emitStateChange("idle")
313
+ #endif
314
+ }
315
+
316
+ private func disconnectInternalSync() {
317
+ NSLog("[FlirManager] disconnect")
318
+ #if FLIR_ENABLED
319
+ stopStreamInternalSync()
320
+ camera?.disconnect()
321
+ camera = nil
322
+ _isConnected = false
323
+ connectedDeviceId = nil
324
+ connectedDeviceName = nil
325
+ _latestImage = nil
326
+
327
+ DispatchQueue.main.async { [weak self] in
328
+ self?.delegate?.onDeviceDisconnected()
329
+ self?.emitStateChange("disconnected")
330
+ }
331
+ #endif
295
332
  }
296
333
 
297
334
  // MARK: - Streaming
@@ -345,13 +382,18 @@ import ThermalSDK
345
382
  #endif
346
383
 
347
384
  @objc public func stopStream() {
385
+ objc_sync_enter(stateLock); defer { objc_sync_exit(stateLock) }
386
+ stopStreamInternalSync()
387
+ }
388
+
389
+ private func stopStreamInternalSync() {
348
390
  NSLog("[FlirManager] stopStream")
349
391
 
350
392
  #if FLIR_ENABLED
393
+ _isStreaming = false
351
394
  stream?.stop()
352
395
  stream = nil
353
396
  streamer = nil
354
- _isStreaming = false
355
397
  _latestImage = nil
356
398
 
357
399
  if _isConnected {
@@ -468,11 +510,22 @@ import ThermalSDK
468
510
  }
469
511
 
470
512
  @objc public func setPalette(_ name: String) {
471
- // stub
513
+ self.currentPaletteName = name
514
+ NSLog("[FlirManager] Requested palette: \(name)")
472
515
  }
473
516
 
474
517
  @objc public func setPaletteFromAcol(_ acol: Float) {
475
- // stub
518
+ // Map acol (0..7) to palette names
519
+ let palettes = ["iron", "rainbow", "grayscale", "arctic", "lava", "contrast", "hotcold", "medical"]
520
+ let idx = Int(acol)
521
+ if idx >= 0 && idx < palettes.count {
522
+ setPalette(palettes[idx])
523
+ }
524
+ }
525
+
526
+ @objc public func captureRadiometricSnapshot(_ path: String) {
527
+ self.pendingSnapshotPath = path
528
+ NSLog("[FlirManager] Pending radiometric snapshot: \(path)")
476
529
  }
477
530
 
478
531
  @objc public func retainClient(_ clientId: String) {
@@ -637,14 +690,40 @@ extension FlirManager: FLIRStreamDelegate {
637
690
 
638
691
  NSLog("[FLIR-TRACE 2️⃣] Processing on renderQueue")
639
692
 
693
+ objc_sync_enter(self.stateLock)
694
+ let currentStreamer = self.streamer
695
+ let streaming = self._isStreaming
696
+ objc_sync_exit(self.stateLock)
697
+
698
+ guard streaming, let streamer = currentStreamer else {
699
+ return
700
+ }
701
+
702
+ let paletteToApply = self.currentPaletteName
703
+ let snapshotPath = self.pendingSnapshotPath
704
+ self.pendingSnapshotPath = nil
705
+
640
706
  do {
641
- // Double check streaming state before update to prevent EXC_BAD_ACCESS during shutdown
642
- if self._isStreaming {
643
- try streamer.update()
644
- NSLog("[FLIR-TRACE 3️⃣] Streamer updated successfully")
645
- } else {
646
- return
707
+ try streamer.update()
708
+
709
+ streamer.withThermalImage { thermalImage in
710
+ // 1. Apply Palette
711
+ if let palette = thermalImage.paletteManager.getDefaultPalettes().first(where: { $0.name.lowercased() == paletteToApply.lowercased() }) {
712
+ thermalImage.palette = palette
713
+ }
714
+
715
+ // 2. Save Radiometric Snapshot if requested
716
+ if let path = snapshotPath {
717
+ do {
718
+ try thermalImage.save(to: path)
719
+ NSLog("[FlirManager] Radiometric snapshot saved to: \(path)")
720
+ } catch {
721
+ NSLog("[FlirManager] Failed to save radiometric snapshot: \(error)")
722
+ }
723
+ }
647
724
  }
725
+
726
+ NSLog("[FLIR-TRACE 3️⃣] Streamer updated successfully")
648
727
  } catch {
649
728
  NSLog("[FLIR-TRACE ❌] Streamer update failed: \(error)")
650
729
  return
@@ -161,7 +161,7 @@ RCT_EXPORT_METHOD(setNetworkDiscoveryEnabled : (BOOL)enabled resolver : (
161
161
  setBool:enabled
162
162
  forKey:@"ilabsFlir.networkDiscoveryEnabled"];
163
163
  }
164
- resolve(@(YES));
164
+ if (resolve) resolve(@(YES));
165
165
  }
166
166
 
167
167
  RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
@@ -178,7 +178,7 @@ RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
178
178
  NSLog(@"[FlirModule] [%@] ⏱ FlirManager.startDiscovery returned",
179
179
  [NSDate date]);
180
180
  }
181
- resolve(@(YES));
181
+ if (resolve) resolve(@(YES));
182
182
  });
183
183
  }
184
184
 
@@ -191,7 +191,7 @@ RCT_EXPORT_METHOD(stopDiscovery : (RCTPromiseResolveBlock)
191
191
  ((void (*)(id, SEL))objc_msgSend)(manager,
192
192
  sel_registerName("stopDiscovery"));
193
193
  }
194
- resolve(@(YES));
194
+ if (resolve) resolve(@(YES));
195
195
  });
196
196
  }
197
197
 
@@ -211,12 +211,16 @@ RCT_EXPORT_METHOD(getDiscoveredDevices : (RCTPromiseResolveBlock)
211
211
  }
212
212
  }
213
213
  }
214
- resolve(arr);
214
+ if (resolve) resolve(arr);
215
215
  });
216
216
  }
217
217
 
218
218
  RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
219
219
  RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
220
+ if (!deviceId) {
221
+ if (reject) reject(@"ERR_INVALID_ARGS", @"deviceId is required", nil);
222
+ return;
223
+ }
220
224
  NSLog(@"[FlirModule] [%@] ⏱ RN->connectToDevice called for: %@",
221
225
  [NSDate date], deviceId);
222
226
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -242,10 +246,10 @@ RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
242
246
  [NSDate date]);
243
247
 
244
248
  // Resolve immediately - connection status will come via events
245
- resolve(@(YES));
249
+ if (resolve) resolve(@(YES));
246
250
  } else {
247
251
  NSLog(@"[FlirModule] [%@] ❌ FlirManager not found", [NSDate date]);
248
- reject(@"ERR_NO_MANAGER", @"FlirManager not found", nil);
252
+ if (reject) reject(@"ERR_NO_MANAGER", @"FlirManager not found", nil);
249
253
  }
250
254
  });
251
255
  }
@@ -261,7 +265,7 @@ RCT_EXPORT_METHOD(disconnect : (RCTPromiseResolveBlock)
261
265
  ((void (*)(id, SEL))objc_msgSend)(manager,
262
266
  sel_registerName("disconnect"));
263
267
  }
264
- resolve(@(YES));
268
+ if (resolve) resolve(@(YES));
265
269
  });
266
270
  }
267
271
 
@@ -274,7 +278,7 @@ RCT_EXPORT_METHOD(stopFlir : (RCTPromiseResolveBlock)
274
278
  [[FlirState shared] reset];
275
279
  ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("stop"));
276
280
  }
277
- resolve(@(YES));
281
+ if (resolve) resolve(@(YES));
278
282
  });
279
283
  }
280
284
 
@@ -295,13 +299,13 @@ RCT_EXPORT_METHOD(startEmulator : (NSString *)emulatorType resolver : (
295
299
 
296
300
  // Initiate emulator start asynchronously
297
301
  ((void (*)(id, SEL, id))objc_msgSend)(
298
- manager, sel_registerName("startEmulatorWithType:"), emulatorType);
302
+ manager, sel_registerName("startEmulatorWithType:"), emulatorType ?: @"FLIR_ONE_EDGE");
299
303
 
300
304
  // Resolve immediately - connection status will come via events
301
- resolve(@(YES));
305
+ if (resolve) resolve(@(YES));
302
306
  } else {
303
307
  // Fallback if selector assumption wrong/mismatch
304
- reject(@"ERR_NOT_IMPL",
308
+ if (reject) reject(@"ERR_NOT_IMPL",
305
309
  @"startEmulator not implemented or signature mismatch", nil);
306
310
  self.connectResolve = nil;
307
311
  self.connectReject = nil;
@@ -323,9 +327,9 @@ RCT_EXPORT_METHOD(getTemperatureAt : (nonnull NSNumber *)x y : (
323
327
  [y intValue]);
324
328
  }
325
329
  if (isnan(temp)) {
326
- resolve([NSNull null]);
330
+ if (resolve) resolve([NSNull null]);
327
331
  } else {
328
- resolve(@(temp));
332
+ if (resolve) resolve(@(temp));
329
333
  }
330
334
  });
331
335
  }
@@ -346,9 +350,9 @@ RCT_EXPORT_METHOD(getTemperatureAtNormalized : (nonnull NSNumber *)nx y : (
346
350
  [nx doubleValue], [ny doubleValue]);
347
351
  }
348
352
  if (isnan(temp)) {
349
- resolve([NSNull null]);
353
+ if (resolve) resolve([NSNull null]);
350
354
  } else {
351
- resolve(@(temp));
355
+ if (resolve) resolve(@(temp));
352
356
  }
353
357
  });
354
358
  }
@@ -360,7 +364,7 @@ RCT_EXPORT_METHOD(getTemperatureFromColor : (NSInteger)color resolver : (
360
364
  int b = color & 0xFF;
361
365
  double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
362
366
  double temp = (lum / 255.0) * 400.0;
363
- resolve(@(temp));
367
+ if (resolve) resolve(@(temp));
364
368
  }
365
369
 
366
370
  RCT_EXPORT_METHOD(isEmulator : (RCTPromiseResolveBlock)
@@ -373,7 +377,7 @@ RCT_EXPORT_METHOD(isEmulator : (RCTPromiseResolveBlock)
373
377
  isEm = ((BOOL (*)(id, SEL))objc_msgSend)(manager,
374
378
  sel_registerName("isEmulator"));
375
379
  }
376
- resolve(@(isEm));
380
+ if (resolve) resolve(@(isEm));
377
381
  });
378
382
  }
379
383
 
@@ -384,15 +388,15 @@ RCT_EXPORT_METHOD(getLatestFrameBitmap : (RCTPromiseResolveBlock)
384
388
  if (!manager ||
385
389
  ![manager
386
390
  respondsToSelector:sel_registerName("latestFrameBitmapBase64")]) {
387
- resolve([NSNull null]);
391
+ if (resolve) resolve([NSNull null]);
388
392
  return;
389
393
  }
390
394
  NSDictionary *dict = ((NSDictionary * (*)(id, SEL)) objc_msgSend)(
391
395
  manager, sel_registerName("latestFrameBitmapBase64"));
392
396
  if (!dict) {
393
- resolve([NSNull null]);
397
+ if (resolve) resolve([NSNull null]);
394
398
  } else {
395
- resolve(dict);
399
+ if (resolve) resolve(dict);
396
400
  }
397
401
  });
398
402
  }
@@ -407,7 +411,7 @@ RCT_EXPORT_METHOD(isDeviceConnected : (RCTPromiseResolveBlock)
407
411
  isC = ((BOOL (*)(id, SEL))objc_msgSend)(manager,
408
412
  sel_registerName("isConnected"));
409
413
  }
410
- resolve(@(isC));
414
+ if (resolve) resolve(@(isC));
411
415
  });
412
416
  }
413
417
 
@@ -421,19 +425,17 @@ RCT_EXPORT_METHOD(getConnectedDeviceInfo : (RCTPromiseResolveBlock)
421
425
  info = ((NSString * (*)(id, SEL)) objc_msgSend)(
422
426
  manager, sel_registerName("getConnectedDeviceInfo"));
423
427
  }
424
- resolve(info);
428
+ if (resolve) resolve(info);
425
429
  });
426
430
  }
427
431
 
428
- RCT_EXPORT_METHOD(isSDKDownloaded : (RCTPromiseResolveBlock)
429
- resolve rejecter : (RCTPromiseRejectBlock)reject) {
430
432
  // Assuming integrated SDK
431
- resolve(@(YES));
433
+ if (resolve) resolve(@(YES));
432
434
  }
433
435
 
434
436
  RCT_EXPORT_METHOD(getSDKStatus : (RCTPromiseResolveBlock)
435
437
  resolve rejecter : (RCTPromiseRejectBlock)reject) {
436
- resolve(@{@"available" : @(YES), @"arch" : @"arm64", @"platform" : @"iOS"});
438
+ if (resolve) resolve(@{@"available" : @(YES), @"arch" : @"arm64", @"platform" : @"iOS"});
437
439
  }
438
440
 
439
441
  RCT_EXPORT_METHOD(getBatteryLevel : (RCTPromiseResolveBlock)
@@ -446,7 +448,7 @@ RCT_EXPORT_METHOD(getBatteryLevel : (RCTPromiseResolveBlock)
446
448
  level = ((int (*)(id, SEL))objc_msgSend)(
447
449
  manager, sel_registerName("getBatteryLevel"));
448
450
  }
449
- resolve(@(level));
451
+ if (resolve) resolve(@(level));
450
452
  });
451
453
  }
452
454
 
@@ -460,7 +462,7 @@ RCT_EXPORT_METHOD(isBatteryCharging : (RCTPromiseResolveBlock)
460
462
  ch = ((BOOL (*)(id, SEL))objc_msgSend)(
461
463
  manager, sel_registerName("isBatteryCharging"));
462
464
  }
463
- resolve(@(ch));
465
+ if (resolve) resolve(@(ch));
464
466
  });
465
467
  }
466
468
 
@@ -473,7 +475,35 @@ RCT_EXPORT_METHOD(setPreferSdkRotation : (BOOL)prefer resolver : (
473
475
  ((void (*)(id, SEL, BOOL))objc_msgSend)(
474
476
  manager, sel_registerName("setPreferSdkRotation:"), prefer);
475
477
  }
476
- resolve(@(YES));
478
+ if (resolve) resolve(@(YES));
479
+ });
480
+ }
481
+
482
+ RCT_EXPORT_METHOD(setPalette : (NSString *)name resolver : (RCTPromiseResolveBlock)
483
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
484
+ dispatch_async(dispatch_get_main_queue(), ^{
485
+ id manager = flir_manager_shared();
486
+ if (manager &&
487
+ [manager respondsToSelector:sel_registerName("setPalette:")]) {
488
+ ((void (*)(id, SEL, id))objc_msgSend)(manager,
489
+ sel_registerName("setPalette:"),
490
+ name);
491
+ }
492
+ if (resolve) resolve(@(YES));
493
+ });
494
+ }
495
+
496
+ RCT_EXPORT_METHOD(captureRadiometricSnapshot : (NSString *)path resolver : (RCTPromiseResolveBlock)
497
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
498
+ dispatch_async(dispatch_get_main_queue(), ^{
499
+ id manager = flir_manager_shared();
500
+ if (manager &&
501
+ [manager respondsToSelector:sel_registerName("captureRadiometricSnapshot:")]) {
502
+ ((void (*)(id, SEL, id))objc_msgSend)(manager,
503
+ sel_registerName("captureRadiometricSnapshot:"),
504
+ path);
505
+ }
506
+ if (resolve) resolve(@(YES));
477
507
  });
478
508
  }
479
509
 
@@ -487,7 +517,7 @@ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
487
517
  v = ((BOOL (*)(id, SEL))objc_msgSend)(
488
518
  manager, sel_registerName("isPreferSdkRotation"));
489
519
  }
490
- resolve(@(v));
520
+ if (resolve) resolve(@(v));
491
521
  });
492
522
  }
493
523
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.2.32",
3
+ "version": "2.3.1",
4
4
  "description": "FLIR Thermal SDK for React Native - iOS & Android (bundled at compile time via postinstall)",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",