ilabs-flir 2.2.25 → 2.2.28

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.
@@ -1,576 +1,640 @@
1
- //
2
- // FlirManager.swift
3
- // Flir
4
- //
5
- // Simplified FLIR camera manager - matches sample app pattern
6
- // scan → connect → stream → disconnect
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
- @objc optional func onFrameReceivedRaw(_ data: Data, width: Int, height: Int, bytesPerRow: Int, timestamp: Double)
47
- func onError(_ message: String)
48
- func onStateChanged(_ state: String, isConnected: Bool, isStreaming: Bool, isEmulator: Bool)
49
- }
50
-
51
- /// Main FLIR Manager - Simplified Singleton
52
- @objc public class FlirManager: NSObject {
53
- @objc public static let shared = FlirManager()
54
-
55
- // MARK: - Singleton
56
-
57
- // MARK: - Properties
58
- @objc public weak var delegate: FlirManagerDelegate?
59
-
60
- private var _isConnected = false
61
- private var _isStreaming = false
62
- private var _isProcessingFrame = false
63
- private var connectedDeviceId: String?
64
- private var connectedDeviceName: String?
65
-
66
- // Dedicated render queue for frame processing (matches sample app pattern)
67
- private let renderQueue = DispatchQueue(label: "com.flir.render")
68
-
69
- // Latest frame
70
- private var _latestImage: UIImage?
71
- @objc public var latestImage: UIImage? { return _latestImage }
72
-
73
- // Discovered devices
74
- private var discoveredDevices: [FlirDeviceInfo] = []
75
-
76
- #if FLIR_ENABLED
77
- private var discovery: FLIRDiscovery?
78
- private var camera: FLIRCamera?
79
- private var stream: FLIRStream?
80
- private var streamer: FLIRThermalStreamer?
81
- private var identityMap: [String: FLIRIdentity] = [:]
82
- #endif
83
-
84
- private override init() {
85
- super.init()
86
- NSLog("[FlirManager] Initialized")
87
- }
88
-
89
- // MARK: - Public State
90
-
91
- @objc public var isConnected: Bool { return _isConnected }
92
- @objc public var isStreaming: Bool { return _isStreaming }
93
- @objc public var isEmulator: Bool {
94
- return connectedDeviceName?.lowercased().contains("emulator") == true
95
- }
96
-
97
- @objc public func getDiscoveredDevices() -> [FlirDeviceInfo] {
98
- return discoveredDevices
99
- }
100
-
101
- // MARK: - Discovery
102
-
103
- @objc public func startDiscovery() {
104
- NSLog("[FlirManager] startDiscovery")
105
-
106
- #if FLIR_ENABLED
107
- discoveredDevices.removeAll()
108
- identityMap.removeAll()
109
-
110
- if discovery == nil {
111
- discovery = FLIRDiscovery()
112
- discovery?.delegate = self
113
- }
114
-
115
- // Match sample app: discover lightning + wireless + emulator + network
116
- discovery?.start([.lightning, .flirOneWireless, .emulator, .network])
117
-
118
- emitStateChange("discovering")
119
- #else
120
- delegate?.onError("FLIR SDK not available")
121
- #endif
122
- }
123
-
124
- @objc public func stopDiscovery() {
125
- NSLog("[FlirManager] stopDiscovery")
126
-
127
- #if FLIR_ENABLED
128
- discovery?.stop()
129
- emitStateChange("idle")
130
- #endif
131
- }
132
-
133
- // MARK: - Connection
134
-
135
- @objc public func connectToDevice(_ deviceId: String) {
136
- NSLog("[FlirManager] connectToDevice: \(deviceId)")
137
-
138
- #if FLIR_ENABLED
139
- // Find identity
140
- guard let identity = identityMap[deviceId] else {
141
- NSLog("[FlirManager] Device not found: \(deviceId)")
142
- delegate?.onError("Device not found: \(deviceId)")
143
- return
144
- }
145
-
146
- // Disconnect if already connected
147
- if _isConnected {
148
- disconnect()
149
- }
150
-
151
- // Connect on background thread (matches sample app)
152
- DispatchQueue.global().async { [weak self] in
153
- guard let self = self else { return }
154
-
155
- do {
156
- if self.camera == nil {
157
- self.camera = FLIRCamera()
158
- self.camera?.delegate = self
159
- }
160
-
161
- guard let cam = self.camera else {
162
- self.notifyError("Failed to create camera")
163
- return
164
- }
165
-
166
- // Authenticate if generic network camera
167
- if identity.cameraType() == .generic {
168
- var status = FLIRAuthenticationStatus.pending
169
- let certName = (Bundle.main.bundleIdentifier ?? "ThermalCamera") + "-cert"
170
- while status == .pending {
171
- status = cam.authenticate(identity, trustedConnectionName: certName)
172
- if status == .pending {
173
- Thread.sleep(forTimeInterval: 0.2)
174
- }
175
- }
176
- }
177
-
178
- // Pair and connect (matches sample app pattern)
179
- try cam.pair(identity, code: 0)
180
- try cam.connect()
181
-
182
- self._isConnected = true
183
- self.connectedDeviceId = identity.deviceId()
184
- self.connectedDeviceName = identity.deviceId()
185
-
186
- NSLog("[FlirManager] Connected to: \(identity.deviceId())")
187
-
188
- // Notify on main thread
189
- let deviceInfo = FlirDeviceInfo(
190
- deviceId: identity.deviceId(),
191
- name: identity.deviceId(),
192
- communicationType: self.interfaceName(identity.communicationInterface()),
193
- isEmulator: identity.communicationInterface() == .emulator
194
- )
195
-
196
- DispatchQueue.main.async {
197
- self.delegate?.onDeviceConnected(deviceInfo)
198
- self.emitStateChange("connected")
199
- }
200
-
201
- // Auto-start streaming (matches sample app)
202
- self.startStreamInternal()
203
-
204
- } catch {
205
- NSLog("[FlirManager] Connection failed: \(error)")
206
- self._isConnected = false
207
- self.camera = nil
208
- DispatchQueue.main.async {
209
- self.emitStateChange("connection_failed")
210
- self.delegate?.onError("Connection failed: \(error.localizedDescription)")
211
- }
212
- }
213
- }
214
- #else
215
- delegate?.onError("FLIR SDK not available")
216
- #endif
217
- }
218
-
219
- @objc public func startEmulator() {
220
- NSLog("[FlirManager] startEmulator")
221
- startDiscovery()
222
- }
223
-
224
- @objc public func disconnect() {
225
- NSLog("[FlirManager] disconnect")
226
-
227
- #if FLIR_ENABLED
228
- stopStream()
229
- camera?.disconnect()
230
- camera = nil
231
- _isConnected = false
232
- connectedDeviceId = nil
233
- connectedDeviceName = nil
234
- _latestImage = nil
235
-
236
- DispatchQueue.main.async { [weak self] in
237
- self?.delegate?.onDeviceDisconnected()
238
- self?.emitStateChange("disconnected")
239
- }
240
- #endif
241
- }
242
-
243
- @objc public func stop() {
244
- stopStream()
245
- disconnect()
246
- stopDiscovery()
247
- }
248
-
249
- // MARK: - Streaming
250
-
251
- @objc public func startStream() {
252
- #if FLIR_ENABLED
253
- guard _isConnected else {
254
- delegate?.onError("Not connected")
255
- return
256
- }
257
-
258
- DispatchQueue.global().async { [weak self] in
259
- self?.startStreamInternal()
260
- }
261
- #endif
262
- }
263
-
264
- #if FLIR_ENABLED
265
- private func startStreamInternal() {
266
- guard let cam = camera else { return }
267
-
268
- let streams = cam.getStreams()
269
- guard !streams.isEmpty else {
270
- NSLog("[FlirManager] No streams available")
271
- return
272
- }
273
-
274
- // Find thermal stream or use first
275
- let thermalStream = streams.first { $0.isThermal } ?? streams.first!
276
-
277
- stream = thermalStream
278
- streamer = FLIRThermalStreamer(stream: thermalStream)
279
- streamer?.autoScale = true
280
- streamer?.renderScale = true
281
- thermalStream.delegate = self
282
-
283
- do {
284
- try thermalStream.start()
285
- _isStreaming = true
286
- NSLog("[FlirManager] Streaming started")
287
- DispatchQueue.main.async { [weak self] in
288
- self?.emitStateChange("streaming")
289
- }
290
- } catch {
291
- NSLog("[FlirManager] Stream start failed: \(error)")
292
- stream = nil
293
- streamer = nil
294
- delegate?.onError("Stream failed: \(error.localizedDescription)")
295
- }
296
- }
297
- #endif
298
-
299
- @objc public func stopStream() {
300
- NSLog("[FlirManager] stopStream")
301
-
302
- #if FLIR_ENABLED
303
- stream?.stop()
304
- stream = nil
305
- streamer = nil
306
- _isStreaming = false
307
- _latestImage = nil
308
-
309
- if _isConnected {
310
- emitStateChange("connected")
311
- }
312
- #endif
313
- }
314
-
315
- // MARK: - Temperature
316
-
317
- @objc public func getTemperatureAt(x: Int, y: Int) -> Double {
318
- #if FLIR_ENABLED
319
- guard let streamer = streamer else { return Double.nan }
320
-
321
- var result = Double.nan
322
- streamer.withThermalImage { thermalImage in
323
- let w = thermalImage.getWidth()
324
- let h = thermalImage.getHeight()
325
- let cx = max(0, min(Int(w) - 1, x))
326
- let cy = max(0, min(Int(h) - 1, y))
327
-
328
- if let measurements = thermalImage.measurements,
329
- let spot = try? measurements.addSpot(CGPoint(x: cx, y: cy)) {
330
-
331
- // getValue() returns non-optional in some SDK versions, or optional in others.
332
- // Compiler says it is NOT optional here, so direct assignment.
333
- let value = spot.getValue()
334
- result = value.value
335
-
336
- try? measurements.remove(spot)
337
- }
338
- }
339
- return result
340
- #else
341
- return Double.nan
342
- #endif
343
- }
344
-
345
-
346
-
347
- @objc public func getTemperatureAtNormalized(_ nx: Double, y: Double) -> Double {
348
- guard let img = latestImage else { return Double.nan }
349
- let px = Int(nx * Double(img.size.width))
350
- let py = Int(y * Double(img.size.height))
351
- return getTemperatureAt(x: px, y: py)
352
- }
353
-
354
- // MARK: - Legacy / Compatibility Methods
355
-
356
- @objc public func setPreferSdkRotation(_ prefer: Bool) {
357
- // No-op in simplified version
358
- }
359
-
360
- @objc public func isPreferSdkRotation() -> Bool {
361
- return false
362
- }
363
-
364
- @objc public func setNetworkDiscoveryEnabled(_ enabled: Bool) {
365
- // No-op - simple discovery always scans all supported types
366
- }
367
-
368
- @objc public func startEmulator(withType type: String) {
369
- NSLog("[FlirManager] startEmulator(withType: \(type))")
370
- startDiscovery()
371
- }
372
-
373
- @objc public func latestFrameBitmapBase64() -> [String: Any]? {
374
- // Legacy method for base64 frame data - simplified version uses onFrameReceived
375
- // If absolutely needed, we could implement jpeg compression here
376
- return nil
377
- }
378
-
379
- @objc public func getConnectedDeviceInfo() -> String {
380
- return connectedDeviceName ?? "Not connected"
381
- }
382
-
383
- // MARK: - Battery (stub - not needed per user)
384
-
385
- // MARK: - Battery (stub - not needed per user)
386
-
387
- @objc public func getBatteryLevel() -> Int { return -1 }
388
- @objc public func isBatteryCharging() -> Bool { return false }
389
-
390
- // MARK: - Shim Compatibility
391
-
392
- @objc public static var isSDKAvailable: Bool {
393
- return true
394
- }
395
-
396
- @objc public func setPalette(_ name: String) {
397
- // stub
398
- }
399
-
400
- @objc public func setPaletteFromAcol(_ acol: Float) {
401
- // stub
402
- }
403
-
404
- @objc public func retainClient(_ clientId: String) {
405
- // Only start discovery if not already connected
406
- // Starting discovery while connected can interfere with active stream
407
- if !_isConnected {
408
- startDiscovery()
409
- }
410
- }
411
-
412
- @objc public func releaseClient(_ clientId: String) {
413
- // simplified manager doesn't track retain counts per client yet
414
- // stopDiscovery() // Optional: could stop if count == 0
415
- }
416
-
417
- // MARK: - Helpers
418
-
419
- private func emitStateChange(_ state: String) {
420
- DispatchQueue.main.async { [weak self] in
421
- guard let self = self else { return }
422
- self.delegate?.onStateChanged(state, isConnected: self._isConnected, isStreaming: self._isStreaming, isEmulator: self.isEmulator)
423
- }
424
- }
425
-
426
- private func notifyError(_ message: String) {
427
- DispatchQueue.main.async { [weak self] in
428
- self?.delegate?.onError(message)
429
- }
430
- }
431
-
432
- #if FLIR_ENABLED
433
- private func interfaceName(_ iface: FLIRCommunicationInterface) -> String {
434
- if iface.contains(.lightning) { return "LIGHTNING" }
435
- if iface.contains(.network) { return "NETWORK" }
436
- if iface.contains(.flirOneWireless) { return "WIRELESS" }
437
- if iface.contains(.emulator) { return "EMULATOR" }
438
- return "UNKNOWN"
439
- }
440
- #endif
441
- }
442
-
443
- // MARK: - Discovery Delegate
444
-
445
- #if FLIR_ENABLED
446
- extension FlirManager: FLIRDiscoveryEventDelegate {
447
- public func cameraDiscovered(_ camera: FLIRDiscoveredCamera) {
448
- let identity = camera.identity
449
- let deviceId = identity.deviceId()
450
-
451
- NSLog("[FlirManager] Device found: \(deviceId)")
452
-
453
- // Store identity
454
- identityMap[deviceId] = identity
455
-
456
- // Create device info
457
- let deviceInfo = FlirDeviceInfo(
458
- deviceId: deviceId,
459
- name: camera.displayName ?? deviceId,
460
- communicationType: interfaceName(identity.communicationInterface()),
461
- isEmulator: identity.communicationInterface() == .emulator
462
- )
463
-
464
- // Add if not exists
465
- if !discoveredDevices.contains(where: { $0.deviceId == deviceId }) {
466
- discoveredDevices.append(deviceInfo)
467
- }
468
-
469
- DispatchQueue.main.async { [weak self] in
470
- guard let self = self else { return }
471
- self.delegate?.onDevicesFound(self.discoveredDevices)
472
- }
473
- }
474
-
475
- public func discoveryError(_ error: String, netServiceError: Int32, on iface: FLIRCommunicationInterface) {
476
- NSLog("[FlirManager] Discovery error: \(error)")
477
- delegate?.onError("Discovery error: \(error)")
478
- }
479
-
480
- public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
481
- NSLog("[FlirManager] Discovery finished: \(iface)")
482
- }
483
-
484
- public func cameraLost(_ cameraIdentity: FLIRIdentity) {
485
- let deviceId = cameraIdentity.deviceId()
486
- NSLog("[FlirManager] Device lost: \(deviceId)")
487
-
488
- identityMap.removeValue(forKey: deviceId)
489
- discoveredDevices.removeAll { $0.deviceId == deviceId }
490
-
491
- DispatchQueue.main.async { [weak self] in
492
- guard let self = self else { return }
493
- self.delegate?.onDevicesFound(self.discoveredDevices)
494
- }
495
- }
496
- }
497
- #endif
498
-
499
- // MARK: - Camera Delegate
500
-
501
- #if FLIR_ENABLED
502
- extension FlirManager: FLIRDataReceivedDelegate {
503
- public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
504
- NSLog("[FlirManager] Camera disconnected: \(error?.localizedDescription ?? "clean")")
505
-
506
- _isConnected = false
507
- _isStreaming = false
508
- self.camera = nil
509
- stream = nil
510
- streamer = nil
511
-
512
- DispatchQueue.main.async { [weak self] in
513
- self?.delegate?.onDeviceDisconnected()
514
- self?.emitStateChange("disconnected")
515
- }
516
- }
517
- }
518
- #endif
519
-
520
- // MARK: - Stream Delegate
521
-
522
- #if FLIR_ENABLED
523
- extension FlirManager: FLIRStreamDelegate {
524
- public func onError(_ error: Error) {
525
- NSLog("[FlirManager] Stream error: \(error)")
526
- delegate?.onError("Stream error: \(error.localizedDescription)")
527
- }
528
-
529
- public func onImageReceived() {
530
- NSLog("[FLIR-TRACE 1️⃣] onImageReceived called on SDK thread")
531
-
532
- // Process frame on dedicated render queue (matches sample app pattern)
533
- // This prevents blocking the SDK callback thread and main thread
534
- // Guard to skip frame if already processing (prevents backpressure/latency)
535
- guard !_isProcessingFrame else {
536
- NSLog("[FLIR-TRACE ⏩] Skipping frame (already processing)")
537
- return
538
- }
539
-
540
- _isProcessingFrame = true
541
- renderQueue.async { [weak self] in
542
- defer { self?._isProcessingFrame = false }
543
- guard let self = self, let streamer = self.streamer else {
544
- NSLog("[FLIR-TRACE ❌] No self or streamer in renderQueue")
545
- return
546
- }
547
-
548
- NSLog("[FLIR-TRACE 2️⃣] Processing on renderQueue")
549
-
550
- do {
551
- try streamer.update()
552
- NSLog("[FLIR-TRACE 3️⃣] Streamer updated successfully")
553
- } catch {
554
- NSLog("[FLIR-TRACE ❌] Streamer update failed: \(error)")
555
- return
556
- }
557
-
558
- guard let image = streamer.getImage() else {
559
- NSLog("[FLIR-TRACE ❌] streamer.getImage() returned nil")
560
- return
561
- }
562
-
563
- NSLog("[FLIR-TRACE 4️⃣] Got image from streamer: \(image.size.width)x\(image.size.height)")
564
-
565
- self._latestImage = image
566
- let width = Int(image.size.width)
567
- let height = Int(image.size.height)
568
-
569
- DispatchQueue.main.async { [weak self] in
570
- NSLog("[FLIR-TRACE 5️⃣] Dispatching to delegate.onFrameReceived on main thread")
571
- self?.delegate?.onFrameReceived(image, width: width, height: height)
572
- }
573
- }
574
- }
575
- }
576
- #endif
1
+ //
2
+ // FlirManager.swift
3
+ // Flir
4
+ //
5
+ // Simplified FLIR camera manager - matches sample app pattern
6
+ // scan → connect → stream → disconnect
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
+ @objc optional func onFrameReceivedRaw(_ data: Data, width: Int, height: Int, bytesPerRow: Int, timestamp: Double)
47
+ func onError(_ message: String)
48
+ func onStateChanged(_ state: String, isConnected: Bool, isStreaming: Bool, isEmulator: Bool)
49
+ }
50
+
51
+ /// Main FLIR Manager - Simplified Singleton
52
+ @objc public class FlirManager: NSObject {
53
+ @objc public static let shared = FlirManager()
54
+
55
+ // MARK: - Singleton
56
+
57
+ // MARK: - Properties
58
+ @objc public weak var delegate: FlirManagerDelegate?
59
+
60
+ private var _isConnected = false
61
+ private var _isStreaming = false
62
+ private var _isProcessingFrame = false
63
+ private var connectedDeviceId: String?
64
+ private var connectedDeviceName: String?
65
+
66
+ // Dedicated render queue for frame processing (matches sample app pattern)
67
+ private let renderQueue = DispatchQueue(label: "com.flir.render")
68
+
69
+ // Latest frame
70
+ private var _latestImage: UIImage?
71
+ @objc public var latestImage: UIImage? { return _latestImage }
72
+
73
+ // Discovered devices
74
+ private var discoveredDevices: [FlirDeviceInfo] = []
75
+
76
+ #if FLIR_ENABLED
77
+ private var discovery: FLIRDiscovery?
78
+ private var camera: FLIRCamera?
79
+ private var stream: FLIRStream?
80
+ private var streamer: FLIRThermalStreamer?
81
+ private var identityMap: [String: FLIRIdentity] = [:]
82
+ #endif
83
+
84
+ private override init() {
85
+ super.init()
86
+ NSLog("[FlirManager] Initialized")
87
+ }
88
+
89
+ // MARK: - Public State
90
+
91
+ @objc public var isConnected: Bool { return _isConnected }
92
+ @objc public var isStreaming: Bool { return _isStreaming }
93
+ @objc public var isEmulator: Bool {
94
+ return connectedDeviceName?.lowercased().contains("emulator") == true
95
+ }
96
+
97
+ @objc public func getDiscoveredDevices() -> [FlirDeviceInfo] {
98
+ return discoveredDevices
99
+ }
100
+
101
+ // MARK: - Discovery
102
+
103
+ @objc public func startDiscovery() {
104
+ NSLog("[FlirManager] startDiscovery")
105
+
106
+ #if FLIR_ENABLED
107
+ discoveredDevices.removeAll()
108
+ identityMap.removeAll()
109
+
110
+ if discovery == nil {
111
+ discovery = FLIRDiscovery()
112
+ discovery?.delegate = self
113
+ }
114
+
115
+ // Match sample app: discover lightning + wireless + emulator + network
116
+ discovery?.start([.lightning, .flirOneWireless, .emulator, .network])
117
+
118
+ emitStateChange("discovering")
119
+ #else
120
+ delegate?.onError("FLIR SDK not available")
121
+ #endif
122
+ }
123
+
124
+ @objc public func stopDiscovery() {
125
+ NSLog("[FlirManager] stopDiscovery")
126
+
127
+ #if FLIR_ENABLED
128
+ discovery?.stop()
129
+ emitStateChange("idle")
130
+ #endif
131
+ }
132
+
133
+ // MARK: - Connection
134
+
135
+ @objc public func connectToDevice(_ deviceId: String) {
136
+ NSLog("[FlirManager] connectToDevice: \(deviceId)")
137
+
138
+ #if FLIR_ENABLED
139
+ // Find identity
140
+ guard let identity = identityMap[deviceId] else {
141
+ NSLog("[FlirManager] Device not found: \(deviceId)")
142
+ delegate?.onError("Device not found: \(deviceId)")
143
+ return
144
+ }
145
+
146
+ // Disconnect if already connected
147
+ if _isConnected {
148
+ disconnect()
149
+ }
150
+
151
+ // Connect on background thread (matches sample app)
152
+ DispatchQueue.global().async { [weak self] in
153
+ guard let self = self else { return }
154
+
155
+ // Create camera instance
156
+ if self.camera == nil {
157
+ self.camera = FLIRCamera()
158
+ self.camera?.delegate = self
159
+ }
160
+
161
+ guard let cam = self.camera else {
162
+ self.notifyError("Failed to create camera")
163
+ return
164
+ }
165
+
166
+ let iface = identity.communicationInterface()
167
+ let camType = identity.cameraType()
168
+ NSLog("[FlirManager] Camera type: \(camType.rawValue), interface: \(iface.rawValue)")
169
+
170
+ // ── AUTHENTICATE for network cameras ──
171
+ // Official FLIR CameraConnector sample checks .generic camera type,
172
+ // but FLIR One Edge Pro over network may report a different type.
173
+ // Check BOTH: camera type == .generic OR interface contains .network
174
+ let needsAuth = (camType == .generic) || iface.contains(.network)
175
+
176
+ if needsAuth {
177
+ NSLog("[FlirManager] Network camera detected — authenticating...")
178
+
179
+ // Use UUID-based persistent certificate name (matches FLIR sample).
180
+ // The camera has a bug where re-auth with a different name conflicts.
181
+ let certName = self.getPersistentCertificateName()
182
+ NSLog("[FlirManager] Using certificate name: \(certName)")
183
+
184
+ var status = FLIRAuthenticationStatus.pending
185
+ var attempts = 0
186
+ let maxAttempts = 30 // ~30 seconds timeout
187
+
188
+ while status == .pending && attempts < maxAttempts {
189
+ status = cam.authenticate(identity, trustedConnectionName: certName)
190
+ NSLog("[FlirManager] Auth attempt \(attempts + 1)/\(maxAttempts) status: \(status.rawValue)")
191
+
192
+ if status == .pending {
193
+ // Camera waiting for user to press "Trust" on its screen
194
+ Thread.sleep(forTimeInterval: 1.0)
195
+ }
196
+ attempts += 1
197
+ }
198
+
199
+ if status != .approved {
200
+ NSLog("[FlirManager] Authentication failed/timed out: \(status.rawValue)")
201
+ self.camera = nil
202
+ DispatchQueue.main.async {
203
+ self.emitStateChange("connection_failed")
204
+ self.delegate?.onError("Camera authentication failed. Check the camera screen for a trust/approve prompt.")
205
+ }
206
+ return
207
+ }
208
+ NSLog("[FlirManager] Authentication approved ✅")
209
+ }
210
+
211
+ // ── PAIR ──
212
+ do {
213
+ try cam.pair(identity, code: 0)
214
+ NSLog("[FlirManager] Pair succeeded")
215
+ } catch {
216
+ NSLog("[FlirManager] Pair failed: \(error)")
217
+ self._isConnected = false
218
+ self.camera = nil
219
+ DispatchQueue.main.async {
220
+ self.emitStateChange("connection_failed")
221
+ self.delegate?.onError("Pairing failed: \(error.localizedDescription)")
222
+ }
223
+ return
224
+ }
225
+
226
+ // ── CONNECT ──
227
+ do {
228
+ try cam.connect()
229
+
230
+ self._isConnected = true
231
+ self.connectedDeviceId = identity.deviceId()
232
+ self.connectedDeviceName = identity.deviceId()
233
+
234
+ NSLog("[FlirManager] Connected to: \(identity.deviceId()) ✅")
235
+
236
+ // Notify on main thread
237
+ let deviceInfo = FlirDeviceInfo(
238
+ deviceId: identity.deviceId(),
239
+ name: identity.deviceId(),
240
+ communicationType: self.interfaceName(identity.communicationInterface()),
241
+ isEmulator: identity.communicationInterface() == .emulator
242
+ )
243
+
244
+ DispatchQueue.main.async {
245
+ self.delegate?.onDeviceConnected(deviceInfo)
246
+ self.emitStateChange("connected")
247
+ }
248
+
249
+ // Auto-start streaming (matches sample app)
250
+ self.startStreamInternal()
251
+
252
+ } catch {
253
+ NSLog("[FlirManager] Connect failed: \(error)")
254
+ self._isConnected = false
255
+ self.camera = nil
256
+ DispatchQueue.main.async {
257
+ self.emitStateChange("connection_failed")
258
+ self.delegate?.onError("Connection failed: \(error.localizedDescription)")
259
+ }
260
+ }
261
+ }
262
+ #else
263
+ delegate?.onError("FLIR SDK not available")
264
+ #endif
265
+ }
266
+
267
+ @objc public func startEmulator() {
268
+ NSLog("[FlirManager] startEmulator")
269
+ startDiscovery()
270
+ }
271
+
272
+ @objc public func disconnect() {
273
+ NSLog("[FlirManager] disconnect")
274
+
275
+ #if FLIR_ENABLED
276
+ stopStream()
277
+ camera?.disconnect()
278
+ camera = nil
279
+ _isConnected = false
280
+ connectedDeviceId = nil
281
+ connectedDeviceName = nil
282
+ _latestImage = nil
283
+
284
+ DispatchQueue.main.async { [weak self] in
285
+ self?.delegate?.onDeviceDisconnected()
286
+ self?.emitStateChange("disconnected")
287
+ }
288
+ #endif
289
+ }
290
+
291
+ @objc public func stop() {
292
+ stopStream()
293
+ disconnect()
294
+ stopDiscovery()
295
+ }
296
+
297
+ // MARK: - Streaming
298
+
299
+ @objc public func startStream() {
300
+ #if FLIR_ENABLED
301
+ guard _isConnected else {
302
+ delegate?.onError("Not connected")
303
+ return
304
+ }
305
+
306
+ DispatchQueue.global().async { [weak self] in
307
+ self?.startStreamInternal()
308
+ }
309
+ #endif
310
+ }
311
+
312
+ #if FLIR_ENABLED
313
+ private func startStreamInternal() {
314
+ guard let cam = camera else { return }
315
+
316
+ let streams = cam.getStreams()
317
+ guard !streams.isEmpty else {
318
+ NSLog("[FlirManager] No streams available")
319
+ return
320
+ }
321
+
322
+ // Find thermal stream or use first
323
+ let thermalStream = streams.first { $0.isThermal } ?? streams.first!
324
+
325
+ stream = thermalStream
326
+ streamer = FLIRThermalStreamer(stream: thermalStream)
327
+ streamer?.autoScale = true
328
+ streamer?.renderScale = true
329
+ thermalStream.delegate = self
330
+
331
+ do {
332
+ try thermalStream.start()
333
+ _isStreaming = true
334
+ NSLog("[FlirManager] Streaming started")
335
+ DispatchQueue.main.async { [weak self] in
336
+ self?.emitStateChange("streaming")
337
+ }
338
+ } catch {
339
+ NSLog("[FlirManager] Stream start failed: \(error)")
340
+ stream = nil
341
+ streamer = nil
342
+ delegate?.onError("Stream failed: \(error.localizedDescription)")
343
+ }
344
+ }
345
+ #endif
346
+
347
+ @objc public func stopStream() {
348
+ NSLog("[FlirManager] stopStream")
349
+
350
+ #if FLIR_ENABLED
351
+ stream?.stop()
352
+ stream = nil
353
+ streamer = nil
354
+ _isStreaming = false
355
+ _latestImage = nil
356
+
357
+ if _isConnected {
358
+ emitStateChange("connected")
359
+ }
360
+ #endif
361
+ }
362
+
363
+ // MARK: - Temperature
364
+
365
+ @objc public func getTemperatureAt(x: Int, y: Int) -> Double {
366
+ #if FLIR_ENABLED
367
+ guard let streamer = streamer else { return Double.nan }
368
+
369
+ var result = Double.nan
370
+ streamer.withThermalImage { thermalImage in
371
+ let w = thermalImage.getWidth()
372
+ let h = thermalImage.getHeight()
373
+ let cx = max(0, min(Int(w) - 1, x))
374
+ let cy = max(0, min(Int(h) - 1, y))
375
+
376
+ if let measurements = thermalImage.measurements,
377
+ let spot = try? measurements.addSpot(CGPoint(x: cx, y: cy)) {
378
+
379
+ // getValue() returns non-optional in some SDK versions, or optional in others.
380
+ // Compiler says it is NOT optional here, so direct assignment.
381
+ let value = spot.getValue()
382
+ result = value.value
383
+
384
+ try? measurements.remove(spot)
385
+ }
386
+ }
387
+ return result
388
+ #else
389
+ return Double.nan
390
+ #endif
391
+ }
392
+
393
+
394
+
395
+ @objc public func getTemperatureAtNormalized(_ nx: Double, y: Double) -> Double {
396
+ guard let img = latestImage else { return Double.nan }
397
+ let px = Int(nx * Double(img.size.width))
398
+ let py = Int(y * Double(img.size.height))
399
+ return getTemperatureAt(x: px, y: py)
400
+ }
401
+
402
+ // MARK: - Legacy / Compatibility Methods
403
+
404
+ @objc public func setPreferSdkRotation(_ prefer: Bool) {
405
+ // No-op in simplified version
406
+ }
407
+
408
+ @objc public func isPreferSdkRotation() -> Bool {
409
+ return false
410
+ }
411
+
412
+ @objc public func setNetworkDiscoveryEnabled(_ enabled: Bool) {
413
+ // No-op - simple discovery always scans all supported types
414
+ }
415
+
416
+ @objc public func startEmulator(withType type: String) {
417
+ NSLog("[FlirManager] startEmulator(withType: \(type))")
418
+ startDiscovery()
419
+ }
420
+
421
+ @objc public func latestFrameBitmapBase64() -> [String: Any]? {
422
+ // Legacy method for base64 frame data - simplified version uses onFrameReceived
423
+ // If absolutely needed, we could implement jpeg compression here
424
+ return nil
425
+ }
426
+
427
+ @objc public func getConnectedDeviceInfo() -> String {
428
+ return connectedDeviceName ?? "Not connected"
429
+ }
430
+
431
+ // MARK: - Battery (stub - not needed per user)
432
+
433
+ // MARK: - Battery (stub - not needed per user)
434
+
435
+ @objc public func getBatteryLevel() -> Int { return -1 }
436
+ @objc public func isBatteryCharging() -> Bool { return false }
437
+
438
+ // MARK: - Shim Compatibility
439
+
440
+ @objc public static var isSDKAvailable: Bool {
441
+ return true
442
+ }
443
+
444
+ @objc public func setPalette(_ name: String) {
445
+ // stub
446
+ }
447
+
448
+ @objc public func setPaletteFromAcol(_ acol: Float) {
449
+ // stub
450
+ }
451
+
452
+ @objc public func retainClient(_ clientId: String) {
453
+ // Only start discovery if not already connected
454
+ // Starting discovery while connected can interfere with active stream
455
+ if !_isConnected {
456
+ startDiscovery()
457
+ }
458
+ }
459
+
460
+ @objc public func releaseClient(_ clientId: String) {
461
+ // simplified manager doesn't track retain counts per client yet
462
+ // stopDiscovery() // Optional: could stop if count == 0
463
+ }
464
+
465
+ // MARK: - Helpers
466
+
467
+ private func emitStateChange(_ state: String) {
468
+ DispatchQueue.main.async { [weak self] in
469
+ guard let self = self else { return }
470
+ self.delegate?.onStateChanged(state, isConnected: self._isConnected, isStreaming: self._isStreaming, isEmulator: self.isEmulator)
471
+ }
472
+ }
473
+
474
+ private func notifyError(_ message: String) {
475
+ DispatchQueue.main.async { [weak self] in
476
+ self?.delegate?.onError(message)
477
+ }
478
+ }
479
+
480
+ /// Persistent UUID-based certificate name for camera authentication.
481
+ /// Matches the pattern from FLIR's official CameraConnector sample.
482
+ /// The camera has a bug where re-auth with a different name can conflict,
483
+ /// so we generate a UUID once and persist it in UserDefaults.
484
+ private func getPersistentCertificateName() -> String {
485
+ guard let bundleID = Bundle.main.bundleIdentifier else { return "flir-cert-fallback" }
486
+ let key = "\(bundleID)-flir-cert-name"
487
+ let defaults = UserDefaults.standard
488
+ if let existing = defaults.string(forKey: key) {
489
+ return existing
490
+ }
491
+ let newName = UUID().uuidString
492
+ defaults.set(newName, forKey: key)
493
+ return newName
494
+ }
495
+
496
+ #if FLIR_ENABLED
497
+ private func interfaceName(_ iface: FLIRCommunicationInterface) -> String {
498
+ if iface.contains(.lightning) { return "LIGHTNING" }
499
+ if iface.contains(.network) { return "NETWORK" }
500
+ if iface.contains(.flirOneWireless) { return "WIRELESS" }
501
+ if iface.contains(.emulator) { return "EMULATOR" }
502
+ return "UNKNOWN"
503
+ }
504
+ #endif
505
+ }
506
+
507
+ // MARK: - Discovery Delegate
508
+
509
+ #if FLIR_ENABLED
510
+ extension FlirManager: FLIRDiscoveryEventDelegate {
511
+ public func cameraDiscovered(_ camera: FLIRDiscoveredCamera) {
512
+ let identity = camera.identity
513
+ let deviceId = identity.deviceId()
514
+
515
+ NSLog("[FlirManager] Device found: \(deviceId)")
516
+
517
+ // Store identity
518
+ identityMap[deviceId] = identity
519
+
520
+ // Create device info
521
+ let deviceInfo = FlirDeviceInfo(
522
+ deviceId: deviceId,
523
+ name: camera.displayName ?? deviceId,
524
+ communicationType: interfaceName(identity.communicationInterface()),
525
+ isEmulator: identity.communicationInterface() == .emulator
526
+ )
527
+
528
+ // Add if not exists
529
+ if !discoveredDevices.contains(where: { $0.deviceId == deviceId }) {
530
+ discoveredDevices.append(deviceInfo)
531
+ }
532
+
533
+ DispatchQueue.main.async { [weak self] in
534
+ guard let self = self else { return }
535
+ self.delegate?.onDevicesFound(self.discoveredDevices)
536
+ }
537
+ }
538
+
539
+ public func discoveryError(_ error: String, netServiceError: Int32, on iface: FLIRCommunicationInterface) {
540
+ NSLog("[FlirManager] Discovery error: \(error)")
541
+ delegate?.onError("Discovery error: \(error)")
542
+ }
543
+
544
+ public func discoveryFinished(_ iface: FLIRCommunicationInterface) {
545
+ NSLog("[FlirManager] Discovery finished: \(iface)")
546
+ }
547
+
548
+ public func cameraLost(_ cameraIdentity: FLIRIdentity) {
549
+ let deviceId = cameraIdentity.deviceId()
550
+ NSLog("[FlirManager] Device lost: \(deviceId)")
551
+
552
+ identityMap.removeValue(forKey: deviceId)
553
+ discoveredDevices.removeAll { $0.deviceId == deviceId }
554
+
555
+ DispatchQueue.main.async { [weak self] in
556
+ guard let self = self else { return }
557
+ self.delegate?.onDevicesFound(self.discoveredDevices)
558
+ }
559
+ }
560
+ }
561
+ #endif
562
+
563
+ // MARK: - Camera Delegate
564
+
565
+ #if FLIR_ENABLED
566
+ extension FlirManager: FLIRDataReceivedDelegate {
567
+ public func onDisconnected(_ camera: FLIRCamera, withError error: Error?) {
568
+ NSLog("[FlirManager] Camera disconnected: \(error?.localizedDescription ?? "clean")")
569
+
570
+ _isConnected = false
571
+ _isStreaming = false
572
+ self.camera = nil
573
+ stream = nil
574
+ streamer = nil
575
+
576
+ DispatchQueue.main.async { [weak self] in
577
+ self?.delegate?.onDeviceDisconnected()
578
+ self?.emitStateChange("disconnected")
579
+ }
580
+ }
581
+ }
582
+ #endif
583
+
584
+ // MARK: - Stream Delegate
585
+
586
+ #if FLIR_ENABLED
587
+ extension FlirManager: FLIRStreamDelegate {
588
+ public func onError(_ error: Error) {
589
+ NSLog("[FlirManager] Stream error: \(error)")
590
+ delegate?.onError("Stream error: \(error.localizedDescription)")
591
+ }
592
+
593
+ public func onImageReceived() {
594
+ NSLog("[FLIR-TRACE 1️⃣] onImageReceived called on SDK thread")
595
+
596
+ // Process frame on dedicated render queue (matches sample app pattern)
597
+ // This prevents blocking the SDK callback thread and main thread
598
+ // Guard to skip frame if already processing (prevents backpressure/latency)
599
+ guard !_isProcessingFrame else {
600
+ NSLog("[FLIR-TRACE ⏩] Skipping frame (already processing)")
601
+ return
602
+ }
603
+
604
+ _isProcessingFrame = true
605
+ renderQueue.async { [weak self] in
606
+ defer { self?._isProcessingFrame = false }
607
+ guard let self = self, let streamer = self.streamer else {
608
+ NSLog("[FLIR-TRACE ❌] No self or streamer in renderQueue")
609
+ return
610
+ }
611
+
612
+ NSLog("[FLIR-TRACE 2️⃣] Processing on renderQueue")
613
+
614
+ do {
615
+ try streamer.update()
616
+ NSLog("[FLIR-TRACE 3️⃣] Streamer updated successfully")
617
+ } catch {
618
+ NSLog("[FLIR-TRACE ❌] Streamer update failed: \(error)")
619
+ return
620
+ }
621
+
622
+ guard let image = streamer.getImage() else {
623
+ NSLog("[FLIR-TRACE ❌] streamer.getImage() returned nil")
624
+ return
625
+ }
626
+
627
+ NSLog("[FLIR-TRACE 4️⃣] Got image from streamer: \(image.size.width)x\(image.size.height)")
628
+
629
+ self._latestImage = image
630
+ let width = Int(image.size.width)
631
+ let height = Int(image.size.height)
632
+
633
+ DispatchQueue.main.async { [weak self] in
634
+ NSLog("[FLIR-TRACE 5️⃣] Dispatching to delegate.onFrameReceived on main thread")
635
+ self?.delegate?.onFrameReceived(image, width: width, height: height)
636
+ }
637
+ }
638
+ }
639
+ }
640
+ #endif