ilabs-flir 2.0.5 → 2.0.7

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.
package/README.md CHANGED
@@ -118,6 +118,8 @@ cd ios
118
118
  pod install
119
119
  ```
120
120
 
121
+ Note: If you installed `ilabs-flir` via npm, `Podfile` autolinking will declare the `Flir` pod for your app automatically. To avoid duplicates, the published npm package will not contain `Flir.podspec` or in-repo podspecs; they are excluded with `.npmignore`. See `docs/MIGRATION_TO_NPM.md` for migration details if you previously used an in-repo `Flir.podspec`.
122
+
121
123
  ##### Building Without FLIR SDK (No Paid License)
122
124
 
123
125
  If you don't have a paid FLIR developer license, you can build the app without the FLIR SDK. The module will provide fallback stub implementations:
@@ -63,6 +63,23 @@ object FlirManager {
63
63
  }
64
64
 
65
65
  fun getLatestBitmap(): Bitmap? = latestBitmap
66
+
67
+ // Preference: ask SDK to deliver oriented/rotated frames (if SDK supports it)
68
+ fun setPreferSdkRotation(prefer: Boolean) {
69
+ sdkManager?.setPreferSdkRotation(prefer)
70
+ }
71
+
72
+ fun isPreferSdkRotation(): Boolean {
73
+ return sdkManager?.isPreferSdkRotation() ?: false
74
+ }
75
+
76
+ fun getBatteryLevel(): Int {
77
+ return sdkManager?.getBatteryLevel() ?: -1
78
+ }
79
+
80
+ fun isBatteryCharging(): Boolean {
81
+ return sdkManager?.isBatteryCharging() ?: false
82
+ }
66
83
 
67
84
  /**
68
85
  * Initialize the FLIR SDK
@@ -335,6 +352,11 @@ object FlirManager {
335
352
  Log.e(TAG, "Error: $message")
336
353
  emitError(message)
337
354
  }
355
+
356
+ override fun onBatteryUpdated(level: Int, isCharging: Boolean) {
357
+ Log.d(TAG, "onBatteryUpdated: level=$level charging=$isCharging")
358
+ emitBatteryState(level, isCharging)
359
+ }
338
360
  }
339
361
 
340
362
  // React Native event emitters
@@ -411,6 +433,24 @@ object FlirManager {
411
433
  Log.e(TAG, "Failed to emit devices found", e)
412
434
  }
413
435
  }
436
+
437
+ private fun emitBatteryState(level: Int, isCharging: Boolean) {
438
+ val ctx = reactContext
439
+ if (ctx == null) {
440
+ Log.w(TAG, "Cannot emit FlirBatteryUpdated - reactContext is null!")
441
+ return
442
+ }
443
+ try {
444
+ val params = Arguments.createMap().apply {
445
+ putInt("level", level)
446
+ putBoolean("isCharging", isCharging)
447
+ }
448
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
449
+ .emit("FlirBatteryUpdated", params)
450
+ } catch (e: Exception) {
451
+ Log.e(TAG, "Failed to emit battery state", e)
452
+ }
453
+ }
414
454
 
415
455
  private fun emitError(message: String) {
416
456
  val ctx = reactContext ?: return
@@ -147,6 +147,46 @@ class FlirModule(private val reactContext: ReactApplicationContext) : ReactConte
147
147
  promise.reject("ERR_FLIR_DEVICES", e)
148
148
  }
149
149
  }
150
+
151
+ @ReactMethod
152
+ fun setPreferSdkRotation(prefer: Boolean, promise: Promise) {
153
+ try {
154
+ FlirManager.setPreferSdkRotation(prefer)
155
+ promise.resolve(true)
156
+ } catch (e: Exception) {
157
+ promise.reject("ERR_FLIR_SET_ROTATION_PREF", e)
158
+ }
159
+ }
160
+
161
+ @ReactMethod
162
+ fun isPreferSdkRotation(promise: Promise) {
163
+ try {
164
+ val v = FlirManager.isPreferSdkRotation()
165
+ promise.resolve(v)
166
+ } catch (e: Exception) {
167
+ promise.reject("ERR_FLIR_GET_ROTATION_PREF", e)
168
+ }
169
+ }
170
+
171
+ @ReactMethod
172
+ fun getBatteryLevel(promise: Promise) {
173
+ try {
174
+ val level = FlirManager.getBatteryLevel()
175
+ promise.resolve(level)
176
+ } catch (e: Exception) {
177
+ promise.reject("ERR_FLIR_GET_BATTERY", e)
178
+ }
179
+ }
180
+
181
+ @ReactMethod
182
+ fun isBatteryCharging(promise: Promise) {
183
+ try {
184
+ val v = FlirManager.isBatteryCharging()
185
+ promise.resolve(v)
186
+ } catch (e: Exception) {
187
+ promise.reject("ERR_FLIR_CHARGING", e)
188
+ }
189
+ }
150
190
 
151
191
  @ReactMethod
152
192
  fun startEmulator(emulatorType: String, promise: Promise) {
@@ -47,6 +47,10 @@ public class FlirSdkManager {
47
47
  private final Executor executor = Executors.newFixedThreadPool(2);
48
48
  // Single-threaded executor for frame processing to ensure ordered processing
49
49
  private final Executor frameExecutor = Executors.newSingleThreadExecutor();
50
+ // Battery poller scheduler - polls battery level & charging state periodically if supported
51
+ private final java.util.concurrent.ScheduledExecutorService batteryPoller = java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
52
+ private volatile int lastPolledBatteryLevel = -1;
53
+ private volatile boolean lastPolledCharging = false;
50
54
  // Frame processing guard - skip frames if still processing previous one
51
55
  private volatile boolean isProcessingFrame = false;
52
56
  private long lastFrameProcessedMs = 0;
@@ -59,6 +63,8 @@ public class FlirSdkManager {
59
63
  private ThermalStreamer streamer;
60
64
  private Stream activeStream;
61
65
  private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
66
+ // When true, prefer getting SDK-provided rotated frames instead of rotating ourselves
67
+ private volatile boolean preferSdkRotation = false;
62
68
 
63
69
  // Listener
64
70
  private Listener listener;
@@ -73,6 +79,7 @@ public class FlirSdkManager {
73
79
  void onDisconnected();
74
80
  void onFrame(Bitmap bitmap);
75
81
  void onError(String message);
82
+ void onBatteryUpdated(int level, boolean isCharging);
76
83
  }
77
84
 
78
85
  // Private constructor for singleton
@@ -96,6 +103,34 @@ public class FlirSdkManager {
96
103
  public void setListener(Listener listener) {
97
104
  this.listener = listener;
98
105
  }
106
+
107
+ public void setPreferSdkRotation(boolean prefer) {
108
+ this.preferSdkRotation = prefer;
109
+ // Try to ask SDK streamer to provide rotated images if possible
110
+ if (streamer != null) {
111
+ try {
112
+ // Try common method names via reflection to avoid hard dependency on exact API signature
113
+ Object obj = streamer;
114
+ java.lang.reflect.Method m = null;
115
+ try { m = obj.getClass().getMethod("setImageRotation", int.class); } catch (Throwable ignored) {}
116
+ if (m == null) {
117
+ try { m = obj.getClass().getMethod("setRotation", int.class); } catch (Throwable ignored) {}
118
+ }
119
+ if (m != null) {
120
+ // If caller asked SDK to rotate, choose 0 = 'auto' or prefer flag; here we request SDK to respect device orientation
121
+ int degrees = prefer ? 0 : 0; // SDK-specific - for now, 0 requests orientation-respected frames if method interprets so
122
+ m.invoke(obj, degrees);
123
+ Log.d(TAG, "setPreferSdkRotation: requested SDK rotation via reflection");
124
+ } else {
125
+ Log.w(TAG, "setPreferSdkRotation: SDK does not expose rotation API (reflection check)");
126
+ }
127
+ } catch (Throwable t) {
128
+ Log.w(TAG, "setPreferSdkRotation failed (reflection)", t);
129
+ }
130
+ }
131
+ }
132
+
133
+ public boolean isPreferSdkRotation() { return preferSdkRotation; }
99
134
 
100
135
  /**
101
136
  * Initialize the FLIR Thermal SDK
@@ -214,6 +249,8 @@ public class FlirSdkManager {
214
249
  if (listener != null) {
215
250
  listener.onConnected(identity);
216
251
  }
252
+ // Start battery poller for continuous updates
253
+ startBatteryPoller();
217
254
  } catch (Exception e) {
218
255
  Log.e(TAG, "Connection failed", e);
219
256
  camera = null;
@@ -236,6 +273,8 @@ public class FlirSdkManager {
236
273
  }
237
274
  camera = null;
238
275
  }
276
+ // stop battery poller
277
+ stopBatteryPoller();
239
278
 
240
279
  if (listener != null) {
241
280
  listener.onDisconnected();
@@ -476,6 +515,67 @@ public class FlirSdkManager {
476
515
  }
477
516
  return names;
478
517
  }
518
+
519
+ /**
520
+ * Best-effort: Fetch battery level from connected camera if SDK exposes battery APIs
521
+ * Returns -1 if unavailable
522
+ */
523
+ public int getBatteryLevel() {
524
+ if (camera == null) return -1;
525
+ try {
526
+ // Common SDK methods to try
527
+ try {
528
+ java.lang.reflect.Method m = camera.getClass().getMethod("getBatteryLevel");
529
+ Object r = m.invoke(camera);
530
+ if (r instanceof Number) return ((Number) r).intValue();
531
+ } catch (Throwable ignored) {}
532
+
533
+ try {
534
+ java.lang.reflect.Method m = camera.getClass().getMethod("getBattery");
535
+ Object batt = m.invoke(camera);
536
+ if (batt != null) {
537
+ try {
538
+ java.lang.reflect.Method levelMethod = batt.getClass().getMethod("getLevel");
539
+ Object lv = levelMethod.invoke(batt);
540
+ if (lv instanceof Number) return ((Number) lv).intValue();
541
+ } catch (Throwable ignored) {}
542
+ }
543
+ } catch (Throwable ignored) {}
544
+ } catch (Throwable t) {
545
+ Log.w(TAG, "Error querying battery level", t);
546
+ }
547
+ return -1;
548
+ }
549
+
550
+ /**
551
+ * Best-effort: Check if the camera is charging
552
+ * Returns false if unknown
553
+ */
554
+ public boolean isBatteryCharging() {
555
+ if (camera == null) return false;
556
+ try {
557
+ try {
558
+ java.lang.reflect.Method m = camera.getClass().getMethod("isCharging");
559
+ Object r = m.invoke(camera);
560
+ if (r instanceof Boolean) return (Boolean) r;
561
+ } catch (Throwable ignored) {}
562
+
563
+ try {
564
+ java.lang.reflect.Method m = camera.getClass().getMethod("getBattery");
565
+ Object batt = m.invoke(camera);
566
+ if (batt != null) {
567
+ try {
568
+ java.lang.reflect.Method isCh = batt.getClass().getMethod("isCharging");
569
+ Object cv = isCh.invoke(batt);
570
+ if (cv instanceof Boolean) return (Boolean) cv;
571
+ } catch (Throwable ignored) {}
572
+ }
573
+ } catch (Throwable ignored) {}
574
+ } catch (Throwable t) {
575
+ Log.w(TAG, "Error querying battery charging state", t);
576
+ }
577
+ return false;
578
+ }
479
579
 
480
580
  // Find palette by name
481
581
  private Palette findPalette(String name) {
@@ -580,4 +680,39 @@ public class FlirSdkManager {
580
680
  instance = null;
581
681
  Log.d(TAG, "Destroyed");
582
682
  }
683
+
684
+ /**
685
+ * Start a background poller to periodically check battery state and notify listener
686
+ */
687
+ private void startBatteryPoller() {
688
+ try {
689
+ batteryPoller.scheduleAtFixedRate(() -> {
690
+ if (camera == null) return;
691
+ try {
692
+ int level = getBatteryLevel();
693
+ boolean charging = isBatteryCharging();
694
+ if (level != lastPolledBatteryLevel || charging != lastPolledCharging) {
695
+ lastPolledBatteryLevel = level;
696
+ lastPolledCharging = charging;
697
+ if (listener != null) {
698
+ try { listener.onBatteryUpdated(level, charging);} catch (Throwable t) {}
699
+ }
700
+ }
701
+ } catch (Throwable t) {
702
+ Log.w(TAG, "Battery poller error", t);
703
+ }
704
+ }, 0, 5, java.util.concurrent.TimeUnit.SECONDS);
705
+ } catch (Throwable t) {
706
+ Log.w(TAG, "Failed to start battery poller", t);
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Stop the battery poller.
712
+ */
713
+ private void stopBatteryPoller() {
714
+ try {
715
+ batteryPoller.shutdownNow();
716
+ } catch (Throwable ignored) {}
717
+ }
583
718
  }
@@ -32,7 +32,7 @@ RCT_EXPORT_MODULE();
32
32
  return @[
33
33
  @"FlirDeviceConnected", @"FlirDeviceDisconnected", @"FlirDevicesFound",
34
34
  @"FlirFrameReceived", @"FlirFrame", @"FlirError", @"FlirStateChanged",
35
- @"FlirTemperatureUpdate"
35
+ @"FlirTemperatureUpdate", @"FlirBatteryUpdated"
36
36
  ];
37
37
  }
38
38
 
@@ -12,6 +12,9 @@ NS_ASSUME_NONNULL_BEGIN
12
12
 
13
13
  @interface FlirModule : RCTEventEmitter <RCTBridgeModule>
14
14
 
15
+ // Utility for other native code to emit battery updates (level 0-100 or -1 if unknown)
16
+ + (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging;
17
+
15
18
  @end
16
19
 
17
20
  NS_ASSUME_NONNULL_END
@@ -77,6 +77,7 @@ RCT_EXPORT_MODULE(FlirModule);
77
77
  @"FlirFrameReceived",
78
78
  @"FlirError",
79
79
  @"FlirStateChanged"
80
+ , @"FlirBatteryUpdated"
80
81
  ];
81
82
  }
82
83
 
@@ -88,6 +89,15 @@ RCT_EXPORT_METHOD(removeListeners:(NSInteger)count) {
88
89
  // Required for RCTEventEmitter
89
90
  }
90
91
 
92
+ // Provide a class helper so other native modules can post a battery update
93
+ + (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
94
+ NSDictionary *payload = @{
95
+ @"level": @(level),
96
+ @"isCharging": @(charging)
97
+ };
98
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirBatteryUpdated" body:payload];
99
+ }
100
+
91
101
  #pragma mark - Discovery Methods
92
102
 
93
103
  RCT_EXPORT_METHOD(startDiscovery:(RCTPromiseResolveBlock)resolve
@@ -334,10 +344,8 @@ RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
334
344
  resolver:(RCTPromiseResolveBlock)resolve
335
345
  rejecter:(RCTPromiseRejectBlock)reject) {
336
346
  dispatch_async(dispatch_get_main_queue(), ^{
337
- double temp = [FlirState shared].lastTemperature;
338
- if (isnan(temp)) {
339
- temp = self.lastTemperature;
340
- }
347
+ // Call into native FLIRManager to query temperature at point
348
+ double temp = [[FLIRManager shared] getTemperatureAtPoint:[x intValue] y:[y intValue]];
341
349
  if (isnan(temp)) {
342
350
  resolve([NSNull null]);
343
351
  } else {
@@ -501,6 +509,51 @@ RCT_EXPORT_METHOD(getLatestFramePath:(RCTPromiseResolveBlock)resolve
501
509
  });
502
510
  }
503
511
 
512
+ RCT_EXPORT_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
513
+ rejecter:(RCTPromiseRejectBlock)reject) {
514
+ dispatch_async(dispatch_get_main_queue(), ^{
515
+ #if FLIR_SDK_AVAILABLE
516
+ int level = [[FLIRManager shared] getBatteryLevel];
517
+ resolve(@(level));
518
+ #else
519
+ resolve(@(-1));
520
+ #endif
521
+ });
522
+ }
523
+
524
+ RCT_EXPORT_METHOD(isBatteryCharging:(RCTPromiseResolveBlock)resolve
525
+ rejecter:(RCTPromiseRejectBlock)reject) {
526
+ dispatch_async(dispatch_get_main_queue(), ^{
527
+ #if FLIR_SDK_AVAILABLE
528
+ BOOL ch = [[FLIRManager shared] isBatteryCharging];
529
+ resolve(@(ch));
530
+ #else
531
+ resolve(@(NO));
532
+ #endif
533
+ });
534
+ }
535
+
536
+ RCT_EXPORT_METHOD(setPreferSdkRotation:(BOOL)prefer
537
+ resolver:(RCTPromiseResolveBlock)resolve
538
+ rejecter:(RCTPromiseRejectBlock)reject) {
539
+ dispatch_async(dispatch_get_main_queue(), ^{
540
+ @try {
541
+ [[FLIRManager shared] setPreferSdkRotation:prefer];
542
+ resolve(@(YES));
543
+ } @catch (NSException *ex) {
544
+ reject(@"ERR_FLIR_SET_ROTATION_PREF", ex.reason, nil);
545
+ }
546
+ });
547
+ }
548
+
549
+ RCT_EXPORT_METHOD(isPreferSdkRotation:(RCTPromiseResolveBlock)resolve
550
+ rejecter:(RCTPromiseRejectBlock)reject) {
551
+ dispatch_async(dispatch_get_main_queue(), ^{
552
+ BOOL v = [[FLIRManager shared] isPreferSdkRotation];
553
+ resolve(@(v));
554
+ });
555
+ }
556
+
504
557
  #pragma mark - Helper Methods
505
558
 
506
559
  - (void)emitDeviceConnected {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
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",
@@ -13,11 +13,9 @@
13
13
  "src/",
14
14
  "android/Flir/src/",
15
15
  "android/Flir/build.gradle.kts",
16
- "android/Flir/libs/",
17
16
  "ios/Flir/src/",
18
17
  "ios/Flir/SDKLoader/",
19
- "ios/Flir/Framework/",
20
- "ios/Flir/Frameworks/",
18
+
21
19
  "app.plugin.js",
22
20
  "Flir.podspec",
23
21
  "sdk-manifest.json",
@@ -96,7 +96,7 @@ function extractZip(zipPath, dest) {
96
96
 
97
97
  if (cmdExists('unzip')) {
98
98
  try {
99
- execSync(`unzip -o '${zipPath}' -d '${TMP_DIR}'`, { stdio: 'inherit' });
99
+ execSync(`unzip -o "${zipPath}" -d "${TMP_DIR}"`, { stdio: 'inherit' });
100
100
  return;
101
101
  } catch (err) {
102
102
  throw new Error(`Failed to extract zip using 'unzip'. Consider installing 'unzip' or adding 'adm-zip' package. Original error: ${err.message}`);
@@ -106,7 +106,11 @@ function extractZip(zipPath, dest) {
106
106
  // 'tar' can sometimes extract zip files (via bsdtar). Try it as a last resort.
107
107
  if (cmdExists('tar')) {
108
108
  try {
109
- execSync(`tar -xf '${zipPath}' -C '${TMP_DIR}'`, { stdio: 'inherit' });
109
+ if (process.platform === 'win32') {
110
+ execSync(`tar -xf "${zipPath}" -C "${TMP_DIR}"`, { stdio: 'inherit' });
111
+ } else {
112
+ execSync(`tar -xf '${zipPath}' -C '${TMP_DIR}'`, { stdio: 'inherit' });
113
+ }
110
114
  return;
111
115
  } catch (err) {
112
116
  throw new Error(`Failed to extract zip using 'tar'. Consider installing 'unzip' or add 'adm-zip' package. Original error: ${err.message}`);
@@ -172,27 +176,67 @@ async function run() {
172
176
  return;
173
177
  }
174
178
 
175
- // Ensure target folders exist - but do NOT create them automatically.
176
- if (!fs.existsSync(DEST_ANDROID)) {
177
- throw new Error(`Android libs folder ${DEST_ANDROID} does not exist. Please create it (e.g. npm pack or ensure published package includes it).`);
178
- }
179
- if (!fs.existsSync(DEST_IOS)) {
180
- throw new Error(`iOS Frameworks folder ${DEST_IOS} does not exist. Please create it before install.`);
181
- }
179
+ // (previous 'ensure target folders exist' logic removed; handling now occurs after args parsing)
182
180
 
183
181
  ensureTmp();
184
182
 
185
183
  const argv = process.argv.slice(2);
186
- const args = argv.reduce((acc, cur) => {
187
- if (cur.startsWith('--')) acc[cur.replace(/^--/, '')] = true;
188
- else acc[cur] = true;
189
- return acc;
190
- }, {});
184
+ const args = {};
185
+ for (let i = 0; i < argv.length; i++) {
186
+ const cur = argv[i];
187
+ if (cur.startsWith('--')) {
188
+ if (cur.includes('=')) {
189
+ const parts = cur.split('=');
190
+ const key = parts.shift().replace(/^--/, '');
191
+ const value = parts.join('=');
192
+ args[key] = value;
193
+ } else {
194
+ // if following arg exists and doesn't start with '-', use it as the value
195
+ const next = argv[i+1];
196
+ if (next && !next.startsWith('-')) {
197
+ args[cur.replace(/^--/, '')] = next;
198
+ i++;
199
+ } else {
200
+ args[cur.replace(/^--/, '')] = true;
201
+ }
202
+ }
203
+ } else if (cur.startsWith('-')) {
204
+ const key = cur.replace(/^-+/, '');
205
+ const next = argv[i+1];
206
+ if (next && !next.startsWith('-')) {
207
+ args[key] = next;
208
+ i++;
209
+ } else {
210
+ args[key] = true;
211
+ }
212
+ } else {
213
+ args[cur] = true;
214
+ }
215
+ }
191
216
 
192
217
  // Skip if present per-platform
193
218
  const skipIfPresent = args['skip-if-present'] || args['skipIfPresent'] || false;
194
219
  const platformArg = args['platform'] || args['p'] || 'all';
195
220
 
221
+ // Determine whether to create missing dest directories automatically; default is true unless explicitly disabled
222
+ const noCreate = process.env.FLIR_SDK_NO_CREATE_DEST === '1' || process.env.FLIR_SDK_NO_CREATE_DEST === 'true' || args['no-create-dest'] || args['noCreateDest'];
223
+ if ((platformArg === 'all' || platformArg === 'android') && !fs.existsSync(DEST_ANDROID)) {
224
+ if (noCreate) {
225
+ throw new Error(`Android libs folder ${DEST_ANDROID} does not exist. Please create it (e.g. npm pack or ensure published package includes it).`);
226
+ } else {
227
+ console.log(`Android libs folder ${DEST_ANDROID} not found — creating it.`);
228
+ fs.mkdirSync(DEST_ANDROID, { recursive: true });
229
+ }
230
+ }
231
+ if ((platformArg === 'all' || platformArg === 'ios') && !fs.existsSync(DEST_IOS)) {
232
+ if (noCreate) {
233
+ throw new Error(`iOS Frameworks folder ${DEST_IOS} does not exist. Please create it before install.`);
234
+ } else {
235
+ console.log(`iOS Frameworks folder ${DEST_IOS} not found — creating it.`);
236
+ fs.mkdirSync(DEST_IOS, { recursive: true });
237
+ }
238
+ }
239
+
196
240
  // Short circuit checks: if skipIfPresent set and files exist, skip per platform
197
241
  if (skipIfPresent && platformArg !== 'ios' && hasAndroidAar(DEST_ANDROID)) {
198
242
  console.log('Android AAR(s) detected in libs folder; skipping Android fetch.');