ilabs-flir 2.2.24 → 2.2.27

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