ilabs-flir 2.0.7 → 2.0.9
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.
|
@@ -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
|
+
}
|
|
@@ -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,182 @@ 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 { [weak self] 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, let s = self, !s.lastTemperature.isNaN {
|
|
131
|
+
result = ["temperature": s.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
|
+
// Use runtime-safe APIs to find and set palette to avoid compile-time
|
|
231
|
+
// coupling to specific SDK versions. Try FLIRPaletteManager.defaultPalettes
|
|
232
|
+
// via ObjC runtime, then attempt a KVC set on the returned image if possible.
|
|
233
|
+
if let pmClass = NSClassFromString("FLIRPaletteManager") as AnyObject?, pmClass.responds(to: Selector(("default"))) {
|
|
234
|
+
if let pmInstance = pmClass.perform(Selector(("default")))?.takeUnretainedValue() as? NSObject,
|
|
235
|
+
pmInstance.responds(to: Selector(("getDefaultPalettes"))) {
|
|
236
|
+
if let arr = pmInstance.perform(Selector(("getDefaultPalettes")))?.takeUnretainedValue() as? NSArray {
|
|
237
|
+
for palette in arr {
|
|
238
|
+
if let p = palette as? NSObject,
|
|
239
|
+
let name = p.value(forKey: "name") as? String,
|
|
240
|
+
name.lowercased() == paletteName.lowercased() {
|
|
241
|
+
if let imgObj = thermalImage as? NSObject {
|
|
242
|
+
// Try both 'palette' and 'Palette' keys depending on SDK
|
|
243
|
+
if imgObj.responds(to: Selector(("setPalette:"))) {
|
|
244
|
+
imgObj.perform(Selector(("setPalette:")), with: p)
|
|
245
|
+
} else {
|
|
246
|
+
imgObj.setValue(p, forKey: "Palette")
|
|
247
|
+
}
|
|
248
|
+
NSLog("[FlirManager] ✅ Palette set to: \(paletteName)")
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
NSLog("[FlirManager] Palette not found: \(paletteName)")
|
|
254
|
+
} else {
|
|
255
|
+
NSLog("[FlirManager] Palette manager returned unexpected type")
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
NSLog("[FlirManager] SDK palette APIs not available - cannot set palette")
|
|
259
|
+
}
|
|
260
|
+
#else
|
|
261
|
+
NSLog("[FlirManager] SDK not available - cannot set palette")
|
|
262
|
+
#endif
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Map a normalized acol value (0..1) to a palette name.
|
|
266
|
+
@objc public static func getPaletteNameFromAcol(_ acol: Float) -> String {
|
|
267
|
+
if acol < 0.125 { return "WhiteHot" }
|
|
268
|
+
else if acol < 0.25 { return "BlackHot" }
|
|
269
|
+
else if acol < 0.375 { return "Iron" }
|
|
270
|
+
else if acol < 0.5 { return "Rainbow" }
|
|
271
|
+
else if acol < 0.625 { return "Lava" }
|
|
272
|
+
else if acol < 0.75 { return "Arctic" }
|
|
273
|
+
else if acol < 0.875 { return "Coldest" }
|
|
274
|
+
else { return "Hottest" }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@objc public func setPaletteFromAcol(_ acol: Float) {
|
|
278
|
+
let paletteName = FlirManager.getPaletteNameFromAcol(acol)
|
|
279
|
+
NSLog("[FlirManager] Setting palette from acol=\(acol) -> \(paletteName)")
|
|
280
|
+
setPalette(paletteName)
|
|
281
|
+
}
|
|
103
282
|
|
|
104
283
|
// MARK: - SDK Availability
|
|
105
284
|
|
|
@@ -205,15 +384,34 @@ import ThermalSDK
|
|
|
205
384
|
}
|
|
206
385
|
}
|
|
207
386
|
|
|
208
|
-
// Connect
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
387
|
+
// Connect (support multiple SDK signatures via ObjC runtime)
|
|
388
|
+
var connectedOK = false
|
|
389
|
+
if let cam = camera {
|
|
390
|
+
if cam.responds(to: Selector(("connect:error:"))) {
|
|
391
|
+
// Call connect:identity error:nil (ignore NSError pointer for compatibility)
|
|
392
|
+
_ = cam.perform(Selector(("connect:error:")), with: identity, with: nil)
|
|
393
|
+
connectedOK = true
|
|
394
|
+
} else if cam.responds(to: Selector(("connect:"))) {
|
|
395
|
+
_ = cam.perform(Selector(("connect:")), with: identity)
|
|
396
|
+
connectedOK = true
|
|
397
|
+
} else {
|
|
398
|
+
NSLog("[FlirManager] No compatible connect API on FLIRCamera")
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if connectedOK {
|
|
403
|
+
connectedIdentity = identity
|
|
404
|
+
connectedDeviceId = identity.deviceId()
|
|
405
|
+
connectedDeviceName = identity.deviceId()
|
|
406
|
+
_isConnected = true
|
|
407
|
+
NSLog("[FlirManager] Connected to: \(identity.deviceId())")
|
|
408
|
+
} else {
|
|
409
|
+
NSLog("[FlirManager] Connection failed: no compatible connect API")
|
|
410
|
+
DispatchQueue.main.async { [weak self] in
|
|
411
|
+
self?.delegate?.onError("Connection failed: unsupported SDK API")
|
|
412
|
+
}
|
|
413
|
+
return
|
|
414
|
+
}
|
|
217
415
|
|
|
218
416
|
// Get streams
|
|
219
417
|
if let streams = camera?.getStreams(), !streams.isEmpty {
|
|
@@ -263,7 +461,7 @@ import ThermalSDK
|
|
|
263
461
|
if iface.contains(.network) { return "NETWORK" }
|
|
264
462
|
if iface.contains(.flirOneWireless) { return "WIRELESS" }
|
|
265
463
|
if iface.contains(.emulator) { return "EMULATOR" }
|
|
266
|
-
if iface.contains(
|
|
464
|
+
if String(describing: iface).lowercased().contains("usb") { return "USB" }
|
|
267
465
|
return "UNKNOWN"
|
|
268
466
|
}
|
|
269
467
|
#endif
|
|
@@ -575,10 +773,23 @@ extension FlirManager: FLIRStreamDelegate {
|
|
|
575
773
|
if let image = streamer.getImage() {
|
|
576
774
|
_latestImage = image
|
|
577
775
|
|
|
578
|
-
// Get temperature from thermal image
|
|
776
|
+
// Get temperature from thermal image (use runtime selectors to be resilient across SDK versions)
|
|
579
777
|
streamer.withThermalImage { [weak self] thermalImage in
|
|
580
|
-
|
|
581
|
-
|
|
778
|
+
var tempVal: Double = Double.nan
|
|
779
|
+
// Try getImageStatistics then fallback to getStatistics (different SDK versions)
|
|
780
|
+
if let statsObj = (thermalImage.perform(Selector(("getImageStatistics")))?.takeUnretainedValue() as? NSObject) ?? (thermalImage.perform(Selector(("getStatistics")))?.takeUnretainedValue() as? NSObject) {
|
|
781
|
+
if statsObj.responds(to: Selector(("getMax"))) {
|
|
782
|
+
if let maxObj = statsObj.perform(Selector(("getMax")))?.takeUnretainedValue() as? NSObject,
|
|
783
|
+
let val = maxObj.value(forKey: "value") as? Double {
|
|
784
|
+
tempVal = val
|
|
785
|
+
}
|
|
786
|
+
} else if let maxVal = statsObj.value(forKey: "max") as? NSObject,
|
|
787
|
+
let val = maxVal.value(forKey: "value") as? Double {
|
|
788
|
+
tempVal = val
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if !tempVal.isNaN {
|
|
792
|
+
self?.lastTemperature = tempVal
|
|
582
793
|
}
|
|
583
794
|
}
|
|
584
795
|
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
#import "FlirState.h"
|
|
12
12
|
#import <React/RCTLog.h>
|
|
13
13
|
#import <React/RCTBridge.h>
|
|
14
|
+
#import <objc/message.h>
|
|
15
|
+
#import <objc/runtime.h>
|
|
14
16
|
|
|
15
17
|
#if __has_include(<ThermalSDK/ThermalSDK.h>)
|
|
16
18
|
#define FLIR_SDK_AVAILABLE 1
|
|
@@ -19,8 +21,62 @@
|
|
|
19
21
|
#define FLIR_SDK_AVAILABLE 0
|
|
20
22
|
#endif
|
|
21
23
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
+
// Use runtime lookup to avoid a hard link-time dependency on `FLIRManager`.
|
|
25
|
+
// This prevents duplicate-definition and missing-symbol build failures when
|
|
26
|
+
// the Swift `FLIRManager` may or may not be available at build/link time.
|
|
27
|
+
static id flir_manager_shared(void) {
|
|
28
|
+
Class cls = NSClassFromString(@"FLIRManager");
|
|
29
|
+
if (!cls) return nil;
|
|
30
|
+
SEL sel = sel_registerName("shared");
|
|
31
|
+
if (![cls respondsToSelector:sel]) return nil;
|
|
32
|
+
id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
|
|
33
|
+
return msgSend0((id)cls, sel);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static double flir_getTemperatureAtPoint(int x, int y) {
|
|
37
|
+
id inst = flir_manager_shared();
|
|
38
|
+
if (!inst) return NAN;
|
|
39
|
+
SEL sel = sel_registerName("getTemperatureAtPoint:y:");
|
|
40
|
+
if (![inst respondsToSelector:sel]) return NAN;
|
|
41
|
+
double (*msgSend2)(id, SEL, int, int) = (double (*)(id, SEL, int, int))objc_msgSend;
|
|
42
|
+
return msgSend2(inst, sel, x, y);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static int flir_getBatteryLevel(void) {
|
|
46
|
+
id inst = flir_manager_shared();
|
|
47
|
+
if (!inst) return -1;
|
|
48
|
+
SEL sel = sel_registerName("getBatteryLevel");
|
|
49
|
+
if (![inst respondsToSelector:sel]) return -1;
|
|
50
|
+
int (*msgSend0)(id, SEL) = (int (*)(id, SEL))objc_msgSend;
|
|
51
|
+
return msgSend0(inst, sel);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static BOOL flir_isBatteryCharging(void) {
|
|
55
|
+
id inst = flir_manager_shared();
|
|
56
|
+
if (!inst) return NO;
|
|
57
|
+
SEL sel = sel_registerName("isBatteryCharging");
|
|
58
|
+
if (![inst respondsToSelector:sel]) return NO;
|
|
59
|
+
BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
|
|
60
|
+
return msgSend0(inst, sel);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static void flir_setPreferSdkRotation(BOOL prefer) {
|
|
64
|
+
id inst = flir_manager_shared();
|
|
65
|
+
if (!inst) return;
|
|
66
|
+
SEL sel = sel_registerName("setPreferSdkRotation:");
|
|
67
|
+
if (![inst respondsToSelector:sel]) return;
|
|
68
|
+
void (*msgSend1)(id, SEL, BOOL) = (void (*)(id, SEL, BOOL))objc_msgSend;
|
|
69
|
+
msgSend1(inst, sel, prefer);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static BOOL flir_isPreferSdkRotation(void) {
|
|
73
|
+
id inst = flir_manager_shared();
|
|
74
|
+
if (!inst) return NO;
|
|
75
|
+
SEL sel = sel_registerName("isPreferSdkRotation");
|
|
76
|
+
if (![inst respondsToSelector:sel]) return NO;
|
|
77
|
+
BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
|
|
78
|
+
return msgSend0(inst, sel);
|
|
79
|
+
}
|
|
24
80
|
|
|
25
81
|
@interface FlirModule()
|
|
26
82
|
#if FLIR_SDK_AVAILABLE
|
|
@@ -344,8 +400,8 @@ RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
|
|
|
344
400
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
345
401
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
346
402
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
347
|
-
// Call into native FLIRManager to query temperature at point
|
|
348
|
-
double temp = [
|
|
403
|
+
// Call into native FLIRManager to query temperature at point (runtime lookup)
|
|
404
|
+
double temp = flir_getTemperatureAtPoint([x intValue], [y intValue]);
|
|
349
405
|
if (isnan(temp)) {
|
|
350
406
|
resolve([NSNull null]);
|
|
351
407
|
} else {
|
|
@@ -513,7 +569,7 @@ RCT_EXPORT_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
|
|
|
513
569
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
514
570
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
515
571
|
#if FLIR_SDK_AVAILABLE
|
|
516
|
-
int level =
|
|
572
|
+
int level = flir_getBatteryLevel();
|
|
517
573
|
resolve(@(level));
|
|
518
574
|
#else
|
|
519
575
|
resolve(@(-1));
|
|
@@ -525,7 +581,7 @@ RCT_EXPORT_METHOD(isBatteryCharging:(RCTPromiseResolveBlock)resolve
|
|
|
525
581
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
526
582
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
527
583
|
#if FLIR_SDK_AVAILABLE
|
|
528
|
-
BOOL ch =
|
|
584
|
+
BOOL ch = flir_isBatteryCharging();
|
|
529
585
|
resolve(@(ch));
|
|
530
586
|
#else
|
|
531
587
|
resolve(@(NO));
|
|
@@ -537,8 +593,8 @@ RCT_EXPORT_METHOD(setPreferSdkRotation:(BOOL)prefer
|
|
|
537
593
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
538
594
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
539
595
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
540
|
-
|
|
541
|
-
|
|
596
|
+
@try {
|
|
597
|
+
flir_setPreferSdkRotation(prefer);
|
|
542
598
|
resolve(@(YES));
|
|
543
599
|
} @catch (NSException *ex) {
|
|
544
600
|
reject(@"ERR_FLIR_SET_ROTATION_PREF", ex.reason, nil);
|
|
@@ -549,7 +605,7 @@ RCT_EXPORT_METHOD(setPreferSdkRotation:(BOOL)prefer
|
|
|
549
605
|
RCT_EXPORT_METHOD(isPreferSdkRotation:(RCTPromiseResolveBlock)resolve
|
|
550
606
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
551
607
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
552
|
-
BOOL v =
|
|
608
|
+
BOOL v = flir_isPreferSdkRotation();
|
|
553
609
|
resolve(@(v));
|
|
554
610
|
});
|
|
555
611
|
}
|
|
@@ -741,7 +797,13 @@ RCT_EXPORT_METHOD(isPreferSdkRotation:(RCTPromiseResolveBlock)resolve
|
|
|
741
797
|
|
|
742
798
|
// Get temperature from thermal image if available
|
|
743
799
|
[self.streamer withThermalImage:^(FLIRThermalImage *thermalImage) {
|
|
744
|
-
|
|
800
|
+
// Some SDK versions call getImageStatistics(), try both selectors
|
|
801
|
+
FLIRImageStatistics *stats = nil;
|
|
802
|
+
if ([thermalImage respondsToSelector:sel_registerName("getImageStatistics")]) {
|
|
803
|
+
stats = ((id (*)(id, SEL))objc_msgSend)((id)thermalImage, sel_registerName("getImageStatistics"));
|
|
804
|
+
} else if ([thermalImage respondsToSelector:sel_registerName("getStatistics")]) {
|
|
805
|
+
stats = ((id (*)(id, SEL))objc_msgSend)((id)thermalImage, sel_registerName("getStatistics"));
|
|
806
|
+
}
|
|
745
807
|
if (stats) {
|
|
746
808
|
self.lastTemperature = [[stats getMax] value];
|
|
747
809
|
[FlirState shared].lastTemperature = self.lastTemperature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilabs-flir",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
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",
|