ilabs-flir 2.0.6 → 2.0.8

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
  }
@@ -0,0 +1,24 @@
1
+ // ObjC shim to expose `FLIRManager` from the npm package and forward to `FlirManager` at runtime
2
+ #import <Foundation/Foundation.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface FLIRManager : NSObject
8
+
9
+ + (instancetype)shared;
10
+
11
+ - (BOOL)isAvailable;
12
+ - (double)getTemperatureAtPoint:(int)x y:(int)y;
13
+ - (double)getTemperatureAtNormalized:(double)nx y:(double)ny;
14
+ - (int)getBatteryLevel;
15
+ - (BOOL)isBatteryCharging;
16
+ - (void)setPreferSdkRotation:(BOOL)prefer;
17
+ - (BOOL)isPreferSdkRotation;
18
+ - (nullable UIImage *)latestFrameImage;
19
+ - (void)startDiscovery;
20
+ - (void)stopDiscovery;
21
+
22
+ @end
23
+
24
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,153 @@
1
+ // ObjC shim implementation forwarding to Swift `FlirManager` via runtime selectors
2
+ #import "FLIRManager.h"
3
+ #import <objc/message.h>
4
+
5
+ @implementation FLIRManager
6
+
7
+ + (instancetype)shared {
8
+ Class cls = NSClassFromString(@"FlirManager");
9
+ if (!cls) return nil;
10
+ SEL sel = sel_registerName("shared");
11
+ if (![cls respondsToSelector:sel]) return nil;
12
+ id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
13
+ return msgSend0((id)cls, sel);
14
+ }
15
+
16
+ - (BOOL)isAvailable {
17
+ Class cls = NSClassFromString(@"FlirManager");
18
+ SEL sel = sel_registerName("isSDKAvailable");
19
+ if (!cls || ![cls respondsToSelector:sel]) return NO;
20
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
21
+ return msgSend0((id)cls, sel);
22
+ }
23
+
24
+ - (double)getTemperatureAtPoint:(int)x y:(int)y {
25
+ id inst = [[self class] shared];
26
+ SEL sel = sel_registerName("getTemperatureAt: y:");
27
+ // Swift method name mangling may differ; fall back to method used by FlirModule
28
+ SEL selAlt = sel_registerName("getTemperatureAtPoint:y:");
29
+ SEL use = NULL;
30
+ if (inst && [inst respondsToSelector:sel]) use = sel;
31
+ if (inst && [inst respondsToSelector:selAlt]) use = selAlt;
32
+ if (!inst || !use) return NAN;
33
+ double (*msgSend2)(id, SEL, int, int) = (double (*)(id, SEL, int, int))objc_msgSend;
34
+ return msgSend2(inst, use, x, y);
35
+ }
36
+
37
+ - (double)getTemperatureAtNormalized:(double)nx y:(double)ny {
38
+ id inst = [[self class] shared];
39
+ SEL sel = sel_registerName("getTemperatureAtNormalized:y:");
40
+ if (!inst || ![inst respondsToSelector:sel]) return NAN;
41
+ double (*msgSend2)(id, SEL, double, double) = (double (*)(id, SEL, double, double))objc_msgSend;
42
+ return msgSend2(inst, sel, nx, ny);
43
+ }
44
+
45
+ - (int)getBatteryLevel {
46
+ id inst = [[self class] shared];
47
+ SEL sel = sel_registerName("getBatteryLevel");
48
+ if (!inst || ![inst respondsToSelector:sel]) return -1;
49
+ int (*msgSend0)(id, SEL) = (int (*)(id, SEL))objc_msgSend;
50
+ return msgSend0(inst, sel);
51
+ }
52
+
53
+ - (BOOL)isBatteryCharging {
54
+ id inst = [[self class] shared];
55
+ SEL sel = sel_registerName("isBatteryCharging");
56
+ if (!inst || ![inst respondsToSelector:sel]) return NO;
57
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
58
+ return msgSend0(inst, sel);
59
+ }
60
+
61
+ - (void)setPreferSdkRotation:(BOOL)prefer {
62
+ id inst = [[self class] shared];
63
+ SEL sel = sel_registerName("setPreferSdkRotation:");
64
+ if (!inst || ![inst respondsToSelector:sel]) return;
65
+ void (*msgSend1)(id, SEL, BOOL) = (void (*)(id, SEL, BOOL))objc_msgSend;
66
+ msgSend1(inst, sel, prefer);
67
+ }
68
+
69
+ - (BOOL)isPreferSdkRotation {
70
+ id inst = [[self class] shared];
71
+ SEL sel = sel_registerName("isPreferSdkRotation");
72
+ if (!inst || ![inst respondsToSelector:sel]) return NO;
73
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
74
+ return msgSend0(inst, sel);
75
+ }
76
+
77
+ - (nullable NSString *)latestFrameBase64 {
78
+ id inst = [[self class] shared];
79
+ SEL sel = sel_registerName("latestFrameBase64");
80
+ if (!inst || ![inst respondsToSelector:sel]) return nil;
81
+ id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
82
+ return (NSString *)msgSend0(inst, sel);
83
+ }
84
+
85
+ - (void)retainClient:(NSString *)clientId {
86
+ id inst = [[self class] shared];
87
+ SEL sel = sel_registerName("retainClient:");
88
+ if (!inst || ![inst respondsToSelector:sel]) return;
89
+ void (*msgSend1)(id, SEL, id) = (void (*)(id, SEL, id))objc_msgSend;
90
+ msgSend1(inst, sel, clientId);
91
+ }
92
+
93
+ - (void)releaseClient:(NSString *)clientId {
94
+ id inst = [[self class] shared];
95
+ SEL sel = sel_registerName("releaseClient:");
96
+ if (!inst || ![inst respondsToSelector:sel]) return;
97
+ void (*msgSend1)(id, SEL, id) = (void (*)(id, SEL, id))objc_msgSend;
98
+ msgSend1(inst, sel, clientId);
99
+ }
100
+
101
+ - (void)setPalette:(NSString *)paletteName {
102
+ id inst = [[self class] shared];
103
+ SEL sel = sel_registerName("setPalette:");
104
+ if (!inst || ![inst respondsToSelector:sel]) return;
105
+ void (*msgSend1)(id, SEL, id) = (void (*)(id, SEL, id))objc_msgSend;
106
+ msgSend1(inst, sel, paletteName);
107
+ }
108
+
109
+ - (void)setPaletteFromAcol:(double)acol {
110
+ id inst = [[self class] shared];
111
+ SEL sel = sel_registerName("setPaletteFromAcol:");
112
+ if (!inst || ![inst respondsToSelector:sel]) return;
113
+ void (*msgSend1)(id, SEL, double) = (void (*)(id, SEL, double))objc_msgSend;
114
+ msgSend1(inst, sel, acol);
115
+ }
116
+
117
+ - (nullable NSString *)getPaletteNameFromAcol:(double)acol {
118
+ Class cls = NSClassFromString(@"FlirManager");
119
+ SEL sel = sel_registerName("getPaletteNameFromAcol:");
120
+ if (!cls || ![cls respondsToSelector:sel]) return nil;
121
+ id (*msgSend1)(id, SEL, double) = (id (*)(id, SEL, double))objc_msgSend;
122
+ return (NSString *)msgSend1((id)cls, sel, acol);
123
+ }
124
+
125
+ - (nullable UIImage *)latestFrameImage {
126
+ id inst = [[self class] shared];
127
+ SEL sel = sel_registerName("latestImage");
128
+ SEL selAlt = sel_registerName("latestFrameImage");
129
+ SEL use = NULL;
130
+ if (inst && [inst respondsToSelector:selAlt]) use = selAlt;
131
+ else if (inst && [inst respondsToSelector:sel]) use = sel;
132
+ if (!inst || !use) return nil;
133
+ id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
134
+ return (UIImage *)msgSend0(inst, use);
135
+ }
136
+
137
+ - (void)startDiscovery {
138
+ id inst = [[self class] shared];
139
+ SEL sel = sel_registerName("startDiscovery");
140
+ if (!inst || ![inst respondsToSelector:sel]) return;
141
+ void (*msgSend0)(id, SEL) = (void (*)(id, SEL))objc_msgSend;
142
+ msgSend0(inst, sel);
143
+ }
144
+
145
+ - (void)stopDiscovery {
146
+ id inst = [[self class] shared];
147
+ SEL sel = sel_registerName("stopDiscovery");
148
+ if (!inst || ![inst respondsToSelector:sel]) return;
149
+ void (*msgSend0)(id, SEL) = (void (*)(id, SEL))objc_msgSend;
150
+ msgSend0(inst, sel);
151
+ }
152
+
153
+ @end
@@ -0,0 +1,48 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ @objc public class FLIRManager: NSObject {
5
+ @objc public static let shared = FLIRManager()
6
+
7
+ @objc public func isAvailable() -> Bool {
8
+ return FlirManager.isSDKAvailable
9
+ }
10
+
11
+ @objc public func getTemperatureAtPoint(x: Int, y: Int) -> Double {
12
+ // FlirManager currently doesn't expose a direct getTemperatureAtPoint API.
13
+ // Return NaN for now (consumers should handle NaN) until full parity is implemented.
14
+ return Double.nan
15
+ }
16
+
17
+ @objc public func getTemperatureAtNormalized(_ nx: Double, y: Double) -> Double {
18
+ return Double.nan
19
+ }
20
+
21
+ @objc public func getBatteryLevel() -> Int {
22
+ return -1
23
+ }
24
+
25
+ @objc public func isBatteryCharging() -> Bool {
26
+ return false
27
+ }
28
+
29
+ @objc public func setPreferSdkRotation(_ prefer: Bool) {
30
+ // FlirManager doesn't currently support rotation preference; no-op
31
+ }
32
+
33
+ @objc public func isPreferSdkRotation() -> Bool {
34
+ return false
35
+ }
36
+
37
+ @objc public func latestFrameImage() -> UIImage? {
38
+ return FlirManager.shared.latestImage
39
+ }
40
+
41
+ @objc public func startDiscovery() {
42
+ FlirManager.shared.startDiscovery()
43
+ }
44
+
45
+ @objc public func stopDiscovery() {
46
+ FlirManager.shared.stopDiscovery()
47
+ }
48
+ }
@@ -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
 
@@ -70,6 +70,9 @@ import ThermalSDK
70
70
 
71
71
  // Discovered devices
72
72
  private var discoveredDevices: [FlirDeviceInfo] = []
73
+ // Client lifecycle for discovery/connection ownership
74
+ private var activeClients: Set<String> = []
75
+ private var shutdownWorkItem: DispatchWorkItem? = nil
73
76
 
74
77
  #if FLIR_ENABLED
75
78
  private var discovery: FLIRDiscovery?
@@ -100,6 +103,164 @@ import ThermalSDK
100
103
  @objc public func getDiscoveredDevices() -> [FlirDeviceInfo] {
101
104
  return discoveredDevices
102
105
  }
106
+
107
+ // MARK: - Temperature & Battery Access
108
+
109
+ /// Returns a temperature data dictionary for the given pixel, or nil if unavailable.
110
+ @objc public func getTemperatureData(x: Int = -1, y: Int = -1) -> [String: Any]? {
111
+ #if FLIR_ENABLED
112
+ guard let streamer = streamer else { return nil }
113
+ var result: [String: Any]? = nil
114
+ streamer.withThermalImage { thermalImage in
115
+ // Attempt to extract per-pixel measurements if available
116
+ if let measurements = thermalImage.measurements as? [NSNumber],
117
+ measurements.count > 0,
118
+ let img = streamer.getImage() {
119
+ let width = Int(img.size.width)
120
+ let height = Int(img.size.height)
121
+ if width > 0 && height > 0 && x >= 0 && y >= 0 && x < width && y < height {
122
+ let idx = y * width + x
123
+ if idx < measurements.count {
124
+ let temp = measurements[idx].doubleValue
125
+ result = ["temperature": temp]
126
+ }
127
+ }
128
+ }
129
+ // Fallback: use lastTemperature if set
130
+ if result == nil && !lastTemperature.isNaN {
131
+ result = ["temperature": lastTemperature]
132
+ }
133
+ }
134
+ return result
135
+ #else
136
+ return nil
137
+ #endif
138
+ }
139
+
140
+ @objc public func getTemperatureAtPoint(_ x: Int, y: Int) -> Double {
141
+ if let data = getTemperatureData(x: x, y: y), let t = data["temperature"] as? Double {
142
+ return t
143
+ }
144
+ return Double.nan
145
+ }
146
+
147
+ @objc public func getTemperatureAtNormalized(_ nx: Double, y: Double) -> Double {
148
+ guard let img = latestImage else { return Double.nan }
149
+ let px = Int(nx * Double(img.size.width))
150
+ let py = Int(y * Double(img.size.height))
151
+ return getTemperatureAtPoint(px, y: py)
152
+ }
153
+
154
+ @objc public func getBatteryLevel() -> Int {
155
+ #if FLIR_ENABLED
156
+ if let cam = camera {
157
+ if let val = cam.value(forKey: "batteryLevel") as? Int { return val }
158
+ if let batt = cam.value(forKey: "battery") as? NSObject,
159
+ let lv = batt.value(forKey: "level") as? Int { return lv }
160
+ }
161
+ #endif
162
+ return -1
163
+ }
164
+
165
+ @objc public func isBatteryCharging() -> Bool {
166
+ #if FLIR_ENABLED
167
+ if let cam = camera {
168
+ if let ch = cam.value(forKey: "isCharging") as? Bool { return ch }
169
+ if let batt = cam.value(forKey: "battery") as? NSObject,
170
+ let ch = batt.value(forKey: "charging") as? Bool { return ch }
171
+ }
172
+ #endif
173
+ return false
174
+ }
175
+
176
+ @objc public func latestFrameImage() -> UIImage? {
177
+ return latestImage
178
+ }
179
+
180
+ @objc public func latestFrameBase64() -> String? {
181
+ guard let img = latestImage else { return nil }
182
+ if let data = img.jpegData(compressionQuality: 0.7) {
183
+ return data.base64EncodedString()
184
+ }
185
+ if let data = img.pngData() {
186
+ return data.base64EncodedString()
187
+ }
188
+ return nil
189
+ }
190
+
191
+ // Client lifecycle helpers: callers (UI/filters) can retain/release to ensure
192
+ // discovery runs while any client is active.
193
+ @objc public func retainClient(_ clientId: String) {
194
+ DispatchQueue.main.async {
195
+ self.activeClients.insert(clientId)
196
+ self.shutdownWorkItem?.cancel()
197
+ self.shutdownWorkItem = nil
198
+ if self.activeClients.count == 1 {
199
+ self.startDiscovery()
200
+ }
201
+ }
202
+ }
203
+
204
+ @objc public func releaseClient(_ clientId: String) {
205
+ DispatchQueue.main.async {
206
+ self.activeClients.remove(clientId)
207
+ self.shutdownWorkItem?.cancel()
208
+ let work = DispatchWorkItem { [weak self] in
209
+ guard let self = self else { return }
210
+ if self.activeClients.isEmpty {
211
+ self.stopDiscovery()
212
+ }
213
+ }
214
+ self.shutdownWorkItem = work
215
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: work)
216
+ }
217
+ }
218
+
219
+ // MARK: - Palette Control
220
+
221
+ /// Set palette by name (case-insensitive). If the SDK isn't available or the
222
+ /// palette cannot be found, this is a no-op.
223
+ @objc public func setPalette(_ paletteName: String) {
224
+ #if FLIR_ENABLED
225
+ guard let streamer = streamer, let thermalImage = streamer.getImage() else {
226
+ NSLog("[FlirManager] Cannot set palette - no active streamer")
227
+ return
228
+ }
229
+
230
+ if let paletteManager = FLIRPaletteManager.defaultPalettes() {
231
+ for palette in paletteManager {
232
+ if let p = palette as? FLIRPalette, p.name.lowercased() == paletteName.lowercased() {
233
+ thermalImage.palette = p
234
+ NSLog("[FlirManager] ✅ Palette set to: \(paletteName)")
235
+ return
236
+ }
237
+ }
238
+ NSLog("[FlirManager] Palette not found: \(paletteName)")
239
+ } else {
240
+ NSLog("[FlirManager] SDK not available - cannot set palette")
241
+ }
242
+ #else
243
+ NSLog("[FlirManager] SDK not available - cannot set palette")
244
+ #endif
245
+ }
246
+
247
+ /// Map a normalized acol value (0..1) to a palette name.
248
+ @objc public static func getPaletteNameFromAcol(_ acol: Float) -> String {
249
+ if acol < 0.125 { return "WhiteHot" }
250
+ else if acol < 0.25 { return "BlackHot" }
251
+ else if acol < 0.375 { return "Iron" }
252
+ else if acol < 0.5 { return "Rainbow" }
253
+ else if acol < 0.625 { return "Lava" }
254
+ else if acol < 0.75 { return "Arctic" }
255
+ else if acol < 0.875 { return "Coldest" }
256
+ else { return "Hottest" }
257
+ }
258
+
259
+ @objc public func setPaletteFromAcol(_ acol: Float) {
260
+ let paletteName = FlirManager.getPaletteNameFromAcol(acol)
261
+ NSLog("[FlirManager] Setting palette from acol=\(acol) -> \(paletteName)")
262
+ setPalette(paletteName)
263
+ }
103
264
 
104
265
  // MARK: - SDK Availability
105
266
 
@@ -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
@@ -11,6 +11,7 @@
11
11
  #import "FlirState.h"
12
12
  #import <React/RCTLog.h>
13
13
  #import <React/RCTBridge.h>
14
+ #import <objc/message.h>
14
15
 
15
16
  #if __has_include(<ThermalSDK/ThermalSDK.h>)
16
17
  #define FLIR_SDK_AVAILABLE 1
@@ -19,8 +20,62 @@
19
20
  #define FLIR_SDK_AVAILABLE 0
20
21
  #endif
21
22
 
22
- // Forward declare Swift class
23
- @class FlirManager;
23
+ // Use runtime lookup to avoid a hard link-time dependency on `FLIRManager`.
24
+ // This prevents duplicate-definition and missing-symbol build failures when
25
+ // the Swift `FLIRManager` may or may not be available at build/link time.
26
+ static id flir_manager_shared(void) {
27
+ Class cls = NSClassFromString(@"FLIRManager");
28
+ if (!cls) return nil;
29
+ SEL sel = sel_registerName("shared");
30
+ if (![cls respondsToSelector:sel]) return nil;
31
+ id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
32
+ return msgSend0((id)cls, sel);
33
+ }
34
+
35
+ static double flir_getTemperatureAtPoint(int x, int y) {
36
+ id inst = flir_manager_shared();
37
+ if (!inst) return NAN;
38
+ SEL sel = sel_registerName("getTemperatureAtPoint:y:");
39
+ if (![inst respondsToSelector:sel]) return NAN;
40
+ double (*msgSend2)(id, SEL, int, int) = (double (*)(id, SEL, int, int))objc_msgSend;
41
+ return msgSend2(inst, sel, x, y);
42
+ }
43
+
44
+ static int flir_getBatteryLevel(void) {
45
+ id inst = flir_manager_shared();
46
+ if (!inst) return -1;
47
+ SEL sel = sel_registerName("getBatteryLevel");
48
+ if (![inst respondsToSelector:sel]) return -1;
49
+ int (*msgSend0)(id, SEL) = (int (*)(id, SEL))objc_msgSend;
50
+ return msgSend0(inst, sel);
51
+ }
52
+
53
+ static BOOL flir_isBatteryCharging(void) {
54
+ id inst = flir_manager_shared();
55
+ if (!inst) return NO;
56
+ SEL sel = sel_registerName("isBatteryCharging");
57
+ if (![inst respondsToSelector:sel]) return NO;
58
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
59
+ return msgSend0(inst, sel);
60
+ }
61
+
62
+ static void flir_setPreferSdkRotation(BOOL prefer) {
63
+ id inst = flir_manager_shared();
64
+ if (!inst) return;
65
+ SEL sel = sel_registerName("setPreferSdkRotation:");
66
+ if (![inst respondsToSelector:sel]) return;
67
+ void (*msgSend1)(id, SEL, BOOL) = (void (*)(id, SEL, BOOL))objc_msgSend;
68
+ msgSend1(inst, sel, prefer);
69
+ }
70
+
71
+ static BOOL flir_isPreferSdkRotation(void) {
72
+ id inst = flir_manager_shared();
73
+ if (!inst) return NO;
74
+ SEL sel = sel_registerName("isPreferSdkRotation");
75
+ if (![inst respondsToSelector:sel]) return NO;
76
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
77
+ return msgSend0(inst, sel);
78
+ }
24
79
 
25
80
  @interface FlirModule()
26
81
  #if FLIR_SDK_AVAILABLE
@@ -77,6 +132,7 @@ RCT_EXPORT_MODULE(FlirModule);
77
132
  @"FlirFrameReceived",
78
133
  @"FlirError",
79
134
  @"FlirStateChanged"
135
+ , @"FlirBatteryUpdated"
80
136
  ];
81
137
  }
82
138
 
@@ -88,6 +144,15 @@ RCT_EXPORT_METHOD(removeListeners:(NSInteger)count) {
88
144
  // Required for RCTEventEmitter
89
145
  }
90
146
 
147
+ // Provide a class helper so other native modules can post a battery update
148
+ + (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
149
+ NSDictionary *payload = @{
150
+ @"level": @(level),
151
+ @"isCharging": @(charging)
152
+ };
153
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirBatteryUpdated" body:payload];
154
+ }
155
+
91
156
  #pragma mark - Discovery Methods
92
157
 
93
158
  RCT_EXPORT_METHOD(startDiscovery:(RCTPromiseResolveBlock)resolve
@@ -334,10 +399,8 @@ RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
334
399
  resolver:(RCTPromiseResolveBlock)resolve
335
400
  rejecter:(RCTPromiseRejectBlock)reject) {
336
401
  dispatch_async(dispatch_get_main_queue(), ^{
337
- double temp = [FlirState shared].lastTemperature;
338
- if (isnan(temp)) {
339
- temp = self.lastTemperature;
340
- }
402
+ // Call into native FLIRManager to query temperature at point (runtime lookup)
403
+ double temp = flir_getTemperatureAtPoint([x intValue], [y intValue]);
341
404
  if (isnan(temp)) {
342
405
  resolve([NSNull null]);
343
406
  } else {
@@ -501,6 +564,51 @@ RCT_EXPORT_METHOD(getLatestFramePath:(RCTPromiseResolveBlock)resolve
501
564
  });
502
565
  }
503
566
 
567
+ RCT_EXPORT_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
568
+ rejecter:(RCTPromiseRejectBlock)reject) {
569
+ dispatch_async(dispatch_get_main_queue(), ^{
570
+ #if FLIR_SDK_AVAILABLE
571
+ int level = flir_getBatteryLevel();
572
+ resolve(@(level));
573
+ #else
574
+ resolve(@(-1));
575
+ #endif
576
+ });
577
+ }
578
+
579
+ RCT_EXPORT_METHOD(isBatteryCharging:(RCTPromiseResolveBlock)resolve
580
+ rejecter:(RCTPromiseRejectBlock)reject) {
581
+ dispatch_async(dispatch_get_main_queue(), ^{
582
+ #if FLIR_SDK_AVAILABLE
583
+ BOOL ch = flir_isBatteryCharging();
584
+ resolve(@(ch));
585
+ #else
586
+ resolve(@(NO));
587
+ #endif
588
+ });
589
+ }
590
+
591
+ RCT_EXPORT_METHOD(setPreferSdkRotation:(BOOL)prefer
592
+ resolver:(RCTPromiseResolveBlock)resolve
593
+ rejecter:(RCTPromiseRejectBlock)reject) {
594
+ dispatch_async(dispatch_get_main_queue(), ^{
595
+ @try {
596
+ flir_setPreferSdkRotation(prefer);
597
+ resolve(@(YES));
598
+ } @catch (NSException *ex) {
599
+ reject(@"ERR_FLIR_SET_ROTATION_PREF", ex.reason, nil);
600
+ }
601
+ });
602
+ }
603
+
604
+ RCT_EXPORT_METHOD(isPreferSdkRotation:(RCTPromiseResolveBlock)resolve
605
+ rejecter:(RCTPromiseRejectBlock)reject) {
606
+ dispatch_async(dispatch_get_main_queue(), ^{
607
+ BOOL v = flir_isPreferSdkRotation();
608
+ resolve(@(v));
609
+ });
610
+ }
611
+
504
612
  #pragma mark - Helper Methods
505
613
 
506
614
  - (void)emitDeviceConnected {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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",
@@ -15,7 +15,6 @@
15
15
  "android/Flir/build.gradle.kts",
16
16
  "ios/Flir/src/",
17
17
  "ios/Flir/SDKLoader/",
18
-
19
18
  "app.plugin.js",
20
19
  "Flir.podspec",
21
20
  "sdk-manifest.json",