ilabs-flir 2.0.4 → 2.0.6

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 +70 -71
  30. package/react-native.config.js +14 -14
  31. package/scripts/fetch-binaries.js +103 -17
  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,599 +1,599 @@
1
- //
2
- // FlirManager.swift
3
- // Flir
4
- //
5
- // Core FLIR camera manager for iOS - handles discovery, connection, and streaming
6
- // Mirrors the Android FlirManager.kt functionality
7
- //
8
-
9
- import Foundation
10
- import UIKit
11
-
12
- #if FLIR_ENABLED
13
- import ThermalSDK
14
- #endif
15
-
16
- /// Device info structure for discovered cameras
17
- @objc public class FlirDeviceInfo: NSObject {
18
- @objc public let deviceId: String
19
- @objc public let name: String
20
- @objc public let communicationType: String
21
- @objc public let isEmulator: Bool
22
-
23
- init(deviceId: String, name: String, communicationType: String, isEmulator: Bool) {
24
- self.deviceId = deviceId
25
- self.name = name
26
- self.communicationType = communicationType
27
- self.isEmulator = isEmulator
28
- }
29
-
30
- @objc public func toDictionary() -> [String: Any] {
31
- return [
32
- "id": deviceId,
33
- "name": name,
34
- "communicationType": communicationType,
35
- "isEmulator": isEmulator
36
- ]
37
- }
38
- }
39
-
40
- /// Callback protocol for FlirManager events
41
- @objc public protocol FlirManagerDelegate: AnyObject {
42
- func onDevicesFound(_ devices: [FlirDeviceInfo])
43
- func onDeviceConnected(_ device: FlirDeviceInfo)
44
- func onDeviceDisconnected()
45
- func onFrameReceived(_ image: UIImage, width: Int, height: Int)
46
- func onError(_ message: String)
47
- func onStateChanged(_ state: String, isConnected: Bool, isStreaming: Bool, isEmulator: Bool)
48
- }
49
-
50
- /// Main FLIR Manager - Singleton that manages all FLIR camera operations
51
- @objc public class FlirManager: NSObject {
52
- @objc public static let shared = FlirManager()
53
-
54
- // MARK: - Properties
55
- @objc public weak var delegate: FlirManagerDelegate?
56
-
57
- private var isInitialized = false
58
- private var isScanning = false
59
- private var _isConnected = false
60
- private var _isStreaming = false
61
- private var connectedDeviceId: String?
62
- private var connectedDeviceName: String?
63
-
64
- // Latest frame for texture updates
65
- private var _latestImage: UIImage?
66
- @objc public var latestImage: UIImage? { return _latestImage }
67
-
68
- // Temperature data
69
- private var lastTemperature: Double = Double.nan
70
-
71
- // Discovered devices
72
- private var discoveredDevices: [FlirDeviceInfo] = []
73
-
74
- #if FLIR_ENABLED
75
- private var discovery: FLIRDiscovery?
76
- private var camera: FLIRCamera?
77
- private var stream: FLIRStream?
78
- private var streamer: FLIRThermalStreamer?
79
- private var connectedIdentity: FLIRIdentity?
80
- #endif
81
-
82
- private override init() {
83
- super.init()
84
- NSLog("[FlirManager] Initialized")
85
- }
86
-
87
- // MARK: - Public State Accessors
88
-
89
- @objc public var isConnected: Bool { return _isConnected }
90
- @objc public var isStreaming: Bool { return _isStreaming }
91
- @objc public var isEmulator: Bool {
92
- return connectedDeviceName?.lowercased().contains("emulator") == true ||
93
- connectedDeviceName?.lowercased().contains("emulat") == true
94
- }
95
-
96
- @objc public func getConnectedDeviceInfo() -> String {
97
- return connectedDeviceName ?? "Not connected"
98
- }
99
-
100
- @objc public func getDiscoveredDevices() -> [FlirDeviceInfo] {
101
- return discoveredDevices
102
- }
103
-
104
- // MARK: - SDK Availability
105
-
106
- @objc public static var isSDKAvailable: Bool {
107
- #if FLIR_ENABLED
108
- return true
109
- #else
110
- return false
111
- #endif
112
- }
113
-
114
- // MARK: - Discovery
115
-
116
- @objc public func startDiscovery() {
117
- NSLog("[FlirManager] Starting discovery...")
118
-
119
- #if FLIR_ENABLED
120
- if isScanning {
121
- NSLog("[FlirManager] Already scanning")
122
- return
123
- }
124
-
125
- isScanning = true
126
- discoveredDevices.removeAll()
127
-
128
- if discovery == nil {
129
- discovery = FLIRDiscovery()
130
- discovery?.delegate = self
131
- }
132
-
133
- // Start discovery on all available interfaces
134
- let interfaces: FLIRCommunicationInterface = [
135
- .lightning,
136
- .network,
137
- .flirOneWireless,
138
- .emulator
139
- ]
140
- discovery?.start(interfaces)
141
-
142
- emitStateChange("discovering")
143
- NSLog("[FlirManager] Discovery started on interfaces: Lightning, Network, FlirOneWireless, Emulator")
144
- #else
145
- NSLog("[FlirManager] FLIR SDK not available - discovery disabled")
146
- delegate?.onError("FLIR SDK not available")
147
- #endif
148
- }
149
-
150
- @objc public func stopDiscovery() {
151
- NSLog("[FlirManager] Stopping discovery...")
152
-
153
- #if FLIR_ENABLED
154
- discovery?.stop()
155
- isScanning = false
156
- NSLog("[FlirManager] Discovery stopped")
157
- #endif
158
- }
159
-
160
- // MARK: - Connection
161
-
162
- @objc public func connectToDevice(_ deviceId: String) {
163
- NSLog("[FlirManager] Connecting to device: \(deviceId)")
164
-
165
- #if FLIR_ENABLED
166
- // Find the identity for this device
167
- guard let identity = findIdentity(for: deviceId) else {
168
- NSLog("[FlirManager] Device not found: \(deviceId)")
169
- delegate?.onError("Device not found: \(deviceId)")
170
- return
171
- }
172
-
173
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
174
- self?.performConnection(identity: identity)
175
- }
176
- #else
177
- delegate?.onError("FLIR SDK not available")
178
- #endif
179
- }
180
-
181
- #if FLIR_ENABLED
182
- private var identityMap: [String: FLIRIdentity] = [:]
183
-
184
- private func findIdentity(for deviceId: String) -> FLIRIdentity? {
185
- return identityMap[deviceId]
186
- }
187
-
188
- private func performConnection(identity: FLIRIdentity) {
189
- do {
190
- if camera == nil {
191
- camera = FLIRCamera()
192
- camera?.delegate = self
193
- }
194
-
195
- // Handle authentication for generic cameras
196
- if identity.cameraType() == .generic {
197
- let certName = getCertificateName()
198
- var status = FLIRAuthenticationStatus.pending
199
- while status == .pending {
200
- status = camera!.authenticate(identity, trustedConnectionName: certName)
201
- if status == .pending {
202
- NSLog("[FlirManager] Waiting for camera authentication approval...")
203
- Thread.sleep(forTimeInterval: 1.0)
204
- }
205
- }
206
- }
207
-
208
- // Connect
209
- try camera?.connect(identity)
210
-
211
- connectedIdentity = identity
212
- connectedDeviceId = identity.deviceId()
213
- connectedDeviceName = identity.deviceId()
214
- _isConnected = true
215
-
216
- NSLog("[FlirManager] Connected to: \(identity.deviceId())")
217
-
218
- // Get streams
219
- if let streams = camera?.getStreams(), !streams.isEmpty {
220
- NSLog("[FlirManager] Found \(streams.count) streams")
221
-
222
- // Auto-start first thermal stream
223
- if let firstStream = streams.first {
224
- startStreamInternal(firstStream)
225
- }
226
- }
227
-
228
- DispatchQueue.main.async { [weak self] in
229
- guard let self = self else { return }
230
- let deviceInfo = FlirDeviceInfo(
231
- deviceId: identity.deviceId(),
232
- name: identity.deviceId(),
233
- communicationType: self.communicationInterfaceName(identity.communicationInterface()),
234
- isEmulator: identity.communicationInterface() == .emulator
235
- )
236
- self.delegate?.onDeviceConnected(deviceInfo)
237
- self.emitStateChange("connected")
238
- }
239
-
240
- } catch {
241
- NSLog("[FlirManager] Connection failed: \(error)")
242
- DispatchQueue.main.async { [weak self] in
243
- self?.delegate?.onError("Connection failed: \(error.localizedDescription)")
244
- }
245
- }
246
- }
247
-
248
- private func getCertificateName() -> String {
249
- let bundleID = Bundle.main.bundleIdentifier ?? "com.flir.app"
250
- let key = "\(bundleID)-cert-name"
251
-
252
- if let existing = UserDefaults.standard.string(forKey: key) {
253
- return existing
254
- }
255
-
256
- let newName = UUID().uuidString
257
- UserDefaults.standard.set(newName, forKey: key)
258
- return newName
259
- }
260
-
261
- private func communicationInterfaceName(_ iface: FLIRCommunicationInterface) -> String {
262
- if iface.contains(.lightning) { return "LIGHTNING" }
263
- if iface.contains(.network) { return "NETWORK" }
264
- if iface.contains(.flirOneWireless) { return "WIRELESS" }
265
- if iface.contains(.emulator) { return "EMULATOR" }
266
- if iface.contains(.usb) { return "USB" }
267
- return "UNKNOWN"
268
- }
269
- #endif
270
-
271
- // MARK: - Streaming
272
-
273
- @objc public func startStream() {
274
- #if FLIR_ENABLED
275
- guard let streams = camera?.getStreams(), !streams.isEmpty else {
276
- NSLog("[FlirManager] No streams available")
277
- return
278
- }
279
- startStreamInternal(streams[0])
280
- #endif
281
- }
282
-
283
- @objc public func stopStream() {
284
- NSLog("[FlirManager] Stopping stream...")
285
-
286
- #if FLIR_ENABLED
287
- stream?.stop()
288
- stream = nil
289
- streamer = nil
290
- _isStreaming = false
291
- emitStateChange("connected")
292
- #endif
293
- }
294
-
295
- #if FLIR_ENABLED
296
- private func startStreamInternal(_ newStream: FLIRStream) {
297
- NSLog("[FlirManager] Starting stream...")
298
-
299
- stream?.stop()
300
- stream = newStream
301
-
302
- if newStream.isThermal {
303
- streamer = FLIRThermalStreamer(stream: newStream)
304
- }
305
-
306
- newStream.delegate = self
307
-
308
- do {
309
- try newStream.start()
310
- _isStreaming = true
311
- emitStateChange("streaming")
312
- NSLog("[FlirManager] Stream started (thermal: \(newStream.isThermal))")
313
- } catch {
314
- NSLog("[FlirManager] Stream start failed: \(error)")
315
- stream = nil
316
- streamer = nil
317
- delegate?.onError("Stream start failed: \(error.localizedDescription)")
318
- }
319
- }
320
- #endif
321
-
322
- // MARK: - Disconnect
323
-
324
- @objc public func disconnect() {
325
- NSLog("[FlirManager] Disconnecting...")
326
-
327
- #if FLIR_ENABLED
328
- stopStream()
329
- camera?.disconnect()
330
- camera = nil
331
- connectedIdentity = nil
332
- connectedDeviceId = nil
333
- connectedDeviceName = nil
334
- _isConnected = false
335
- _isStreaming = false
336
- _latestImage = nil
337
-
338
- DispatchQueue.main.async { [weak self] in
339
- self?.delegate?.onDeviceDisconnected()
340
- self?.emitStateChange("disconnected")
341
- }
342
- #endif
343
- }
344
-
345
- @objc public func stop() {
346
- stopStream()
347
- disconnect()
348
- stopDiscovery()
349
- _latestImage = nil
350
- }
351
-
352
- // MARK: - Temperature
353
-
354
- @objc public func getTemperatureAt(x: Int, y: Int) -> Double {
355
- #if FLIR_ENABLED
356
- // Get temperature from thermal image at point
357
- if let thermalStreamer = streamer {
358
- var temp: Double = Double.nan
359
- thermalStreamer.withThermalImage { thermalImage in
360
- if let measurements = thermalImage.measurements {
361
- // Try to get temperature at point
362
- // For now, return the last known temperature
363
- temp = self.lastTemperature
364
- }
365
- }
366
- return temp
367
- }
368
- #endif
369
- return lastTemperature
370
- }
371
-
372
- @objc public func getLastTemperature() -> Double {
373
- return lastTemperature
374
- }
375
-
376
- // MARK: - Emulator
377
-
378
- @objc public func startEmulator(type: String) {
379
- NSLog("[FlirManager] Starting emulator: \(type)")
380
-
381
- #if FLIR_ENABLED
382
- // Create emulator identity
383
- var cameraType: FLIRCameraType = .flirOne
384
- if type.lowercased().contains("edge") {
385
- cameraType = .flirOneEdge
386
- } else if type.lowercased().contains("pro") {
387
- cameraType = .flirOneEdgePro
388
- }
389
-
390
- if let emulatorIdentity = FLIRIdentity(emulatorType: cameraType) {
391
- discoveredDevices.append(FlirDeviceInfo(
392
- deviceId: emulatorIdentity.deviceId(),
393
- name: "FLIR Emulator",
394
- communicationType: "EMULATOR",
395
- isEmulator: true
396
- ))
397
- identityMap[emulatorIdentity.deviceId()] = emulatorIdentity
398
-
399
- // Auto-connect to emulator
400
- performConnection(identity: emulatorIdentity)
401
- }
402
- #else
403
- delegate?.onError("FLIR SDK not available - emulator disabled")
404
- #endif
405
- }
406
-
407
- // MARK: - State Emission
408
-
409
- private func emitStateChange(_ state: String) {
410
- DispatchQueue.main.async { [weak self] in
411
- guard let self = self else { return }
412
- self.delegate?.onStateChanged(
413
- state,
414
- isConnected: self._isConnected,
415
- isStreaming: self._isStreaming,
416
- isEmulator: self.isEmulator
417
- )
418
- }
419
- }
420
-
421
- // MARK: - Fallback Frame
422
-
423
- /// Generate a fallback gradient image when SDK is not available
424
- @objc public static func generateFallbackFrame(width: Int, height: Int) -> UIImage {
425
- let size = CGSize(width: width, height: height)
426
- UIGraphicsBeginImageContextWithOptions(size, true, 1.0)
427
- defer { UIGraphicsEndImageContext() }
428
-
429
- guard let context = UIGraphicsGetCurrentContext() else {
430
- return UIImage()
431
- }
432
-
433
- // Create a thermal-looking gradient
434
- let colors: [CGColor] = [
435
- UIColor(red: 0.0, green: 0.0, blue: 0.5, alpha: 1.0).cgColor, // Dark blue (cold)
436
- UIColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0).cgColor, // Cyan
437
- UIColor(red: 0.0, green: 0.8, blue: 0.0, alpha: 1.0).cgColor, // Green
438
- UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0).cgColor, // Yellow
439
- UIColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0).cgColor, // Orange
440
- UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0).cgColor, // Red (hot)
441
- UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0).cgColor // White (hottest)
442
- ]
443
-
444
- let colorSpace = CGColorSpaceCreateDeviceRGB()
445
- let locations: [CGFloat] = [0.0, 0.15, 0.3, 0.5, 0.7, 0.85, 1.0]
446
-
447
- if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) {
448
- context.drawLinearGradient(
449
- gradient,
450
- start: CGPoint(x: 0, y: size.height),
451
- end: CGPoint(x: size.width, y: 0),
452
- options: []
453
- )
454
- }
455
-
456
- // Add "FALLBACK" text
457
- let text = "FLIR FALLBACK"
458
- let attributes: [NSAttributedString.Key: Any] = [
459
- .font: UIFont.boldSystemFont(ofSize: 14),
460
- .foregroundColor: UIColor.white
461
- ]
462
- let textSize = text.size(withAttributes: attributes)
463
- let textRect = CGRect(
464
- x: (size.width - textSize.width) / 2,
465
- y: (size.height - textSize.height) / 2,
466
- width: textSize.width,
467
- height: textSize.height
468
- )
469
- text.draw(in: textRect, withAttributes: attributes)
470
-
471
- return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
472
- }
473
- }
474
-
475
- // MARK: - FLIRDiscoveryEventDelegate
476
-
477
- #if FLIR_ENABLED
478
- extension FlirManager: FLIRDiscoveryEventDelegate {
479
- public func cameraDiscovered(_ discoveredCamera: FLIRDiscoveredCamera) {
480
- let identity = discoveredCamera.identity
481
- let deviceId = identity.deviceId()
482
-
483
- NSLog("[FlirManager] Camera discovered: \(deviceId)")
484
-
485
- // Store identity for later connection
486
- identityMap[deviceId] = identity
487
-
488
- // Create device info
489
- let deviceInfo = FlirDeviceInfo(
490
- deviceId: deviceId,
491
- name: discoveredCamera.displayName ?? deviceId,
492
- communicationType: communicationInterfaceName(identity.communicationInterface()),
493
- isEmulator: identity.communicationInterface() == .emulator
494
- )
495
-
496
- // Add to discovered list if not already present
497
- if !discoveredDevices.contains(where: { $0.deviceId == deviceId }) {
498
- discoveredDevices.append(deviceInfo)
499
- }
500
-
501
- // Notify delegate
502
- DispatchQueue.main.async { [weak self] in
503
- guard let self = self else { return }
504
- self.delegate?.onDevicesFound(self.discoveredDevices)
505
- }
506
- }
507
-
508
- public func cameraLost(_ cameraIdentity: FLIRIdentity) {
509
- let deviceId = cameraIdentity.deviceId()
510
- NSLog("[FlirManager] Camera lost: \(deviceId)")
511
-
512
- identityMap.removeValue(forKey: deviceId)
513
- discoveredDevices.removeAll { $0.deviceId == deviceId }
514
-
515
- // If this was our connected device, handle disconnect
516
- if connectedDeviceId == deviceId {
517
- disconnect()
518
- }
519
-
520
- DispatchQueue.main.async { [weak self] in
521
- guard let self = self else { return }
522
- self.delegate?.onDevicesFound(self.discoveredDevices)
523
- }
524
- }
525
-
526
- public func discoveryError(_ error: String, netServiceError nsnetserviceserror: Int32, on iface: FLIRCommunicationInterface) {
527
- NSLog("[FlirManager] Discovery error: \(error) (\(nsnetserviceserror)) on interface: \(iface)")
528
-
529
- DispatchQueue.main.async { [weak self] in
530
- self?.delegate?.onError("Discovery error: \(error)")
531
- }
532
- }
533
-
534
- public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
535
- NSLog("[FlirManager] Discovery finished on interface: \(iface)")
536
- isScanning = false
537
- }
538
- }
539
-
540
- // MARK: - FLIRDataReceivedDelegate
541
-
542
- extension FlirManager: FLIRDataReceivedDelegate {
543
- public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
544
- NSLog("[FlirManager] Camera disconnected: \(error?.localizedDescription ?? "no error")")
545
-
546
- _isConnected = false
547
- _isStreaming = false
548
- connectedDeviceId = nil
549
- connectedDeviceName = nil
550
-
551
- DispatchQueue.main.async { [weak self] in
552
- self?.delegate?.onDeviceDisconnected()
553
- self?.emitStateChange("disconnected")
554
- }
555
- }
556
- }
557
-
558
- // MARK: - FLIRStreamDelegate
559
-
560
- extension FlirManager: FLIRStreamDelegate {
561
- public func onError(_ error: Error) {
562
- NSLog("[FlirManager] Stream error: \(error)")
563
-
564
- DispatchQueue.main.async { [weak self] in
565
- self?.delegate?.onError("Stream error: \(error.localizedDescription)")
566
- }
567
- }
568
-
569
- public func onImageReceived() {
570
- guard let streamer = streamer else { return }
571
-
572
- do {
573
- try streamer.update()
574
-
575
- if let image = streamer.getImage() {
576
- _latestImage = image
577
-
578
- // Get temperature from thermal image
579
- streamer.withThermalImage { [weak self] thermalImage in
580
- if let stats = thermalImage.getStatistics() {
581
- self?.lastTemperature = stats.getMax().value
582
- }
583
- }
584
-
585
- DispatchQueue.main.async { [weak self] in
586
- guard let self = self else { return }
587
- self.delegate?.onFrameReceived(
588
- image,
589
- width: Int(image.size.width),
590
- height: Int(image.size.height)
591
- )
592
- }
593
- }
594
- } catch {
595
- NSLog("[FlirManager] Streamer update error: \(error)")
596
- }
597
- }
598
- }
599
- #endif
1
+ //
2
+ // FlirManager.swift
3
+ // Flir
4
+ //
5
+ // Core FLIR camera manager for iOS - handles discovery, connection, and streaming
6
+ // Mirrors the Android FlirManager.kt functionality
7
+ //
8
+
9
+ import Foundation
10
+ import UIKit
11
+
12
+ #if FLIR_ENABLED
13
+ import ThermalSDK
14
+ #endif
15
+
16
+ /// Device info structure for discovered cameras
17
+ @objc public class FlirDeviceInfo: NSObject {
18
+ @objc public let deviceId: String
19
+ @objc public let name: String
20
+ @objc public let communicationType: String
21
+ @objc public let isEmulator: Bool
22
+
23
+ init(deviceId: String, name: String, communicationType: String, isEmulator: Bool) {
24
+ self.deviceId = deviceId
25
+ self.name = name
26
+ self.communicationType = communicationType
27
+ self.isEmulator = isEmulator
28
+ }
29
+
30
+ @objc public func toDictionary() -> [String: Any] {
31
+ return [
32
+ "id": deviceId,
33
+ "name": name,
34
+ "communicationType": communicationType,
35
+ "isEmulator": isEmulator
36
+ ]
37
+ }
38
+ }
39
+
40
+ /// Callback protocol for FlirManager events
41
+ @objc public protocol FlirManagerDelegate: AnyObject {
42
+ func onDevicesFound(_ devices: [FlirDeviceInfo])
43
+ func onDeviceConnected(_ device: FlirDeviceInfo)
44
+ func onDeviceDisconnected()
45
+ func onFrameReceived(_ image: UIImage, width: Int, height: Int)
46
+ func onError(_ message: String)
47
+ func onStateChanged(_ state: String, isConnected: Bool, isStreaming: Bool, isEmulator: Bool)
48
+ }
49
+
50
+ /// Main FLIR Manager - Singleton that manages all FLIR camera operations
51
+ @objc public class FlirManager: NSObject {
52
+ @objc public static let shared = FlirManager()
53
+
54
+ // MARK: - Properties
55
+ @objc public weak var delegate: FlirManagerDelegate?
56
+
57
+ private var isInitialized = false
58
+ private var isScanning = false
59
+ private var _isConnected = false
60
+ private var _isStreaming = false
61
+ private var connectedDeviceId: String?
62
+ private var connectedDeviceName: String?
63
+
64
+ // Latest frame for texture updates
65
+ private var _latestImage: UIImage?
66
+ @objc public var latestImage: UIImage? { return _latestImage }
67
+
68
+ // Temperature data
69
+ private var lastTemperature: Double = Double.nan
70
+
71
+ // Discovered devices
72
+ private var discoveredDevices: [FlirDeviceInfo] = []
73
+
74
+ #if FLIR_ENABLED
75
+ private var discovery: FLIRDiscovery?
76
+ private var camera: FLIRCamera?
77
+ private var stream: FLIRStream?
78
+ private var streamer: FLIRThermalStreamer?
79
+ private var connectedIdentity: FLIRIdentity?
80
+ #endif
81
+
82
+ private override init() {
83
+ super.init()
84
+ NSLog("[FlirManager] Initialized")
85
+ }
86
+
87
+ // MARK: - Public State Accessors
88
+
89
+ @objc public var isConnected: Bool { return _isConnected }
90
+ @objc public var isStreaming: Bool { return _isStreaming }
91
+ @objc public var isEmulator: Bool {
92
+ return connectedDeviceName?.lowercased().contains("emulator") == true ||
93
+ connectedDeviceName?.lowercased().contains("emulat") == true
94
+ }
95
+
96
+ @objc public func getConnectedDeviceInfo() -> String {
97
+ return connectedDeviceName ?? "Not connected"
98
+ }
99
+
100
+ @objc public func getDiscoveredDevices() -> [FlirDeviceInfo] {
101
+ return discoveredDevices
102
+ }
103
+
104
+ // MARK: - SDK Availability
105
+
106
+ @objc public static var isSDKAvailable: Bool {
107
+ #if FLIR_ENABLED
108
+ return true
109
+ #else
110
+ return false
111
+ #endif
112
+ }
113
+
114
+ // MARK: - Discovery
115
+
116
+ @objc public func startDiscovery() {
117
+ NSLog("[FlirManager] Starting discovery...")
118
+
119
+ #if FLIR_ENABLED
120
+ if isScanning {
121
+ NSLog("[FlirManager] Already scanning")
122
+ return
123
+ }
124
+
125
+ isScanning = true
126
+ discoveredDevices.removeAll()
127
+
128
+ if discovery == nil {
129
+ discovery = FLIRDiscovery()
130
+ discovery?.delegate = self
131
+ }
132
+
133
+ // Start discovery on all available interfaces
134
+ let interfaces: FLIRCommunicationInterface = [
135
+ .lightning,
136
+ .network,
137
+ .flirOneWireless,
138
+ .emulator
139
+ ]
140
+ discovery?.start(interfaces)
141
+
142
+ emitStateChange("discovering")
143
+ NSLog("[FlirManager] Discovery started on interfaces: Lightning, Network, FlirOneWireless, Emulator")
144
+ #else
145
+ NSLog("[FlirManager] FLIR SDK not available - discovery disabled")
146
+ delegate?.onError("FLIR SDK not available")
147
+ #endif
148
+ }
149
+
150
+ @objc public func stopDiscovery() {
151
+ NSLog("[FlirManager] Stopping discovery...")
152
+
153
+ #if FLIR_ENABLED
154
+ discovery?.stop()
155
+ isScanning = false
156
+ NSLog("[FlirManager] Discovery stopped")
157
+ #endif
158
+ }
159
+
160
+ // MARK: - Connection
161
+
162
+ @objc public func connectToDevice(_ deviceId: String) {
163
+ NSLog("[FlirManager] Connecting to device: \(deviceId)")
164
+
165
+ #if FLIR_ENABLED
166
+ // Find the identity for this device
167
+ guard let identity = findIdentity(for: deviceId) else {
168
+ NSLog("[FlirManager] Device not found: \(deviceId)")
169
+ delegate?.onError("Device not found: \(deviceId)")
170
+ return
171
+ }
172
+
173
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
174
+ self?.performConnection(identity: identity)
175
+ }
176
+ #else
177
+ delegate?.onError("FLIR SDK not available")
178
+ #endif
179
+ }
180
+
181
+ #if FLIR_ENABLED
182
+ private var identityMap: [String: FLIRIdentity] = [:]
183
+
184
+ private func findIdentity(for deviceId: String) -> FLIRIdentity? {
185
+ return identityMap[deviceId]
186
+ }
187
+
188
+ private func performConnection(identity: FLIRIdentity) {
189
+ do {
190
+ if camera == nil {
191
+ camera = FLIRCamera()
192
+ camera?.delegate = self
193
+ }
194
+
195
+ // Handle authentication for generic cameras
196
+ if identity.cameraType() == .generic {
197
+ let certName = getCertificateName()
198
+ var status = FLIRAuthenticationStatus.pending
199
+ while status == .pending {
200
+ status = camera!.authenticate(identity, trustedConnectionName: certName)
201
+ if status == .pending {
202
+ NSLog("[FlirManager] Waiting for camera authentication approval...")
203
+ Thread.sleep(forTimeInterval: 1.0)
204
+ }
205
+ }
206
+ }
207
+
208
+ // Connect
209
+ try camera?.connect(identity)
210
+
211
+ connectedIdentity = identity
212
+ connectedDeviceId = identity.deviceId()
213
+ connectedDeviceName = identity.deviceId()
214
+ _isConnected = true
215
+
216
+ NSLog("[FlirManager] Connected to: \(identity.deviceId())")
217
+
218
+ // Get streams
219
+ if let streams = camera?.getStreams(), !streams.isEmpty {
220
+ NSLog("[FlirManager] Found \(streams.count) streams")
221
+
222
+ // Auto-start first thermal stream
223
+ if let firstStream = streams.first {
224
+ startStreamInternal(firstStream)
225
+ }
226
+ }
227
+
228
+ DispatchQueue.main.async { [weak self] in
229
+ guard let self = self else { return }
230
+ let deviceInfo = FlirDeviceInfo(
231
+ deviceId: identity.deviceId(),
232
+ name: identity.deviceId(),
233
+ communicationType: self.communicationInterfaceName(identity.communicationInterface()),
234
+ isEmulator: identity.communicationInterface() == .emulator
235
+ )
236
+ self.delegate?.onDeviceConnected(deviceInfo)
237
+ self.emitStateChange("connected")
238
+ }
239
+
240
+ } catch {
241
+ NSLog("[FlirManager] Connection failed: \(error)")
242
+ DispatchQueue.main.async { [weak self] in
243
+ self?.delegate?.onError("Connection failed: \(error.localizedDescription)")
244
+ }
245
+ }
246
+ }
247
+
248
+ private func getCertificateName() -> String {
249
+ let bundleID = Bundle.main.bundleIdentifier ?? "com.flir.app"
250
+ let key = "\(bundleID)-cert-name"
251
+
252
+ if let existing = UserDefaults.standard.string(forKey: key) {
253
+ return existing
254
+ }
255
+
256
+ let newName = UUID().uuidString
257
+ UserDefaults.standard.set(newName, forKey: key)
258
+ return newName
259
+ }
260
+
261
+ private func communicationInterfaceName(_ iface: FLIRCommunicationInterface) -> String {
262
+ if iface.contains(.lightning) { return "LIGHTNING" }
263
+ if iface.contains(.network) { return "NETWORK" }
264
+ if iface.contains(.flirOneWireless) { return "WIRELESS" }
265
+ if iface.contains(.emulator) { return "EMULATOR" }
266
+ if iface.contains(.usb) { return "USB" }
267
+ return "UNKNOWN"
268
+ }
269
+ #endif
270
+
271
+ // MARK: - Streaming
272
+
273
+ @objc public func startStream() {
274
+ #if FLIR_ENABLED
275
+ guard let streams = camera?.getStreams(), !streams.isEmpty else {
276
+ NSLog("[FlirManager] No streams available")
277
+ return
278
+ }
279
+ startStreamInternal(streams[0])
280
+ #endif
281
+ }
282
+
283
+ @objc public func stopStream() {
284
+ NSLog("[FlirManager] Stopping stream...")
285
+
286
+ #if FLIR_ENABLED
287
+ stream?.stop()
288
+ stream = nil
289
+ streamer = nil
290
+ _isStreaming = false
291
+ emitStateChange("connected")
292
+ #endif
293
+ }
294
+
295
+ #if FLIR_ENABLED
296
+ private func startStreamInternal(_ newStream: FLIRStream) {
297
+ NSLog("[FlirManager] Starting stream...")
298
+
299
+ stream?.stop()
300
+ stream = newStream
301
+
302
+ if newStream.isThermal {
303
+ streamer = FLIRThermalStreamer(stream: newStream)
304
+ }
305
+
306
+ newStream.delegate = self
307
+
308
+ do {
309
+ try newStream.start()
310
+ _isStreaming = true
311
+ emitStateChange("streaming")
312
+ NSLog("[FlirManager] Stream started (thermal: \(newStream.isThermal))")
313
+ } catch {
314
+ NSLog("[FlirManager] Stream start failed: \(error)")
315
+ stream = nil
316
+ streamer = nil
317
+ delegate?.onError("Stream start failed: \(error.localizedDescription)")
318
+ }
319
+ }
320
+ #endif
321
+
322
+ // MARK: - Disconnect
323
+
324
+ @objc public func disconnect() {
325
+ NSLog("[FlirManager] Disconnecting...")
326
+
327
+ #if FLIR_ENABLED
328
+ stopStream()
329
+ camera?.disconnect()
330
+ camera = nil
331
+ connectedIdentity = nil
332
+ connectedDeviceId = nil
333
+ connectedDeviceName = nil
334
+ _isConnected = false
335
+ _isStreaming = false
336
+ _latestImage = nil
337
+
338
+ DispatchQueue.main.async { [weak self] in
339
+ self?.delegate?.onDeviceDisconnected()
340
+ self?.emitStateChange("disconnected")
341
+ }
342
+ #endif
343
+ }
344
+
345
+ @objc public func stop() {
346
+ stopStream()
347
+ disconnect()
348
+ stopDiscovery()
349
+ _latestImage = nil
350
+ }
351
+
352
+ // MARK: - Temperature
353
+
354
+ @objc public func getTemperatureAt(x: Int, y: Int) -> Double {
355
+ #if FLIR_ENABLED
356
+ // Get temperature from thermal image at point
357
+ if let thermalStreamer = streamer {
358
+ var temp: Double = Double.nan
359
+ thermalStreamer.withThermalImage { thermalImage in
360
+ if let measurements = thermalImage.measurements {
361
+ // Try to get temperature at point
362
+ // For now, return the last known temperature
363
+ temp = self.lastTemperature
364
+ }
365
+ }
366
+ return temp
367
+ }
368
+ #endif
369
+ return lastTemperature
370
+ }
371
+
372
+ @objc public func getLastTemperature() -> Double {
373
+ return lastTemperature
374
+ }
375
+
376
+ // MARK: - Emulator
377
+
378
+ @objc public func startEmulator(type: String) {
379
+ NSLog("[FlirManager] Starting emulator: \(type)")
380
+
381
+ #if FLIR_ENABLED
382
+ // Create emulator identity
383
+ var cameraType: FLIRCameraType = .flirOne
384
+ if type.lowercased().contains("edge") {
385
+ cameraType = .flirOneEdge
386
+ } else if type.lowercased().contains("pro") {
387
+ cameraType = .flirOneEdgePro
388
+ }
389
+
390
+ if let emulatorIdentity = FLIRIdentity(emulatorType: cameraType) {
391
+ discoveredDevices.append(FlirDeviceInfo(
392
+ deviceId: emulatorIdentity.deviceId(),
393
+ name: "FLIR Emulator",
394
+ communicationType: "EMULATOR",
395
+ isEmulator: true
396
+ ))
397
+ identityMap[emulatorIdentity.deviceId()] = emulatorIdentity
398
+
399
+ // Auto-connect to emulator
400
+ performConnection(identity: emulatorIdentity)
401
+ }
402
+ #else
403
+ delegate?.onError("FLIR SDK not available - emulator disabled")
404
+ #endif
405
+ }
406
+
407
+ // MARK: - State Emission
408
+
409
+ private func emitStateChange(_ state: String) {
410
+ DispatchQueue.main.async { [weak self] in
411
+ guard let self = self else { return }
412
+ self.delegate?.onStateChanged(
413
+ state,
414
+ isConnected: self._isConnected,
415
+ isStreaming: self._isStreaming,
416
+ isEmulator: self.isEmulator
417
+ )
418
+ }
419
+ }
420
+
421
+ // MARK: - Fallback Frame
422
+
423
+ /// Generate a fallback gradient image when SDK is not available
424
+ @objc public static func generateFallbackFrame(width: Int, height: Int) -> UIImage {
425
+ let size = CGSize(width: width, height: height)
426
+ UIGraphicsBeginImageContextWithOptions(size, true, 1.0)
427
+ defer { UIGraphicsEndImageContext() }
428
+
429
+ guard let context = UIGraphicsGetCurrentContext() else {
430
+ return UIImage()
431
+ }
432
+
433
+ // Create a thermal-looking gradient
434
+ let colors: [CGColor] = [
435
+ UIColor(red: 0.0, green: 0.0, blue: 0.5, alpha: 1.0).cgColor, // Dark blue (cold)
436
+ UIColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0).cgColor, // Cyan
437
+ UIColor(red: 0.0, green: 0.8, blue: 0.0, alpha: 1.0).cgColor, // Green
438
+ UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0).cgColor, // Yellow
439
+ UIColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0).cgColor, // Orange
440
+ UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0).cgColor, // Red (hot)
441
+ UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0).cgColor // White (hottest)
442
+ ]
443
+
444
+ let colorSpace = CGColorSpaceCreateDeviceRGB()
445
+ let locations: [CGFloat] = [0.0, 0.15, 0.3, 0.5, 0.7, 0.85, 1.0]
446
+
447
+ if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) {
448
+ context.drawLinearGradient(
449
+ gradient,
450
+ start: CGPoint(x: 0, y: size.height),
451
+ end: CGPoint(x: size.width, y: 0),
452
+ options: []
453
+ )
454
+ }
455
+
456
+ // Add "FALLBACK" text
457
+ let text = "FLIR FALLBACK"
458
+ let attributes: [NSAttributedString.Key: Any] = [
459
+ .font: UIFont.boldSystemFont(ofSize: 14),
460
+ .foregroundColor: UIColor.white
461
+ ]
462
+ let textSize = text.size(withAttributes: attributes)
463
+ let textRect = CGRect(
464
+ x: (size.width - textSize.width) / 2,
465
+ y: (size.height - textSize.height) / 2,
466
+ width: textSize.width,
467
+ height: textSize.height
468
+ )
469
+ text.draw(in: textRect, withAttributes: attributes)
470
+
471
+ return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
472
+ }
473
+ }
474
+
475
+ // MARK: - FLIRDiscoveryEventDelegate
476
+
477
+ #if FLIR_ENABLED
478
+ extension FlirManager: FLIRDiscoveryEventDelegate {
479
+ public func cameraDiscovered(_ discoveredCamera: FLIRDiscoveredCamera) {
480
+ let identity = discoveredCamera.identity
481
+ let deviceId = identity.deviceId()
482
+
483
+ NSLog("[FlirManager] Camera discovered: \(deviceId)")
484
+
485
+ // Store identity for later connection
486
+ identityMap[deviceId] = identity
487
+
488
+ // Create device info
489
+ let deviceInfo = FlirDeviceInfo(
490
+ deviceId: deviceId,
491
+ name: discoveredCamera.displayName ?? deviceId,
492
+ communicationType: communicationInterfaceName(identity.communicationInterface()),
493
+ isEmulator: identity.communicationInterface() == .emulator
494
+ )
495
+
496
+ // Add to discovered list if not already present
497
+ if !discoveredDevices.contains(where: { $0.deviceId == deviceId }) {
498
+ discoveredDevices.append(deviceInfo)
499
+ }
500
+
501
+ // Notify delegate
502
+ DispatchQueue.main.async { [weak self] in
503
+ guard let self = self else { return }
504
+ self.delegate?.onDevicesFound(self.discoveredDevices)
505
+ }
506
+ }
507
+
508
+ public func cameraLost(_ cameraIdentity: FLIRIdentity) {
509
+ let deviceId = cameraIdentity.deviceId()
510
+ NSLog("[FlirManager] Camera lost: \(deviceId)")
511
+
512
+ identityMap.removeValue(forKey: deviceId)
513
+ discoveredDevices.removeAll { $0.deviceId == deviceId }
514
+
515
+ // If this was our connected device, handle disconnect
516
+ if connectedDeviceId == deviceId {
517
+ disconnect()
518
+ }
519
+
520
+ DispatchQueue.main.async { [weak self] in
521
+ guard let self = self else { return }
522
+ self.delegate?.onDevicesFound(self.discoveredDevices)
523
+ }
524
+ }
525
+
526
+ public func discoveryError(_ error: String, netServiceError nsnetserviceserror: Int32, on iface: FLIRCommunicationInterface) {
527
+ NSLog("[FlirManager] Discovery error: \(error) (\(nsnetserviceserror)) on interface: \(iface)")
528
+
529
+ DispatchQueue.main.async { [weak self] in
530
+ self?.delegate?.onError("Discovery error: \(error)")
531
+ }
532
+ }
533
+
534
+ public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
535
+ NSLog("[FlirManager] Discovery finished on interface: \(iface)")
536
+ isScanning = false
537
+ }
538
+ }
539
+
540
+ // MARK: - FLIRDataReceivedDelegate
541
+
542
+ extension FlirManager: FLIRDataReceivedDelegate {
543
+ public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
544
+ NSLog("[FlirManager] Camera disconnected: \(error?.localizedDescription ?? "no error")")
545
+
546
+ _isConnected = false
547
+ _isStreaming = false
548
+ connectedDeviceId = nil
549
+ connectedDeviceName = nil
550
+
551
+ DispatchQueue.main.async { [weak self] in
552
+ self?.delegate?.onDeviceDisconnected()
553
+ self?.emitStateChange("disconnected")
554
+ }
555
+ }
556
+ }
557
+
558
+ // MARK: - FLIRStreamDelegate
559
+
560
+ extension FlirManager: FLIRStreamDelegate {
561
+ public func onError(_ error: Error) {
562
+ NSLog("[FlirManager] Stream error: \(error)")
563
+
564
+ DispatchQueue.main.async { [weak self] in
565
+ self?.delegate?.onError("Stream error: \(error.localizedDescription)")
566
+ }
567
+ }
568
+
569
+ public func onImageReceived() {
570
+ guard let streamer = streamer else { return }
571
+
572
+ do {
573
+ try streamer.update()
574
+
575
+ if let image = streamer.getImage() {
576
+ _latestImage = image
577
+
578
+ // Get temperature from thermal image
579
+ streamer.withThermalImage { [weak self] thermalImage in
580
+ if let stats = thermalImage.getStatistics() {
581
+ self?.lastTemperature = stats.getMax().value
582
+ }
583
+ }
584
+
585
+ DispatchQueue.main.async { [weak self] in
586
+ guard let self = self else { return }
587
+ self.delegate?.onFrameReceived(
588
+ image,
589
+ width: Int(image.size.width),
590
+ height: Int(image.size.height)
591
+ )
592
+ }
593
+ }
594
+ } catch {
595
+ NSLog("[FlirManager] Streamer update error: \(error)")
596
+ }
597
+ }
598
+ }
599
+ #endif