diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..40c64a77 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,14 @@ +--indent 4 +--indentcase false +--trimwhitespace always +--voidtype tuple +--nospaceoperators ..<,... +--ifdef noindent +--stripunusedargs closure-only +--maxwidth 100 +--wraparguments before-first +--funcattributes prev-line +--typeattributes prev-line +--varattributes prev-line +--disable andOperator +--swiftversion 5.6 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..4804241a --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,21 @@ +disabled_rules: + - trailing_comma + - identifier_name + - void_return + - operator_whitespace + - nesting + - cyclomatic_complexity + - multiple_closures_with_trailing_closure + - type_name + - todo + - large_tuple + - opening_brace + +line_length: 100 + +function_body_length: + - 50 + +included: + - Sources + - Tests diff --git a/Examples/OCADevice/DeviceApp.swift b/Examples/OCADevice/DeviceApp.swift new file mode 100644 index 00000000..dd0ca134 --- /dev/null +++ b/Examples/OCADevice/DeviceApp.swift @@ -0,0 +1,42 @@ +import Foundation +import SwiftOCA +import SwiftOCADevice + +private var localhost = sockaddr_in( + sin_len: UInt8(MemoryLayout.size), + sin_family: sa_family_t(AF_INET), + sin_port: in_port_t(65000).bigEndian, + sin_addr: in_addr(s_addr: INADDR_ANY), + sin_zero: (0, 0, 0, 0, 0, 0, 0, 0) +) + +class TestActuator: SwiftOCADevice.OcaBooleanActuator { + override public func handleCommand( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async throws -> Ocp1Response { + debugPrint("got command \(command) from controller \(controller)") + return try await super.handleCommand(command, from: controller) + } +} + +@main +public enum DeviceApp { + static var testActuator: SwiftOCADevice.OcaBooleanActuator? + + public static func main() async throws { + var device: AES70OCP1Device! + + withUnsafePointer(to: &localhost) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { cSockAddr in + device = AES70OCP1Device(address: cSockAddr) + } + } + + testActuator = try await SwiftOCADevice.OcaBooleanActuator( + role: "Test Actuator", + deviceDelegate: device + ) + try await device.start() + } +} diff --git a/Package.swift b/Package.swift index 121fa76e..648356ea 100644 --- a/Package.swift +++ b/Package.swift @@ -24,6 +24,7 @@ let package = Package( .package(url: "https://github.com/PADL/swift-binary-coder", branch: "inferno"), .package(url: "https://github.com/lhoward/AsyncExtensions", branch: "linux"), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"), + .package(url: "https://github.com/swhitty/FlyingFox", branch: "main"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a @@ -40,6 +41,20 @@ let package = Package( .product(name: "FlyingSocks", package: "FlyingFox"), ] ), + .target( + name: "SwiftOCADevice", + dependencies: [ + "SwiftOCA", + .product(name: "FlyingSocks", package: "FlyingFox"), + ] + ), + .executableTarget( + name: "OCADevice", + dependencies: [ + "SwiftOCADevice", + ], + path: "Examples/OCADevice" + ), ], swiftLanguageVersions: [.v5] ) diff --git a/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor+Codable.swift b/Sources/SwiftOCA/AES70/AES70OCP1Connection+Codable.swift similarity index 95% rename from Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor+Codable.swift rename to Sources/SwiftOCA/AES70/AES70OCP1Connection+Codable.swift index 3d1b7f5a..77c30a25 100644 --- a/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor+Codable.swift +++ b/Sources/SwiftOCA/AES70/AES70OCP1Connection+Codable.swift @@ -32,8 +32,8 @@ private extension Ocp1Message { } } -extension AES70OCP1Connection { - func encodeOcp1MessagePdu( +public extension AES70OCP1Connection { + static func encodeOcp1MessagePdu( _ messages: [Ocp1Message], type messageType: OcaMessageType ) throws -> Data { @@ -54,10 +54,11 @@ extension AES70OCP1Connection { messagePduData.encodeInteger(OcaUint32(messagePduData.count - 1), index: 3) return messagePduData } -} -extension AES70OCP1Connection.Monitor { - func decodeOcp1MessagePdu(from data: Data, messages: inout [Data]) throws -> OcaMessageType { + static func decodeOcp1MessagePdu( + from data: Data, + messages: inout [Data] + ) throws -> OcaMessageType { precondition(data.count >= Self.MinimumPduSize) precondition(data[0] == Ocp1SyncValue) @@ -116,7 +117,7 @@ extension AES70OCP1Connection.Monitor { return messageType } - func decodeOcp1Message( + nonisolated static func decodeOcp1Message( from messageData: Data, type messageType: OcaMessageType ) throws -> Ocp1Message { diff --git a/Sources/SwiftOCA/AES70/AES70OCP1Connection+Messages.swift b/Sources/SwiftOCA/AES70/AES70OCP1Connection+Messages.swift index d9bbf0b7..b9df0d90 100644 --- a/Sources/SwiftOCA/AES70/AES70OCP1Connection+Messages.swift +++ b/Sources/SwiftOCA/AES70/AES70OCP1Connection+Messages.swift @@ -25,7 +25,7 @@ extension AES70OCP1Connection { _ messages: [Ocp1Message], type messageType: OcaMessageType ) async throws { - let messagePduData = try encodeOcp1MessagePdu(messages, type: messageType) + let messagePduData = try Self.encodeOcp1MessagePdu(messages, type: messageType) do { guard try await write(messagePduData) == messagePduData.count else { diff --git a/Sources/SwiftOCA/AES70/AES70OCP1Connection.swift b/Sources/SwiftOCA/AES70/AES70OCP1Connection.swift index 0258f559..9ce3429c 100644 --- a/Sources/SwiftOCA/AES70/AES70OCP1Connection.swift +++ b/Sources/SwiftOCA/AES70/AES70OCP1Connection.swift @@ -55,6 +55,8 @@ public struct AES70OCP1ConnectionOptions { @MainActor public class AES70OCP1Connection: CustomStringConvertible, ObservableObject { + public static let MinimumPduSize = 7 + let options: AES70OCP1ConnectionOptions /// Keepalive/ping interval (only necessary for UDP) @@ -86,8 +88,6 @@ public class AES70OCP1Connection: CustomStringConvertible, ObservableObject { /// Monitor structure for matching requests and responses actor Monitor { - static let MinimumPduSize = 7 - private let connection: AES70OCP1Connection! typealias Continuation = CheckedContinuation private var continuations = [OcaUint32: Continuation]() diff --git a/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor.swift b/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor.swift index 26d99d6b..756bcb1c 100644 --- a/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor.swift +++ b/Sources/SwiftOCA/AES70/AES70OCP1ConnectionMonitor.swift @@ -23,11 +23,11 @@ extension AES70OCP1Connection.Monitor { _ connection: AES70OCP1Connection, messages: inout [Data] ) async throws -> OcaMessageType { - var messagePduData = try await connection.read(Self.MinimumPduSize) + var messagePduData = try await connection.read(AES70OCP1Connection.MinimumPduSize) /// just parse enough of the protocol in order to read rest of message /// `syncVal: OcaUint8` || `protocolVersion: OcaUint16` || `pduSize: OcaUint32` - guard messagePduData.count >= Self.MinimumPduSize else { + guard messagePduData.count >= AES70OCP1Connection.MinimumPduSize else { debugPrint("receiveMessagePdu: PDU of size \(messagePduData.count) is too short") throw Ocp1Error.pduTooShort } @@ -39,17 +39,21 @@ extension AES70OCP1Connection.Monitor { } let pduSize: OcaUint32 = messagePduData.decodeInteger(index: 3) - guard pduSize >= (Self.MinimumPduSize - 1) else { // doesn't include sync byte + guard pduSize >= (AES70OCP1Connection.MinimumPduSize - 1) + else { // doesn't include sync byte debugPrint("receiveMessagePdu: PDU size \(pduSize) is less than minimum PDU size") throw Ocp1Error.invalidPduSize } - let bytesLeft = Int(pduSize) - (Self.MinimumPduSize - 1) + let bytesLeft = Int(pduSize) - (AES70OCP1Connection.MinimumPduSize - 1) if bytesLeft > 0 { messagePduData += try await connection.read(bytesLeft) } - return try decodeOcp1MessagePdu(from: messagePduData, messages: &messages) + return try await AES70OCP1Connection.decodeOcp1MessagePdu( + from: messagePduData, + messages: &messages + ) } private func processMessage( @@ -83,7 +87,7 @@ extension AES70OCP1Connection.Monitor { var messagePdus = [Data]() let messageType = try await receiveMessagePdu(connection, messages: &messagePdus) let messages = try messagePdus.map { - try decodeOcp1Message(from: $0, type: messageType) + try AES70OCP1Connection.decodeOcp1Message(from: $0, type: messageType) } updateLastMessageReceivedTime() diff --git a/Sources/SwiftOCA/AES70/BinaryCodingConfiguration+OCP1.swift b/Sources/SwiftOCA/AES70/BinaryCodingConfiguration+OCP1.swift index e3def9c8..99971106 100644 --- a/Sources/SwiftOCA/AES70/BinaryCodingConfiguration+OCP1.swift +++ b/Sources/SwiftOCA/AES70/BinaryCodingConfiguration+OCP1.swift @@ -17,7 +17,7 @@ import BinaryCoder import Foundation -extension BinaryCodingConfiguration { +public extension BinaryCodingConfiguration { static var ocp1Configuration: Self { BinaryCodingConfiguration( endianness: .bigEndian, diff --git a/Sources/SwiftOCA/AES70/SocketBackend/AES70OCP1SocketConnection.swift b/Sources/SwiftOCA/AES70/SocketBackend/AES70OCP1SocketConnection.swift deleted file mode 100644 index 4858a878..00000000 --- a/Sources/SwiftOCA/AES70/SocketBackend/AES70OCP1SocketConnection.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// Copyright (c) 2023 PADL Software Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the License); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an 'AS IS' BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if canImport(Socket) - -import Foundation -@_implementationOnly -import Socket - -private extension Errno { - var connectionFailed: Bool { - self == .badFileDescriptor || self == .socketShutdown - } -} - -private extension Data { - var socketAddress: any SocketAddress { - var data = self - return data.withUnsafeMutableBytes { unbound -> (any SocketAddress) in - unbound - .withMemoryRebound(to: sockaddr.self) { sa -> (any SocketAddress) in - let socketAddress: any SocketAddress - - switch sa.baseAddress!.pointee.sa_family { - case UInt8(AF_INET): - socketAddress = IPv4SocketAddress.withUnsafePointer(sa.baseAddress!) - case UInt8(AF_INET6): - socketAddress = IPv6SocketAddress.withUnsafePointer(sa.baseAddress!) - case UInt8(AF_LINK): - socketAddress = LinkLayerSocketAddress.withUnsafePointer(sa.baseAddress!) - default: - fatalError("unsupported address family") - } - - return socketAddress - } - } - } -} - -public class AES70OCP1SocketConnection: AES70OCP1Connection { - let monitorInterval: UInt64 = 10 * NSEC_PER_MSEC - fileprivate let deviceAddress: any SocketAddress - var socket: Socket? - - public init( - deviceAddress: Data, - options: AES70OCP1ConnectionOptions = AES70OCP1ConnectionOptions() - ) { - self.deviceAddress = deviceAddress.socketAddress - super.init(options: options) - } - - override func connectDevice() async throws { - guard let socket else { - throw Ocp1Error.notConnected - } - - // TODO: should this be done in a separate task? - debugPrint("Connecting to \(deviceAddress) on socket \(socket)") - do { - try await socket.connect(to: deviceAddress) - } catch Errno.socketIsConnected { - } catch { - debugPrint("Socket connection error \(error)") - throw error - } - - try await super.connectDevice() - } - - override func disconnectDevice(clearObjectCache: Bool) async throws { - if let socket { - debugPrint("Closing socket \(socket)") - await socket.close() - } - try await super.disconnectDevice(clearObjectCache: clearObjectCache) - } - - fileprivate func withMappedError( - _ block: (_ socket: Socket) async throws - -> T - ) async throws -> T { - guard let socket else { - throw Ocp1Error.notConnected - } - - do { - return try await block(socket) - } catch let error as Errno { - if error.connectionFailed { - throw Ocp1Error.notConnected - } else { - throw error - } - } - } -} - -public class AES70OCP1SocketUDPConnection: AES70OCP1SocketConnection { - private static let mtu = 1500 - - override public var keepAliveInterval: OcaUint16 { - 1 - } - - override func connectDevice() async throws { - if socket == nil { - Socket.configuration = AsyncSocketConfiguration(monitorInterval: monitorInterval) - socket = try await Socket(IPv4Protocol.udp) - } - try await super.connectDevice() - } - - override func read(_ length: Int) async throws -> Data { - try await withMappedError { socket in - try await socket.receiveMessage(Self.mtu) - } - } - - override func write(_ data: Data) async throws -> Int { - try await withMappedError { socket in - try await socket.sendMessage(data) - } - } - - override public var connectionPrefix: String { - "\(OcaUdpConnectionPrefix)/\(DeviceAddressToString(deviceAddress))" - } -} - -public class AES70OCP1SocketTCPConnection: AES70OCP1SocketConnection { - override func connectDevice() async throws { - if socket == nil { - Socket.configuration = AsyncSocketConfiguration(monitorInterval: monitorInterval) - socket = try await Socket(IPv4Protocol.tcp) - } - try await super.connectDevice() - } - - override func read(_ length: Int) async throws -> Data { - try await withMappedError { socket in - var bytesLeft = length - var data = Data() - - repeat { - let fragment = try await socket.read(bytesLeft) - bytesLeft -= fragment.count - data += fragment - } while bytesLeft > 0 - return data - } - } - - override func write(_ data: Data) async throws -> Int { - try await withMappedError { socket in - var bytesWritten = 0 - - repeat { - bytesWritten += try await socket.write(data.subdata(in: bytesWritten.. String { - deviceAddress.withUnsafePointer { address, _ in - DeviceAddressToString(address) - } -} - -#endif diff --git a/Sources/SwiftOCA/OCA/OCF/Data+IntegerCodable.swift b/Sources/SwiftOCA/OCA/OCF/Data+IntegerCodable.swift index ba133b8b..6e869e57 100644 --- a/Sources/SwiftOCA/OCA/OCF/Data+IntegerCodable.swift +++ b/Sources/SwiftOCA/OCA/OCF/Data+IntegerCodable.swift @@ -1,6 +1,6 @@ import Foundation -extension Data { +public extension Data { enum Endianness { case little case big diff --git a/Sources/SwiftOCA/OCA/OCF/LengthTaggedData.swift b/Sources/SwiftOCA/OCA/OCF/LengthTaggedData.swift index ebd0b11e..013e9566 100644 --- a/Sources/SwiftOCA/OCA/OCF/LengthTaggedData.swift +++ b/Sources/SwiftOCA/OCA/OCF/LengthTaggedData.swift @@ -16,7 +16,9 @@ import Foundation -public struct LengthTaggedData: MutableDataProtocol, ContiguousBytes, Equatable, Hashable { +public struct LengthTaggedData: MutableDataProtocol, ContiguousBytes, Equatable, Hashable, + Sendable +{ public var startIndex: Data.Index { wrappedValue.startIndex } public var endIndex: Data.Index { wrappedValue.endIndex } public var regions: CollectionOfOne { CollectionOfOne(self) } diff --git a/Sources/SwiftOCA/OCC/ControlClasses/Managers/DeviceManager.swift b/Sources/SwiftOCA/OCC/ControlClasses/Managers/DeviceManager.swift index 42de7520..ccd6ea58 100644 --- a/Sources/SwiftOCA/OCC/ControlClasses/Managers/DeviceManager.swift +++ b/Sources/SwiftOCA/OCC/ControlClasses/Managers/DeviceManager.swift @@ -149,7 +149,7 @@ public class OcaDeviceManager: OcaManager { ) public var deviceRevisionID: OcaProperty.State - convenience init() { + convenience init() async throws { self.init(objectNumber: OcaDeviceManagerONo) } } diff --git a/Sources/SwiftOCA/OCC/ControlClasses/Managers/SubscriptionManager.swift b/Sources/SwiftOCA/OCC/ControlClasses/Managers/SubscriptionManager.swift index d2a21309..69d02ed4 100644 --- a/Sources/SwiftOCA/OCC/ControlClasses/Managers/SubscriptionManager.swift +++ b/Sources/SwiftOCA/OCC/ControlClasses/Managers/SubscriptionManager.swift @@ -71,7 +71,7 @@ public class OcaSubscriptionManager: OcaManager { try await sendCommandRrq(methodID: OcaMethodID("3.3")) } - func renableNotifications() async throws { + func reenableNotifications() async throws { try await sendCommandRrq(methodID: OcaMethodID("3.4")) } diff --git a/Sources/SwiftOCA/OCC/ControlClasses/Root.swift b/Sources/SwiftOCA/OCC/ControlClasses/Root.swift index 5b058afa..58ef14d8 100644 --- a/Sources/SwiftOCA/OCC/ControlClasses/Root.swift +++ b/Sources/SwiftOCA/OCC/ControlClasses/Root.swift @@ -151,17 +151,6 @@ public extension OcaRoot { ) } - func property(keyPath: PartialKeyPath) -> OcaProperty.State { - let storageKeyPath = keyPath as! ReferenceWritableKeyPath> - let wrappedKeyPath = keyPath - .appending(path: \OcaProperty.currentValue) as! ReferenceWritableKeyPath< - OcaRoot, - OcaProperty.State - > - return OcaProperty[_enclosingInstance: self, wrapped: wrappedKeyPath, - storage: storageKeyPath] - } - @MainActor internal func propertyDidChange(eventData: Ocp1EventData) { let decoder = BinaryDecoder(config: .ocp1Configuration) diff --git a/Sources/SwiftOCA/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift b/Sources/SwiftOCA/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift index a86e24dc..8a669f99 100644 --- a/Sources/SwiftOCA/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift +++ b/Sources/SwiftOCA/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift @@ -17,13 +17,26 @@ import Foundation public struct OcaBlockMember: Codable { - let memberObjectIdentification: OcaObjectIdentification - let containerObjectNumber: OcaONo + public let memberObjectIdentification: OcaObjectIdentification + public let containerObjectNumber: OcaONo + + public init( + memberObjectIdentification: OcaObjectIdentification, + containerObjectNumber: OcaONo + ) { + self.memberObjectIdentification = memberObjectIdentification + self.containerObjectNumber = containerObjectNumber + } } public struct OcaContainerObjectMember { - let memberObject: OcaRoot - let containerObjectNumber: OcaONo + public let memberObject: OcaRoot + public let containerObjectNumber: OcaONo + + public init(memberObject: OcaRoot, containerObjectNumber: OcaONo) { + self.memberObject = memberObject + self.containerObjectNumber = containerObjectNumber + } } public class OcaBlock: OcaWorker { diff --git a/Sources/SwiftOCA/OCC/ControlDataTypes/BaseDataTypes.swift b/Sources/SwiftOCA/OCC/ControlDataTypes/BaseDataTypes.swift index a306cbf6..66e4bae5 100644 --- a/Sources/SwiftOCA/OCC/ControlDataTypes/BaseDataTypes.swift +++ b/Sources/SwiftOCA/OCC/ControlDataTypes/BaseDataTypes.swift @@ -72,7 +72,7 @@ public enum OcaBaseDataType: OcaUint8, Codable { case ocaBit = 16 } -public enum OcaStatus: OcaUint8, Codable { +public enum OcaStatus: OcaUint8, Codable, Sendable { case ok = 0 case protocolVersionError = 1 case deviceError = 2 @@ -238,7 +238,7 @@ public struct OcaClassID: Codable, Hashable, CustomStringConvertible { } } -public struct OcaMethodID: Codable, Hashable, CustomStringConvertible { +public struct OcaMethodID: Codable, Hashable, Sendable, CustomStringConvertible { public let defLevel: OcaUint16 public let methodIndex: OcaUint16 @@ -253,7 +253,7 @@ public struct OcaMethodID: Codable, Hashable, CustomStringConvertible { } } -public struct OcaMethod: Codable { +public struct OcaMethod: Codable, Equatable, Hashable { public let oNo: OcaONo public let methodID: OcaMethodID @@ -293,7 +293,7 @@ public struct OcaGlobalTypeIdentifier: Codable { public struct OcaOrganizationID: Codable, CustomStringConvertible { public let id: (OcaUint8, OcaUint8, OcaUint8) - init(_ id: (OcaUint8, OcaUint8, OcaUint8)) { + public init(_ id: (OcaUint8, OcaUint8, OcaUint8)) { self.id = id } diff --git a/Sources/SwiftOCA/OCC/ControlDataTypes/EventDataTypes.swift b/Sources/SwiftOCA/OCC/ControlDataTypes/EventDataTypes.swift index bb9e9c56..5057c208 100644 --- a/Sources/SwiftOCA/OCC/ControlDataTypes/EventDataTypes.swift +++ b/Sources/SwiftOCA/OCC/ControlDataTypes/EventDataTypes.swift @@ -16,12 +16,12 @@ import Foundation -public enum OcaNotificationDeliveryMode: OcaUint8, Codable { +public enum OcaNotificationDeliveryMode: OcaUint8, Codable, Sendable { case reliable = 1 case fast = 2 } -public struct OcaEventID: Codable, Hashable, CustomStringConvertible { +public struct OcaEventID: Codable, Hashable, Sendable, CustomStringConvertible { public let defLevel: OcaUint16 public let eventIndex: OcaUint16 @@ -31,7 +31,7 @@ public struct OcaEventID: Codable, Hashable, CustomStringConvertible { eventIndex = s[1] } - init(defLevel: OcaUint16, eventIndex: OcaUint16) { + public init(defLevel: OcaUint16, eventIndex: OcaUint16) { self.defLevel = defLevel self.eventIndex = eventIndex } @@ -41,9 +41,14 @@ public struct OcaEventID: Codable, Hashable, CustomStringConvertible { } } -public struct OcaEvent: Codable, Hashable, Equatable { +public struct OcaEvent: Codable, Hashable, Equatable, Sendable { public let emitterONo: OcaONo public let eventID: OcaEventID + + public init(emitterONo: OcaONo, eventID: OcaEventID) { + self.emitterONo = emitterONo + self.eventID = eventID + } } public enum OcaPropertyChangeType: OcaUint8, Codable, Equatable { @@ -71,4 +76,4 @@ public struct OcaPropertyChangedEventData: Codable { } } -let OcaPropertyChangedEventID = OcaEventID(defLevel: 1, eventIndex: 1) +public let OcaPropertyChangedEventID = OcaEventID(defLevel: 1, eventIndex: 1) diff --git a/Sources/SwiftOCA/OCC/ControlDataTypes/ModelDataTypes.swift b/Sources/SwiftOCA/OCC/ControlDataTypes/ModelDataTypes.swift index 2b428fcd..caa42dfc 100644 --- a/Sources/SwiftOCA/OCC/ControlDataTypes/ModelDataTypes.swift +++ b/Sources/SwiftOCA/OCC/ControlDataTypes/ModelDataTypes.swift @@ -24,6 +24,12 @@ public struct OcaModelDescription: Codable, CustomStringConvertible { public var description: String { "\(manufacturer) \(name) \(version)" } + + public init(manufacturer: OcaString, name: OcaString, version: OcaString) { + self.manufacturer = manufacturer + self.name = name + self.version = version + } } public struct OcaModelGUID: Codable, CustomStringConvertible { @@ -34,4 +40,10 @@ public struct OcaModelGUID: Codable, CustomStringConvertible { public var description: String { mfrCode.description + String(format: "%08X", modelCode) } + + public init(reserved: OcaUint8, mfrCode: OcaOrganizationID, modelCode: OcaUint32) { + self.reserved = reserved + self.mfrCode = mfrCode + self.modelCode = modelCode + } } diff --git a/Sources/SwiftOCA/OCF/Errors.swift b/Sources/SwiftOCA/OCF/Errors.swift index 0aa78ed6..476200bc 100644 --- a/Sources/SwiftOCA/OCF/Errors.swift +++ b/Sources/SwiftOCA/OCF/Errors.swift @@ -19,6 +19,7 @@ import Foundation public enum Ocp1Error: Error, Equatable { /// An OCA status received from a device; should not be used for local errors case status(OcaStatus) + case bonjourRegistrationFailed case notConnected case notImplemented case noConnectionDelegate @@ -36,6 +37,7 @@ public enum Ocp1Error: Error, Equatable { case requestParameterOutOfRange case responseParameterOutOfRange case serviceResolutionFailed + case unhandledMethod case unknownServiceType case noInitialValue case unhandledEvent diff --git a/Sources/SwiftOCA/OCF/Messages/Command.swift b/Sources/SwiftOCA/OCF/Messages/Command.swift index 7a3325a4..391050fd 100644 --- a/Sources/SwiftOCA/OCF/Messages/Command.swift +++ b/Sources/SwiftOCA/OCF/Messages/Command.swift @@ -16,17 +16,40 @@ import Foundation -public struct Ocp1Parameters: Codable { +public struct Ocp1Parameters: Codable, Sendable { public var parameterCount: OcaUint8 public var parameterData: Data + + public init(parameterCount: OcaUint8, parameterData: Data) { + self.parameterCount = parameterCount + self.parameterData = parameterData + } + + public init() { + self.init(parameterCount: 0, parameterData: Data()) + } } -public struct Ocp1Command: Ocp1Message, Codable { +public struct Ocp1Command: Ocp1Message, Codable, Sendable { public let commandSize: OcaUint32 public let handle: OcaUint32 public let targetONo: OcaONo public let methodID: OcaMethodID public let parameters: Ocp1Parameters - var messageSize: OcaUint32 { commandSize } + public var messageSize: OcaUint32 { commandSize } + + public init( + commandSize: OcaUint32, + handle: OcaUint32, + targetONo: OcaONo, + methodID: OcaMethodID, + parameters: Ocp1Parameters + ) { + self.commandSize = commandSize + self.handle = handle + self.targetONo = targetONo + self.methodID = methodID + self.parameters = parameters + } } diff --git a/Sources/SwiftOCA/OCF/Messages/Header.swift b/Sources/SwiftOCA/OCF/Messages/Header.swift index 61a20a69..533f88ef 100644 --- a/Sources/SwiftOCA/OCF/Messages/Header.swift +++ b/Sources/SwiftOCA/OCF/Messages/Header.swift @@ -16,15 +16,15 @@ import Foundation -let Ocp1SyncValue: OcaUint8 = 0x3B -let Ocp1ProtocolVersion1: OcaUint16 = 1 -let Ocp1ProtocolVersion: OcaUint16 = Ocp1ProtocolVersion1 +public let Ocp1SyncValue: OcaUint8 = 0x3B +public let Ocp1ProtocolVersion1: OcaUint16 = 1 +public let Ocp1ProtocolVersion: OcaUint16 = Ocp1ProtocolVersion1 -struct Ocp1Header: Codable { - var protocolVersion: OcaUint16 - var pduSize: OcaUint32 - var pduType: OcaMessageType - var messageCount: OcaUint16 +public struct Ocp1Header: Codable, Sendable { + public var protocolVersion: OcaUint16 + public var pduSize: OcaUint32 + public var pduType: OcaMessageType + public var messageCount: OcaUint16 init(pduType: OcaMessageType, messageCount: OcaUint16) { protocolVersion = Ocp1ProtocolVersion @@ -38,11 +38,11 @@ struct Ocp1Header: Codable { } } -protocol Ocp1MessagePdu: Codable { +public protocol Ocp1MessagePdu: Codable, Sendable { var syncVal: OcaUint8 { get } var header: Ocp1Header { get } } -protocol Ocp1Message: Codable { +public protocol Ocp1Message: Codable, Sendable { var messageSize: OcaUint32 { get } } diff --git a/Sources/SwiftOCA/OCF/Messages/KeepAlive.swift b/Sources/SwiftOCA/OCF/Messages/KeepAlive.swift index 4394653e..304e7154 100644 --- a/Sources/SwiftOCA/OCF/Messages/KeepAlive.swift +++ b/Sources/SwiftOCA/OCF/Messages/KeepAlive.swift @@ -16,16 +16,24 @@ import Foundation -struct Ocp1KeepAlive1: Ocp1Message, Codable { - let heartBeatTime: OcaUint16 // sec +public struct Ocp1KeepAlive1: Ocp1Message, Codable { + public let heartBeatTime: OcaUint16 // sec - var messageSize: OcaUint32 { 2 } + public var messageSize: OcaUint32 { 2 } + + public init(heartBeatTime: OcaUint16) { + self.heartBeatTime = heartBeatTime + } } -struct Ocp1KeepAlive2: Ocp1Message, Codable { - let heartBeatTime: OcaUint32 // msec +public struct Ocp1KeepAlive2: Ocp1Message, Codable { + public let heartBeatTime: OcaUint32 // msec + + public var messageSize: OcaUint32 { 4 } - var messageSize: OcaUint32 { 4 } + public init(heartBeatTime: OcaUint32) { + self.heartBeatTime = heartBeatTime + } } -typealias Ocp1KeepAlive = Ocp1KeepAlive1 +public typealias Ocp1KeepAlive = Ocp1KeepAlive1 diff --git a/Sources/SwiftOCA/OCF/Messages/MessageType.swift b/Sources/SwiftOCA/OCF/Messages/MessageType.swift index 262dec65..c2ece312 100644 --- a/Sources/SwiftOCA/OCF/Messages/MessageType.swift +++ b/Sources/SwiftOCA/OCF/Messages/MessageType.swift @@ -16,7 +16,7 @@ import Foundation -enum OcaMessageType: OcaUint8, Codable { +public enum OcaMessageType: OcaUint8, Codable, Sendable { case ocaCmd = 0 case ocaCmdRrq = 1 case ocaNtf = 2 diff --git a/Sources/SwiftOCA/OCF/Messages/Notification.swift b/Sources/SwiftOCA/OCF/Messages/Notification.swift index 53d1b52e..ac6bc65f 100644 --- a/Sources/SwiftOCA/OCF/Messages/Notification.swift +++ b/Sources/SwiftOCA/OCF/Messages/Notification.swift @@ -16,22 +16,45 @@ import Foundation -public struct Ocp1EventData: Codable { +public struct Ocp1EventData: Codable, Sendable { let event: OcaEvent let eventParameters: Data + + public init(event: OcaEvent, eventParameters: Data) { + self.event = event + self.eventParameters = eventParameters + } } -struct Ocp1NtfParams: Codable { +public struct Ocp1NtfParams: Codable, Sendable { let parameterCount: OcaUint8 let context: OcaBlob let eventData: Ocp1EventData + + public init(parameterCount: OcaUint8, context: OcaBlob, eventData: Ocp1EventData) { + self.parameterCount = parameterCount + self.context = context + self.eventData = eventData + } } -struct Ocp1Notification: Ocp1Message, Codable { +public struct Ocp1Notification: Ocp1Message, Codable, Sendable { let notificationSize: OcaUint32 let targetONo: OcaONo let methodID: OcaMethodID let parameters: Ocp1NtfParams - var messageSize: OcaUint32 { notificationSize } + public var messageSize: OcaUint32 { notificationSize } + + public init( + notificationSize: OcaUint32 = 0, + targetONo: OcaONo, + methodID: OcaMethodID, + parameters: Ocp1NtfParams + ) { + self.notificationSize = notificationSize + self.targetONo = targetONo + self.methodID = methodID + self.parameters = parameters + } } diff --git a/Sources/SwiftOCA/OCF/Messages/Response.swift b/Sources/SwiftOCA/OCF/Messages/Response.swift index 3da3f13a..91bdc0ba 100644 --- a/Sources/SwiftOCA/OCF/Messages/Response.swift +++ b/Sources/SwiftOCA/OCF/Messages/Response.swift @@ -22,5 +22,17 @@ public struct Ocp1Response: Ocp1Message, Codable { public let statusCode: OcaStatus public let parameters: Ocp1Parameters - var messageSize: OcaUint32 { responseSize } + public var messageSize: OcaUint32 { responseSize } + + public init( + responseSize: OcaUint32 = 0, + handle: OcaUint32 = 0, + statusCode: OcaStatus = .ok, + parameters: Ocp1Parameters = Ocp1Parameters() + ) { + self.responseSize = responseSize + self.handle = handle + self.statusCode = statusCode + self.parameters = parameters + } } diff --git a/Sources/SwiftOCADevice/AES70/Controller.swift b/Sources/SwiftOCADevice/AES70/Controller.swift new file mode 100644 index 00000000..1b8b2cd0 --- /dev/null +++ b/Sources/SwiftOCADevice/AES70/Controller.swift @@ -0,0 +1,262 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AsyncAlgorithms +import AsyncExtensions +@_implementationOnly +import FlyingSocks +import Foundation +import SwiftOCA + +/// A remote endpoint +public final class AES70OCP1Controller { + typealias ControllerMessage = (Ocp1Message, Bool) + + let hostname: String + private nonisolated let socket: AsyncSocket + private nonisolated let logger: Logging? + private let _messages: AsyncThrowingStream, Error> + private var keepAliveTask: Task<(), Error>? + private var subscriptions = [OcaONo: NSMutableSet]() + var notificationsEnabled = true + + var messages: AnyAsyncSequence { + _messages.joined().eraseToAnyAsyncSequence() + } + + init(socket: AsyncSocket, logger: Logging?) { + hostname = Self.makeIdentifier(from: socket.socket) + self.socket = socket + self.logger = logger + _messages = AsyncThrowingStream.decodingMessages(from: socket.bytes) + } + + private func findSubscription( + _ event: OcaEvent, + subscriber: OcaMethod? = nil + ) -> AES70OCP1Subscription? { + guard let subscriptions = subscriptions[event.emitterONo] else { + return nil + } + return subscriptions.first(where: { + let subscription = $0 as! AES70OCP1Subscription + return subscription.event == event && + subscriber == nil ? true : subscription.subscriber == subscriber + }) as? AES70OCP1Subscription + } + + func hasSubscription(_ subscription: AES70OCP1Subscription) -> Bool { + findSubscription( + subscription.event, + subscriber: subscription.subscriber + ) != nil + } + + func addSubscription( + _ subscription: AES70OCP1Subscription + ) async throws { + guard !hasSubscription(subscription) else { + throw Ocp1Error.alreadySubscribedToEvent + } + var subscriptions = subscriptions[subscription.event.emitterONo] + if subscriptions == nil { + subscriptions = [subscription] + self.subscriptions[subscription.event.emitterONo] = subscriptions + } else { + subscriptions?.add(subscription) + } + } + + func removeSubscription( + _ event: OcaEvent, + subscriber: OcaMethod + ) async throws { + guard let subscription = findSubscription(event, subscriber: subscriber) + else { + return + } + + subscriptions[event.emitterONo]?.remove(subscription) + } + + func notifySubscribers( + _ event: OcaEvent, + parameters: Data + ) async throws { + guard let subscription = findSubscription(event) else { + return + } + + let eventData = Ocp1EventData(event: event, eventParameters: parameters) + let ntfParams = Ocp1NtfParams( + parameterCount: 2, + context: subscription.subscriberContext, + eventData: eventData + ) + let notification = Ocp1Notification( + targetONo: subscription.event.emitterONo, + methodID: subscription.subscriber.methodID, + parameters: ntfParams + ) + + try await sendMessage(notification, type: .ocaNtf) + } + + var keepAliveInterval: UInt64 = 0 { + didSet { + if keepAliveInterval != 0 { + keepAliveTask = Task<(), Error> { + repeat { + try await sendKeepAlive() + try await Task.sleep(nanoseconds: keepAliveInterval) + } while !Task.isCancelled + } + } else { + keepAliveTask?.cancel() + keepAliveTask = nil + } + } + } + + deinit { + keepAliveTask?.cancel() + } + + func sendMessages( + _ messages: AnyAsyncSequence, + type messageType: OcaMessageType + ) async throws { + let messages = try await messages.collect() + let messagePduData = try await AES70OCP1Connection.encodeOcp1MessagePdu( + messages, + type: messageType + ) + try await socket.write(messagePduData) + } + + func sendMessage( + _ message: Ocp1Message, + type messageType: OcaMessageType + ) async throws { + let sequence: AsyncSyncSequence<[Ocp1Message]> = [message].async + try await sendMessages(sequence.eraseToAnyAsyncSequence(), type: messageType) + } + + private func sendKeepAlive() async throws { + let keepAlive = Ocp1KeepAlive1(heartBeatTime: OcaUint16(keepAliveInterval / NSEC_PER_SEC)) + try await sendMessage(keepAlive, type: .ocaKeepAlive) + } + + func close() throws { + try socket.close() + } + + nonisolated var identifier: String { + "<\(hostname)>" + } +} + +extension AES70OCP1Controller: Equatable { + public static func == (lhs: AES70OCP1Controller, rhs: AES70OCP1Controller) -> Bool { + lhs.socket.socket.file == rhs.socket.socket.file + } +} + +extension AES70OCP1Controller: Hashable { + public func hash(into hasher: inout Hasher) { + socket.socket.file.hash(into: &hasher) + } +} + +extension AES70OCP1Controller { + static func makeIdentifier(from socket: Socket) -> String { + guard let peer = try? socket.remotePeer() else { + return "unknown" + } + + if case .unix = peer, let unixAddress = try? socket.sockname() { + return makeIdentifier(from: unixAddress) + } else { + return makeIdentifier(from: peer) + } + } + + static func makeIdentifier(from peer: Socket.Address) -> String { + switch peer { + case .ip4(let address, port: _): + return address + case .ip6(let address, port: _): + return address + case let .unix(path): + return path + } + } +} + +extension AES70OCP1Controller { + static func decodeOcp1Messages(from bytes: S) async throws -> ([Ocp1Message], Bool) + where S: AsyncChunkedSequence, S.Element == UInt8 + { + var iterator = bytes.makeAsyncIterator() + + guard var messagePduData = try await iterator + .nextChunk(count: AES70OCP1Connection.MinimumPduSize) + else { + throw Ocp1Error.pduTooShort + } + guard messagePduData[0] == Ocp1SyncValue else { + throw Ocp1Error.invalidSyncValue + } + let pduSize: OcaUint32 = Data(messagePduData).decodeInteger(index: 3) + guard await pduSize >= (AES70OCP1Connection.MinimumPduSize - 1) else { + throw Ocp1Error.invalidPduSize + } + let bytesLeft = await Int(pduSize) - (AES70OCP1Connection.MinimumPduSize - 1) + guard let remainder = try await iterator.nextChunk(count: bytesLeft) else { + throw Ocp1Error.pduTooShort + } + messagePduData += remainder + + var messagePdus = [Data]() + let messageType = try await AES70OCP1Connection.decodeOcp1MessagePdu( + from: Data(messagePduData), + messages: &messagePdus + ) + let messages = try messagePdus.map { + try AES70OCP1Connection.decodeOcp1Message(from: $0, type: messageType) + } + return (messages, messageType == .ocaCmdRrq) + } +} + +extension AsyncThrowingStream + where Element == AsyncSyncSequence<[AES70OCP1Controller.ControllerMessage]>, Failure == Error +{ + static func decodingMessages(from bytes: S) -> Self + where S.Element == UInt8 + { + AsyncThrowingStream, Error> { + do { + let (messages, rrq) = try await AES70OCP1Controller.decodeOcp1Messages(from: bytes) + return messages.map { ($0, rrq) }.async + } catch Ocp1Error.pduTooShort { + return nil + } catch { + throw error + } + } + } +} diff --git a/Sources/SwiftOCADevice/AES70/Device.swift b/Sources/SwiftOCADevice/AES70/Device.swift new file mode 100644 index 00000000..8d6cfdc8 --- /dev/null +++ b/Sources/SwiftOCADevice/AES70/Device.swift @@ -0,0 +1,461 @@ +// +// Device.swift +// +// Copyright (c) 2022 Simon Whitty. All rights reserved. +// Portions Copyright (c) 2023 PADL Software Pty Ltd. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +@_implementationOnly +import FlyingSocks +@_spi(Private) @_implementationOnly +import func FlyingSocks.withThrowingTimeout +import Foundation +import SwiftOCA + +public actor AES70OCP1Device { + let pool: AsyncSocketPool + private var address: sockaddr_storage + private let timeout: TimeInterval + private let logger: Logging? + private var nextObjectNumber: OcaONo = 4096 + /// Object interning, main thread only + var objects = [OcaONo: OcaRoot]() + + /// Root block, immutable + public var rootBlock: OcaBlock? + public var subscriptionManager: OcaSubscriptionManager? + public var deviceManager: OcaDeviceManager? + + public init( + address: UnsafePointer, + timeout: TimeInterval = 15 + ) { + var _address = sockaddr_storage() + address.withMemoryRebound(to: sockaddr_storage.self, capacity: 1) { cSockaddrStorage in + _address = cSockaddrStorage.pointee + } + self.address = _address + self.timeout = timeout + logger = Self.defaultLogger() + pool = Self.defaultPool(logger: logger) + } + + var listeningAddress: Socket.Address? { + try? state?.socket.sockname() + } + + public func register(object: OcaRoot) throws { + precondition( + object.objectNumber != OcaInvalidONo, + "cannot register object with invalid ONo" + ) + guard objects[object.objectNumber] == nil else { + throw Ocp1Error.status(.badONo) + } + objects[object.objectNumber] = object + if object.objectNumber != OcaRootBlockONo { + rootBlock?.addMember(object) + } + } + + public func allocateObjectNumber() -> OcaONo { + defer { nextObjectNumber += 1 } + return nextObjectNumber + } + + private func registerDefaultObjects() async throws { + rootBlock = try await OcaBlock(objectNumber: OcaRootBlockONo, deviceDelegate: self) + // pick up any objects that were registered beforehand + for object in objects.values { + guard object != rootBlock else { + continue + } + rootBlock?.addMember(object) + } + + subscriptionManager = try await OcaSubscriptionManager(deviceDelegate: self) + deviceManager = try await OcaDeviceManager(deviceDelegate: self) + } + + private func unregisterDefaultObjects() async throws { + if let subscriptionManager { rootBlock?.removeMember(subscriptionManager) } + if let deviceManager { rootBlock?.removeMember(deviceManager) } + rootBlock = nil + subscriptionManager = nil + deviceManager = nil + } + + public func start() async throws { + let socket = try await preparePoolAndSocket() + do { + try await registerDefaultObjects() + try? startBonjour() + + let task = Task { try await start(on: socket, pool: pool) } + state = (socket: socket, task: task) + defer { state = nil } + try await task.getValue(cancelling: .whenParentIsCancelled) + } catch { + logger?.logCritical("server error: \(error.localizedDescription)") + try? socket.close() + throw error + } + } + + func preparePoolAndSocket() async throws -> Socket { + do { + try await pool.prepare() + return try makeSocketAndListen() + } catch { + logger?.logCritical("server error: \(error.localizedDescription)") + throw error + } + } + + var waiting: Set = [] + private(set) var state: (socket: Socket, task: Task<(), Error>)? { + didSet { isListeningDidUpdate(from: oldValue != nil) } + } + + public func stop(timeout: TimeInterval = 0) async { + guard let (socket, task) = state else { return } + try? socket.close() + try? await task.getValue(cancelling: .afterTimeout(seconds: timeout)) + stopBonjour() + try? await unregisterDefaultObjects() + } + + func makeSocketAndListen() throws -> Socket { + let socket = try Socket(domain: Int32(address.ss_family)) + try socket.setValue(true, for: .localAddressReuse) + #if canImport(Darwin) + try socket.setValue(true, for: .noSIGPIPE) + #endif + try socket.bind(to: address) + try socket.listen() + logger?.logListening(on: socket) + return socket + } + + func start(on socket: Socket, pool: AsyncSocketPool) async throws { + let asyncSocket = try AsyncSocket(socket: socket, pool: pool) + + return try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await pool.run() + } + group.addTask { + try await self.listenForControllers(on: asyncSocket) + } + try await group.next() + } + } + + private func listenForControllers(on socket: AsyncSocket) async throws { + #if compiler(>=5.9) + if #available(macOS 14.0, iOS 17.0, tvOS 17.0, *) { + try await listenForControllersDiscarding(on: socket) + } else { + try await listenForControllersFallback(on: socket) + } + #else + try await listenForControllersFallback(on: socket) + #endif + } + + #if compiler(>=5.9) + @available(macOS 14.0, iOS 17.0, tvOS 17.0, *) + private func listenForControllersDiscarding(on socket: AsyncSocket) async throws { + try await withThrowingDiscardingTaskGroup { [logger] group in + for try await socket in socket.sockets { + group.addTask { + await self.handleController(AES70OCP1Controller(socket: socket, logger: logger)) + } + } + } + throw SocketError.disconnected + } + #endif + + @available(macOS, deprecated: 17.0, renamed: "listenForControllersDiscarding(on:)") + @available(iOS, deprecated: 17.0, renamed: "listenForControllersDiscarding(on:)") + @available(tvOS, deprecated: 17.0, renamed: "listenForControllersDiscarding(on:)") + private func listenForControllersFallback(on socket: AsyncSocket) async throws { + try await withThrowingTaskGroup(of: Void.self) { [logger] group in + for try await socket in socket.sockets { + group.addTask { + await self.handleController(AES70OCP1Controller(socket: socket, logger: logger)) + } + } + } + throw SocketError.disconnected + } + + private(set) var controllers: Set = [] + + private func handleController(_ controller: AES70OCP1Controller) async { + logger?.logControllerAdded(controller) + controllers.insert(controller) + do { + for try await (message, rrq) in controller.messages { + var response: Ocp1Response? + + switch message { + case let command as Ocp1Command: + logger?.logCommand(command, on: controller) + let commandResponse = await handleCommand(command, from: controller) + response = Ocp1Response( + handle: command.handle, + statusCode: commandResponse.statusCode, + parameters: commandResponse.parameters + ) + case let keepAlive as Ocp1KeepAlive1: + controller.keepAliveInterval = UInt64(keepAlive.heartBeatTime) * NSEC_PER_SEC + case let keepAlive as Ocp1KeepAlive2: + controller.keepAliveInterval = UInt64(keepAlive.heartBeatTime) * NSEC_PER_MSEC + default: + throw Ocp1Error.invalidMessageType + } + + if rrq, let response { + try await controller.sendMessage(response, type: .ocaRsp) + } + } + } catch { + logger?.logError(error, on: controller) + } + controllers.remove(controller) + try? controller.close() + logger?.logControllerRemoved(controller) + } + + func notifySubscribers( + _ event: OcaEvent, + parameters: Data + ) async throws { + await withTaskGroup(of: Void.self) { taskGroup in + for controller in controllers { + taskGroup.addTask { + try? await controller.notifySubscribers( + event, + parameters: parameters + ) + } + } + } + } + + func handleCommand( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async -> Ocp1Response { + await handleCommand(command, timeout: timeout, from: controller) + } + + func handleCommand( + _ command: Ocp1Command, + timeout: TimeInterval, + from controller: AES70OCP1Controller + ) async -> Ocp1Response { + do { + let object = objects[command.targetONo] + guard let object else { + throw Ocp1Error.status(.badONo) + } + + return try await withThrowingTimeout(seconds: timeout) { + try await object.handleCommand(command, from: controller) + } + } catch let Ocp1Error.status(status) { + return .init(responseSize: 0, handle: command.handle, statusCode: status) + } catch { + return .init(responseSize: 0, handle: command.handle, statusCode: .invalidRequest) + } + } + + static func defaultPool(logger: Logging? = nil) -> AsyncSocketPool { + #if canImport(Darwin) + return .kQueue(logger: logger) + #elseif canImport(CSystemLinux) + return .ePoll(logger: logger) + #else + return .poll(logger: logger) + #endif + } + + #if os(macOS) + private var netService: CFNetService? + + func createNetService() -> CFNetService? { + // FIXME: add support for UDP, WS, etc + + let serviceType = "_oca._tcp" + let serviceName = (deviceManager?.deviceName ?? "SwiftOCA") + "@" + Host.current() + .localizedName! + let domain = "" + + guard let port else { return nil } + return CFNetServiceCreate( + nil, + domain as CFString, + serviceType as CFString, + serviceName as CFString, + Int32(port) + ).takeRetainedValue() + } + + func startBonjour() throws { + var error = CFStreamError() + var clientContext = CFNetServiceClientContext() + netService = createNetService() + if let netService { + CFNetServiceSetClient(netService, registerCallback, &clientContext) + CFNetServiceScheduleWithRunLoop( + netService, + CFRunLoopGetCurrent(), + CFRunLoopMode.commonModes!.rawValue + ) + guard CFNetServiceRegisterWithOptions(netService, 0, &error) else { + stopBonjour() + throw Ocp1Error.bonjourRegistrationFailed + } + } + } + + func stopBonjour() { + if let netService { + CFNetServiceUnscheduleFromRunLoop( + netService, + CFRunLoopGetCurrent(), + CFRunLoopMode.commonModes!.rawValue + ) + CFNetServiceSetClient(netService, nil, nil) + self.netService = nil + } + } + + var port: UInt16? { + var port: UInt16? + switch address.ss_family { + case sa_family_t(AF_INET): + withUnsafePointer(to: &address) { + $0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { + port = UInt16(bigEndian: $0.pointee.sin_port) + } + } + case sa_family_t(AF_INET6): + withUnsafePointer(to: &address) { + $0.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { + port = UInt16(bigEndian: $0.pointee.sin6_port) + } + } + default: + port = nil + } + return port + } + #endif +} + +extension Logging { + func logControllerAdded(_ controller: AES70OCP1Controller) { + logInfo("\(controller.identifier) controller added") + } + + func logControllerRemoved(_ controller: AES70OCP1Controller) { + logInfo("\(controller.identifier) controller removed") + } + + func logCommand(_ command: Ocp1Command, on controller: AES70OCP1Controller) { + logInfo("\(controller.identifier) command: \(command)") + } + + func logError(_ error: Error, on controller: AES70OCP1Controller) { + logError("\(controller.identifier) error: \(error.localizedDescription)") + } + + func logListening(on socket: Socket) { + logInfo(Self.makeListening(on: try? socket.sockname())) + } + + static func makeListening(on addr: Socket.Address?) -> String { + var comps = ["starting server"] + guard let addr = addr else { + return comps.joined() + } + + switch addr { + case let .ip4(address, port: port): + if address == "0.0.0.0" { + comps.append("port: \(port)") + } else { + comps.append("\(address):\(port)") + } + case let .ip6(address, port: port): + if address == "::" { + comps.append("port: \(port)") + } else { + comps.append("\(address):\(port)") + } + case let .unix(path): + comps.append("path: \(path)") + } + return comps.joined(separator: " ") + } +} + +extension AES70OCP1Device { + public var isListening: Bool { state != nil } + + public func waitUntilListening(timeout: TimeInterval = 5) async throws { + try await withThrowingTimeout(seconds: timeout) { + try await self.doWaitUntilListening() + } + } + + private func doWaitUntilListening() async throws { + guard !isListening else { return } + let continuation = Continuation() + waiting.insert(continuation) + defer { waiting.remove(continuation) } + return try await continuation.value + } + + func isListeningDidUpdate(from previous: Bool) { + guard isListening else { return } + let waiting = waiting + self.waiting = [] + + for continuation in waiting { + continuation.resume() + } + } + + typealias Continuation = CancellingContinuation<(), Never> +} + +private func registerCallback( + _ theService: CFNetService, + _ error: UnsafeMutablePointer?, + _ info: UnsafeMutableRawPointer? +) { + debugPrint("registered network service \(theService)") +} diff --git a/Sources/SwiftOCADevice/AES70/Logging.swift b/Sources/SwiftOCADevice/AES70/Logging.swift new file mode 100644 index 00000000..4ef6759c --- /dev/null +++ b/Sources/SwiftOCADevice/AES70/Logging.swift @@ -0,0 +1,50 @@ +// +// Logging.swift +// +// Copyright (c) 2022 Simon Whitty. All rights reserved. +// Portions Copyright (c) 2023 PADL Software Pty Ltd. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +@_implementationOnly +import FlyingSocks + +extension Logging where Self == PrintLogger { + static func print(category: String = "FlyingFox") -> Self { + PrintLogger(category: category) + } +} + +extension AES70OCP1Device { + static func defaultLogger(category: String = "AES70") -> Logging { + defaultLogger(category: category, forceFallback: false) + } + + static func defaultLogger(category: String = "AES70", forceFallback: Bool) -> Logging { + guard !forceFallback, #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) else { + return .print(category: category) + } + #if canImport(OSLog) + return .oslog(category: category) + #else + return .print(category: category) + #endif + } +} diff --git a/Sources/SwiftOCADevice/AES70/Subscription.swift b/Sources/SwiftOCADevice/AES70/Subscription.swift new file mode 100644 index 00000000..81879c15 --- /dev/null +++ b/Sources/SwiftOCADevice/AES70/Subscription.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +/// A subscription +struct AES70OCP1Subscription: Codable, Equatable, Hashable { + let event: OcaEvent + let subscriber: OcaMethod + let subscriberContext: OcaBlob + let notificationDeliveryMode: OcaNotificationDeliveryMode + let destinationInformation: OcaNetworkAddress +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/DeviceManager.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/DeviceManager.swift new file mode 100644 index 00000000..1b2e55cc --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/DeviceManager.swift @@ -0,0 +1,143 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaDeviceManager: OcaManager { + override public class var classID: OcaClassID { OcaClassID("1.3.1") } + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.1"), + getMethodID: OcaMethodID("3.2") + ) + public var modelGUID = OcaModelGUID( + reserved: 0, + mfrCode: OcaOrganizationID((0, 0, 0)), + modelCode: 0 + ) + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.2"), + getMethodID: OcaMethodID("3.3") + ) + public var serialNumber = UUID.platformUUID + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.3"), + getMethodID: OcaMethodID("3.6") + ) + public var modelDescription = OcaModelDescription( + manufacturer: "PADL", + name: "SwiftOCA", + version: "0.0" + ) + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.4"), + getMethodID: OcaMethodID("3.4"), + setMethodID: OcaMethodID("3.5") + ) + public var deviceName = "SwiftOCA" + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.5"), + getMethodID: OcaMethodID("3.1") + ) + public var version = 1 + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.6"), + getMethodID: OcaMethodID("3.7"), + setMethodID: OcaMethodID("3.8") + ) + public var deviceRole = "" + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.7"), + getMethodID: OcaMethodID("3.9"), + setMethodID: OcaMethodID("3.10") + ) + public var userInventoryCode = "" + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.8"), + getMethodID: OcaMethodID("3.11"), + setMethodID: OcaMethodID("3.12") + ) + public var enabled = true + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.9"), + getMethodID: OcaMethodID("3.13") + ) + public var state = OcaDeviceState() + + @OcaDeviceProperty(propertyID: OcaPropertyID("3.10")) + public var busy = false + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.11"), + getMethodID: OcaMethodID("3.15") + ) + public var resetCause = OcaResetCause.powerOn + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.12"), + getMethodID: OcaMethodID("3.17"), + setMethodID: OcaMethodID("3.18") + ) + public var message = "" + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.13"), + getMethodID: OcaMethodID("3.19") + ) + public var managers = OcaList() + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.14"), + getMethodID: OcaMethodID("3.20") + ) + public var deviceRevisionID = "" + + public convenience init(deviceDelegate: AES70OCP1Device? = nil) async throws { + try await self.init( + objectNumber: OcaDeviceManagerONo, + role: "Device Manager", + deviceDelegate: deviceDelegate + ) + } +} + +extension UUID { + static var platformUUID: String { + let platformExpertDevice = IOServiceMatching("IOPlatformExpertDevice") + let platformExpert: io_service_t = IOServiceGetMatchingService( + kIOMasterPortDefault, + platformExpertDevice + ) + let serialNumberAsCFString = IORegistryEntryCreateCFProperty( + platformExpert, + kIOPlatformUUIDKey as CFString, + kCFAllocatorDefault, + 0 + ) + IOObjectRelease(platformExpert) + + return serialNumberAsCFString?.takeUnretainedValue() as! String + } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/Manager.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/Manager.swift new file mode 100644 index 00000000..9488c6f2 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/Manager.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaManager: OcaRoot { + override public class var classID: OcaClassID { OcaClassID("1.3") } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/NetworkManager.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/NetworkManager.swift new file mode 100644 index 00000000..13723f1a --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/NetworkManager.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaNetworkManager: OcaManager { + override public class var classID: OcaClassID { OcaClassID("1.3.6") } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/SubscriptionManager.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/SubscriptionManager.swift new file mode 100644 index 00000000..20668516 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Managers/SubscriptionManager.swift @@ -0,0 +1,86 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +public class OcaSubscriptionManager: OcaManager { + override public class var classID: OcaClassID { OcaClassID("1.3.4") } + + typealias AddSubscriptionParameters = AES70OCP1Subscription + + struct RemoveSubscriptionParameters: Codable { + let event: OcaEvent + let subscriber: OcaMethod + } + + func addSubscription( + _ subscription: AddSubscriptionParameters, + from controller: AES70OCP1Controller + ) async throws { + try await controller.addSubscription(subscription) + } + + func removeSubscription( + _ subscription: RemoveSubscriptionParameters, + from controller: AES70OCP1Controller + ) async throws { + try await controller.removeSubscription( + subscription.event, + subscriber: subscription.subscriber + ) + } + + func disableNotifications(from controller: AES70OCP1Controller) async throws { + controller.notificationsEnabled = false + } + + func reenableNotifications(from controller: AES70OCP1Controller) async throws { + controller.notificationsEnabled = true + } + + override public func handleCommand( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async throws -> Ocp1Response { + switch command.methodID { + case OcaMethodID("3.1"): + let subscription: AddSubscriptionParameters = try decodeCommand(command) + try await addSubscription(subscription, from: controller) + return Ocp1Response() + case OcaMethodID("3.2"): + let subscription: RemoveSubscriptionParameters = try decodeCommand(command) + try await removeSubscription(subscription, from: controller) + return Ocp1Response() + case OcaMethodID("3.3"): + try await disableNotifications(from: controller) + return Ocp1Response() + case OcaMethodID("3.4"): + try await reenableNotifications(from: controller) + return Ocp1Response() + default: + return try await super.handleCommand(command, from: controller) + } + } + + public convenience init(deviceDelegate: AES70OCP1Device? = nil) async throws { + try await self.init( + objectNumber: OcaSubscriptionManagerONo, + role: "Subscription Manager", + deviceDelegate: deviceDelegate + ) + } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Root+Commands.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Root+Commands.swift new file mode 100644 index 00000000..1e945a3d --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Root+Commands.swift @@ -0,0 +1,62 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BinaryCoder +import Foundation +import SwiftOCA + +extension OcaRoot { + func decodeCommand(_ command: Ocp1Command) throws -> U { + // FIXME: verify parameterCount + let decoder = BinaryDecoder(config: .ocp1Configuration) + return try decoder.decode(U.self, from: command.parameters.parameterData) + } + + private func parameterCount(for mirror: Mirror) -> OcaUint8 { + var count: OcaUint8 + + switch mirror.displayStyle { + case .struct: + fallthrough + case .class: + fallthrough + case .enum: + count = OcaUint8(mirror.children.count) + // FIXME: we'll probably need to use Echo for Optional + default: + count = 1 + } + + return count + } + + func encodeResponse( + _ parameters: T, + statusCode: OcaStatus = .ok + ) throws -> Ocp1Response { + let encoder = BinaryEncoder(config: .ocp1Configuration) + + let mirror = Mirror(reflecting: parameters) + let parameterCount = parameterCount(for: mirror) + + let parameters = Ocp1Parameters( + parameterCount: parameterCount, + parameterData: try encoder.encode(parameters) + ) + + return Ocp1Response(statusCode: statusCode, parameters: parameters) + } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Root.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Root.swift new file mode 100644 index 00000000..ccad7449 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Root.swift @@ -0,0 +1,148 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaRoot: CustomStringConvertible { + public class var classID: OcaClassID { OcaClassID("1") } + public class var classVersion: OcaClassVersionNumber { 2 } + public let objectNumber: OcaONo + public let lockable: OcaBoolean + public let role: OcaString + + weak var deviceDelegate: AES70OCP1Device? + + public class var classIdentification: OcaClassIdentification { + OcaClassIdentification(classID: classID, classVersion: classVersion) + } + + public var objectIdentification: OcaObjectIdentification { + OcaObjectIdentification(oNo: objectNumber, classIdentification: Self.classIdentification) + } + + public init( + objectNumber: OcaONo? = nil, + lockable: OcaBoolean = false, + role: OcaString = "Root", + deviceDelegate: AES70OCP1Device? = nil + ) async throws { + if let objectNumber { + precondition(objectNumber != OcaInvalidONo) + self.objectNumber = objectNumber + } else { + self.objectNumber = await deviceDelegate?.allocateObjectNumber() ?? OcaInvalidONo + } + self.lockable = lockable + self.role = role + self.deviceDelegate = deviceDelegate + if let deviceDelegate { + try await deviceDelegate.register(object: self) + } + } + + public var description: String { + "\(type(of: self))(objectNumber: \(objectNumber), role: \(role))" + } + + func handlePropertyAccessor( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async throws -> Ocp1Response { + for (_, propertyKeyPath) in allPropertyKeyPaths { + guard var property = + self[keyPath: propertyKeyPath] as? (any OcaDevicePropertyRepresentable) + else { + continue + } + + if command.methodID == property.getMethodID { + return try await property.get(object: self) + } else if command.methodID == property.setMethodID { + // FIXME: this doesn't actually mutate the value in the class + try await property.set(object: self, command: command) + return Ocp1Response() + } + } + throw Ocp1Error.unhandledMethod + } + + open func handleCommand( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async throws -> Ocp1Response { + switch command.methodID { + case OcaMethodID("1.1"): + return try encodeResponse(Self.classID) + case OcaMethodID("1.2"): + return try encodeResponse(Self.classVersion) + case OcaMethodID("1.3"): + return try encodeResponse(objectNumber) + case OcaMethodID("1.4"): + return try encodeResponse(lockable) + case OcaMethodID("1.5"): + return try encodeResponse(role) + default: + return try await handlePropertyAccessor(command, from: controller) + } + } + + public var isContainer: Bool { + false + } +} + +extension OcaRoot: Equatable { + public static func == (lhs: OcaRoot, rhs: OcaRoot) -> Bool { + lhs.objectNumber == rhs.objectNumber + } +} + +extension OcaRoot: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(objectNumber) + } +} + +extension OcaRoot { + private subscript(checkedMirrorDescendant key: String) -> Any { + Mirror(reflecting: self).descendant(key)! + } + + private var allKeyPaths: [String: PartialKeyPath] { + // TODO: Mirror is inefficient + var membersToKeyPaths = [String: PartialKeyPath]() + let mirror = Mirror(reflecting: self) + + for case let (key?, _) in mirror.children { + guard let dictionaryKey = key.deletingPrefix("_") else { continue } + membersToKeyPaths[dictionaryKey] = \Self + .[checkedMirrorDescendant: key] as PartialKeyPath + } + return membersToKeyPaths + } + + var allPropertyKeyPaths: [String: PartialKeyPath] { + allKeyPaths.filter { self[keyPath: $0.value] is any OcaDevicePropertyRepresentable } + } +} + +private extension String { + func deletingPrefix(_ prefix: String) -> String? { + guard hasPrefix(prefix) else { return nil } + return String(dropFirst(prefix.count)) + } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/Actuator.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/Actuator.swift new file mode 100644 index 00000000..f84033f4 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/Actuator.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaActuator: OcaWorker { + // 3.1 + override public class var classID: OcaClassID { OcaClassID("1.1.1") } + + // 3.2 + override public class var classVersion: OcaClassVersionNumber { OcaClassVersionNumber(2) } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/BasicActuators.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/BasicActuators.swift new file mode 100644 index 00000000..8a0f22c9 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Actuators/BasicActuators.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaBasicActuator: OcaActuator { + override public class var classID: OcaClassID { OcaClassID("1.1.1.1") } +} + +open class OcaBooleanActuator: OcaActuator { + override public class var classID: OcaClassID { OcaClassID("1.1.1.1.1") } + + @OcaDeviceProperty( + propertyID: OcaPropertyID("5.1"), + getMethodID: OcaMethodID("5.1"), + setMethodID: OcaMethodID("5.2") + ) + public var setting = false +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift new file mode 100644 index 00000000..7e3076c6 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/BlocksAndMatrices/Block.swift @@ -0,0 +1,124 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftOCA + +open class OcaBlock: OcaWorker { + override public class var classID: OcaClassID { OcaClassID("1.1.3") } + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.1"), + getMethodID: OcaMethodID("3.1") + ) + public var type: OcaONo = OcaInvalidONo + + /* + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.2"), + getMethodID: OcaMethodID("3.5") + ) + public var members = OcaList() + */ + + public var members = Set() + + func addMember(_ object: OcaRoot) { + precondition(object != self) + members.insert(object) + } + + func removeMember(_ object: OcaRoot) { + members.remove(object) + } + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.3"), + getMethodID: OcaMethodID("3.16") + ) + public var signalPaths = OcaMap() + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.4"), + getMethodID: OcaMethodID("3.11") + ) + public var mostRecentParamSetIdentifier: OcaLibVolIdentifier? + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.5"), + getMethodID: OcaMethodID("3.15") + ) + public var globalType: OcaGlobalTypeIdentifier? + + @OcaDeviceProperty( + propertyID: OcaPropertyID("3.6"), + getMethodID: OcaMethodID("3.16") + ) + public var oNoMap = OcaMap() + + func getRecursive(from controller: AES70OCP1Controller) async throws + -> OcaList + { + var members = [OcaBlockMember]() + + for member in self.members { + precondition(member != self) + if let member = member as? OcaBlock, + let recursiveMembers: [OcaBlockMember] = try? await member + .getRecursive(from: controller) + { + members.append(contentsOf: recursiveMembers) + } else { + members + .append(OcaBlockMember( + memberObjectIdentification: member.objectIdentification, + containerObjectNumber: objectNumber + )) + } + } + + return members + } + + func getRecursive(from controller: AES70OCP1Controller) async throws + -> OcaMap + { + throw Ocp1Error.notImplemented + } + + override public func handleCommand( + _ command: Ocp1Command, + from controller: AES70OCP1Controller + ) async throws -> Ocp1Response { + switch command.methodID { + case OcaMethodID("3.5"): + let members = members.map(\.objectIdentification) + return try encodeResponse(members) + case OcaMethodID("3.6"): + let members: [OcaBlockMember] = try await getRecursive(from: controller) + return try encodeResponse(members) + case OcaMethodID("3.10"): + let members: [OcaUint16: OcaSignalPath] = try await getRecursive(from: controller) + return try encodeResponse(members) + default: + return try await super.handleCommand(command, from: controller) + } + } + + override public var isContainer: Bool { + true + } +} diff --git a/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Worker.swift b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Worker.swift new file mode 100644 index 00000000..d305c5bd --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/ControlClasses/Workers/Worker.swift @@ -0,0 +1,19 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +open class OcaWorker: OcaRoot {} diff --git a/Sources/SwiftOCADevice/OCC/PropertyTypes/DeviceProperty.swift b/Sources/SwiftOCADevice/OCC/PropertyTypes/DeviceProperty.swift new file mode 100644 index 00000000..5aeb8b30 --- /dev/null +++ b/Sources/SwiftOCADevice/OCC/PropertyTypes/DeviceProperty.swift @@ -0,0 +1,144 @@ +// +// Copyright (c) 2023 PADL Software Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BinaryCoder +import Foundation +import SwiftOCA + +public protocol OcaDevicePropertyRepresentable { + associatedtype Value = Codable + + var propertyIDs: [OcaPropertyID] { get } + var getMethodID: OcaMethodID? { get } + var setMethodID: OcaMethodID? { get } + var wrappedValue: Value { get } + + func get(object: OcaRoot) async throws -> Ocp1Response + mutating func set(object: OcaRoot, command: Ocp1Command) async throws +} + +@propertyWrapper +public struct OcaDeviceProperty: OcaDevicePropertyRepresentable { + /// All property IDs supported by this property + public var propertyIDs: [OcaPropertyID] { + [propertyID] + } + + /// The OCA property ID + public let propertyID: OcaPropertyID + + /// The OCA get method ID + public let getMethodID: OcaMethodID? + + /// The OCA set method ID, if present + public let setMethodID: OcaMethodID? + + /// Placeholder only + public var wrappedValue: Value { + get { _storage } + nonmutating set { fatalError() } + } + + private var _storage: Value + + public init( + wrappedValue: Value, + propertyID: OcaPropertyID, + getMethodID: OcaMethodID? = nil, + setMethodID: OcaMethodID? = nil + ) { + self.propertyID = propertyID + self.getMethodID = getMethodID + self.setMethodID = setMethodID + _storage = wrappedValue + } + + public init( + propertyID: OcaPropertyID, + getMethodID: OcaMethodID? = nil, + setMethodID: OcaMethodID? = nil + ) where Value: ExpressibleByNilLiteral { + self.propertyID = propertyID + self.getMethodID = getMethodID + self.setMethodID = setMethodID + _storage = nil + } + + func _get(_enclosingInstance object: OcaRoot) -> Value { + _storage + } + + mutating func _set(_enclosingInstance object: OcaRoot, _ newValue: Value) { + _storage = newValue + } + + private func _didSet(_enclosingInstance object: OcaRoot, _ newValue: Value) async throws { + let event = OcaEvent(emitterONo: object.objectNumber, eventID: OcaPropertyChangedEventID) + let encoder = BinaryEncoder(config: .ocp1Configuration) + let parameters = OcaPropertyChangedEventData( + propertyID: propertyID, + propertyValue: newValue, + changeType: .currentChanged + ) + + try await object.deviceDelegate?.notifySubscribers( + event, + parameters: try encoder.encode(parameters) + ) + } + + public static subscript( + _enclosingInstance object: T, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + object[keyPath: storageKeyPath]._get(_enclosingInstance: object) + } + set { + object[keyPath: storageKeyPath]._set(_enclosingInstance: object, newValue) + Task { + try? await object[keyPath: storageKeyPath] + ._didSet(_enclosingInstance: object, newValue) + } + } + } + + public func get(object: OcaRoot) async throws -> Ocp1Response { + let value = _get(_enclosingInstance: object) + if isNil(value) { + throw Ocp1Error.status(.parameterOutOfRange) + } + return try object.encodeResponse(value) + } + + public mutating func set(object: OcaRoot, command: Ocp1Command) async throws { + let newValue: Value = try object.decodeCommand(command) + _set(_enclosingInstance: object, newValue) + try await _didSet(_enclosingInstance: object, newValue) + } +} + +func isNil(_ value: Value) -> Bool { + if let value = value as? ExpressibleByNilLiteral, + let value = value as? Value?, + case .none = value + { + return true + } else { + return false + } +} diff --git a/Sources/SwiftOCAUI/Views/Discovery/BonjourDiscoveryView.swift b/Sources/SwiftOCAUI/Views/Discovery/BonjourDiscoveryView.swift index 06d3a5fc..65473052 100644 --- a/Sources/SwiftOCAUI/Views/Discovery/BonjourDiscoveryView.swift +++ b/Sources/SwiftOCAUI/Views/Discovery/BonjourDiscoveryView.swift @@ -46,26 +46,14 @@ public struct OcaBonjourDiscoveryView: View { } } .task { - await withTaskGroup(of: NetService.self) { [self] _ in - for await result in udpBrowser.channel { - switch result { - case let .didFind(netService): - services.append(netService) - case let .didRemove(netService): - services.removeAll(where: { $0 == netService }) - case let .didNotSearch(error): - debugPrint("OcaBonjourDiscoveryView: \(error)") - } - } - for await result in tcpBrowser.channel { - switch result { - case let .didFind(netService): - services.append(netService) - case let .didRemove(netService): - services.removeAll(where: { $0 == netService }) - case let .didNotSearch(error): - debugPrint("OcaBonjourDiscoveryView: \(error)") - } + for await result in merge(udpBrowser.channel, tcpBrowser.channel) { + switch result { + case let .didFind(netService): + services.append(netService) + case let .didRemove(netService): + services.removeAll(where: { $0 == netService }) + case let .didNotSearch(error): + debugPrint("OcaBonjourDiscoveryView: \(error)") } } } diff --git a/SwiftOCA.xcodeproj/project.pbxproj b/SwiftOCA.xcodeproj/project.pbxproj index 2c1e250e..f861e86c 100644 --- a/SwiftOCA.xcodeproj/project.pbxproj +++ b/SwiftOCA.xcodeproj/project.pbxproj @@ -11,11 +11,28 @@ D306556D2A524D720083F093 /* PhysicalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D306556C2A524D720083F093 /* PhysicalPosition.swift */; }; D306556F2A5254D70083F093 /* TimeSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D306556E2A5254D70083F093 /* TimeSource.swift */; }; D31528802A526E24004B9BAD /* WorkerDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D315287F2A526E24004B9BAD /* WorkerDataTypes.swift */; }; + D33B52882A6FD190001A1BA7 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52872A6FD190001A1BA7 /* Device.swift */; }; + D33B528C2A6FD206001A1BA7 /* libSwiftOCA.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D3E6E1642A3ACA0B00BF7095 /* libSwiftOCA.dylib */; }; + D33B52902A6FD332001A1BA7 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B528F2A6FD332001A1BA7 /* Root.swift */; }; + D33B52932A6FD3A7001A1BA7 /* DeviceProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52922A6FD3A7001A1BA7 /* DeviceProperty.swift */; }; + D33B52952A6FE075001A1BA7 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52942A6FE075001A1BA7 /* Controller.swift */; }; + D33B52972A6FE08D001A1BA7 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52962A6FE08D001A1BA7 /* Subscription.swift */; }; + D33B52992A6FE140001A1BA7 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52982A6FE140001A1BA7 /* Logging.swift */; }; + D33B52A32A702CED001A1BA7 /* Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52A22A702CED001A1BA7 /* Block.swift */; }; + D33B52A52A702D04001A1BA7 /* Worker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52A42A702D04001A1BA7 /* Worker.swift */; }; + D33B52A82A702D29001A1BA7 /* Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52A72A702D29001A1BA7 /* Manager.swift */; }; + D33B52AA2A702D3F001A1BA7 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52A92A702D3F001A1BA7 /* NetworkManager.swift */; }; + D33B52AC2A702D4D001A1BA7 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52AB2A702D4D001A1BA7 /* DeviceManager.swift */; }; + D33B52AE2A702D92001A1BA7 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52AD2A702D92001A1BA7 /* SubscriptionManager.swift */; }; + D33B52B02A703AEA001A1BA7 /* Root+Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52AF2A703AEA001A1BA7 /* Root+Commands.swift */; }; + D33B52B42A7060BC001A1BA7 /* Actuator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52B32A7060BC001A1BA7 /* Actuator.swift */; }; + D33B52B62A7060E0001A1BA7 /* BasicActuators.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52B52A7060E0001A1BA7 /* BasicActuators.swift */; }; + D33B52C62A706235001A1BA7 /* DeviceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33B52C52A706235001A1BA7 /* DeviceApp.swift */; }; + D33B52C72A706245001A1BA7 /* libSwiftOCADevice.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D33B52812A6FD171001A1BA7 /* libSwiftOCADevice.dylib */; }; + D33B52CE2A706FB9001A1BA7 /* libSwiftOCA.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D3E6E1642A3ACA0B00BF7095 /* libSwiftOCA.dylib */; }; + D33B52CF2A706FB9001A1BA7 /* libSwiftOCA.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = D3E6E1642A3ACA0B00BF7095 /* libSwiftOCA.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D33B52D42A706FD7001A1BA7 /* FlyingSocks in Frameworks */ = {isa = PBXBuildFile; productRef = D33B52D32A706FD7001A1BA7 /* FlyingSocks */; }; D36ECA212A5AEB9B0015174A /* AES70OCP1FlyingSocksConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36ECA202A5AEB9B0015174A /* AES70OCP1FlyingSocksConnection.swift */; }; - D36ECA232A5AF9B10015174A /* FlyingSocks in Frameworks */ = {isa = PBXBuildFile; productRef = D36ECA222A5AF9B10015174A /* FlyingSocks */; }; - D36ECA252A5B13DE0015174A /* AES70OCP1SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36ECA242A5B13DE0015174A /* AES70OCP1SocketConnection.swift */; }; - D36ECA272A5B1AA70015174A /* Socket in Frameworks */ = {isa = PBXBuildFile; productRef = D36ECA262A5B1AA70015174A /* Socket */; }; - D36ECA2A2A5B20480015174A /* FlyingSocks in Frameworks */ = {isa = PBXBuildFile; productRef = D36ECA292A5B20480015174A /* FlyingSocks */; }; D36ECA2C2A5B24B30015174A /* AES70OCP1CFSocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36ECA2B2A5B24B30015174A /* AES70OCP1CFSocketConnection.swift */; }; D37F7E1B2A6D2602005F035F /* DeviceAddressToString.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37F7E1A2A6D2602005F035F /* DeviceAddressToString.swift */; }; D3DA69DD2A3DA844001250A1 /* OcaRoot+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA69D82A3DA7D6001250A1 /* OcaRoot+Identifiable.swift */; }; @@ -30,7 +47,7 @@ D3DA69F72A3FE538001250A1 /* DynamicStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA69F62A3FE538001250A1 /* DynamicStack.swift */; }; D3DA69FB2A3FEF52001250A1 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA69FA2A3FEF52001250A1 /* DeviceManager.swift */; }; D3DA69FE2A427C2F001250A1 /* BooleanActuatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DA69FD2A427C2F001250A1 /* BooleanActuatorView.swift */; }; - D3E6E1B32A3ACA7B00BF7095 /* AES70OCP1ConnectionMonitor+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1ConnectionMonitor+Codable.swift */; }; + D3E6E1B32A3ACA7B00BF7095 /* AES70OCP1Connection+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1Connection+Codable.swift */; }; D3E6E1B42A3ACA7B00BF7095 /* AES70OCP1Connection+Objects.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E1AA2A3ACA6C00BF7095 /* AES70OCP1Connection+Objects.swift */; }; D3E6E1B52A3ACA7B00BF7095 /* AES70OCP1Connection+Subscribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E1AC2A3ACA6C00BF7095 /* AES70OCP1Connection+Subscribe.swift */; }; D3E6E1B62A3ACA7B00BF7095 /* AES70ClassRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E1AF2A3ACA6C00BF7095 /* AES70ClassRegistry.swift */; }; @@ -112,13 +129,63 @@ D3E6E2A02A452D0A00BF7095 /* ScaledLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E6E29F2A452D0A00BF7095 /* ScaledLegendView.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + D33B52D02A706FB9001A1BA7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D3E6E15C2A3ACA0B00BF7095 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D3E6E1632A3ACA0B00BF7095; + remoteInfo = SwiftOCA; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + D33B52BA2A706201001A1BA7 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + D33B52D22A706FB9001A1BA7 /* Embed Libraries */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D33B52CF2A706FB9001A1BA7 /* libSwiftOCA.dylib in Embed Libraries */, + ); + name = "Embed Libraries"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ D3028C1D2A418BAE00B8DB42 /* BonjourDeviceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonjourDeviceView.swift; sourceTree = ""; }; D306556C2A524D720083F093 /* PhysicalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhysicalPosition.swift; sourceTree = ""; }; D306556E2A5254D70083F093 /* TimeSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSource.swift; sourceTree = ""; }; D315287F2A526E24004B9BAD /* WorkerDataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkerDataTypes.swift; sourceTree = ""; }; + D33B52812A6FD171001A1BA7 /* libSwiftOCADevice.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libSwiftOCADevice.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + D33B52872A6FD190001A1BA7 /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; + D33B528F2A6FD332001A1BA7 /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = ""; }; + D33B52922A6FD3A7001A1BA7 /* DeviceProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProperty.swift; sourceTree = ""; }; + D33B52942A6FE075001A1BA7 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = ""; }; + D33B52962A6FE08D001A1BA7 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; + D33B52982A6FE140001A1BA7 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + D33B52A22A702CED001A1BA7 /* Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Block.swift; sourceTree = ""; }; + D33B52A42A702D04001A1BA7 /* Worker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Worker.swift; sourceTree = ""; }; + D33B52A72A702D29001A1BA7 /* Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Manager.swift; sourceTree = ""; }; + D33B52A92A702D3F001A1BA7 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + D33B52AB2A702D4D001A1BA7 /* DeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = ""; }; + D33B52AD2A702D92001A1BA7 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; + D33B52AF2A703AEA001A1BA7 /* Root+Commands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Root+Commands.swift"; sourceTree = ""; }; + D33B52B32A7060BC001A1BA7 /* Actuator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actuator.swift; sourceTree = ""; }; + D33B52B52A7060E0001A1BA7 /* BasicActuators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicActuators.swift; sourceTree = ""; }; + D33B52BC2A706201001A1BA7 /* OCADevice */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = OCADevice; sourceTree = BUILT_PRODUCTS_DIR; }; + D33B52C52A706235001A1BA7 /* DeviceApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceApp.swift; sourceTree = ""; }; D36ECA202A5AEB9B0015174A /* AES70OCP1FlyingSocksConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AES70OCP1FlyingSocksConnection.swift; sourceTree = ""; }; - D36ECA242A5B13DE0015174A /* AES70OCP1SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AES70OCP1SocketConnection.swift; sourceTree = ""; }; D36ECA2B2A5B24B30015174A /* AES70OCP1CFSocketConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AES70OCP1CFSocketConnection.swift; sourceTree = ""; }; D37F7E1A2A6D2602005F035F /* DeviceAddressToString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAddressToString.swift; sourceTree = ""; }; D3DA69D82A3DA7D6001250A1 /* OcaRoot+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OcaRoot+Identifiable.swift"; sourceTree = ""; }; @@ -182,7 +249,7 @@ D3E6E1AB2A3ACA6C00BF7095 /* AES70OCP1Connection+Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AES70OCP1Connection+Messages.swift"; sourceTree = ""; }; D3E6E1AC2A3ACA6C00BF7095 /* AES70OCP1Connection+Subscribe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AES70OCP1Connection+Subscribe.swift"; sourceTree = ""; }; D3E6E1AD2A3ACA6C00BF7095 /* BinaryCodingConfiguration+OCP1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryCodingConfiguration+OCP1.swift"; sourceTree = ""; }; - D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1ConnectionMonitor+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AES70OCP1ConnectionMonitor+Codable.swift"; sourceTree = ""; }; + D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1Connection+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AES70OCP1Connection+Codable.swift"; sourceTree = ""; }; D3E6E1AF2A3ACA6C00BF7095 /* AES70ClassRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AES70ClassRegistry.swift; sourceTree = ""; }; D3E6E1B12A3ACA6C00BF7095 /* AES70OCP1Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AES70OCP1Connection.swift; sourceTree = ""; }; D3E6E1F62A3ACF5900BF7095 /* OCABrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OCABrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -213,16 +280,31 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D33B527F2A6FD171001A1BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D33B528C2A6FD206001A1BA7 /* libSwiftOCA.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D33B52B92A706201001A1BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D33B52CE2A706FB9001A1BA7 /* libSwiftOCA.dylib in Frameworks */, + D33B52C72A706245001A1BA7 /* libSwiftOCADevice.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3E6E1622A3ACA0B00BF7095 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D36ECA272A5B1AA70015174A /* Socket in Frameworks */, - D36ECA2A2A5B20480015174A /* FlyingSocks in Frameworks */, D3E6E25B2A3D28DD00BF7095 /* AsyncExtensions in Frameworks */, D3E6E1EB2A3ACAED00BF7095 /* BinaryCoder in Frameworks */, D3E6E1EE2A3ACB0700BF7095 /* AsyncAlgorithms in Frameworks */, - D36ECA232A5AF9B10015174A /* FlyingSocks in Frameworks */, + D33B52D42A706FD7001A1BA7 /* FlyingSocks in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -276,12 +358,130 @@ path = CollectionTypes; sourceTree = ""; }; + D33B52852A6FD190001A1BA7 /* Sources/SwiftOCADevice */ = { + isa = PBXGroup; + children = ( + D33B529C2A7018CE001A1BA7 /* OCA */, + D33B528D2A6FD322001A1BA7 /* OCC */, + D33B52862A6FD190001A1BA7 /* AES70 */, + ); + path = Sources/SwiftOCADevice; + sourceTree = ""; + }; + D33B52862A6FD190001A1BA7 /* AES70 */ = { + isa = PBXGroup; + children = ( + D33B52942A6FE075001A1BA7 /* Controller.swift */, + D33B52872A6FD190001A1BA7 /* Device.swift */, + D33B52982A6FE140001A1BA7 /* Logging.swift */, + D33B52962A6FE08D001A1BA7 /* Subscription.swift */, + ); + path = AES70; + sourceTree = ""; + }; + D33B528D2A6FD322001A1BA7 /* OCC */ = { + isa = PBXGroup; + children = ( + D33B52912A6FD39C001A1BA7 /* PropertyTypes */, + D33B528E2A6FD327001A1BA7 /* ControlClasses */, + ); + path = OCC; + sourceTree = ""; + }; + D33B528E2A6FD327001A1BA7 /* ControlClasses */ = { + isa = PBXGroup; + children = ( + D33B528F2A6FD332001A1BA7 /* Root.swift */, + D33B52AF2A703AEA001A1BA7 /* Root+Commands.swift */, + D33B52A62A702D1C001A1BA7 /* Managers */, + D33B52A02A702CD6001A1BA7 /* Workers */, + ); + path = ControlClasses; + sourceTree = ""; + }; + D33B52912A6FD39C001A1BA7 /* PropertyTypes */ = { + isa = PBXGroup; + children = ( + D33B52922A6FD3A7001A1BA7 /* DeviceProperty.swift */, + ); + path = PropertyTypes; + sourceTree = ""; + }; + D33B529C2A7018CE001A1BA7 /* OCA */ = { + isa = PBXGroup; + children = ( + D33B529D2A7018D0001A1BA7 /* OCF */, + ); + path = OCA; + sourceTree = ""; + }; + D33B529D2A7018D0001A1BA7 /* OCF */ = { + isa = PBXGroup; + children = ( + ); + path = OCF; + sourceTree = ""; + }; + D33B52A02A702CD6001A1BA7 /* Workers */ = { + isa = PBXGroup; + children = ( + D33B52B22A7060B6001A1BA7 /* Actuators */, + D33B52B12A7060B4001A1BA7 /* Sensors */, + D33B52A12A702CD8001A1BA7 /* BlocksAndMatrices */, + D33B52A42A702D04001A1BA7 /* Worker.swift */, + ); + path = Workers; + sourceTree = ""; + }; + D33B52A12A702CD8001A1BA7 /* BlocksAndMatrices */ = { + isa = PBXGroup; + children = ( + D33B52A22A702CED001A1BA7 /* Block.swift */, + ); + path = BlocksAndMatrices; + sourceTree = ""; + }; + D33B52A62A702D1C001A1BA7 /* Managers */ = { + isa = PBXGroup; + children = ( + D33B52A72A702D29001A1BA7 /* Manager.swift */, + D33B52A92A702D3F001A1BA7 /* NetworkManager.swift */, + D33B52AB2A702D4D001A1BA7 /* DeviceManager.swift */, + D33B52AD2A702D92001A1BA7 /* SubscriptionManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; + D33B52B12A7060B4001A1BA7 /* Sensors */ = { + isa = PBXGroup; + children = ( + ); + path = Sensors; + sourceTree = ""; + }; + D33B52B22A7060B6001A1BA7 /* Actuators */ = { + isa = PBXGroup; + children = ( + D33B52B32A7060BC001A1BA7 /* Actuator.swift */, + D33B52B52A7060E0001A1BA7 /* BasicActuators.swift */, + ); + path = Actuators; + sourceTree = ""; + }; + D33B52C32A706235001A1BA7 /* OCADevice */ = { + isa = PBXGroup; + children = ( + D33B52C52A706235001A1BA7 /* DeviceApp.swift */, + ); + name = OCADevice; + path = Examples/OCADevice; + sourceTree = ""; + }; D37F7E192A6BC015005F035F /* SocketBackend */ = { isa = PBXGroup; children = ( D36ECA2B2A5B24B30015174A /* AES70OCP1CFSocketConnection.swift */, D36ECA202A5AEB9B0015174A /* AES70OCP1FlyingSocksConnection.swift */, - D36ECA242A5B13DE0015174A /* AES70OCP1SocketConnection.swift */, ); path = SocketBackend; sourceTree = ""; @@ -350,6 +550,8 @@ D3E6E15B2A3ACA0B00BF7095 = { isa = PBXGroup; children = ( + D33B52C32A706235001A1BA7 /* OCADevice */, + D33B52852A6FD190001A1BA7 /* Sources/SwiftOCADevice */, D3E6E2472A3C0C7700BF7095 /* SwiftOCAUI */, D3028C1C2A3AD57900B8DB42 /* Sources/SwiftOCA */, D3E6E1F72A3ACF5900BF7095 /* Examples/OCABrowser */, @@ -364,6 +566,8 @@ D3E6E1642A3ACA0B00BF7095 /* libSwiftOCA.dylib */, D3E6E1F62A3ACF5900BF7095 /* OCABrowser.app */, D3E6E2432A3C0BA000BF7095 /* libSwiftOCAUI.dylib */, + D33B52812A6FD171001A1BA7 /* libSwiftOCADevice.dylib */, + D33B52BC2A706201001A1BA7 /* OCADevice */, ); name = Products; sourceTree = ""; @@ -501,7 +705,7 @@ D37F7E192A6BC015005F035F /* SocketBackend */, D3E6E1B12A3ACA6C00BF7095 /* AES70OCP1Connection.swift */, D3E6E1A92A3ACA6C00BF7095 /* AES70OCP1ConnectionMonitor.swift */, - D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1ConnectionMonitor+Codable.swift */, + D3E6E1AE2A3ACA6C00BF7095 /* AES70OCP1Connection+Codable.swift */, D3E6E1AA2A3ACA6C00BF7095 /* AES70OCP1Connection+Objects.swift */, D3E6E1AB2A3ACA6C00BF7095 /* AES70OCP1Connection+Messages.swift */, D3E6E1AC2A3ACA6C00BF7095 /* AES70OCP1Connection+Subscribe.swift */, @@ -635,6 +839,13 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + D33B527D2A6FD171001A1BA7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3E6E1602A3ACA0B00BF7095 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -652,6 +863,46 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + D33B52802A6FD171001A1BA7 /* SwiftOCADevice */ = { + isa = PBXNativeTarget; + buildConfigurationList = D33B52842A6FD171001A1BA7 /* Build configuration list for PBXNativeTarget "SwiftOCADevice" */; + buildPhases = ( + D33B527D2A6FD171001A1BA7 /* Headers */, + D33B527E2A6FD171001A1BA7 /* Sources */, + D33B527F2A6FD171001A1BA7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftOCADevice; + packageProductDependencies = ( + ); + productName = SwiftOCADevice; + productReference = D33B52812A6FD171001A1BA7 /* libSwiftOCADevice.dylib */; + productType = "com.apple.product-type.library.dynamic"; + }; + D33B52BB2A706201001A1BA7 /* OCADevice */ = { + isa = PBXNativeTarget; + buildConfigurationList = D33B52C02A706201001A1BA7 /* Build configuration list for PBXNativeTarget "OCADevice" */; + buildPhases = ( + D33B52B82A706201001A1BA7 /* Sources */, + D33B52B92A706201001A1BA7 /* Frameworks */, + D33B52BA2A706201001A1BA7 /* CopyFiles */, + D33B52D22A706FB9001A1BA7 /* Embed Libraries */, + ); + buildRules = ( + ); + dependencies = ( + D33B52D12A706FB9001A1BA7 /* PBXTargetDependency */, + ); + name = OCADevice; + packageProductDependencies = ( + ); + productName = DeviceApp; + productReference = D33B52BC2A706201001A1BA7 /* OCADevice */; + productType = "com.apple.product-type.tool"; + }; D3E6E1632A3ACA0B00BF7095 /* SwiftOCA */ = { isa = PBXNativeTarget; buildConfigurationList = D3E6E1682A3ACA0B00BF7095 /* Build configuration list for PBXNativeTarget "SwiftOCA" */; @@ -669,9 +920,7 @@ D3E6E1EA2A3ACAED00BF7095 /* BinaryCoder */, D3E6E1ED2A3ACB0700BF7095 /* AsyncAlgorithms */, D3E6E25A2A3D28DD00BF7095 /* AsyncExtensions */, - D36ECA222A5AF9B10015174A /* FlyingSocks */, - D36ECA262A5B1AA70015174A /* Socket */, - D36ECA292A5B20480015174A /* FlyingSocks */, + D33B52D32A706FD7001A1BA7 /* FlyingSocks */, ); productName = SwiftOCA; productReference = D3E6E1642A3ACA0B00BF7095 /* libSwiftOCA.dylib */; @@ -724,6 +973,12 @@ LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1420; TargetAttributes = { + D33B52802A6FD171001A1BA7 = { + CreatedOnToolsVersion = 14.2; + }; + D33B52BB2A706201001A1BA7 = { + CreatedOnToolsVersion = 14.2; + }; D3E6E1632A3ACA0B00BF7095 = { CreatedOnToolsVersion = 14.2; }; @@ -747,7 +1002,6 @@ packageReferences = ( D3E6E1E92A3ACAED00BF7095 /* XCRemoteSwiftPackageReference "swift-binary-coder" */, D3E6E1EC2A3ACB0700BF7095 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - D3E6E1EF2A3ACB1E00BF7095 /* XCRemoteSwiftPackageReference "Socket" */, D3E6E2592A3D28DD00BF7095 /* XCRemoteSwiftPackageReference "AsyncExtensions" */, D3E6E2842A43B8D000BF7095 /* XCRemoteSwiftPackageReference "swiftui-sliders" */, D36ECA282A5B20480015174A /* XCRemoteSwiftPackageReference "FlyingFox" */, @@ -759,6 +1013,8 @@ D3E6E1632A3ACA0B00BF7095 /* SwiftOCA */, D3E6E1F52A3ACF5900BF7095 /* OCABrowser */, D3E6E2422A3C0BA000BF7095 /* SwiftOCAUI */, + D33B52802A6FD171001A1BA7 /* SwiftOCADevice */, + D33B52BB2A706201001A1BA7 /* OCADevice */, ); }; /* End PBXProject section */ @@ -776,6 +1032,36 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D33B527E2A6FD171001A1BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D33B52B42A7060BC001A1BA7 /* Actuator.swift in Sources */, + D33B52882A6FD190001A1BA7 /* Device.swift in Sources */, + D33B52A52A702D04001A1BA7 /* Worker.swift in Sources */, + D33B52AA2A702D3F001A1BA7 /* NetworkManager.swift in Sources */, + D33B52AC2A702D4D001A1BA7 /* DeviceManager.swift in Sources */, + D33B52A32A702CED001A1BA7 /* Block.swift in Sources */, + D33B52B62A7060E0001A1BA7 /* BasicActuators.swift in Sources */, + D33B52992A6FE140001A1BA7 /* Logging.swift in Sources */, + D33B52932A6FD3A7001A1BA7 /* DeviceProperty.swift in Sources */, + D33B52902A6FD332001A1BA7 /* Root.swift in Sources */, + D33B52AE2A702D92001A1BA7 /* SubscriptionManager.swift in Sources */, + D33B52952A6FE075001A1BA7 /* Controller.swift in Sources */, + D33B52B02A703AEA001A1BA7 /* Root+Commands.swift in Sources */, + D33B52A82A702D29001A1BA7 /* Manager.swift in Sources */, + D33B52972A6FE08D001A1BA7 /* Subscription.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D33B52B82A706201001A1BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D33B52C62A706235001A1BA7 /* DeviceApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3E6E1612A3ACA0B00BF7095 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -838,8 +1124,7 @@ D3E6E1B92A3ACA7B00BF7095 /* BinaryCodingConfiguration+OCP1.swift in Sources */, D3E6E2392A3BD52E00BF7095 /* AES70Browser.swift in Sources */, D3E6E1DE2A3ACAA900BF7095 /* WellKnownObjectNumbers.swift in Sources */, - D3E6E1B32A3ACA7B00BF7095 /* AES70OCP1ConnectionMonitor+Codable.swift in Sources */, - D36ECA252A5B13DE0015174A /* AES70OCP1SocketConnection.swift in Sources */, + D3E6E1B32A3ACA7B00BF7095 /* AES70OCP1Connection+Codable.swift in Sources */, D3E6E1BE2A3ACA8200BF7095 /* Command.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -890,7 +1175,68 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + D33B52D12A706FB9001A1BA7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D3E6E1632A3ACA0B00BF7095 /* SwiftOCA */; + targetProxy = D33B52D02A706FB9001A1BA7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + D33B52822A6FD171001A1BA7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T53Q9CN4T9; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D33B52832A6FD171001A1BA7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T53Q9CN4T9; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + D33B52C12A706201001A1BA7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T53Q9CN4T9; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D33B52C22A706201001A1BA7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T53Q9CN4T9; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; D3E6E1662A3ACA0B00BF7095 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1156,6 +1502,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D33B52842A6FD171001A1BA7 /* Build configuration list for PBXNativeTarget "SwiftOCADevice" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D33B52822A6FD171001A1BA7 /* Debug */, + D33B52832A6FD171001A1BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D33B52C02A706201001A1BA7 /* Build configuration list for PBXNativeTarget "OCADevice" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D33B52C12A706201001A1BA7 /* Debug */, + D33B52C22A706201001A1BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D3E6E15F2A3ACA0B00BF7095 /* Build configuration list for PBXProject "SwiftOCA" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1219,14 +1583,6 @@ minimumVersion = 0.1.0; }; }; - D3E6E1EF2A3ACB1E00BF7095 /* XCRemoteSwiftPackageReference "Socket" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "git@github.com:PureSwift/Socket.git"; - requirement = { - branch = main; - kind = branch; - }; - }; D3E6E2592A3D28DD00BF7095 /* XCRemoteSwiftPackageReference "AsyncExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:sideeffect-io/AsyncExtensions.git"; @@ -1246,16 +1602,7 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D36ECA222A5AF9B10015174A /* FlyingSocks */ = { - isa = XCSwiftPackageProductDependency; - productName = FlyingSocks; - }; - D36ECA262A5B1AA70015174A /* Socket */ = { - isa = XCSwiftPackageProductDependency; - package = D3E6E1EF2A3ACB1E00BF7095 /* XCRemoteSwiftPackageReference "Socket" */; - productName = Socket; - }; - D36ECA292A5B20480015174A /* FlyingSocks */ = { + D33B52D32A706FD7001A1BA7 /* FlyingSocks */ = { isa = XCSwiftPackageProductDependency; package = D36ECA282A5B20480015174A /* XCRemoteSwiftPackageReference "FlyingFox" */; productName = FlyingSocks; diff --git a/SwiftOCA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftOCA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2521a7f0..03b7650c 100644 --- a/SwiftOCA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftOCA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,15 +18,6 @@ "version" : "0.12.1" } }, - { - "identity" : "socket", - "kind" : "remoteSourceControl", - "location" : "git@github.com:PureSwift/Socket.git", - "state" : { - "branch" : "main", - "revision" : "489e63b9cf0998f820f9f994f25cd5cb5f602404" - } - }, { "identity" : "swift-async-algorithms", "kind" : "remoteSourceControl", @@ -54,15 +45,6 @@ "version" : "1.0.4" } }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/PureSwift/swift-system", - "state" : { - "branch" : "feature/dynamic-lib", - "revision" : "0abad32db1fc3bf5c7008b523d267c71390df3ac" - } - }, { "identity" : "swiftui-sliders", "kind" : "remoteSourceControl",