ilabs-flir 2.1.31 → 2.1.33

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.
@@ -3,102 +3,98 @@
3
3
  // Flir
4
4
  //
5
5
  // React Native bridge module for FLIR thermal camera SDK
6
- // Provides discovery, connection, and streaming functionality
6
+ // Delegate to FlirManager (Swift) for all functionality.
7
7
  //
8
8
 
9
9
  #import "FlirModule.h"
10
10
  #import "FlirEventEmitter.h"
11
11
  #import "FlirState.h"
12
- #import <React/RCTLog.h>
13
12
  #import <React/RCTBridge.h>
13
+ #import <React/RCTLog.h>
14
14
  #import <objc/message.h>
15
15
  #import <objc/runtime.h>
16
16
 
17
- #if __has_include(<ThermalSDK/ThermalSDK.h>)
18
- #define FLIR_SDK_AVAILABLE 1
19
- #import <ThermalSDK/ThermalSDK.h>
20
- #else
21
- #define FLIR_SDK_AVAILABLE 0
17
+ // Import Swift-generated header for FlirManagerDelegate protocol
18
+ #if __has_include("Flir-Swift.h")
19
+ #import "Flir-Swift.h"
20
+ #elif __has_include(<Flir/Flir-Swift.h>)
21
+ #import <Flir/Flir-Swift.h>
22
22
  #endif
23
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.
24
+ // Helper to access FlirManager singleton
27
25
  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
-
26
+ Class cls = NSClassFromString(@"Flir.FlirManager");
27
+ if (!cls) {
28
+ cls = NSClassFromString(@"FlirManager");
29
+ }
30
+ if (!cls)
31
+ return nil;
32
+ SEL sel = sel_registerName("shared");
33
+ if (![cls respondsToSelector:sel])
34
+ return nil;
35
+ id (*msgSend0)(id, SEL) = (id (*)(id, SEL))objc_msgSend;
36
+ return msgSend0((id)cls, sel);
37
+ }
38
+
39
+ // Helper for primitives
36
40
  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);
41
+ id inst = flir_manager_shared();
42
+ if (!inst)
43
+ return NAN;
44
+ SEL sel = sel_registerName("getTemperatureAtPoint:y:");
45
+ if (![inst respondsToSelector:sel])
46
+ return NAN;
47
+ double (*msgSend2)(id, SEL, int, int) =
48
+ (double (*)(id, SEL, int, int))objc_msgSend;
49
+ return msgSend2(inst, sel, x, y);
43
50
  }
44
51
 
45
52
  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);
53
+ id inst = flir_manager_shared();
54
+ if (!inst)
55
+ return -1;
56
+ SEL sel = sel_registerName("getBatteryLevel");
57
+ if (![inst respondsToSelector:sel])
58
+ return -1;
59
+ int (*msgSend0)(id, SEL) = (int (*)(id, SEL))objc_msgSend;
60
+ return msgSend0(inst, sel);
52
61
  }
53
62
 
54
63
  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);
64
+ id inst = flir_manager_shared();
65
+ if (!inst)
66
+ return NO;
67
+ SEL sel = sel_registerName("isBatteryCharging");
68
+ if (![inst respondsToSelector:sel])
69
+ return NO;
70
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
71
+ return msgSend0(inst, sel);
61
72
  }
62
-
63
73
  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);
74
+ id inst = flir_manager_shared();
75
+ if (!inst)
76
+ return;
77
+ SEL sel = sel_registerName("setPreferSdkRotation:");
78
+ if (![inst respondsToSelector:sel])
79
+ return;
80
+ void (*msgSend1)(id, SEL, BOOL) = (void (*)(id, SEL, BOOL))objc_msgSend;
81
+ msgSend1(inst, sel, prefer);
70
82
  }
71
83
 
72
84
  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);
85
+ id inst = flir_manager_shared();
86
+ if (!inst)
87
+ return NO;
88
+ SEL sel = sel_registerName("isPreferSdkRotation");
89
+ if (![inst respondsToSelector:sel])
90
+ return NO;
91
+ BOOL (*msgSend0)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
92
+ return msgSend0(inst, sel);
79
93
  }
80
94
 
81
- @interface FlirModule()
82
- #if FLIR_SDK_AVAILABLE
83
- <FLIRDiscoveryEventDelegate, FLIRDataReceivedDelegate, FLIRStreamDelegate>
84
- #endif
85
-
86
- #if FLIR_SDK_AVAILABLE
87
- @property (nonatomic, strong) FLIRDiscovery *discovery;
88
- @property (nonatomic, strong) FLIRCamera *camera;
89
- @property (nonatomic, strong) FLIRStream *stream;
90
- @property (nonatomic, strong) FLIRThermalStreamer *streamer;
91
- @property (nonatomic, strong) FLIRIdentity *connectedIdentity;
92
- @property (nonatomic, strong) NSMutableDictionary<NSString *, FLIRIdentity *> *identityMap;
93
- #endif
94
-
95
- @property (nonatomic, strong) NSMutableArray<NSDictionary *> *discoveredDevices;
96
- @property (nonatomic, assign) BOOL isScanning;
97
- @property (nonatomic, assign) BOOL isConnected;
98
- @property (nonatomic, assign) BOOL isStreaming;
99
- @property (nonatomic, copy) NSString *connectedDeviceId;
100
- @property (nonatomic, copy) NSString *connectedDeviceName;
101
- @property (nonatomic, assign) double lastTemperature;
95
+ @interface FlirModule () <FlirManagerDelegate>
96
+ @property(nonatomic, copy) RCTPromiseResolveBlock connectResolve;
97
+ @property(nonatomic, copy) RCTPromiseRejectBlock connectReject;
102
98
  @end
103
99
 
104
100
  @implementation FlirModule
@@ -106,907 +102,357 @@ static BOOL flir_isPreferSdkRotation(void) {
106
102
  RCT_EXPORT_MODULE(FlirModule);
107
103
 
108
104
  + (BOOL)requiresMainQueueSetup {
109
- return YES;
105
+ return YES;
110
106
  }
111
107
 
112
108
  - (instancetype)init {
113
- if (self = [super init]) {
114
- #if FLIR_SDK_AVAILABLE
115
- _identityMap = [NSMutableDictionary new];
116
- #endif
117
- _discoveredDevices = [NSMutableArray new];
118
- _isScanning = NO;
119
- _isConnected = NO;
120
- _isStreaming = NO;
121
- _lastTemperature = NAN;
109
+ if (self = [super init]) {
110
+ // Wire up delegate
111
+ id manager = flir_manager_shared();
112
+ if (manager) {
113
+ [manager setValue:self forKey:@"delegate"];
122
114
  }
123
- return self;
115
+ }
116
+ return self;
124
117
  }
125
118
 
126
119
  #pragma mark - Event Emitter Support
127
120
 
128
121
  - (NSArray<NSString *> *)supportedEvents {
129
- return @[
130
- @"FlirDeviceConnected",
131
- @"FlirDeviceDisconnected",
132
- @"FlirDevicesFound",
133
- @"FlirFrameReceived",
134
- @"FlirError",
135
- @"FlirStateChanged"
136
- , @"FlirBatteryUpdated"
137
- ];
122
+ return @[
123
+ @"FlirDeviceConnected", @"FlirDeviceDisconnected", @"FlirDevicesFound",
124
+ @"FlirFrameReceived", @"FlirError", @"FlirStateChanged",
125
+ @"FlirBatteryUpdated"
126
+ ];
138
127
  }
139
128
 
140
- RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {
141
- // Required for RCTEventEmitter
129
+ RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
130
+ // Required for RCTEventEmitter
142
131
  }
143
132
 
144
- RCT_EXPORT_METHOD(removeListeners:(NSInteger)count) {
145
- // Required for RCTEventEmitter
133
+ RCT_EXPORT_METHOD(removeListeners : (NSInteger)count) {
134
+ // Required for RCTEventEmitter
146
135
  }
147
136
 
148
- // Provide a class helper so other native modules can post a battery update
149
137
  + (void)emitBatteryUpdateWithLevel:(NSInteger)level charging:(BOOL)charging {
150
- NSDictionary *payload = @{
151
- @"level": @(level),
152
- @"isCharging": @(charging)
153
- };
154
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirBatteryUpdated" body:payload];
155
- }
156
-
157
- #pragma mark - Discovery Methods
158
-
159
- // Network discovery on iOS 14+ requires Local Network privacy keys.
160
- // In USB/Bluetooth-only builds (or when the user denied permission), attempting
161
- // Bonjour discovery can fail noisily or crash depending on SDK internals.
162
- // We default to enabling network discovery only when the host app declares
163
- // NSLocalNetworkUsageDescription, and allow an explicit override via
164
- // setNetworkDiscoveryEnabled.
165
- - (BOOL)shouldEnableNetworkDiscovery {
166
- // Explicit override if app sets it.
167
- NSString *key = @"ilabsFlir.networkDiscoveryEnabled";
168
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
169
- if ([defaults objectForKey:key] != nil) {
170
- return [defaults boolForKey:key];
138
+ NSDictionary *payload = @{@"level" : @(level), @"isCharging" : @(charging)};
139
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirBatteryUpdated"
140
+ body:payload];
141
+ }
142
+
143
+ #pragma mark - Methods
144
+
145
+ RCT_EXPORT_METHOD(setNetworkDiscoveryEnabled : (BOOL)enabled resolver : (
146
+ RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
147
+ // FlirManager uses UserDefaults directly for this too
148
+ id manager = flir_manager_shared();
149
+ if (manager &&
150
+ [manager
151
+ respondsToSelector:sel_registerName("setNetworkDiscoveryEnabled:")]) {
152
+ ((void (*)(id, SEL, BOOL))objc_msgSend)(
153
+ manager, sel_registerName("setNetworkDiscoveryEnabled:"), enabled);
154
+ } else {
155
+ [[NSUserDefaults standardUserDefaults]
156
+ setBool:enabled
157
+ forKey:@"ilabsFlir.networkDiscoveryEnabled"];
158
+ }
159
+ resolve(@(YES));
160
+ }
161
+
162
+ RCT_EXPORT_METHOD(startDiscovery : (RCTPromiseResolveBlock)
163
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
164
+ dispatch_async(dispatch_get_main_queue(), ^{
165
+ id manager = flir_manager_shared();
166
+ if (manager &&
167
+ [manager respondsToSelector:sel_registerName("startDiscovery")]) {
168
+ ((void (*)(id, SEL))objc_msgSend)(manager,
169
+ sel_registerName("startDiscovery"));
171
170
  }
172
-
173
- // Safe default: require Local Network usage description to be present.
174
- id desc = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocalNetworkUsageDescription"];
175
- if ([desc isKindOfClass:[NSString class]] && ((NSString *)desc).length > 0) {
176
- return YES;
177
- }
178
- return NO;
179
- }
180
-
181
- RCT_EXPORT_METHOD(setNetworkDiscoveryEnabled:(BOOL)enabled
182
- resolver:(RCTPromiseResolveBlock)resolve
183
- rejecter:(RCTPromiseRejectBlock)reject) {
184
- #if FLIR_SDK_AVAILABLE
185
- [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"ilabsFlir.networkDiscoveryEnabled"];
186
171
  resolve(@(YES));
187
- #else
188
- resolve(@(YES));
189
- #endif
190
- }
191
-
192
- RCT_EXPORT_METHOD(startDiscovery:(RCTPromiseResolveBlock)resolve
193
- rejecter:(RCTPromiseRejectBlock)reject) {
194
- #if FLIR_SDK_AVAILABLE
195
- dispatch_async(dispatch_get_main_queue(), ^{
196
- if (self.isScanning) {
197
- RCTLogInfo(@"[FlirModule] Already scanning");
198
- resolve(@(YES));
199
- return;
200
- }
201
-
202
- self.isScanning = YES;
203
- [self.discoveredDevices removeAllObjects];
204
- [self.identityMap removeAllObjects];
205
-
206
- // Always expose emulator options to JS/UI so the user can connect even when
207
- // physical devices are not present.
208
- NSDictionary *emuOne = @{
209
- @"id": @"emu:FLIR_ONE",
210
- @"name": @"FLIR One Emulator",
211
- @"communicationType": @"EMULATOR",
212
- @"isEmulator": @(YES)
213
- };
214
- NSDictionary *emuEdge = @{
215
- @"id": @"emu:FLIR_ONE_EDGE",
216
- @"name": @"FLIR One Edge Emulator",
217
- @"communicationType": @"EMULATOR",
218
- @"isEmulator": @(YES)
219
- };
220
- [self.discoveredDevices addObjectsFromArray:@[ emuOne, emuEdge ]];
221
-
222
- NSDictionary *initialDevicesBody = @{
223
- @"devices": self.discoveredDevices,
224
- @"count": @(self.discoveredDevices.count)
225
- };
226
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:initialDevicesBody];
227
-
228
- if (!self.discovery) {
229
- self.discovery = [[FLIRDiscovery alloc] init];
230
- self.discovery.delegate = self;
231
- }
232
-
233
- // Start discovery on allowed interfaces.
234
- // Always include wired/BLE/emulator. Only include network when the app has
235
- // Local Network usage description (or the app explicitly enabled it).
236
- FLIRCommunicationInterface interfaces = FLIRCommunicationInterfaceLightning |
237
- FLIRCommunicationInterfaceFlirOneWireless |
238
- FLIRCommunicationInterfaceEmulator |
239
- FLIRCommunicationInterfaceUSB;
240
- if ([self shouldEnableNetworkDiscovery]) {
241
- interfaces |= FLIRCommunicationInterfaceNetwork;
242
- } else {
243
- RCTLogInfo(@"[FlirModule] Network discovery disabled (missing NSLocalNetworkUsageDescription or overridden)");
244
- }
245
- [self.discovery start:interfaces];
246
-
247
- [self emitStateChange:@"discovering"];
248
- RCTLogInfo(@"[FlirModule] Discovery started");
249
-
250
- __weak typeof(self) weakSelf = self;
251
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
252
- __strong typeof(self) strongSelf = weakSelf;
253
- if (!strongSelf) return;
254
- if (!strongSelf.isScanning || strongSelf.isConnected) return;
255
-
256
- BOOL hasRealDevice = NO;
257
- for (NSDictionary *dev in strongSelf.discoveredDevices) {
258
- NSString *did = dev[@"id"];
259
- if (did.length > 0 && ![did hasPrefix:@"emu:"]) {
260
- hasRealDevice = YES;
261
- break;
262
- }
263
- }
264
- if (!hasRealDevice) {
265
- [strongSelf emitStateChange:@"no_device_found"];
266
- }
267
- });
268
-
269
- resolve(@(YES));
270
- });
271
- #else
272
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
273
- #endif
172
+ });
274
173
  }
275
174
 
276
- RCT_EXPORT_METHOD(stopDiscovery:(RCTPromiseResolveBlock)resolve
277
- rejecter:(RCTPromiseRejectBlock)reject) {
278
- #if FLIR_SDK_AVAILABLE
279
- dispatch_async(dispatch_get_main_queue(), ^{
280
- [self.discovery stop];
281
- self.isScanning = NO;
282
- RCTLogInfo(@"[FlirModule] Discovery stopped");
283
- resolve(@(YES));
284
- });
285
- #else
286
- resolve(@(YES));
287
- #endif
288
- }
289
-
290
- RCT_EXPORT_METHOD(getDiscoveredDevices:(RCTPromiseResolveBlock)resolve
291
- rejecter:(RCTPromiseRejectBlock)reject) {
292
- dispatch_async(dispatch_get_main_queue(), ^{
293
- resolve(self.discoveredDevices);
294
- });
295
- }
296
-
297
- #pragma mark - Connection Methods
298
-
299
- RCT_EXPORT_METHOD(connectToDevice:(NSString *)deviceId
300
- resolver:(RCTPromiseResolveBlock)resolve
301
- rejecter:(RCTPromiseRejectBlock)reject) {
302
- #if FLIR_SDK_AVAILABLE
303
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
304
- // Synthetic emulator ids exposed during discovery.
305
- if ([deviceId hasPrefix:@"emu:"]) {
306
- NSString *typePart = [deviceId substringFromIndex:4];
307
- FLIRCameraType cameraType = FLIRCameraType_flirOne;
308
- if ([typePart.lowercaseString containsString:@"edge"]) {
309
- cameraType = FLIRCameraType_flirOneEdge;
310
- }
311
-
312
- FLIRIdentity *emulatorIdentity = [[FLIRIdentity alloc] initWithEmulatorType:cameraType];
313
- if (!emulatorIdentity) {
314
- dispatch_async(dispatch_get_main_queue(), ^{
315
- reject(@"ERR_EMULATOR_INIT", @"Failed to create emulator identity", nil);
316
- });
317
- return;
318
- }
319
-
320
- self.identityMap[[emulatorIdentity deviceId]] = emulatorIdentity;
321
-
322
- [self performConnectionWithIdentity:emulatorIdentity completion:^(BOOL success, NSError *error) {
323
- dispatch_async(dispatch_get_main_queue(), ^{
324
- if (success) {
325
- resolve(@(YES));
326
- } else {
327
- reject(@"ERR_CONNECTION_FAILED", error.localizedDescription ?: @"Connection failed", error);
328
- }
329
- });
330
- }];
331
- return;
332
- }
333
-
334
- FLIRIdentity *identity = self.identityMap[deviceId];
335
- if (!identity) {
336
- dispatch_async(dispatch_get_main_queue(), ^{
337
- reject(@"ERR_DEVICE_NOT_FOUND", [NSString stringWithFormat:@"Device not found: %@", deviceId], nil);
338
- });
339
- return;
340
- }
341
-
342
- [self performConnectionWithIdentity:identity completion:^(BOOL success, NSError *error) {
343
- dispatch_async(dispatch_get_main_queue(), ^{
344
- if (success) {
345
- resolve(@(YES));
346
- } else {
347
- reject(@"ERR_CONNECTION_FAILED", error.localizedDescription ?: @"Connection failed", error);
348
- }
349
- });
350
- }];
351
- });
352
- #else
353
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
354
- #endif
355
- }
356
-
357
- #if FLIR_SDK_AVAILABLE
358
- - (void)performConnectionWithIdentity:(FLIRIdentity *)identity completion:(void(^)(BOOL success, NSError *error))completion {
359
- if (!self.camera) {
360
- self.camera = [[FLIRCamera alloc] init];
361
- self.camera.delegate = self;
362
- }
363
-
364
- NSError *error = nil;
365
-
366
- // Handle authentication for generic cameras (network cameras)
367
- if ([identity cameraType] == FLIRCameraType_generic) {
368
- NSString *certName = [self getCertificateName];
369
- FLIRAuthenticationStatus status = pending;
370
- while (status == pending) {
371
- status = [self.camera authenticate:identity trustedConnectionName:certName];
372
- if (status == pending) {
373
- RCTLogInfo(@"[FlirModule] Waiting for camera authentication...");
374
- [NSThread sleepForTimeInterval:1.0];
375
- }
376
- }
377
- RCTLogInfo(@"[FlirModule] Authentication status: %d", (int)status);
378
- }
379
-
380
- // Step 1: Pair with camera (required for FLIR One devices)
381
- @try {
382
- if (![self.camera pair:identity code:0 error:&error]) {
383
- RCTLogError(@"[FlirModule] Pair failed: %@", error.localizedDescription);
384
- if (completion) completion(NO, error);
385
- return;
386
- }
387
- RCTLogInfo(@"[FlirModule] Paired with: %@", [identity deviceId]);
388
- } @catch (NSException *exception) {
389
- RCTLogError(@"[FlirModule] Pair exception: %@", exception.reason);
390
- NSError *pairError = [NSError errorWithDomain:@"FlirModule" code:1001 userInfo:@{NSLocalizedDescriptionKey: exception.reason ?: @"Pair failed"}];
391
- if (completion) completion(NO, pairError);
392
- return;
393
- }
394
-
395
- // Step 2: Connect to camera
396
- BOOL connected = NO;
397
- @try {
398
- if (![self.camera connect:&error]) {
399
- RCTLogError(@"[FlirModule] Connect failed: %@", error.localizedDescription);
400
- if (completion) completion(NO, error);
401
- return;
402
- }
403
- connected = YES;
404
- RCTLogInfo(@"[FlirModule] Connected to: %@", [identity deviceId]);
405
- } @catch (NSException *exception) {
406
- RCTLogError(@"[FlirModule] Connect exception: %@", exception.reason);
407
- error = [NSError errorWithDomain:@"FlirModule" code:1002 userInfo:@{NSLocalizedDescriptionKey: exception.reason ?: @"Connect failed"}];
408
- connected = NO;
175
+ RCT_EXPORT_METHOD(stopDiscovery : (RCTPromiseResolveBlock)
176
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
177
+ dispatch_async(dispatch_get_main_queue(), ^{
178
+ id manager = flir_manager_shared();
179
+ if (manager &&
180
+ [manager respondsToSelector:sel_registerName("stopDiscovery")]) {
181
+ ((void (*)(id, SEL))objc_msgSend)(manager,
182
+ sel_registerName("stopDiscovery"));
409
183
  }
410
-
411
- if (connected) {
412
- self.connectedIdentity = identity;
413
- self.connectedDeviceId = [identity deviceId];
414
- NSString *displayName = [identity deviceId];
415
- if ([identity communicationInterface] == FLIRCommunicationInterfaceEmulator) {
416
- if ([identity cameraType] == FLIRCameraType_flirOneEdge || [identity cameraType] == FLIRCameraType_flirOneEdgePro) {
417
- displayName = @"FLIR One Edge Emulator";
418
- } else {
419
- displayName = @"FLIR One Emulator";
420
- }
421
- }
422
- self.connectedDeviceName = displayName;
423
- self.isConnected = YES;
424
-
425
- RCTLogInfo(@"[FlirModule] Successfully connected to: %@", displayName);
426
-
427
- // Get available streams and prefer thermal stream
428
- NSArray<FLIRStream *> *streams = [self.camera getStreams];
429
- if (streams.count > 0) {
430
- RCTLogInfo(@"[FlirModule] Found %lu stream(s)", (unsigned long)streams.count);
431
-
432
- // Find thermal stream (preferred) or use first stream
433
- FLIRStream *streamToStart = nil;
434
- for (FLIRStream *stream in streams) {
435
- if (stream.isThermal) {
436
- streamToStart = stream;
437
- break;
438
- }
439
- }
440
- if (!streamToStart) {
441
- streamToStart = streams[0];
442
- }
443
- [self startStreamInternal:streamToStart];
184
+ resolve(@(YES));
185
+ });
186
+ }
187
+
188
+ RCT_EXPORT_METHOD(getDiscoveredDevices : (RCTPromiseResolveBlock)
189
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
190
+ dispatch_async(dispatch_get_main_queue(), ^{
191
+ id manager = flir_manager_shared();
192
+ NSMutableArray *arr = [NSMutableArray new];
193
+ if (manager &&
194
+ [manager respondsToSelector:sel_registerName("getDiscoveredDevices")]) {
195
+ NSArray *devs = ((NSArray * (*)(id, SEL)) objc_msgSend)(
196
+ manager, sel_registerName("getDiscoveredDevices"));
197
+ for (id d in devs) {
198
+ if ([d respondsToSelector:sel_registerName("toDictionary")]) {
199
+ [arr addObject:((NSDictionary * (*)(id, SEL)) objc_msgSend)(
200
+ d, sel_registerName("toDictionary"))];
444
201
  }
445
-
446
- dispatch_async(dispatch_get_main_queue(), ^{
447
- [self emitDeviceConnected];
448
- });
449
-
450
- if (completion) completion(YES, nil);
451
- } else {
452
- RCTLogError(@"[FlirModule] Connection failed: %@", error.localizedDescription);
453
- self.camera = nil;
454
- if (completion) completion(NO, error);
202
+ }
455
203
  }
204
+ resolve(arr);
205
+ });
456
206
  }
457
207
 
208
+ RCT_EXPORT_METHOD(connectToDevice : (NSString *)deviceId resolver : (
209
+ RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
210
+ dispatch_async(dispatch_get_main_queue(), ^{
211
+ self.connectResolve = resolve;
212
+ self.connectReject = reject;
458
213
 
459
- - (NSString *)getCertificateName {
460
- NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.flir.app";
461
- NSString *key = [NSString stringWithFormat:@"%@-cert-name", bundleID];
462
-
463
- NSString *existing = [[NSUserDefaults standardUserDefaults] stringForKey:key];
464
- if (existing) {
465
- return existing;
214
+ id manager = flir_manager_shared();
215
+ if (manager &&
216
+ [manager respondsToSelector:sel_registerName("connectToDevice:")]) {
217
+ ((void (*)(id, SEL, id))objc_msgSend)(
218
+ manager, sel_registerName("connectToDevice:"), deviceId);
219
+ } else {
220
+ reject(@"ERR_NO_MANAGER", @"FlirManager not found", nil);
221
+ self.connectResolve = nil;
222
+ self.connectReject = nil;
466
223
  }
467
-
468
- NSString *newName = [[NSUUID UUID] UUIDString];
469
- [[NSUserDefaults standardUserDefaults] setObject:newName forKey:key];
470
- return newName;
224
+ });
471
225
  }
472
- #endif
473
226
 
474
- RCT_EXPORT_METHOD(disconnect:(RCTPromiseResolveBlock)resolve
475
- rejecter:(RCTPromiseRejectBlock)reject) {
476
- #if FLIR_SDK_AVAILABLE
477
- dispatch_async(dispatch_get_main_queue(), ^{
478
- [self stopStreamInternal];
479
- [self.camera disconnect];
480
- self.camera = nil;
481
- self.connectedIdentity = nil;
482
- self.connectedDeviceId = nil;
483
- self.connectedDeviceName = nil;
484
- self.isConnected = NO;
485
- self.isStreaming = NO;
486
-
487
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
488
- [self emitStateChange:@"disconnected"];
489
-
490
- RCTLogInfo(@"[FlirModule] Disconnected");
491
- resolve(@(YES));
492
- });
493
- #else
227
+ RCT_EXPORT_METHOD(disconnect : (RCTPromiseResolveBlock)
228
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
229
+ dispatch_async(dispatch_get_main_queue(), ^{
230
+ id manager = flir_manager_shared();
231
+ if (manager &&
232
+ [manager respondsToSelector:sel_registerName("disconnect")]) {
233
+ ((void (*)(id, SEL))objc_msgSend)(manager,
234
+ sel_registerName("disconnect"));
235
+ }
494
236
  resolve(@(YES));
495
- #endif
237
+ });
496
238
  }
497
239
 
498
- RCT_EXPORT_METHOD(stopFlir:(RCTPromiseResolveBlock)resolve
499
- rejecter:(RCTPromiseRejectBlock)reject) {
500
- #if FLIR_SDK_AVAILABLE
501
- dispatch_async(dispatch_get_main_queue(), ^{
502
- [self stopStreamInternal];
503
- [self.camera disconnect];
504
- [self.discovery stop];
505
-
506
- self.camera = nil;
507
- self.connectedIdentity = nil;
508
- self.connectedDeviceId = nil;
509
- self.connectedDeviceName = nil;
510
- self.isConnected = NO;
511
- self.isStreaming = NO;
512
- self.isScanning = NO;
513
-
514
- [self emitStateChange:@"stopped"];
515
- RCTLogInfo(@"[FlirModule] Stopped");
516
- resolve(@(YES));
517
- });
518
- #else
240
+ RCT_EXPORT_METHOD(stopFlir : (RCTPromiseResolveBlock)
241
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
242
+ dispatch_async(dispatch_get_main_queue(), ^{
243
+ id manager = flir_manager_shared();
244
+ if (manager && [manager respondsToSelector:sel_registerName("stop")]) {
245
+ ((void (*)(id, SEL))objc_msgSend)(manager, sel_registerName("stop"));
246
+ }
519
247
  resolve(@(YES));
520
- #endif
248
+ });
249
+ }
250
+
251
+ RCT_EXPORT_METHOD(startEmulator : (NSString *)emulatorType resolver : (
252
+ RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
253
+ dispatch_async(dispatch_get_main_queue(), ^{
254
+ self.connectResolve = resolve;
255
+ self.connectReject = reject;
256
+ id manager = flir_manager_shared();
257
+ if (manager && [manager respondsToSelector:sel_registerName(
258
+ "startEmulatorWithType:")]) {
259
+ // Swift: startEmulator(type: String) -> exposed as startEmulatorWithType:
260
+ // ? Or startEmulatorWith? Swift default naming: startEmulator(type:) ->
261
+ // startEmulatorWithType:
262
+ ((void (*)(id, SEL, id))objc_msgSend)(
263
+ manager, sel_registerName("startEmulatorWithType:"), emulatorType);
264
+ } else {
265
+ // Fallback if selector assumption wrong/mismatch
266
+ reject(@"ERR_NOT_IMPL",
267
+ @"startEmulator not implemented or signature mismatch", nil);
268
+ self.connectResolve = nil;
269
+ self.connectReject = nil;
270
+ }
271
+ });
521
272
  }
522
273
 
523
- #pragma mark - Streaming
524
-
525
- #if FLIR_SDK_AVAILABLE
526
- - (void)startStreamInternal:(FLIRStream *)newStream {
527
- [self stopStreamInternal];
528
-
529
- self.stream = newStream;
530
-
531
- if (newStream.isThermal) {
532
- self.streamer = [[FLIRThermalStreamer alloc] initWithStream:newStream];
274
+ RCT_EXPORT_METHOD(getTemperatureAt : (nonnull NSNumber *)x y : (
275
+ nonnull NSNumber *)y resolver : (RCTPromiseResolveBlock)
276
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
277
+ dispatch_async(dispatch_get_main_queue(), ^{
278
+ double temp = flir_getTemperatureAtPoint([x intValue], [y intValue]);
279
+ if (isnan(temp)) {
280
+ resolve([NSNull null]);
281
+ } else {
282
+ resolve(@(temp));
533
283
  }
534
-
535
- newStream.delegate = self;
536
-
537
- NSError *error = nil;
538
- // Use try-catch pattern from samples
539
- @try {
540
- if (![newStream start:&error]) {
541
- RCTLogError(@"[FlirModule] Stream start failed: %@", error.localizedDescription);
542
- self.stream = nil;
543
- self.streamer = nil;
544
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{@"error": error.localizedDescription ?: @"Stream start failed"}];
545
- return;
546
- }
547
- } @catch (NSException *exception) {
548
- RCTLogError(@"[FlirModule] Stream start exception: %@\", exception.reason);
549
- self.stream = nil;
550
- self.streamer = nil;
551
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{@"error": exception.reason ?: @"Stream start exception"}];
552
- return;
284
+ });
285
+ }
286
+
287
+ RCT_EXPORT_METHOD(getTemperatureFromColor : (NSInteger)color resolver : (
288
+ RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
289
+ int r = (color >> 16) & 0xFF;
290
+ int g = (color >> 8) & 0xFF;
291
+ int b = color & 0xFF;
292
+ double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
293
+ double temp = (lum / 255.0) * 400.0;
294
+ resolve(@(temp));
295
+ }
296
+
297
+ RCT_EXPORT_METHOD(isEmulator : (RCTPromiseResolveBlock)
298
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
299
+ dispatch_async(dispatch_get_main_queue(), ^{
300
+ id manager = flir_manager_shared();
301
+ BOOL isEm = NO;
302
+ if (manager &&
303
+ [manager respondsToSelector:sel_registerName("isEmulator")]) {
304
+ isEm = ((BOOL (*)(id, SEL))objc_msgSend)(manager,
305
+ sel_registerName("isEmulator"));
553
306
  }
554
-
555
- self.isStreaming = YES;
556
- [self emitStateChange:@"streaming"];
557
- RCTLogInfo(@"[FlirModule] Stream started (thermal: %@)\", newStream.isThermal ? @\"YES\" : @\"NO\");
558
- }
559
-
560
- - (void)stopStreamInternal {
561
- [self.stream stop];
562
- self.stream = nil;
563
- self.streamer = nil;
564
- self.isStreaming = NO;
307
+ resolve(@(isEm));
308
+ });
309
+ }
310
+
311
+ RCT_EXPORT_METHOD(isDeviceConnected : (RCTPromiseResolveBlock)
312
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
313
+ dispatch_async(dispatch_get_main_queue(), ^{
314
+ id manager = flir_manager_shared();
315
+ BOOL isC = NO;
316
+ if (manager &&
317
+ [manager respondsToSelector:sel_registerName("isConnected")]) {
318
+ isC = ((BOOL (*)(id, SEL))objc_msgSend)(manager,
319
+ sel_registerName("isConnected"));
320
+ }
321
+ resolve(@(isC));
322
+ });
323
+ }
324
+
325
+ RCT_EXPORT_METHOD(getConnectedDeviceInfo : (RCTPromiseResolveBlock)
326
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
327
+ dispatch_async(dispatch_get_main_queue(), ^{
328
+ id manager = flir_manager_shared();
329
+ NSString *info = @"Not connected";
330
+ if (manager && [manager respondsToSelector:sel_registerName(
331
+ "getConnectedDeviceInfo")]) {
332
+ info = ((NSString * (*)(id, SEL)) objc_msgSend)(
333
+ manager, sel_registerName("getConnectedDeviceInfo"));
334
+ }
335
+ resolve(info);
336
+ });
565
337
  }
566
- #endif
567
338
 
568
- #pragma mark - Temperature Methods
569
-
570
- RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
571
- y:(nonnull NSNumber *)y
572
- resolver:(RCTPromiseResolveBlock)resolve
573
- rejecter:(RCTPromiseRejectBlock)reject) {
574
- dispatch_async(dispatch_get_main_queue(), ^{
575
- // Call into native FLIRManager to query temperature at point (runtime lookup)
576
- double temp = flir_getTemperatureAtPoint([x intValue], [y intValue]);
577
- if (isnan(temp)) {
578
- resolve([NSNull null]);
579
- } else {
580
- resolve(@(temp));
581
- }
582
- });
339
+ RCT_EXPORT_METHOD(isSDKDownloaded : (RCTPromiseResolveBlock)
340
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
341
+ // Assuming integrated SDK
342
+ resolve(@(YES));
583
343
  }
584
344
 
585
- RCT_EXPORT_METHOD(getTemperatureFromColor:(NSInteger)color
586
- resolver:(RCTPromiseResolveBlock)resolve
587
- rejecter:(RCTPromiseRejectBlock)reject) {
588
- // Placeholder: Convert ARGB color to pseudo-temperature
589
- int r = (color >> 16) & 0xFF;
590
- int g = (color >> 8) & 0xFF;
591
- int b = color & 0xFF;
592
- double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
593
- double temp = (lum / 255.0) * 400.0;
594
- resolve(@(temp));
345
+ RCT_EXPORT_METHOD(getSDKStatus : (RCTPromiseResolveBlock)
346
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
347
+ resolve(@{@"available" : @(YES), @"arch" : @"arm64", @"platform" : @"iOS"});
595
348
  }
596
349
 
597
- #pragma mark - Status Methods
598
-
599
- RCT_EXPORT_METHOD(isEmulator:(RCTPromiseResolveBlock)resolve
600
- rejecter:(RCTPromiseRejectBlock)reject) {
601
- dispatch_async(dispatch_get_main_queue(), ^{
602
- BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"] ||
603
- [self.connectedDeviceName.lowercaseString containsString:@"emulat"];
604
- resolve(@(isEmu));
605
- });
350
+ RCT_EXPORT_METHOD(getBatteryLevel : (RCTPromiseResolveBlock)
351
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
352
+ dispatch_async(dispatch_get_main_queue(), ^{
353
+ int level = flir_getBatteryLevel();
354
+ resolve(@(level));
355
+ });
606
356
  }
607
357
 
608
- RCT_EXPORT_METHOD(isDeviceConnected:(RCTPromiseResolveBlock)resolve
609
- rejecter:(RCTPromiseRejectBlock)reject) {
610
- dispatch_async(dispatch_get_main_queue(), ^{
611
- resolve(@(self.isConnected));
612
- });
358
+ RCT_EXPORT_METHOD(isBatteryCharging : (RCTPromiseResolveBlock)
359
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
360
+ dispatch_async(dispatch_get_main_queue(), ^{
361
+ BOOL ch = flir_isBatteryCharging();
362
+ resolve(@(ch));
363
+ });
613
364
  }
614
365
 
615
- RCT_EXPORT_METHOD(getConnectedDeviceInfo:(RCTPromiseResolveBlock)resolve
616
- rejecter:(RCTPromiseRejectBlock)reject) {
617
- dispatch_async(dispatch_get_main_queue(), ^{
618
- resolve(self.connectedDeviceName ?: @"Not connected");
619
- });
620
- }
621
-
622
- RCT_EXPORT_METHOD(isSDKDownloaded:(RCTPromiseResolveBlock)resolve
623
- rejecter:(RCTPromiseRejectBlock)reject) {
624
- #if FLIR_SDK_AVAILABLE
366
+ RCT_EXPORT_METHOD(setPreferSdkRotation : (BOOL)prefer resolver : (
367
+ RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) {
368
+ dispatch_async(dispatch_get_main_queue(), ^{
369
+ flir_setPreferSdkRotation(prefer);
625
370
  resolve(@(YES));
626
- #else
627
- resolve(@(NO));
628
- #endif
371
+ });
629
372
  }
630
373
 
631
- RCT_EXPORT_METHOD(getSDKStatus:(RCTPromiseResolveBlock)resolve
632
- rejecter:(RCTPromiseRejectBlock)reject) {
633
- NSDictionary *status = @{
634
- #if FLIR_SDK_AVAILABLE
635
- @"available": @(YES),
636
- #else
637
- @"available": @(NO),
638
- #endif
639
- @"arch": @"arm64",
640
- @"platform": @"iOS"
641
- };
642
- resolve(status);
643
- }
644
-
645
- #pragma mark - Emulator
646
-
647
- RCT_EXPORT_METHOD(startEmulator:(NSString *)emulatorType
648
- resolver:(RCTPromiseResolveBlock)resolve
649
- rejecter:(RCTPromiseRejectBlock)reject) {
650
- #if FLIR_SDK_AVAILABLE
651
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
652
- FLIRCameraType cameraType = FLIRCameraType_flirOne;
653
- if ([emulatorType.lowercaseString containsString:@"edge"]) {
654
- cameraType = FLIRCameraType_flirOneEdge;
655
- } else if ([emulatorType.lowercaseString containsString:@"pro"]) {
656
- cameraType = FLIRCameraType_flirOneEdgePro;
657
- }
658
-
659
- FLIRIdentity *emulatorIdentity = [[FLIRIdentity alloc] initWithEmulatorType:cameraType];
660
- if (emulatorIdentity) {
661
- self.identityMap[[emulatorIdentity deviceId]] = emulatorIdentity;
662
-
663
- [self performConnectionWithIdentity:emulatorIdentity completion:^(BOOL success, NSError *error) {
664
- dispatch_async(dispatch_get_main_queue(), ^{
665
- if (success) {
666
- resolve(@(YES));
667
- } else {
668
- reject(@"ERR_EMULATOR_FAILED", error.localizedDescription ?: @"Emulator start failed", error);
669
- }
670
- });
671
- }];
672
- } else {
673
- dispatch_async(dispatch_get_main_queue(), ^{
674
- reject(@"ERR_EMULATOR_INIT", @"Failed to create emulator identity", nil);
675
- });
676
- }
677
- });
678
- #else
679
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
680
- #endif
374
+ RCT_EXPORT_METHOD(isPreferSdkRotation : (RCTPromiseResolveBlock)
375
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
376
+ dispatch_async(dispatch_get_main_queue(), ^{
377
+ BOOL v = flir_isPreferSdkRotation();
378
+ resolve(@(v));
379
+ });
681
380
  }
682
381
 
683
- #pragma mark - Debug
382
+ #pragma mark - FlirManagerDelegate
684
383
 
685
- RCT_EXPORT_METHOD(initializeSDK:(RCTPromiseResolveBlock)resolve
686
- rejecter:(RCTPromiseRejectBlock)reject) {
687
- NSDictionary *result = @{
688
- #if FLIR_SDK_AVAILABLE
689
- @"initialized": @(YES),
690
- @"message": @"SDK initialized successfully"
691
- #else
692
- @"initialized": @(NO),
693
- @"message": @"SDK not available - built without FLIR"
694
- #endif
695
- };
696
- resolve(result);
697
- }
698
-
699
- RCT_EXPORT_METHOD(getDebugInfo:(RCTPromiseResolveBlock)resolve
700
- rejecter:(RCTPromiseRejectBlock)reject) {
701
- dispatch_async(dispatch_get_main_queue(), ^{
702
- NSDictionary *info = @{
703
- #if FLIR_SDK_AVAILABLE
704
- @"sdkAvailable": @(YES),
705
- #else
706
- @"sdkAvailable": @(NO),
707
- #endif
708
- @"arch": @"arm64",
709
- @"discoveredDeviceCount": @(self.discoveredDevices.count),
710
- @"isConnected": @(self.isConnected),
711
- @"isStreaming": @(self.isStreaming),
712
- @"connectedDevice": self.connectedDeviceName ?: @"None"
713
- };
714
- resolve(info);
715
- });
716
- }
717
-
718
- RCT_EXPORT_METHOD(getLatestFramePath:(RCTPromiseResolveBlock)resolve
719
- rejecter:(RCTPromiseRejectBlock)reject) {
720
- dispatch_async(dispatch_get_main_queue(), ^{
721
- UIImage *image = [FlirState shared].latestImage;
722
- if (!image) {
723
- resolve([NSNull null]);
724
- return;
725
- }
726
-
727
- NSData *jpegData = UIImageJPEGRepresentation(image, 0.9);
728
- if (!jpegData) {
729
- resolve([NSNull null]);
730
- return;
731
- }
732
-
733
- NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:
734
- [NSString stringWithFormat:@"flir_frame_%lld.jpg", (long long)[[NSDate date] timeIntervalSince1970] * 1000]];
735
- [jpegData writeToFile:tempPath atomically:YES];
736
- resolve(tempPath);
737
- });
738
- }
739
-
740
- RCT_EXPORT_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
741
- rejecter:(RCTPromiseRejectBlock)reject) {
742
- dispatch_async(dispatch_get_main_queue(), ^{
743
- #if FLIR_SDK_AVAILABLE
744
- int level = flir_getBatteryLevel();
745
- resolve(@(level));
746
- #else
747
- resolve(@(-1));
748
- #endif
749
- });
750
- }
751
-
752
- RCT_EXPORT_METHOD(isBatteryCharging:(RCTPromiseResolveBlock)resolve
753
- rejecter:(RCTPromiseRejectBlock)reject) {
754
- dispatch_async(dispatch_get_main_queue(), ^{
755
- #if FLIR_SDK_AVAILABLE
756
- BOOL ch = flir_isBatteryCharging();
757
- resolve(@(ch));
758
- #else
759
- resolve(@(NO));
760
- #endif
761
- });
762
- }
763
-
764
- RCT_EXPORT_METHOD(setPreferSdkRotation:(BOOL)prefer
765
- resolver:(RCTPromiseResolveBlock)resolve
766
- rejecter:(RCTPromiseRejectBlock)reject) {
767
- dispatch_async(dispatch_get_main_queue(), ^{
768
- @try {
769
- flir_setPreferSdkRotation(prefer);
770
- resolve(@(YES));
771
- } @catch (NSException *ex) {
772
- reject(@"ERR_FLIR_SET_ROTATION_PREF", ex.reason, nil);
773
- }
774
- });
775
- }
776
-
777
- RCT_EXPORT_METHOD(isPreferSdkRotation:(RCTPromiseResolveBlock)resolve
778
- rejecter:(RCTPromiseRejectBlock)reject) {
779
- dispatch_async(dispatch_get_main_queue(), ^{
780
- BOOL v = flir_isPreferSdkRotation();
781
- resolve(@(v));
782
- });
783
- }
784
-
785
- #pragma mark - Helper Methods
786
-
787
- - (void)emitDeviceConnected {
788
- [self emitStateChange:@"connected"];
789
- }
790
-
791
- - (void)emitStateChange:(NSString *)state {
792
- BOOL isEmu = NO;
793
- #if FLIR_SDK_AVAILABLE
794
- if (self.connectedIdentity) {
795
- isEmu = ([self.connectedIdentity communicationInterface] == FLIRCommunicationInterfaceEmulator);
796
- } else {
797
- isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
384
+ - (void)onDevicesFound:(NSArray *)devices {
385
+ NSMutableArray *arr = [NSMutableArray new];
386
+ for (id d in devices) {
387
+ if ([d respondsToSelector:sel_registerName("toDictionary")]) {
388
+ [arr addObject:((NSDictionary * (*)(id, SEL))
389
+ objc_msgSend)(d, sel_registerName("toDictionary"))];
798
390
  }
799
- #else
800
- isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
801
- #endif
802
-
803
- NSDictionary *body = @{
804
- @"state": state,
805
- @"isConnected": @(self.isConnected),
806
- @"isStreaming": @(self.isStreaming),
807
- @"isEmulator": @(isEmu),
808
- @"deviceName": self.connectedDeviceName ?: @"",
809
- @"deviceId": self.connectedDeviceId ?: @"",
810
- @"identity": @{
811
- @"deviceId": self.connectedDeviceId ?: @"",
812
- @"isEmulator": @(isEmu)
813
- }
814
- };
815
-
816
- // App JS listens for FlirDeviceConnected state transitions.
817
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceConnected" body:body];
818
-
819
- // Keep legacy event for backwards compatibility.
820
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirStateChanged" body:body];
821
- }
822
-
823
- #if FLIR_SDK_AVAILABLE
824
- - (NSString *)communicationInterfaceName:(FLIRCommunicationInterface)iface {
825
- if (iface & FLIRCommunicationInterfaceLightning) return @"LIGHTNING";
826
- if (iface & FLIRCommunicationInterfaceNetwork) return @"NETWORK";
827
- if (iface & FLIRCommunicationInterfaceFlirOneWireless) return @"WIRELESS";
828
- if (iface & FLIRCommunicationInterfaceEmulator) return @"EMULATOR";
829
- if (iface & FLIRCommunicationInterfaceUSB) return @"USB";
830
- return @"UNKNOWN";
831
- }
832
- #endif
833
-
834
- #pragma mark - FLIRDiscoveryEventDelegate
835
-
836
- #if FLIR_SDK_AVAILABLE
837
- - (void)cameraDiscovered:(FLIRDiscoveredCamera *)discoveredCamera {
838
- FLIRIdentity *identity = discoveredCamera.identity;
839
- NSString *deviceId = [identity deviceId];
840
-
841
- RCTLogInfo(@"[FlirModule] Camera discovered: %@", deviceId);
842
-
843
- // Store identity
844
- self.identityMap[deviceId] = identity;
845
-
846
- // Create device info
847
- NSDictionary *deviceInfo = @{
848
- @"id": deviceId,
849
- @"name": discoveredCamera.displayName ?: deviceId,
850
- @"communicationType": [self communicationInterfaceName:[identity communicationInterface]],
851
- @"isEmulator": @([identity communicationInterface] == FLIRCommunicationInterfaceEmulator)
852
- };
853
-
854
- // Add if not already present
855
- BOOL found = NO;
856
- for (NSDictionary *existing in self.discoveredDevices) {
857
- if ([existing[@"id"] isEqualToString:deviceId]) {
858
- found = YES;
859
- break;
860
- }
861
- }
862
-
863
- if (!found) {
864
- [self.discoveredDevices addObject:deviceInfo];
865
- }
866
-
867
- // Emit devices found event
868
- dispatch_async(dispatch_get_main_queue(), ^{
869
- NSDictionary *body = @{
870
- @"devices": self.discoveredDevices,
871
- @"count": @(self.discoveredDevices.count)
872
- };
873
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
874
- });
875
- }
876
-
877
- - (void)cameraLost:(FLIRIdentity *)cameraIdentity {
878
- NSString *deviceId = [cameraIdentity deviceId];
879
- RCTLogInfo(@"[FlirModule] Camera lost: %@", deviceId);
880
-
881
- [self.identityMap removeObjectForKey:deviceId];
882
-
883
- NSMutableArray *toRemove = [NSMutableArray new];
884
- for (NSDictionary *device in self.discoveredDevices) {
885
- if ([device[@"id"] isEqualToString:deviceId]) {
886
- [toRemove addObject:device];
887
- }
888
- }
889
- [self.discoveredDevices removeObjectsInArray:toRemove];
890
-
891
- // If this was our connected device, handle disconnect
892
- if ([self.connectedDeviceId isEqualToString:deviceId]) {
893
- dispatch_async(dispatch_get_main_queue(), ^{
894
- [self stopStreamInternal];
895
- self.camera = nil;
896
- self.connectedIdentity = nil;
897
- self.connectedDeviceId = nil;
898
- self.connectedDeviceName = nil;
899
- self.isConnected = NO;
900
- self.isStreaming = NO;
901
-
902
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
903
- [self emitStateChange:@"disconnected"];
904
- });
905
- }
906
-
907
- dispatch_async(dispatch_get_main_queue(), ^{
908
- NSDictionary *body = @{
909
- @"devices": self.discoveredDevices,
910
- @"count": @(self.discoveredDevices.count)
911
- };
912
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
913
- });
914
- }
915
-
916
- - (void)discoveryError:(NSString *)error netServiceError:(int)nsnetserviceserror on:(FLIRCommunicationInterface)iface {
917
- // Network discovery failures are expected when Local Network permission is missing/denied.
918
- // Do not surface those as fatal errors; keep USB/BLE discovery running.
919
- if ((iface & FLIRCommunicationInterfaceNetwork) == FLIRCommunicationInterfaceNetwork) {
920
- RCTLogInfo(@"[FlirModule] Network discovery error (suppressed): %@ (%d)", error, nsnetserviceserror);
921
- return;
922
- }
923
-
924
- RCTLogError(@"[FlirModule] Discovery error: %@ (%d)", error, nsnetserviceserror);
925
-
926
- dispatch_async(dispatch_get_main_queue(), ^{
927
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
928
- @"error": error ?: @"Unknown discovery error",
929
- @"type": @"discovery"
930
- }];
931
- });
932
- }
933
-
934
- - (void)discoveryFinished:(FLIRCommunicationInterface)iface {
935
- RCTLogInfo(@"[FlirModule] Discovery finished");
936
- self.isScanning = NO;
937
- }
938
- #endif
939
-
940
- #pragma mark - FLIRDataReceivedDelegate
941
-
942
- #if FLIR_SDK_AVAILABLE
943
- - (void)onDisconnected:(FLIRCamera *)camera withError:(NSError *)error {
944
- RCTLogInfo(@"[FlirModule] Camera disconnected: %@", error.localizedDescription);
945
-
946
- dispatch_async(dispatch_get_main_queue(), ^{
947
- self.isConnected = NO;
948
- self.isStreaming = NO;
949
- self.connectedDeviceId = nil;
950
- self.connectedDeviceName = nil;
951
-
952
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
953
- [self emitStateChange:@"disconnected"];
954
- });
391
+ }
392
+ [[FlirEventEmitter shared]
393
+ sendDeviceEvent:@"FlirDevicesFound"
394
+ body:@{@"devices" : arr, @"count" : @(arr.count)}];
395
+ }
396
+
397
+ - (void)onDeviceConnected:(id)device {
398
+ if (self.connectResolve) {
399
+ self.connectResolve(@(YES));
400
+ self.connectResolve = nil;
401
+ self.connectReject = nil;
402
+ }
403
+
404
+ // device is FlirDeviceInfo
405
+ NSMutableDictionary *body = [NSMutableDictionary new];
406
+ if ([device respondsToSelector:sel_registerName("toDictionary")]) {
407
+ [body
408
+ addEntriesFromDictionary:((NSDictionary * (*)(id, SEL)) objc_msgSend)(
409
+ device, sel_registerName("toDictionary"))];
410
+ }
411
+
412
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceConnected" body:body];
413
+ }
414
+
415
+ - (void)onDeviceDisconnected {
416
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected"
417
+ body:@{}];
418
+ }
419
+
420
+ - (void)onFrameReceived:(UIImage *)image
421
+ width:(NSInteger)width
422
+ height:(NSInteger)height {
423
+ // Also emit event for JS consumers (though slow, some might use it)
424
+ [[FlirEventEmitter shared]
425
+ sendDeviceEvent:@"FlirFrameReceived"
426
+ body:@{
427
+ @"width" : @(width),
428
+ @"height" : @(height),
429
+ @"timestamp" :
430
+ @([[NSDate date] timeIntervalSince1970] * 1000)
431
+ }];
432
+ }
433
+
434
+ - (void)onError:(NSString *)message {
435
+ if (self.connectReject) {
436
+ self.connectReject(@"ERR_FLIR", message, nil);
437
+ self.connectResolve = nil;
438
+ self.connectReject = nil;
439
+ }
440
+ [[FlirEventEmitter shared]
441
+ sendDeviceEvent:@"FlirError"
442
+ body:@{@"error" : message ?: @"Unknown error"}];
443
+ }
444
+
445
+ - (void)onStateChanged:(NSString *)state
446
+ isConnected:(BOOL)isConnected
447
+ isStreaming:(BOOL)isStreaming
448
+ isEmulator:(BOOL)isEmulator {
449
+ NSDictionary *body = @{
450
+ @"state" : state,
451
+ @"isConnected" : @(isConnected),
452
+ @"isStreaming" : @(isStreaming),
453
+ @"isEmulator" : @(isEmulator)
454
+ };
455
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirStateChanged" body:body];
955
456
  }
956
- #endif
957
-
958
- #pragma mark - FLIRStreamDelegate
959
-
960
- #if FLIR_SDK_AVAILABLE
961
- - (void)onError:(NSError *)error {
962
- RCTLogError(@"[FlirModule] Stream error: %@", error.localizedDescription);
963
-
964
- dispatch_async(dispatch_get_main_queue(), ^{
965
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
966
- @"error": error.localizedDescription ?: @"Stream error",
967
- @"type": @"stream"
968
- }];
969
- });
970
- }
971
-
972
- - (void)onImageReceived {
973
- if (!self.streamer) return;
974
-
975
- NSError *error = nil;
976
- if ([self.streamer update:&error]) {
977
- UIImage *image = [self.streamer getImage];
978
- if (image) {
979
- // Update shared state
980
- [[FlirState shared] updateFrame:image];
981
-
982
- // Get temperature from thermal image if available
983
- [self.streamer withThermalImage:^(FLIRThermalImage *thermalImage) {
984
- // Some SDK versions call getImageStatistics(), try both selectors
985
- FLIRImageStatistics *stats = nil;
986
- if ([thermalImage respondsToSelector:sel_registerName("getImageStatistics")]) {
987
- stats = ((id (*)(id, SEL))objc_msgSend)((id)thermalImage, sel_registerName("getImageStatistics"));
988
- } else if ([thermalImage respondsToSelector:sel_registerName("getStatistics")]) {
989
- stats = ((id (*)(id, SEL))objc_msgSend)((id)thermalImage, sel_registerName("getStatistics"));
990
- }
991
- if (stats) {
992
- self.lastTemperature = [[stats getMax] value];
993
- [FlirState shared].lastTemperature = self.lastTemperature;
994
- }
995
- }];
996
-
997
- // Emit frame received event (rate-limited by RN event queue)
998
- dispatch_async(dispatch_get_main_queue(), ^{
999
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirFrameReceived" body:@{
1000
- @"width": @(image.size.width),
1001
- @"height": @(image.size.height),
1002
- @"timestamp": @([[NSDate date] timeIntervalSince1970] * 1000)
1003
- }];
1004
- });
1005
- }
1006
- } else {
1007
- RCTLogError(@"[FlirModule] Streamer update error: %@", error.localizedDescription);
1008
- }
1009
- }
1010
- #endif
1011
457
 
1012
458
  @end