ilabs-flir 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Flir.podspec +139 -139
- package/README.md +1066 -1066
- package/android/Flir/build.gradle.kts +72 -72
- package/android/Flir/src/main/AndroidManifest.xml +45 -45
- package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -136
- package/android/Flir/src/main/java/flir/android/FlirFrameCache.kt +6 -6
- package/android/Flir/src/main/java/flir/android/FlirManager.kt +476 -476
- package/android/Flir/src/main/java/flir/android/FlirModule.kt +257 -257
- package/android/Flir/src/main/java/flir/android/FlirPackage.kt +18 -18
- package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +74 -74
- package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +583 -583
- package/android/Flir/src/main/java/flir/android/FlirStatus.kt +12 -12
- package/android/Flir/src/main/java/flir/android/FlirView.kt +48 -48
- package/android/Flir/src/main/java/flir/android/FlirViewManager.kt +13 -13
- package/app.plugin.js +381 -381
- package/expo-module.config.json +5 -5
- package/ios/Flir/src/Flir-Bridging-Header.h +34 -34
- package/ios/Flir/src/FlirEventEmitter.h +25 -25
- package/ios/Flir/src/FlirEventEmitter.m +63 -63
- package/ios/Flir/src/FlirManager.swift +599 -599
- package/ios/Flir/src/FlirModule.h +17 -17
- package/ios/Flir/src/FlirModule.m +713 -713
- package/ios/Flir/src/FlirPreviewView.h +13 -13
- package/ios/Flir/src/FlirPreviewView.m +171 -171
- package/ios/Flir/src/FlirState.h +68 -68
- package/ios/Flir/src/FlirState.m +135 -135
- package/ios/Flir/src/FlirViewManager.h +16 -16
- package/ios/Flir/src/FlirViewManager.m +27 -27
- package/package.json +72 -71
- package/react-native.config.js +14 -14
- package/scripts/fetch-binaries.js +47 -5
- package/sdk-manifest.json +50 -50
- package/src/index.d.ts +63 -63
- package/src/index.js +7 -7
- 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
|