ilabs-flir 2.0.4 → 2.0.5

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.
Files changed (35) hide show
  1. package/Flir.podspec +139 -139
  2. package/README.md +1066 -1066
  3. package/android/Flir/build.gradle.kts +72 -72
  4. package/android/Flir/src/main/AndroidManifest.xml +45 -45
  5. package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
  6. package/android/Flir/src/main/java/flir/android/FlirFrameCache.kt +6 -6
  7. package/android/Flir/src/main/java/flir/android/FlirManager.kt +476 -476
  8. package/android/Flir/src/main/java/flir/android/FlirModule.kt +257 -257
  9. package/android/Flir/src/main/java/flir/android/FlirPackage.kt +18 -18
  10. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +74 -74
  11. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +583 -583
  12. package/android/Flir/src/main/java/flir/android/FlirStatus.kt +12 -12
  13. package/android/Flir/src/main/java/flir/android/FlirView.kt +48 -48
  14. package/android/Flir/src/main/java/flir/android/FlirViewManager.kt +13 -13
  15. package/app.plugin.js +381 -381
  16. package/expo-module.config.json +5 -5
  17. package/ios/Flir/src/Flir-Bridging-Header.h +34 -34
  18. package/ios/Flir/src/FlirEventEmitter.h +25 -25
  19. package/ios/Flir/src/FlirEventEmitter.m +63 -63
  20. package/ios/Flir/src/FlirManager.swift +599 -599
  21. package/ios/Flir/src/FlirModule.h +17 -17
  22. package/ios/Flir/src/FlirModule.m +713 -713
  23. package/ios/Flir/src/FlirPreviewView.h +13 -13
  24. package/ios/Flir/src/FlirPreviewView.m +171 -171
  25. package/ios/Flir/src/FlirState.h +68 -68
  26. package/ios/Flir/src/FlirState.m +135 -135
  27. package/ios/Flir/src/FlirViewManager.h +16 -16
  28. package/ios/Flir/src/FlirViewManager.m +27 -27
  29. package/package.json +72 -71
  30. package/react-native.config.js +14 -14
  31. package/scripts/fetch-binaries.js +47 -5
  32. package/sdk-manifest.json +50 -50
  33. package/src/index.d.ts +63 -63
  34. package/src/index.js +7 -7
  35. package/src/index.ts +6 -6
@@ -1,713 +1,713 @@
1
- //
2
- // FlirModule.m
3
- // Flir
4
- //
5
- // React Native bridge module for FLIR thermal camera SDK
6
- // Provides discovery, connection, and streaming functionality
7
- //
8
-
9
- #import "FlirModule.h"
10
- #import "FlirEventEmitter.h"
11
- #import "FlirState.h"
12
- #import <React/RCTLog.h>
13
- #import <React/RCTBridge.h>
14
-
15
- #if __has_include(<ThermalSDK/ThermalSDK.h>)
16
- #define FLIR_SDK_AVAILABLE 1
17
- #import <ThermalSDK/ThermalSDK.h>
18
- #else
19
- #define FLIR_SDK_AVAILABLE 0
20
- #endif
21
-
22
- // Forward declare Swift class
23
- @class FlirManager;
24
-
25
- @interface FlirModule()
26
- #if FLIR_SDK_AVAILABLE
27
- <FLIRDiscoveryEventDelegate, FLIRDataReceivedDelegate, FLIRStreamDelegate>
28
- #endif
29
-
30
- #if FLIR_SDK_AVAILABLE
31
- @property (nonatomic, strong) FLIRDiscovery *discovery;
32
- @property (nonatomic, strong) FLIRCamera *camera;
33
- @property (nonatomic, strong) FLIRStream *stream;
34
- @property (nonatomic, strong) FLIRThermalStreamer *streamer;
35
- @property (nonatomic, strong) FLIRIdentity *connectedIdentity;
36
- @property (nonatomic, strong) NSMutableDictionary<NSString *, FLIRIdentity *> *identityMap;
37
- #endif
38
-
39
- @property (nonatomic, strong) NSMutableArray<NSDictionary *> *discoveredDevices;
40
- @property (nonatomic, assign) BOOL isScanning;
41
- @property (nonatomic, assign) BOOL isConnected;
42
- @property (nonatomic, assign) BOOL isStreaming;
43
- @property (nonatomic, copy) NSString *connectedDeviceId;
44
- @property (nonatomic, copy) NSString *connectedDeviceName;
45
- @property (nonatomic, assign) double lastTemperature;
46
- @end
47
-
48
- @implementation FlirModule
49
-
50
- RCT_EXPORT_MODULE(FlirModule);
51
-
52
- + (BOOL)requiresMainQueueSetup {
53
- return YES;
54
- }
55
-
56
- - (instancetype)init {
57
- if (self = [super init]) {
58
- #if FLIR_SDK_AVAILABLE
59
- _identityMap = [NSMutableDictionary new];
60
- #endif
61
- _discoveredDevices = [NSMutableArray new];
62
- _isScanning = NO;
63
- _isConnected = NO;
64
- _isStreaming = NO;
65
- _lastTemperature = NAN;
66
- }
67
- return self;
68
- }
69
-
70
- #pragma mark - Event Emitter Support
71
-
72
- - (NSArray<NSString *> *)supportedEvents {
73
- return @[
74
- @"FlirDeviceConnected",
75
- @"FlirDeviceDisconnected",
76
- @"FlirDevicesFound",
77
- @"FlirFrameReceived",
78
- @"FlirError",
79
- @"FlirStateChanged"
80
- ];
81
- }
82
-
83
- RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {
84
- // Required for RCTEventEmitter
85
- }
86
-
87
- RCT_EXPORT_METHOD(removeListeners:(NSInteger)count) {
88
- // Required for RCTEventEmitter
89
- }
90
-
91
- #pragma mark - Discovery Methods
92
-
93
- RCT_EXPORT_METHOD(startDiscovery:(RCTPromiseResolveBlock)resolve
94
- rejecter:(RCTPromiseRejectBlock)reject) {
95
- #if FLIR_SDK_AVAILABLE
96
- dispatch_async(dispatch_get_main_queue(), ^{
97
- if (self.isScanning) {
98
- RCTLogInfo(@"[FlirModule] Already scanning");
99
- resolve(@(YES));
100
- return;
101
- }
102
-
103
- self.isScanning = YES;
104
- [self.discoveredDevices removeAllObjects];
105
- [self.identityMap removeAllObjects];
106
-
107
- if (!self.discovery) {
108
- self.discovery = [[FLIRDiscovery alloc] init];
109
- self.discovery.delegate = self;
110
- }
111
-
112
- // Start discovery on all available interfaces
113
- FLIRCommunicationInterface interfaces = FLIRCommunicationInterfaceLightning |
114
- FLIRCommunicationInterfaceNetwork |
115
- FLIRCommunicationInterfaceFlirOneWireless |
116
- FLIRCommunicationInterfaceEmulator;
117
- [self.discovery start:interfaces];
118
-
119
- [self emitStateChange:@"discovering"];
120
- RCTLogInfo(@"[FlirModule] Discovery started");
121
- resolve(@(YES));
122
- });
123
- #else
124
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
125
- #endif
126
- }
127
-
128
- RCT_EXPORT_METHOD(stopDiscovery:(RCTPromiseResolveBlock)resolve
129
- rejecter:(RCTPromiseRejectBlock)reject) {
130
- #if FLIR_SDK_AVAILABLE
131
- dispatch_async(dispatch_get_main_queue(), ^{
132
- [self.discovery stop];
133
- self.isScanning = NO;
134
- RCTLogInfo(@"[FlirModule] Discovery stopped");
135
- resolve(@(YES));
136
- });
137
- #else
138
- resolve(@(YES));
139
- #endif
140
- }
141
-
142
- RCT_EXPORT_METHOD(getDiscoveredDevices:(RCTPromiseResolveBlock)resolve
143
- rejecter:(RCTPromiseRejectBlock)reject) {
144
- dispatch_async(dispatch_get_main_queue(), ^{
145
- resolve(self.discoveredDevices);
146
- });
147
- }
148
-
149
- #pragma mark - Connection Methods
150
-
151
- RCT_EXPORT_METHOD(connectToDevice:(NSString *)deviceId
152
- resolver:(RCTPromiseResolveBlock)resolve
153
- rejecter:(RCTPromiseRejectBlock)reject) {
154
- #if FLIR_SDK_AVAILABLE
155
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
156
- FLIRIdentity *identity = self.identityMap[deviceId];
157
- if (!identity) {
158
- dispatch_async(dispatch_get_main_queue(), ^{
159
- reject(@"ERR_DEVICE_NOT_FOUND", [NSString stringWithFormat:@"Device not found: %@", deviceId], nil);
160
- });
161
- return;
162
- }
163
-
164
- [self performConnectionWithIdentity:identity completion:^(BOOL success, NSError *error) {
165
- dispatch_async(dispatch_get_main_queue(), ^{
166
- if (success) {
167
- resolve(@(YES));
168
- } else {
169
- reject(@"ERR_CONNECTION_FAILED", error.localizedDescription ?: @"Connection failed", error);
170
- }
171
- });
172
- }];
173
- });
174
- #else
175
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
176
- #endif
177
- }
178
-
179
- #if FLIR_SDK_AVAILABLE
180
- - (void)performConnectionWithIdentity:(FLIRIdentity *)identity completion:(void(^)(BOOL success, NSError *error))completion {
181
- if (!self.camera) {
182
- self.camera = [[FLIRCamera alloc] init];
183
- self.camera.delegate = self;
184
- }
185
-
186
- NSError *error = nil;
187
-
188
- // Handle authentication for generic cameras
189
- if ([identity cameraType] == FLIRCameraType_generic) {
190
- NSString *certName = [self getCertificateName];
191
- FLIRAuthenticationStatus status = pending;
192
- while (status == pending) {
193
- status = [self.camera authenticate:identity trustedConnectionName:certName];
194
- if (status == pending) {
195
- RCTLogInfo(@"[FlirModule] Waiting for camera authentication...");
196
- [NSThread sleepForTimeInterval:1.0];
197
- }
198
- }
199
- }
200
-
201
- BOOL connected = [self.camera connect:identity error:&error];
202
-
203
- if (connected) {
204
- self.connectedIdentity = identity;
205
- self.connectedDeviceId = [identity deviceId];
206
- self.connectedDeviceName = [identity deviceId];
207
- self.isConnected = YES;
208
-
209
- RCTLogInfo(@"[FlirModule] Connected to: %@", [identity deviceId]);
210
-
211
- // Get available streams
212
- NSArray<FLIRStream *> *streams = [self.camera getStreams];
213
- if (streams.count > 0) {
214
- RCTLogInfo(@"[FlirModule] Found %lu streams", (unsigned long)streams.count);
215
- // Auto-start first stream
216
- [self startStreamInternal:streams[0]];
217
- }
218
-
219
- dispatch_async(dispatch_get_main_queue(), ^{
220
- [self emitDeviceConnected];
221
- [self emitStateChange:@"connected"];
222
- });
223
-
224
- if (completion) completion(YES, nil);
225
- } else {
226
- RCTLogError(@"[FlirModule] Connection failed: %@", error.localizedDescription);
227
- if (completion) completion(NO, error);
228
- }
229
- }
230
-
231
- - (NSString *)getCertificateName {
232
- NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.flir.app";
233
- NSString *key = [NSString stringWithFormat:@"%@-cert-name", bundleID];
234
-
235
- NSString *existing = [[NSUserDefaults standardUserDefaults] stringForKey:key];
236
- if (existing) {
237
- return existing;
238
- }
239
-
240
- NSString *newName = [[NSUUID UUID] UUIDString];
241
- [[NSUserDefaults standardUserDefaults] setObject:newName forKey:key];
242
- return newName;
243
- }
244
- #endif
245
-
246
- RCT_EXPORT_METHOD(disconnect:(RCTPromiseResolveBlock)resolve
247
- rejecter:(RCTPromiseRejectBlock)reject) {
248
- #if FLIR_SDK_AVAILABLE
249
- dispatch_async(dispatch_get_main_queue(), ^{
250
- [self stopStreamInternal];
251
- [self.camera disconnect];
252
- self.camera = nil;
253
- self.connectedIdentity = nil;
254
- self.connectedDeviceId = nil;
255
- self.connectedDeviceName = nil;
256
- self.isConnected = NO;
257
- self.isStreaming = NO;
258
-
259
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
260
- [self emitStateChange:@"disconnected"];
261
-
262
- RCTLogInfo(@"[FlirModule] Disconnected");
263
- resolve(@(YES));
264
- });
265
- #else
266
- resolve(@(YES));
267
- #endif
268
- }
269
-
270
- RCT_EXPORT_METHOD(stopFlir:(RCTPromiseResolveBlock)resolve
271
- rejecter:(RCTPromiseRejectBlock)reject) {
272
- #if FLIR_SDK_AVAILABLE
273
- dispatch_async(dispatch_get_main_queue(), ^{
274
- [self stopStreamInternal];
275
- [self.camera disconnect];
276
- [self.discovery stop];
277
-
278
- self.camera = nil;
279
- self.connectedIdentity = nil;
280
- self.connectedDeviceId = nil;
281
- self.connectedDeviceName = nil;
282
- self.isConnected = NO;
283
- self.isStreaming = NO;
284
- self.isScanning = NO;
285
-
286
- [self emitStateChange:@"stopped"];
287
- RCTLogInfo(@"[FlirModule] Stopped");
288
- resolve(@(YES));
289
- });
290
- #else
291
- resolve(@(YES));
292
- #endif
293
- }
294
-
295
- #pragma mark - Streaming
296
-
297
- #if FLIR_SDK_AVAILABLE
298
- - (void)startStreamInternal:(FLIRStream *)newStream {
299
- [self stopStreamInternal];
300
-
301
- self.stream = newStream;
302
-
303
- if (newStream.isThermal) {
304
- self.streamer = [[FLIRThermalStreamer alloc] initWithStream:newStream];
305
- }
306
-
307
- newStream.delegate = self;
308
-
309
- NSError *error = nil;
310
- if ([newStream start:&error]) {
311
- self.isStreaming = YES;
312
- [self emitStateChange:@"streaming"];
313
- RCTLogInfo(@"[FlirModule] Stream started (thermal: %@)", newStream.isThermal ? @"YES" : @"NO");
314
- } else {
315
- RCTLogError(@"[FlirModule] Stream start failed: %@", error.localizedDescription);
316
- self.stream = nil;
317
- self.streamer = nil;
318
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{@"error": error.localizedDescription ?: @"Stream start failed"}];
319
- }
320
- }
321
-
322
- - (void)stopStreamInternal {
323
- [self.stream stop];
324
- self.stream = nil;
325
- self.streamer = nil;
326
- self.isStreaming = NO;
327
- }
328
- #endif
329
-
330
- #pragma mark - Temperature Methods
331
-
332
- RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
333
- y:(nonnull NSNumber *)y
334
- resolver:(RCTPromiseResolveBlock)resolve
335
- rejecter:(RCTPromiseRejectBlock)reject) {
336
- dispatch_async(dispatch_get_main_queue(), ^{
337
- double temp = [FlirState shared].lastTemperature;
338
- if (isnan(temp)) {
339
- temp = self.lastTemperature;
340
- }
341
- if (isnan(temp)) {
342
- resolve([NSNull null]);
343
- } else {
344
- resolve(@(temp));
345
- }
346
- });
347
- }
348
-
349
- RCT_EXPORT_METHOD(getTemperatureFromColor:(NSInteger)color
350
- resolver:(RCTPromiseResolveBlock)resolve
351
- rejecter:(RCTPromiseRejectBlock)reject) {
352
- // Placeholder: Convert ARGB color to pseudo-temperature
353
- int r = (color >> 16) & 0xFF;
354
- int g = (color >> 8) & 0xFF;
355
- int b = color & 0xFF;
356
- double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
357
- double temp = (lum / 255.0) * 400.0;
358
- resolve(@(temp));
359
- }
360
-
361
- #pragma mark - Status Methods
362
-
363
- RCT_EXPORT_METHOD(isEmulator:(RCTPromiseResolveBlock)resolve
364
- rejecter:(RCTPromiseRejectBlock)reject) {
365
- dispatch_async(dispatch_get_main_queue(), ^{
366
- BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"] ||
367
- [self.connectedDeviceName.lowercaseString containsString:@"emulat"];
368
- resolve(@(isEmu));
369
- });
370
- }
371
-
372
- RCT_EXPORT_METHOD(isDeviceConnected:(RCTPromiseResolveBlock)resolve
373
- rejecter:(RCTPromiseRejectBlock)reject) {
374
- dispatch_async(dispatch_get_main_queue(), ^{
375
- resolve(@(self.isConnected));
376
- });
377
- }
378
-
379
- RCT_EXPORT_METHOD(getConnectedDeviceInfo:(RCTPromiseResolveBlock)resolve
380
- rejecter:(RCTPromiseRejectBlock)reject) {
381
- dispatch_async(dispatch_get_main_queue(), ^{
382
- resolve(self.connectedDeviceName ?: @"Not connected");
383
- });
384
- }
385
-
386
- RCT_EXPORT_METHOD(isSDKDownloaded:(RCTPromiseResolveBlock)resolve
387
- rejecter:(RCTPromiseRejectBlock)reject) {
388
- #if FLIR_SDK_AVAILABLE
389
- resolve(@(YES));
390
- #else
391
- resolve(@(NO));
392
- #endif
393
- }
394
-
395
- RCT_EXPORT_METHOD(getSDKStatus:(RCTPromiseResolveBlock)resolve
396
- rejecter:(RCTPromiseRejectBlock)reject) {
397
- NSDictionary *status = @{
398
- #if FLIR_SDK_AVAILABLE
399
- @"available": @(YES),
400
- #else
401
- @"available": @(NO),
402
- #endif
403
- @"arch": @"arm64",
404
- @"platform": @"iOS"
405
- };
406
- resolve(status);
407
- }
408
-
409
- #pragma mark - Emulator
410
-
411
- RCT_EXPORT_METHOD(startEmulator:(NSString *)emulatorType
412
- resolver:(RCTPromiseResolveBlock)resolve
413
- rejecter:(RCTPromiseRejectBlock)reject) {
414
- #if FLIR_SDK_AVAILABLE
415
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
416
- FLIRCameraType cameraType = FLIRCameraType_flirOne;
417
- if ([emulatorType.lowercaseString containsString:@"edge"]) {
418
- cameraType = FLIRCameraType_flirOneEdge;
419
- } else if ([emulatorType.lowercaseString containsString:@"pro"]) {
420
- cameraType = FLIRCameraType_flirOneEdgePro;
421
- }
422
-
423
- FLIRIdentity *emulatorIdentity = [[FLIRIdentity alloc] initWithEmulatorType:cameraType];
424
- if (emulatorIdentity) {
425
- self.identityMap[[emulatorIdentity deviceId]] = emulatorIdentity;
426
-
427
- [self performConnectionWithIdentity:emulatorIdentity completion:^(BOOL success, NSError *error) {
428
- dispatch_async(dispatch_get_main_queue(), ^{
429
- if (success) {
430
- resolve(@(YES));
431
- } else {
432
- reject(@"ERR_EMULATOR_FAILED", error.localizedDescription ?: @"Emulator start failed", error);
433
- }
434
- });
435
- }];
436
- } else {
437
- dispatch_async(dispatch_get_main_queue(), ^{
438
- reject(@"ERR_EMULATOR_INIT", @"Failed to create emulator identity", nil);
439
- });
440
- }
441
- });
442
- #else
443
- reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
444
- #endif
445
- }
446
-
447
- #pragma mark - Debug
448
-
449
- RCT_EXPORT_METHOD(initializeSDK:(RCTPromiseResolveBlock)resolve
450
- rejecter:(RCTPromiseRejectBlock)reject) {
451
- NSDictionary *result = @{
452
- #if FLIR_SDK_AVAILABLE
453
- @"initialized": @(YES),
454
- @"message": @"SDK initialized successfully"
455
- #else
456
- @"initialized": @(NO),
457
- @"message": @"SDK not available - built without FLIR"
458
- #endif
459
- };
460
- resolve(result);
461
- }
462
-
463
- RCT_EXPORT_METHOD(getDebugInfo:(RCTPromiseResolveBlock)resolve
464
- rejecter:(RCTPromiseRejectBlock)reject) {
465
- dispatch_async(dispatch_get_main_queue(), ^{
466
- NSDictionary *info = @{
467
- #if FLIR_SDK_AVAILABLE
468
- @"sdkAvailable": @(YES),
469
- #else
470
- @"sdkAvailable": @(NO),
471
- #endif
472
- @"arch": @"arm64",
473
- @"discoveredDeviceCount": @(self.discoveredDevices.count),
474
- @"isConnected": @(self.isConnected),
475
- @"isStreaming": @(self.isStreaming),
476
- @"connectedDevice": self.connectedDeviceName ?: @"None"
477
- };
478
- resolve(info);
479
- });
480
- }
481
-
482
- RCT_EXPORT_METHOD(getLatestFramePath:(RCTPromiseResolveBlock)resolve
483
- rejecter:(RCTPromiseRejectBlock)reject) {
484
- dispatch_async(dispatch_get_main_queue(), ^{
485
- UIImage *image = [FlirState shared].latestImage;
486
- if (!image) {
487
- resolve([NSNull null]);
488
- return;
489
- }
490
-
491
- NSData *jpegData = UIImageJPEGRepresentation(image, 0.9);
492
- if (!jpegData) {
493
- resolve([NSNull null]);
494
- return;
495
- }
496
-
497
- NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:
498
- [NSString stringWithFormat:@"flir_frame_%lld.jpg", (long long)[[NSDate date] timeIntervalSince1970] * 1000]];
499
- [jpegData writeToFile:tempPath atomically:YES];
500
- resolve(tempPath);
501
- });
502
- }
503
-
504
- #pragma mark - Helper Methods
505
-
506
- - (void)emitDeviceConnected {
507
- BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
508
-
509
- NSDictionary *body = @{
510
- @"identity": @{
511
- @"deviceId": self.connectedDeviceId ?: @"Unknown",
512
- @"isEmulator": @(isEmu)
513
- },
514
- @"deviceType": isEmu ? @"emulator" : @"device",
515
- @"isEmulator": @(isEmu),
516
- @"state": @"connected"
517
- };
518
-
519
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceConnected" body:body];
520
- }
521
-
522
- - (void)emitStateChange:(NSString *)state {
523
- BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
524
-
525
- NSDictionary *body = @{
526
- @"state": state,
527
- @"isConnected": @(self.isConnected),
528
- @"isStreaming": @(self.isStreaming),
529
- @"isEmulator": @(isEmu),
530
- @"deviceName": self.connectedDeviceName ?: @"",
531
- @"deviceId": self.connectedDeviceId ?: @""
532
- };
533
-
534
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirStateChanged" body:body];
535
- }
536
-
537
- #if FLIR_SDK_AVAILABLE
538
- - (NSString *)communicationInterfaceName:(FLIRCommunicationInterface)iface {
539
- if (iface & FLIRCommunicationInterfaceLightning) return @"LIGHTNING";
540
- if (iface & FLIRCommunicationInterfaceNetwork) return @"NETWORK";
541
- if (iface & FLIRCommunicationInterfaceFlirOneWireless) return @"WIRELESS";
542
- if (iface & FLIRCommunicationInterfaceEmulator) return @"EMULATOR";
543
- if (iface & FLIRCommunicationInterfaceUSB) return @"USB";
544
- return @"UNKNOWN";
545
- }
546
- #endif
547
-
548
- #pragma mark - FLIRDiscoveryEventDelegate
549
-
550
- #if FLIR_SDK_AVAILABLE
551
- - (void)cameraDiscovered:(FLIRDiscoveredCamera *)discoveredCamera {
552
- FLIRIdentity *identity = discoveredCamera.identity;
553
- NSString *deviceId = [identity deviceId];
554
-
555
- RCTLogInfo(@"[FlirModule] Camera discovered: %@", deviceId);
556
-
557
- // Store identity
558
- self.identityMap[deviceId] = identity;
559
-
560
- // Create device info
561
- NSDictionary *deviceInfo = @{
562
- @"id": deviceId,
563
- @"name": discoveredCamera.displayName ?: deviceId,
564
- @"communicationType": [self communicationInterfaceName:[identity communicationInterface]],
565
- @"isEmulator": @([identity communicationInterface] == FLIRCommunicationInterfaceEmulator)
566
- };
567
-
568
- // Add if not already present
569
- BOOL found = NO;
570
- for (NSDictionary *existing in self.discoveredDevices) {
571
- if ([existing[@"id"] isEqualToString:deviceId]) {
572
- found = YES;
573
- break;
574
- }
575
- }
576
-
577
- if (!found) {
578
- [self.discoveredDevices addObject:deviceInfo];
579
- }
580
-
581
- // Emit devices found event
582
- dispatch_async(dispatch_get_main_queue(), ^{
583
- NSDictionary *body = @{
584
- @"devices": self.discoveredDevices,
585
- @"count": @(self.discoveredDevices.count)
586
- };
587
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
588
- });
589
- }
590
-
591
- - (void)cameraLost:(FLIRIdentity *)cameraIdentity {
592
- NSString *deviceId = [cameraIdentity deviceId];
593
- RCTLogInfo(@"[FlirModule] Camera lost: %@", deviceId);
594
-
595
- [self.identityMap removeObjectForKey:deviceId];
596
-
597
- NSMutableArray *toRemove = [NSMutableArray new];
598
- for (NSDictionary *device in self.discoveredDevices) {
599
- if ([device[@"id"] isEqualToString:deviceId]) {
600
- [toRemove addObject:device];
601
- }
602
- }
603
- [self.discoveredDevices removeObjectsInArray:toRemove];
604
-
605
- // If this was our connected device, handle disconnect
606
- if ([self.connectedDeviceId isEqualToString:deviceId]) {
607
- dispatch_async(dispatch_get_main_queue(), ^{
608
- [self stopStreamInternal];
609
- self.camera = nil;
610
- self.connectedIdentity = nil;
611
- self.connectedDeviceId = nil;
612
- self.connectedDeviceName = nil;
613
- self.isConnected = NO;
614
- self.isStreaming = NO;
615
-
616
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
617
- [self emitStateChange:@"disconnected"];
618
- });
619
- }
620
-
621
- dispatch_async(dispatch_get_main_queue(), ^{
622
- NSDictionary *body = @{
623
- @"devices": self.discoveredDevices,
624
- @"count": @(self.discoveredDevices.count)
625
- };
626
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
627
- });
628
- }
629
-
630
- - (void)discoveryError:(NSString *)error netServiceError:(int)nsnetserviceserror on:(FLIRCommunicationInterface)iface {
631
- RCTLogError(@"[FlirModule] Discovery error: %@ (%d)", error, nsnetserviceserror);
632
-
633
- dispatch_async(dispatch_get_main_queue(), ^{
634
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
635
- @"error": error ?: @"Unknown discovery error",
636
- @"type": @"discovery"
637
- }];
638
- });
639
- }
640
-
641
- - (void)discoveryFinished:(FLIRCommunicationInterface)iface {
642
- RCTLogInfo(@"[FlirModule] Discovery finished");
643
- self.isScanning = NO;
644
- }
645
- #endif
646
-
647
- #pragma mark - FLIRDataReceivedDelegate
648
-
649
- #if FLIR_SDK_AVAILABLE
650
- - (void)onDisconnected:(FLIRCamera *)camera withError:(NSError *)error {
651
- RCTLogInfo(@"[FlirModule] Camera disconnected: %@", error.localizedDescription);
652
-
653
- dispatch_async(dispatch_get_main_queue(), ^{
654
- self.isConnected = NO;
655
- self.isStreaming = NO;
656
- self.connectedDeviceId = nil;
657
- self.connectedDeviceName = nil;
658
-
659
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
660
- [self emitStateChange:@"disconnected"];
661
- });
662
- }
663
- #endif
664
-
665
- #pragma mark - FLIRStreamDelegate
666
-
667
- #if FLIR_SDK_AVAILABLE
668
- - (void)onError:(NSError *)error {
669
- RCTLogError(@"[FlirModule] Stream error: %@", error.localizedDescription);
670
-
671
- dispatch_async(dispatch_get_main_queue(), ^{
672
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
673
- @"error": error.localizedDescription ?: @"Stream error",
674
- @"type": @"stream"
675
- }];
676
- });
677
- }
678
-
679
- - (void)onImageReceived {
680
- if (!self.streamer) return;
681
-
682
- NSError *error = nil;
683
- if ([self.streamer update:&error]) {
684
- UIImage *image = [self.streamer getImage];
685
- if (image) {
686
- // Update shared state
687
- [[FlirState shared] updateFrame:image];
688
-
689
- // Get temperature from thermal image if available
690
- [self.streamer withThermalImage:^(FLIRThermalImage *thermalImage) {
691
- FLIRImageStatistics *stats = [thermalImage getStatistics];
692
- if (stats) {
693
- self.lastTemperature = [[stats getMax] value];
694
- [FlirState shared].lastTemperature = self.lastTemperature;
695
- }
696
- }];
697
-
698
- // Emit frame received event (rate-limited by RN event queue)
699
- dispatch_async(dispatch_get_main_queue(), ^{
700
- [[FlirEventEmitter shared] sendDeviceEvent:@"FlirFrameReceived" body:@{
701
- @"width": @(image.size.width),
702
- @"height": @(image.size.height),
703
- @"timestamp": @([[NSDate date] timeIntervalSince1970] * 1000)
704
- }];
705
- });
706
- }
707
- } else {
708
- RCTLogError(@"[FlirModule] Streamer update error: %@", error.localizedDescription);
709
- }
710
- }
711
- #endif
712
-
713
- @end
1
+ //
2
+ // FlirModule.m
3
+ // Flir
4
+ //
5
+ // React Native bridge module for FLIR thermal camera SDK
6
+ // Provides discovery, connection, and streaming functionality
7
+ //
8
+
9
+ #import "FlirModule.h"
10
+ #import "FlirEventEmitter.h"
11
+ #import "FlirState.h"
12
+ #import <React/RCTLog.h>
13
+ #import <React/RCTBridge.h>
14
+
15
+ #if __has_include(<ThermalSDK/ThermalSDK.h>)
16
+ #define FLIR_SDK_AVAILABLE 1
17
+ #import <ThermalSDK/ThermalSDK.h>
18
+ #else
19
+ #define FLIR_SDK_AVAILABLE 0
20
+ #endif
21
+
22
+ // Forward declare Swift class
23
+ @class FlirManager;
24
+
25
+ @interface FlirModule()
26
+ #if FLIR_SDK_AVAILABLE
27
+ <FLIRDiscoveryEventDelegate, FLIRDataReceivedDelegate, FLIRStreamDelegate>
28
+ #endif
29
+
30
+ #if FLIR_SDK_AVAILABLE
31
+ @property (nonatomic, strong) FLIRDiscovery *discovery;
32
+ @property (nonatomic, strong) FLIRCamera *camera;
33
+ @property (nonatomic, strong) FLIRStream *stream;
34
+ @property (nonatomic, strong) FLIRThermalStreamer *streamer;
35
+ @property (nonatomic, strong) FLIRIdentity *connectedIdentity;
36
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, FLIRIdentity *> *identityMap;
37
+ #endif
38
+
39
+ @property (nonatomic, strong) NSMutableArray<NSDictionary *> *discoveredDevices;
40
+ @property (nonatomic, assign) BOOL isScanning;
41
+ @property (nonatomic, assign) BOOL isConnected;
42
+ @property (nonatomic, assign) BOOL isStreaming;
43
+ @property (nonatomic, copy) NSString *connectedDeviceId;
44
+ @property (nonatomic, copy) NSString *connectedDeviceName;
45
+ @property (nonatomic, assign) double lastTemperature;
46
+ @end
47
+
48
+ @implementation FlirModule
49
+
50
+ RCT_EXPORT_MODULE(FlirModule);
51
+
52
+ + (BOOL)requiresMainQueueSetup {
53
+ return YES;
54
+ }
55
+
56
+ - (instancetype)init {
57
+ if (self = [super init]) {
58
+ #if FLIR_SDK_AVAILABLE
59
+ _identityMap = [NSMutableDictionary new];
60
+ #endif
61
+ _discoveredDevices = [NSMutableArray new];
62
+ _isScanning = NO;
63
+ _isConnected = NO;
64
+ _isStreaming = NO;
65
+ _lastTemperature = NAN;
66
+ }
67
+ return self;
68
+ }
69
+
70
+ #pragma mark - Event Emitter Support
71
+
72
+ - (NSArray<NSString *> *)supportedEvents {
73
+ return @[
74
+ @"FlirDeviceConnected",
75
+ @"FlirDeviceDisconnected",
76
+ @"FlirDevicesFound",
77
+ @"FlirFrameReceived",
78
+ @"FlirError",
79
+ @"FlirStateChanged"
80
+ ];
81
+ }
82
+
83
+ RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {
84
+ // Required for RCTEventEmitter
85
+ }
86
+
87
+ RCT_EXPORT_METHOD(removeListeners:(NSInteger)count) {
88
+ // Required for RCTEventEmitter
89
+ }
90
+
91
+ #pragma mark - Discovery Methods
92
+
93
+ RCT_EXPORT_METHOD(startDiscovery:(RCTPromiseResolveBlock)resolve
94
+ rejecter:(RCTPromiseRejectBlock)reject) {
95
+ #if FLIR_SDK_AVAILABLE
96
+ dispatch_async(dispatch_get_main_queue(), ^{
97
+ if (self.isScanning) {
98
+ RCTLogInfo(@"[FlirModule] Already scanning");
99
+ resolve(@(YES));
100
+ return;
101
+ }
102
+
103
+ self.isScanning = YES;
104
+ [self.discoveredDevices removeAllObjects];
105
+ [self.identityMap removeAllObjects];
106
+
107
+ if (!self.discovery) {
108
+ self.discovery = [[FLIRDiscovery alloc] init];
109
+ self.discovery.delegate = self;
110
+ }
111
+
112
+ // Start discovery on all available interfaces
113
+ FLIRCommunicationInterface interfaces = FLIRCommunicationInterfaceLightning |
114
+ FLIRCommunicationInterfaceNetwork |
115
+ FLIRCommunicationInterfaceFlirOneWireless |
116
+ FLIRCommunicationInterfaceEmulator;
117
+ [self.discovery start:interfaces];
118
+
119
+ [self emitStateChange:@"discovering"];
120
+ RCTLogInfo(@"[FlirModule] Discovery started");
121
+ resolve(@(YES));
122
+ });
123
+ #else
124
+ reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
125
+ #endif
126
+ }
127
+
128
+ RCT_EXPORT_METHOD(stopDiscovery:(RCTPromiseResolveBlock)resolve
129
+ rejecter:(RCTPromiseRejectBlock)reject) {
130
+ #if FLIR_SDK_AVAILABLE
131
+ dispatch_async(dispatch_get_main_queue(), ^{
132
+ [self.discovery stop];
133
+ self.isScanning = NO;
134
+ RCTLogInfo(@"[FlirModule] Discovery stopped");
135
+ resolve(@(YES));
136
+ });
137
+ #else
138
+ resolve(@(YES));
139
+ #endif
140
+ }
141
+
142
+ RCT_EXPORT_METHOD(getDiscoveredDevices:(RCTPromiseResolveBlock)resolve
143
+ rejecter:(RCTPromiseRejectBlock)reject) {
144
+ dispatch_async(dispatch_get_main_queue(), ^{
145
+ resolve(self.discoveredDevices);
146
+ });
147
+ }
148
+
149
+ #pragma mark - Connection Methods
150
+
151
+ RCT_EXPORT_METHOD(connectToDevice:(NSString *)deviceId
152
+ resolver:(RCTPromiseResolveBlock)resolve
153
+ rejecter:(RCTPromiseRejectBlock)reject) {
154
+ #if FLIR_SDK_AVAILABLE
155
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
156
+ FLIRIdentity *identity = self.identityMap[deviceId];
157
+ if (!identity) {
158
+ dispatch_async(dispatch_get_main_queue(), ^{
159
+ reject(@"ERR_DEVICE_NOT_FOUND", [NSString stringWithFormat:@"Device not found: %@", deviceId], nil);
160
+ });
161
+ return;
162
+ }
163
+
164
+ [self performConnectionWithIdentity:identity completion:^(BOOL success, NSError *error) {
165
+ dispatch_async(dispatch_get_main_queue(), ^{
166
+ if (success) {
167
+ resolve(@(YES));
168
+ } else {
169
+ reject(@"ERR_CONNECTION_FAILED", error.localizedDescription ?: @"Connection failed", error);
170
+ }
171
+ });
172
+ }];
173
+ });
174
+ #else
175
+ reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
176
+ #endif
177
+ }
178
+
179
+ #if FLIR_SDK_AVAILABLE
180
+ - (void)performConnectionWithIdentity:(FLIRIdentity *)identity completion:(void(^)(BOOL success, NSError *error))completion {
181
+ if (!self.camera) {
182
+ self.camera = [[FLIRCamera alloc] init];
183
+ self.camera.delegate = self;
184
+ }
185
+
186
+ NSError *error = nil;
187
+
188
+ // Handle authentication for generic cameras
189
+ if ([identity cameraType] == FLIRCameraType_generic) {
190
+ NSString *certName = [self getCertificateName];
191
+ FLIRAuthenticationStatus status = pending;
192
+ while (status == pending) {
193
+ status = [self.camera authenticate:identity trustedConnectionName:certName];
194
+ if (status == pending) {
195
+ RCTLogInfo(@"[FlirModule] Waiting for camera authentication...");
196
+ [NSThread sleepForTimeInterval:1.0];
197
+ }
198
+ }
199
+ }
200
+
201
+ BOOL connected = [self.camera connect:identity error:&error];
202
+
203
+ if (connected) {
204
+ self.connectedIdentity = identity;
205
+ self.connectedDeviceId = [identity deviceId];
206
+ self.connectedDeviceName = [identity deviceId];
207
+ self.isConnected = YES;
208
+
209
+ RCTLogInfo(@"[FlirModule] Connected to: %@", [identity deviceId]);
210
+
211
+ // Get available streams
212
+ NSArray<FLIRStream *> *streams = [self.camera getStreams];
213
+ if (streams.count > 0) {
214
+ RCTLogInfo(@"[FlirModule] Found %lu streams", (unsigned long)streams.count);
215
+ // Auto-start first stream
216
+ [self startStreamInternal:streams[0]];
217
+ }
218
+
219
+ dispatch_async(dispatch_get_main_queue(), ^{
220
+ [self emitDeviceConnected];
221
+ [self emitStateChange:@"connected"];
222
+ });
223
+
224
+ if (completion) completion(YES, nil);
225
+ } else {
226
+ RCTLogError(@"[FlirModule] Connection failed: %@", error.localizedDescription);
227
+ if (completion) completion(NO, error);
228
+ }
229
+ }
230
+
231
+ - (NSString *)getCertificateName {
232
+ NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.flir.app";
233
+ NSString *key = [NSString stringWithFormat:@"%@-cert-name", bundleID];
234
+
235
+ NSString *existing = [[NSUserDefaults standardUserDefaults] stringForKey:key];
236
+ if (existing) {
237
+ return existing;
238
+ }
239
+
240
+ NSString *newName = [[NSUUID UUID] UUIDString];
241
+ [[NSUserDefaults standardUserDefaults] setObject:newName forKey:key];
242
+ return newName;
243
+ }
244
+ #endif
245
+
246
+ RCT_EXPORT_METHOD(disconnect:(RCTPromiseResolveBlock)resolve
247
+ rejecter:(RCTPromiseRejectBlock)reject) {
248
+ #if FLIR_SDK_AVAILABLE
249
+ dispatch_async(dispatch_get_main_queue(), ^{
250
+ [self stopStreamInternal];
251
+ [self.camera disconnect];
252
+ self.camera = nil;
253
+ self.connectedIdentity = nil;
254
+ self.connectedDeviceId = nil;
255
+ self.connectedDeviceName = nil;
256
+ self.isConnected = NO;
257
+ self.isStreaming = NO;
258
+
259
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
260
+ [self emitStateChange:@"disconnected"];
261
+
262
+ RCTLogInfo(@"[FlirModule] Disconnected");
263
+ resolve(@(YES));
264
+ });
265
+ #else
266
+ resolve(@(YES));
267
+ #endif
268
+ }
269
+
270
+ RCT_EXPORT_METHOD(stopFlir:(RCTPromiseResolveBlock)resolve
271
+ rejecter:(RCTPromiseRejectBlock)reject) {
272
+ #if FLIR_SDK_AVAILABLE
273
+ dispatch_async(dispatch_get_main_queue(), ^{
274
+ [self stopStreamInternal];
275
+ [self.camera disconnect];
276
+ [self.discovery stop];
277
+
278
+ self.camera = nil;
279
+ self.connectedIdentity = nil;
280
+ self.connectedDeviceId = nil;
281
+ self.connectedDeviceName = nil;
282
+ self.isConnected = NO;
283
+ self.isStreaming = NO;
284
+ self.isScanning = NO;
285
+
286
+ [self emitStateChange:@"stopped"];
287
+ RCTLogInfo(@"[FlirModule] Stopped");
288
+ resolve(@(YES));
289
+ });
290
+ #else
291
+ resolve(@(YES));
292
+ #endif
293
+ }
294
+
295
+ #pragma mark - Streaming
296
+
297
+ #if FLIR_SDK_AVAILABLE
298
+ - (void)startStreamInternal:(FLIRStream *)newStream {
299
+ [self stopStreamInternal];
300
+
301
+ self.stream = newStream;
302
+
303
+ if (newStream.isThermal) {
304
+ self.streamer = [[FLIRThermalStreamer alloc] initWithStream:newStream];
305
+ }
306
+
307
+ newStream.delegate = self;
308
+
309
+ NSError *error = nil;
310
+ if ([newStream start:&error]) {
311
+ self.isStreaming = YES;
312
+ [self emitStateChange:@"streaming"];
313
+ RCTLogInfo(@"[FlirModule] Stream started (thermal: %@)", newStream.isThermal ? @"YES" : @"NO");
314
+ } else {
315
+ RCTLogError(@"[FlirModule] Stream start failed: %@", error.localizedDescription);
316
+ self.stream = nil;
317
+ self.streamer = nil;
318
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{@"error": error.localizedDescription ?: @"Stream start failed"}];
319
+ }
320
+ }
321
+
322
+ - (void)stopStreamInternal {
323
+ [self.stream stop];
324
+ self.stream = nil;
325
+ self.streamer = nil;
326
+ self.isStreaming = NO;
327
+ }
328
+ #endif
329
+
330
+ #pragma mark - Temperature Methods
331
+
332
+ RCT_EXPORT_METHOD(getTemperatureAt:(nonnull NSNumber *)x
333
+ y:(nonnull NSNumber *)y
334
+ resolver:(RCTPromiseResolveBlock)resolve
335
+ rejecter:(RCTPromiseRejectBlock)reject) {
336
+ dispatch_async(dispatch_get_main_queue(), ^{
337
+ double temp = [FlirState shared].lastTemperature;
338
+ if (isnan(temp)) {
339
+ temp = self.lastTemperature;
340
+ }
341
+ if (isnan(temp)) {
342
+ resolve([NSNull null]);
343
+ } else {
344
+ resolve(@(temp));
345
+ }
346
+ });
347
+ }
348
+
349
+ RCT_EXPORT_METHOD(getTemperatureFromColor:(NSInteger)color
350
+ resolver:(RCTPromiseResolveBlock)resolve
351
+ rejecter:(RCTPromiseRejectBlock)reject) {
352
+ // Placeholder: Convert ARGB color to pseudo-temperature
353
+ int r = (color >> 16) & 0xFF;
354
+ int g = (color >> 8) & 0xFF;
355
+ int b = color & 0xFF;
356
+ double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
357
+ double temp = (lum / 255.0) * 400.0;
358
+ resolve(@(temp));
359
+ }
360
+
361
+ #pragma mark - Status Methods
362
+
363
+ RCT_EXPORT_METHOD(isEmulator:(RCTPromiseResolveBlock)resolve
364
+ rejecter:(RCTPromiseRejectBlock)reject) {
365
+ dispatch_async(dispatch_get_main_queue(), ^{
366
+ BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"] ||
367
+ [self.connectedDeviceName.lowercaseString containsString:@"emulat"];
368
+ resolve(@(isEmu));
369
+ });
370
+ }
371
+
372
+ RCT_EXPORT_METHOD(isDeviceConnected:(RCTPromiseResolveBlock)resolve
373
+ rejecter:(RCTPromiseRejectBlock)reject) {
374
+ dispatch_async(dispatch_get_main_queue(), ^{
375
+ resolve(@(self.isConnected));
376
+ });
377
+ }
378
+
379
+ RCT_EXPORT_METHOD(getConnectedDeviceInfo:(RCTPromiseResolveBlock)resolve
380
+ rejecter:(RCTPromiseRejectBlock)reject) {
381
+ dispatch_async(dispatch_get_main_queue(), ^{
382
+ resolve(self.connectedDeviceName ?: @"Not connected");
383
+ });
384
+ }
385
+
386
+ RCT_EXPORT_METHOD(isSDKDownloaded:(RCTPromiseResolveBlock)resolve
387
+ rejecter:(RCTPromiseRejectBlock)reject) {
388
+ #if FLIR_SDK_AVAILABLE
389
+ resolve(@(YES));
390
+ #else
391
+ resolve(@(NO));
392
+ #endif
393
+ }
394
+
395
+ RCT_EXPORT_METHOD(getSDKStatus:(RCTPromiseResolveBlock)resolve
396
+ rejecter:(RCTPromiseRejectBlock)reject) {
397
+ NSDictionary *status = @{
398
+ #if FLIR_SDK_AVAILABLE
399
+ @"available": @(YES),
400
+ #else
401
+ @"available": @(NO),
402
+ #endif
403
+ @"arch": @"arm64",
404
+ @"platform": @"iOS"
405
+ };
406
+ resolve(status);
407
+ }
408
+
409
+ #pragma mark - Emulator
410
+
411
+ RCT_EXPORT_METHOD(startEmulator:(NSString *)emulatorType
412
+ resolver:(RCTPromiseResolveBlock)resolve
413
+ rejecter:(RCTPromiseRejectBlock)reject) {
414
+ #if FLIR_SDK_AVAILABLE
415
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
416
+ FLIRCameraType cameraType = FLIRCameraType_flirOne;
417
+ if ([emulatorType.lowercaseString containsString:@"edge"]) {
418
+ cameraType = FLIRCameraType_flirOneEdge;
419
+ } else if ([emulatorType.lowercaseString containsString:@"pro"]) {
420
+ cameraType = FLIRCameraType_flirOneEdgePro;
421
+ }
422
+
423
+ FLIRIdentity *emulatorIdentity = [[FLIRIdentity alloc] initWithEmulatorType:cameraType];
424
+ if (emulatorIdentity) {
425
+ self.identityMap[[emulatorIdentity deviceId]] = emulatorIdentity;
426
+
427
+ [self performConnectionWithIdentity:emulatorIdentity completion:^(BOOL success, NSError *error) {
428
+ dispatch_async(dispatch_get_main_queue(), ^{
429
+ if (success) {
430
+ resolve(@(YES));
431
+ } else {
432
+ reject(@"ERR_EMULATOR_FAILED", error.localizedDescription ?: @"Emulator start failed", error);
433
+ }
434
+ });
435
+ }];
436
+ } else {
437
+ dispatch_async(dispatch_get_main_queue(), ^{
438
+ reject(@"ERR_EMULATOR_INIT", @"Failed to create emulator identity", nil);
439
+ });
440
+ }
441
+ });
442
+ #else
443
+ reject(@"ERR_FLIR_NOT_AVAILABLE", @"FLIR SDK not available", nil);
444
+ #endif
445
+ }
446
+
447
+ #pragma mark - Debug
448
+
449
+ RCT_EXPORT_METHOD(initializeSDK:(RCTPromiseResolveBlock)resolve
450
+ rejecter:(RCTPromiseRejectBlock)reject) {
451
+ NSDictionary *result = @{
452
+ #if FLIR_SDK_AVAILABLE
453
+ @"initialized": @(YES),
454
+ @"message": @"SDK initialized successfully"
455
+ #else
456
+ @"initialized": @(NO),
457
+ @"message": @"SDK not available - built without FLIR"
458
+ #endif
459
+ };
460
+ resolve(result);
461
+ }
462
+
463
+ RCT_EXPORT_METHOD(getDebugInfo:(RCTPromiseResolveBlock)resolve
464
+ rejecter:(RCTPromiseRejectBlock)reject) {
465
+ dispatch_async(dispatch_get_main_queue(), ^{
466
+ NSDictionary *info = @{
467
+ #if FLIR_SDK_AVAILABLE
468
+ @"sdkAvailable": @(YES),
469
+ #else
470
+ @"sdkAvailable": @(NO),
471
+ #endif
472
+ @"arch": @"arm64",
473
+ @"discoveredDeviceCount": @(self.discoveredDevices.count),
474
+ @"isConnected": @(self.isConnected),
475
+ @"isStreaming": @(self.isStreaming),
476
+ @"connectedDevice": self.connectedDeviceName ?: @"None"
477
+ };
478
+ resolve(info);
479
+ });
480
+ }
481
+
482
+ RCT_EXPORT_METHOD(getLatestFramePath:(RCTPromiseResolveBlock)resolve
483
+ rejecter:(RCTPromiseRejectBlock)reject) {
484
+ dispatch_async(dispatch_get_main_queue(), ^{
485
+ UIImage *image = [FlirState shared].latestImage;
486
+ if (!image) {
487
+ resolve([NSNull null]);
488
+ return;
489
+ }
490
+
491
+ NSData *jpegData = UIImageJPEGRepresentation(image, 0.9);
492
+ if (!jpegData) {
493
+ resolve([NSNull null]);
494
+ return;
495
+ }
496
+
497
+ NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:
498
+ [NSString stringWithFormat:@"flir_frame_%lld.jpg", (long long)[[NSDate date] timeIntervalSince1970] * 1000]];
499
+ [jpegData writeToFile:tempPath atomically:YES];
500
+ resolve(tempPath);
501
+ });
502
+ }
503
+
504
+ #pragma mark - Helper Methods
505
+
506
+ - (void)emitDeviceConnected {
507
+ BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
508
+
509
+ NSDictionary *body = @{
510
+ @"identity": @{
511
+ @"deviceId": self.connectedDeviceId ?: @"Unknown",
512
+ @"isEmulator": @(isEmu)
513
+ },
514
+ @"deviceType": isEmu ? @"emulator" : @"device",
515
+ @"isEmulator": @(isEmu),
516
+ @"state": @"connected"
517
+ };
518
+
519
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceConnected" body:body];
520
+ }
521
+
522
+ - (void)emitStateChange:(NSString *)state {
523
+ BOOL isEmu = [self.connectedDeviceName.lowercaseString containsString:@"emulator"];
524
+
525
+ NSDictionary *body = @{
526
+ @"state": state,
527
+ @"isConnected": @(self.isConnected),
528
+ @"isStreaming": @(self.isStreaming),
529
+ @"isEmulator": @(isEmu),
530
+ @"deviceName": self.connectedDeviceName ?: @"",
531
+ @"deviceId": self.connectedDeviceId ?: @""
532
+ };
533
+
534
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirStateChanged" body:body];
535
+ }
536
+
537
+ #if FLIR_SDK_AVAILABLE
538
+ - (NSString *)communicationInterfaceName:(FLIRCommunicationInterface)iface {
539
+ if (iface & FLIRCommunicationInterfaceLightning) return @"LIGHTNING";
540
+ if (iface & FLIRCommunicationInterfaceNetwork) return @"NETWORK";
541
+ if (iface & FLIRCommunicationInterfaceFlirOneWireless) return @"WIRELESS";
542
+ if (iface & FLIRCommunicationInterfaceEmulator) return @"EMULATOR";
543
+ if (iface & FLIRCommunicationInterfaceUSB) return @"USB";
544
+ return @"UNKNOWN";
545
+ }
546
+ #endif
547
+
548
+ #pragma mark - FLIRDiscoveryEventDelegate
549
+
550
+ #if FLIR_SDK_AVAILABLE
551
+ - (void)cameraDiscovered:(FLIRDiscoveredCamera *)discoveredCamera {
552
+ FLIRIdentity *identity = discoveredCamera.identity;
553
+ NSString *deviceId = [identity deviceId];
554
+
555
+ RCTLogInfo(@"[FlirModule] Camera discovered: %@", deviceId);
556
+
557
+ // Store identity
558
+ self.identityMap[deviceId] = identity;
559
+
560
+ // Create device info
561
+ NSDictionary *deviceInfo = @{
562
+ @"id": deviceId,
563
+ @"name": discoveredCamera.displayName ?: deviceId,
564
+ @"communicationType": [self communicationInterfaceName:[identity communicationInterface]],
565
+ @"isEmulator": @([identity communicationInterface] == FLIRCommunicationInterfaceEmulator)
566
+ };
567
+
568
+ // Add if not already present
569
+ BOOL found = NO;
570
+ for (NSDictionary *existing in self.discoveredDevices) {
571
+ if ([existing[@"id"] isEqualToString:deviceId]) {
572
+ found = YES;
573
+ break;
574
+ }
575
+ }
576
+
577
+ if (!found) {
578
+ [self.discoveredDevices addObject:deviceInfo];
579
+ }
580
+
581
+ // Emit devices found event
582
+ dispatch_async(dispatch_get_main_queue(), ^{
583
+ NSDictionary *body = @{
584
+ @"devices": self.discoveredDevices,
585
+ @"count": @(self.discoveredDevices.count)
586
+ };
587
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
588
+ });
589
+ }
590
+
591
+ - (void)cameraLost:(FLIRIdentity *)cameraIdentity {
592
+ NSString *deviceId = [cameraIdentity deviceId];
593
+ RCTLogInfo(@"[FlirModule] Camera lost: %@", deviceId);
594
+
595
+ [self.identityMap removeObjectForKey:deviceId];
596
+
597
+ NSMutableArray *toRemove = [NSMutableArray new];
598
+ for (NSDictionary *device in self.discoveredDevices) {
599
+ if ([device[@"id"] isEqualToString:deviceId]) {
600
+ [toRemove addObject:device];
601
+ }
602
+ }
603
+ [self.discoveredDevices removeObjectsInArray:toRemove];
604
+
605
+ // If this was our connected device, handle disconnect
606
+ if ([self.connectedDeviceId isEqualToString:deviceId]) {
607
+ dispatch_async(dispatch_get_main_queue(), ^{
608
+ [self stopStreamInternal];
609
+ self.camera = nil;
610
+ self.connectedIdentity = nil;
611
+ self.connectedDeviceId = nil;
612
+ self.connectedDeviceName = nil;
613
+ self.isConnected = NO;
614
+ self.isStreaming = NO;
615
+
616
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
617
+ [self emitStateChange:@"disconnected"];
618
+ });
619
+ }
620
+
621
+ dispatch_async(dispatch_get_main_queue(), ^{
622
+ NSDictionary *body = @{
623
+ @"devices": self.discoveredDevices,
624
+ @"count": @(self.discoveredDevices.count)
625
+ };
626
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDevicesFound" body:body];
627
+ });
628
+ }
629
+
630
+ - (void)discoveryError:(NSString *)error netServiceError:(int)nsnetserviceserror on:(FLIRCommunicationInterface)iface {
631
+ RCTLogError(@"[FlirModule] Discovery error: %@ (%d)", error, nsnetserviceserror);
632
+
633
+ dispatch_async(dispatch_get_main_queue(), ^{
634
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
635
+ @"error": error ?: @"Unknown discovery error",
636
+ @"type": @"discovery"
637
+ }];
638
+ });
639
+ }
640
+
641
+ - (void)discoveryFinished:(FLIRCommunicationInterface)iface {
642
+ RCTLogInfo(@"[FlirModule] Discovery finished");
643
+ self.isScanning = NO;
644
+ }
645
+ #endif
646
+
647
+ #pragma mark - FLIRDataReceivedDelegate
648
+
649
+ #if FLIR_SDK_AVAILABLE
650
+ - (void)onDisconnected:(FLIRCamera *)camera withError:(NSError *)error {
651
+ RCTLogInfo(@"[FlirModule] Camera disconnected: %@", error.localizedDescription);
652
+
653
+ dispatch_async(dispatch_get_main_queue(), ^{
654
+ self.isConnected = NO;
655
+ self.isStreaming = NO;
656
+ self.connectedDeviceId = nil;
657
+ self.connectedDeviceName = nil;
658
+
659
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirDeviceDisconnected" body:@{}];
660
+ [self emitStateChange:@"disconnected"];
661
+ });
662
+ }
663
+ #endif
664
+
665
+ #pragma mark - FLIRStreamDelegate
666
+
667
+ #if FLIR_SDK_AVAILABLE
668
+ - (void)onError:(NSError *)error {
669
+ RCTLogError(@"[FlirModule] Stream error: %@", error.localizedDescription);
670
+
671
+ dispatch_async(dispatch_get_main_queue(), ^{
672
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirError" body:@{
673
+ @"error": error.localizedDescription ?: @"Stream error",
674
+ @"type": @"stream"
675
+ }];
676
+ });
677
+ }
678
+
679
+ - (void)onImageReceived {
680
+ if (!self.streamer) return;
681
+
682
+ NSError *error = nil;
683
+ if ([self.streamer update:&error]) {
684
+ UIImage *image = [self.streamer getImage];
685
+ if (image) {
686
+ // Update shared state
687
+ [[FlirState shared] updateFrame:image];
688
+
689
+ // Get temperature from thermal image if available
690
+ [self.streamer withThermalImage:^(FLIRThermalImage *thermalImage) {
691
+ FLIRImageStatistics *stats = [thermalImage getStatistics];
692
+ if (stats) {
693
+ self.lastTemperature = [[stats getMax] value];
694
+ [FlirState shared].lastTemperature = self.lastTemperature;
695
+ }
696
+ }];
697
+
698
+ // Emit frame received event (rate-limited by RN event queue)
699
+ dispatch_async(dispatch_get_main_queue(), ^{
700
+ [[FlirEventEmitter shared] sendDeviceEvent:@"FlirFrameReceived" body:@{
701
+ @"width": @(image.size.width),
702
+ @"height": @(image.size.height),
703
+ @"timestamp": @([[NSDate date] timeIntervalSince1970] * 1000)
704
+ }];
705
+ });
706
+ }
707
+ } else {
708
+ RCTLogError(@"[FlirModule] Streamer update error: %@", error.localizedDescription);
709
+ }
710
+ }
711
+ #endif
712
+
713
+ @end