ilabs-flir 2.2.1 → 2.2.3

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.
@@ -351,7 +351,20 @@ object FlirManager {
351
351
 
352
352
  override fun onError(message: String) {
353
353
  Log.e(TAG, "Error: $message")
354
- emitError(message)
354
+
355
+ // Parse error code if present (format: "CODE: message")
356
+ val parts = message.split(": ", limit = 2)
357
+ val errorCode = if (parts.size == 2) parts[0] else "FLIR_ERROR"
358
+ val errorMessage = if (parts.size == 2) parts[1] else message
359
+
360
+ // Auto-disable streaming on critical errors to allow retry
361
+ if (errorCode.contains("NATIVE") || errorCode.contains("INIT")) {
362
+ Log.w(TAG, "[Flir-BRIDGE-ERROR] Critical error detected, stopping stream")
363
+ isStreaming = false
364
+ stopStream()
365
+ }
366
+
367
+ emitError(errorCode, errorMessage)
355
368
  }
356
369
 
357
370
  override fun onBatteryUpdated(level: Int, isCharging: Boolean) {
@@ -454,13 +467,21 @@ object FlirManager {
454
467
  }
455
468
 
456
469
  private fun emitError(message: String) {
470
+ emitError("FLIR_ERROR", message)
471
+ }
472
+
473
+ private fun emitError(errorCode: String, message: String) {
457
474
  val ctx = reactContext ?: return
458
475
  try {
459
476
  val params = Arguments.createMap().apply {
477
+ putString("code", errorCode)
460
478
  putString("error", message)
479
+ putString("message", message) // For backward compatibility
480
+ putBoolean("canRetry", errorCode.contains("NATIVE") || errorCode.contains("INIT"))
461
481
  }
462
482
  ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
463
483
  .emit("FlirError", params)
484
+ Log.d(TAG, "[Flir-BRIDGE-ERROR] Emitted FlirError: [$errorCode] $message")
464
485
  } catch (e: Exception) {
465
486
  Log.e(TAG, "Failed to emit error", e)
466
487
  }
@@ -328,6 +328,19 @@ public class FlirSdkManager {
328
328
  return;
329
329
  }
330
330
 
331
+ // CRITICAL FIX: Prevent starting stream if previous stream is still active
332
+ // This prevents race conditions and resource conflicts
333
+ if (streamer != null || activeStream != null) {
334
+ Log.w(TAG, "[Flir-STREAMING] Stream already active, stopping first");
335
+ stopStream();
336
+
337
+ // Wait for cleanup to complete
338
+ try {
339
+ Thread.sleep(200);
340
+ } catch (InterruptedException ignored) {
341
+ }
342
+ }
343
+
331
344
  executor.execute(() -> {
332
345
  try {
333
346
  // Get available streams
@@ -351,7 +364,46 @@ public class FlirSdkManager {
351
364
  }
352
365
 
353
366
  activeStream = thermalStream;
354
- streamer = new ThermalStreamer(thermalStream);
367
+
368
+ // CRITICAL FIX: Validate stream before creating ThermalStreamer
369
+ // The FLIR SDK native library can crash if stream is in invalid state
370
+ if (!thermalStream.isAvailable()) {
371
+ notifyError("Thermal stream not available. Please reconnect device.");
372
+ return;
373
+ }
374
+
375
+ // CRITICAL FIX: Wrap ThermalStreamer creation in try-catch
376
+ // While native crashes usually bypass Java exception handling,
377
+ // we can catch:
378
+ // 1. JNI errors (UnsatisfiedLinkError, Error subclasses)
379
+ // 2. SDK errors before they reach native code
380
+ // 3. Resource initialization failures
381
+ // This won't prevent SIGSEGV/SIGABRT but reduces crash frequency
382
+ try {
383
+ // Small delay to ensure stream and resources are fully initialized
384
+ // This prevents race conditions in the native filter chain setup
385
+ Thread.sleep(150);
386
+
387
+ streamer = new ThermalStreamer(thermalStream);
388
+ Log.d(TAG, "[Flir-STREAMING] ThermalStreamer created successfully");
389
+ } catch (UnsatisfiedLinkError e) {
390
+ // JNI library loading error
391
+ Log.e(TAG, "[Flir-STREAMING] JNI library error creating ThermalStreamer", e);
392
+ notifyError("FLIR_NATIVE_ERROR", "Failed to load native library. Please restart app.");
393
+ return;
394
+ } catch (Exception e) {
395
+ // Java exception during initialization
396
+ Log.e(TAG, "[Flir-STREAMING] Failed to create ThermalStreamer", e);
397
+ notifyError("FLIR_INIT_ERROR", "Failed to initialize thermal camera: " + e.getMessage());
398
+ return;
399
+ } catch (Error e) {
400
+ // Catch native errors/crashes from FLIR SDK
401
+ // Note: True SIGSEGV crashes will still kill the process,
402
+ // but some JNI errors can be caught here
403
+ Log.e(TAG, "[Flir-STREAMING] Native error creating ThermalStreamer", e);
404
+ notifyError("FLIR_NATIVE_ERROR", "Native error from FLIR device. Please reconnect and retry.");
405
+ return;
406
+ }
355
407
 
356
408
  // Start receiving frames using OnReceived and OnRemoteError
357
409
  thermalStream.start(
@@ -419,7 +471,15 @@ public class FlirSdkManager {
419
471
  activeStream = null;
420
472
  }
421
473
 
422
- streamer = null;
474
+ // CRITICAL FIX: Properly cleanup streamer to prevent resource leaks
475
+ if (streamer != null) {
476
+ try {
477
+ // Give streamer time to cleanup before nulling
478
+ Thread.sleep(50);
479
+ } catch (InterruptedException ignored) {
480
+ }
481
+ streamer = null;
482
+ }
423
483
 
424
484
  // Reset frame processing state
425
485
  isProcessingFrame = false;
@@ -712,6 +772,17 @@ public class FlirSdkManager {
712
772
  listener.onError(message);
713
773
  }
714
774
  }
775
+
776
+ /**
777
+ * Notify error with error code for better handling
778
+ */
779
+ private void notifyError(String errorCode, String message) {
780
+ Log.e(TAG, "[" + errorCode + "] " + message);
781
+ if (listener != null) {
782
+ // Send both code and message - listener can parse it
783
+ listener.onError(errorCode + ": " + message);
784
+ }
785
+ }
715
786
 
716
787
  /**
717
788
  * Cleanup resources
@@ -174,25 +174,35 @@ import ThermalSDK
174
174
  }
175
175
 
176
176
  @objc public func getBatteryLevel() -> Int {
177
- #if FLIR_ENABLED
178
- if let cam = camera {
179
- if let val = cam.value(forKey: "batteryLevel") as? Int { return val }
180
- if let batt = cam.value(forKey: "battery") as? NSObject,
181
- let lv = batt.value(forKey: "level") as? Int { return lv }
177
+ // Emulators don't have battery - return -1 immediately
178
+ if isEmulator {
179
+ return -1
182
180
  }
183
- #endif
181
+
182
+ #if FLIR_ENABLED
183
+ // Only try battery access on real hardware (not emulators)
184
+ // Note: KVC can throw NSException which Swift can't catch
185
+ // So we avoid using KVC entirely for battery
186
+ return -1 // TODO: Find safe API to access battery without KVC
187
+ #else
184
188
  return -1
189
+ #endif
185
190
  }
186
191
 
187
192
  @objc public func isBatteryCharging() -> Bool {
188
- #if FLIR_ENABLED
189
- if let cam = camera {
190
- if let ch = cam.value(forKey: "isCharging") as? Bool { return ch }
191
- if let batt = cam.value(forKey: "battery") as? NSObject,
192
- let ch = batt.value(forKey: "charging") as? Bool { return ch }
193
+ // Emulators don't have battery - return false immediately
194
+ if isEmulator {
195
+ return false
193
196
  }
194
- #endif
197
+
198
+ #if FLIR_ENABLED
199
+ // Only try battery access on real hardware (not emulators)
200
+ // Note: KVC can throw NSException which Swift can't catch
201
+ // So we avoid using KVC entirely for battery
202
+ return false // TODO: Find safe API to access battery without KVC
203
+ #else
195
204
  return false
205
+ #endif
196
206
  }
197
207
 
198
208
  @objc public func latestFrameImage() -> UIImage? {
@@ -701,7 +711,10 @@ import ThermalSDK
701
711
  // MARK: - Battery Polling (like Android)
702
712
 
703
713
  private func startBatteryPolling() {
704
- FlirLogger.log(.battery, "Starting battery polling timer (5s interval)")
714
+ // Don't poll battery on emulators - they don't have batteries
715
+ if isEmulator {
716
+ return
717
+ }
705
718
 
706
719
  // Cancel any existing timer
707
720
  stopBatteryPolling()
@@ -717,9 +730,6 @@ import ThermalSDK
717
730
  }
718
731
 
719
732
  private func stopBatteryPolling() {
720
- if batteryPollingTimer != nil {
721
- FlirLogger.log(.battery, "Stopping battery polling timer")
722
- }
723
733
  batteryPollingTimer?.invalidate()
724
734
  batteryPollingTimer = nil
725
735
  }
@@ -728,13 +738,11 @@ import ThermalSDK
728
738
  let level = getBatteryLevel()
729
739
  let charging = isBatteryCharging()
730
740
 
731
- // Only log and emit if values changed
741
+ // Only emit if values changed
732
742
  if level != lastPolledBatteryLevel || charging != lastPolledCharging {
733
743
  lastPolledBatteryLevel = level
734
744
  lastPolledCharging = charging
735
745
 
736
- FlirLogger.logBattery(level: level, isCharging: charging)
737
-
738
746
  // Emit to delegate/RN via notification
739
747
  NotificationCenter.default.post(
740
748
  name: Notification.Name("FlirBatteryUpdated"),
@@ -129,10 +129,24 @@ RCT_EXPORT_MODULE(FlirModule);
129
129
  ];
130
130
  }
131
131
 
132
+ - (void)startObserving {
133
+ // Called automatically by RCTEventEmitter when first listener is added
134
+ // This ensures the parent class knows we have listeners
135
+ }
136
+
137
+ - (void)stopObserving {
138
+ // Called automatically by RCTEventEmitter when last listener is removed
139
+ }
140
+
132
141
  RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
133
142
  _listenerCount++;
134
143
  NSLog(@"[FlirModule] addListener: %@ (count: %ld)", eventName, (long)_listenerCount);
135
144
 
145
+ // CRITICAL: Call parent to register with RCTEventEmitter's internal tracking
146
+ // Without this, sendEventWithName will show "no listeners registered" warning
147
+ // and may not deliver events properly
148
+ [super addListener:eventName];
149
+
136
150
  // When FlirDevicesFound listener is added, immediately emit current device list
137
151
  // This handles the case where discovery happened before React Native mounted
138
152
  if ([eventName isEqualToString:@"FlirDevicesFound"]) {
@@ -154,6 +168,9 @@ RCT_EXPORT_METHOD(removeListeners : (NSInteger)count) {
154
168
  _listenerCount -= count;
155
169
  if (_listenerCount < 0) _listenerCount = 0;
156
170
  NSLog(@"[FlirModule] removeListeners: %ld (remaining: %ld)", (long)count, (long)_listenerCount);
171
+
172
+ // CRITICAL: Call parent to unregister with RCTEventEmitter's internal tracking
173
+ [super removeListeners:count];
157
174
  }
158
175
 
159
176
  + (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
@@ -234,18 +251,26 @@ RCT_EXPORT_METHOD(getDiscoveredDevices : (RCTPromiseResolveBlock)
234
251
  RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
235
252
  RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
236
253
  dispatch_async(dispatch_get_main_queue(), ^{
237
- self.connectResolve = resolve;
238
- self.connectReject = reject;
254
+ NSLog(@"[FlirModule] connectToDevice called for: %@", deviceId);
239
255
 
240
256
  id manager = flir_manager_shared();
241
257
  if (manager &&
242
258
  [manager respondsToSelector:sel_registerName("connectToDevice:")]) {
259
+ NSLog(@"[FlirModule] Calling FlirManager.connectToDevice");
260
+
261
+ // Store callbacks for event-driven updates (but don't block on them)
262
+ self.connectResolve = nil; // Don't use promise for blocking
263
+ self.connectReject = nil;
264
+
265
+ // Initiate connection asynchronously
243
266
  ((void (*)(id, SEL, id))objc_msgSend)(
244
267
  manager, sel_registerName("connectToDevice:"), deviceId);
268
+
269
+ // Resolve immediately - connection status will come via events
270
+ resolve(@(YES));
245
271
  } else {
272
+ NSLog(@"[FlirModule] FlirManager not found");
246
273
  reject(@"ERR_NO_MANAGER", @"FlirManager not found", nil);
247
- self.connectResolve = nil;
248
- self.connectReject = nil;
249
274
  }
250
275
  });
251
276
  }
@@ -277,16 +302,21 @@ RCT_EXPORT_METHOD(stopFlir : (RCTPromiseResolveBlock)
277
302
  RCT_EXPORT_METHOD(startEmulator : (NSString *)emulatorType resolver : (
278
303
  RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
279
304
  dispatch_async(dispatch_get_main_queue(), ^{
280
- self.connectResolve = resolve;
281
- self.connectReject = reject;
305
+ NSLog(@"[FlirModule] startEmulator called for type: %@", emulatorType);
306
+
282
307
  id manager = flir_manager_shared();
283
308
  if (manager && [manager respondsToSelector:sel_registerName(
284
309
  "startEmulatorWithType:")]) {
285
- // Swift: startEmulator(type: String) -> exposed as startEmulatorWithType:
286
- // ? Or startEmulatorWith? Swift default naming: startEmulator(type:) ->
287
- // startEmulatorWithType:
310
+ // Store callbacks for event-driven updates (but don't block on them)
311
+ self.connectResolve = nil;
312
+ self.connectReject = nil;
313
+
314
+ // Initiate emulator start asynchronously
288
315
  ((void (*)(id, SEL, id))objc_msgSend)(
289
316
  manager, sel_registerName("startEmulatorWithType:"), emulatorType);
317
+
318
+ // Resolve immediately - connection status will come via events
319
+ resolve(@(YES));
290
320
  } else {
291
321
  // Fallback if selector assumption wrong/mismatch
292
322
  reject(@"ERR_NOT_IMPL",
@@ -444,12 +474,6 @@ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
444
474
  }
445
475
 
446
476
  - (void)onDeviceConnected:(id)device {
447
- if (self.connectResolve) {
448
- self.connectResolve(@(YES));
449
- self.connectResolve = nil;
450
- self.connectReject = nil;
451
- }
452
-
453
477
  // device is FlirDeviceInfo
454
478
  NSMutableDictionary *body = [NSMutableDictionary new];
455
479
  if ([device respondsToSelector:sel_registerName("toDictionary")]) {
@@ -497,11 +521,6 @@ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
497
521
  }
498
522
 
499
523
  - (void)onError:(NSString *)message {
500
- if (self.connectReject) {
501
- self.connectReject(@"ERR_FLIR", message, nil);
502
- self.connectResolve = nil;
503
- self.connectReject = nil;
504
- }
505
524
  NSLog(@"[FlirModule] onError - emitting FlirError: %@", message);
506
525
  [self sendEventWithName:@"FlirError"
507
526
  body:@{@"error" : message ?: @"Unknown error"}];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.2.001",
3
+ "version": "2.2.3",
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",