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