diff --git a/Package.swift b/Package.swift index c31e0b2..1a01e8b 100644 --- a/Package.swift +++ b/Package.swift @@ -91,7 +91,8 @@ var package = Package( package: "Bluetooth", condition: .when(platforms: [.macOS, .linux]) ) - ] + ], + swiftSettings: [.swiftLanguageMode(.v5)] ) ] ) diff --git a/Tests/GATTTests/GATTTests.swift b/Tests/GATTTests/GATTTests.swift index 61deff0..39d9fbb 100644 --- a/Tests/GATTTests/GATTTests.swift +++ b/Tests/GATTTests/GATTTests.swift @@ -16,7 +16,7 @@ import BluetoothHCI final class GATTTests: XCTestCase { - typealias TestPeripheral = GATTPeripheral + typealias TestPeripheral = GATTPeripheral typealias TestCentral = GATTCentral func testScanData() { @@ -150,7 +150,7 @@ final class GATTTests: XCTestCase { (ATTReadByGroupTypeResponse(attributeData: [ ATTReadByGroupTypeResponse.AttributeData(attributeHandle: 0x001, endGroupHandle: 0x0004, - value: BluetoothUUID.batteryService.littleEndian.data) + value: Data(BluetoothUUID.batteryService.littleEndian)) ])!, [0x11, 0x06, 0x01, 0x00, 0x04, 0x00, 0x0F, 0x18]), /** @@ -181,17 +181,17 @@ final class GATTTests: XCTestCase { let batteryLevel = GATTBatteryLevel(level: .min) let characteristics = [ - GATTAttribute.Characteristic( + GATTAttribute.Characteristic( uuid: type(of: batteryLevel).uuid, value: batteryLevel.data, permissions: [.read], properties: [.read, .notify], - descriptors: [GATTClientCharacteristicConfiguration().descriptor]) + descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])]) ] - let service = GATTAttribute.Service( + let service = GATTAttribute.Service( uuid: .batteryService, - primary: true, + isPrimary: true, characteristics: characteristics ) @@ -235,7 +235,7 @@ final class GATTTests: XCTestCase { let batteryLevel = GATTBatteryLevel(level: .max) let characteristics = [ - GATTAttribute.Characteristic( + GATTAttribute.Characteristic( uuid: type(of: batteryLevel).uuid, value: batteryLevel.data, permissions: [.read, .write], @@ -244,9 +244,9 @@ final class GATTTests: XCTestCase { ) ] - let service = GATTAttribute.Service( + let service = GATTAttribute.Service( uuid: .batteryService, - primary: true, + isPrimary: true, characteristics: characteristics ) @@ -313,18 +313,18 @@ final class GATTTests: XCTestCase { let batteryLevel = GATTBatteryLevel(level: .max) let characteristics = [ - GATTAttribute.Characteristic( + GATTAttribute.Characteristic( uuid: type(of: batteryLevel).uuid, value: batteryLevel.data, permissions: [.read], properties: [.read, .notify], - descriptors: [GATTClientCharacteristicConfiguration().descriptor] + descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])] ) ] - let service = GATTAttribute.Service( + let service = GATTAttribute.Service( uuid: .batteryService, - primary: true, + isPrimary: true, characteristics: characteristics ) @@ -378,18 +378,18 @@ final class GATTTests: XCTestCase { let batteryLevel = GATTBatteryLevel(level: .max) let characteristics = [ - GATTAttribute.Characteristic( + GATTAttribute.Characteristic( uuid: type(of: batteryLevel).uuid, value: batteryLevel.data, permissions: [.read], properties: [.read, .indicate], - descriptors: [GATTClientCharacteristicConfiguration().descriptor] + descriptors: [.init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write])] ) ] - let service = GATTAttribute.Service( + let service = GATTAttribute.Service( uuid: .batteryService, - primary: true, + isPrimary: true, characteristics: characteristics ) @@ -440,28 +440,28 @@ final class GATTTests: XCTestCase { func testDescriptors() async throws { let descriptors = [ - GATTClientCharacteristicConfiguration().descriptor, + .init(GATTClientCharacteristicConfiguration(), permissions: [.read, .write]), //GATTUserDescription(userDescription: "Characteristic").descriptor, - GATTAttribute.Descriptor(uuid: BluetoothUUID(), + GATTAttribute.Descriptor(uuid: BluetoothUUID(), value: Data("UInt128 Descriptor".utf8), permissions: [.read, .write]), - GATTAttribute.Descriptor(uuid: .savantSystems, + GATTAttribute.Descriptor(uuid: .savantSystems, value: Data("Savant".utf8), permissions: [.read]), - GATTAttribute.Descriptor(uuid: .savantSystems2, + GATTAttribute.Descriptor(uuid: .savantSystems2, value: Data("Savant2".utf8), permissions: [.write]) ] - let characteristic = GATTAttribute.Characteristic(uuid: BluetoothUUID(), + let characteristic = GATTAttribute.Characteristic(uuid: BluetoothUUID(), value: Data(), permissions: [.read], properties: [.read], descriptors: descriptors) - let service = GATTAttribute.Service( + let service = GATTAttribute.Service( uuid: BluetoothUUID(), - primary: true, + isPrimary: true, characteristics: [characteristic] ) @@ -540,7 +540,7 @@ extension GATTTests { let peripheral = TestPeripheral( hostController: serverHostController, options: serverOptions, - socket: TestL2CAPSocket.self + socket: TestL2CAPServer.self ) peripheral.log = { print("Peripheral:", $0) } try await server(peripheral) @@ -578,10 +578,10 @@ extension GATTTests { // decode and compare for (testPDU, testData) in testPDUs { - guard let decodedPDU = type(of: testPDU).init(data: Data(testData)) + guard let decodedPDU = type(of: testPDU).init(data: testData) else { XCTFail("Could not decode \(type(of: testPDU))"); return } - XCTAssertEqual(decodedPDU.data, Data(testData), file: file, line: line) + XCTAssertEqual(Data(decodedPDU), Data(testData), file: file, line: line) } } diff --git a/Tests/GATTTests/TestHostController.swift b/Tests/GATTTests/TestHostController.swift index b22eae4..5fc80ea 100644 --- a/Tests/GATTTests/TestHostController.swift +++ b/Tests/GATTTests/TestHostController.swift @@ -12,6 +12,8 @@ import Bluetooth import BluetoothHCI final class TestHostController: BluetoothHostControllerInterface { + + typealias Data = Foundation.Data /// All controllers on the host. static var controllers: [TestHostController] { return [TestHostController(address: .min)] } @@ -83,7 +85,7 @@ final class TestHostController: BluetoothHostControllerInterface { /// Sends a command to the device and waits for a response with return parameter values. func deviceRequest (_ commandReturnType : Return.Type, timeout: HCICommandTimeout) throws -> Return { if commandReturnType == HCIReadDeviceAddress.self { - return HCIReadDeviceAddress(data: self.address.littleEndian.data)! as! Return + return HCIReadDeviceAddress(data: Data(self.address.littleEndian))! as! Return } fatalError("\(commandReturnType) not mocked") } @@ -96,10 +98,9 @@ final class TestHostController: BluetoothHostControllerInterface { } /// Polls and waits for events. - /// Polls and waits for events. - func recieve(_ eventType: Event.Type) async throws -> Event where Event : HCIEventParameter, Event.HCIEventType == HCIGeneralEvent { + func receive(_ eventType: Event.Type) async throws -> Event where Event : BluetoothHCI.HCIEventParameter, Event.HCIEventType == BluetoothHCI.HCIGeneralEvent { - guard eventType == HCILowEnergyMetaEvent.self + guard eventType == HCILowEnergyMetaEvent.self else { fatalError("Invalid event parameter type") } while self.advertisingReports.isEmpty { @@ -120,6 +121,8 @@ final class TestHostController: BluetoothHostControllerInterface { assert(eventHeader?.event.rawValue == Event.event.rawValue) return eventParameter } + + } internal extension Array { diff --git a/Tests/GATTTests/TestL2CAPSocket.swift b/Tests/GATTTests/TestL2CAPSocket.swift index 7404d51..6dc7c52 100644 --- a/Tests/GATTTests/TestL2CAPSocket.swift +++ b/Tests/GATTTests/TestL2CAPSocket.swift @@ -11,58 +11,111 @@ import Foundation import Bluetooth import GATT -/// Test L2CAP socket -internal actor TestL2CAPSocket: L2CAPSocket { - - private actor Cache { +internal final class TestL2CAPServer: L2CAPServer { - static let shared = Cache() + typealias Error = POSIXError + + enum Cache { - private init() { } + static let lock = NSLock() - var pendingClients = [BluetoothAddress: [TestL2CAPSocket]]() + nonisolated(unsafe) static var pendingClients = [BluetoothAddress: [TestL2CAPSocket]]() - func queue(client socket: TestL2CAPSocket, server: BluetoothAddress) { + static func queue(client socket: TestL2CAPSocket, server: BluetoothAddress) { + lock.lock() + defer { lock.unlock() } pendingClients[server, default: []].append(socket) } - func dequeue(server: BluetoothAddress) -> TestL2CAPSocket? { + static func dequeue(server: BluetoothAddress) -> TestL2CAPSocket? { + lock.lock() + defer { lock.unlock() } guard let socket = pendingClients[server]?.first else { return nil } pendingClients[server]?.removeFirst() return socket } + + static func canAccept(server: BluetoothAddress) -> Bool { + lock.lock() + defer { lock.unlock() } + return pendingClients[server, default: []].isEmpty + } } - static func lowEnergyClient( - address: BluetoothAddress, - destination: BluetoothAddress, - isRandom: Bool - ) async throws -> TestL2CAPSocket { - let socket = TestL2CAPSocket( - address: address, - name: "Client" + let name: String + + let address: BluetoothAddress + + var status: L2CAPSocketStatus { + .init( + send: false, + recieve: false, + accept: Cache.canAccept(server: address) ) - print("Client \(address) will connect to \(destination)") - // append to pending clients - await Cache.shared.queue(client: socket, server: destination) - // wait until client has connected - while await (Cache.shared.pendingClients[destination] ?? []).contains(where: { $0 === socket }) { - try await Task.sleep(nanoseconds: 10_000_000) - } - return socket + } + + init(name: String, address: BluetoothAddress) { + self.name = name + self.address = address + } + + deinit { + close() } static func lowEnergyServer( address: BluetoothAddress, isRandom: Bool, backlog: Int - ) async throws -> TestL2CAPSocket { - return TestL2CAPSocket( + ) throws(POSIXError) -> TestL2CAPServer { + return TestL2CAPServer( + name: "Server", + address: address + ) + } + + func accept() throws(POSIXError) -> TestL2CAPSocket { + // dequeue socket + guard let client = Cache.dequeue(server: address) else { + throw POSIXError(.EAGAIN) + } + let newConnection = TestL2CAPSocket( + address: client.address, + destination: self.address, + name: "Server connection" + ) + // connect sockets + newConnection.connect(to: client) + client.connect(to: newConnection) + return newConnection + } + + func close() { + + } +} + +/// Test L2CAP socket +internal final class TestL2CAPSocket: L2CAPConnection { + + typealias Data = Foundation.Data + + typealias Error = POSIXError + + static func lowEnergyClient( + address: BluetoothAddress, + destination: BluetoothAddress, + isRandom: Bool + ) throws(POSIXError) -> TestL2CAPSocket { + let socket = TestL2CAPSocket( address: address, - name: "Server" + destination: destination, + name: "Client" ) + TestL2CAPServer.Cache.queue(client: socket, server: destination) + return socket } // MARK: - Properties @@ -71,96 +124,94 @@ internal actor TestL2CAPSocket: L2CAPSocket { let address: BluetoothAddress - public let event: L2CAPSocketEventStream + let destination: Bluetooth.BluetoothAddress - private var eventContinuation: L2CAPSocketEventStream.Continuation! + var status: L2CAPSocketStatus { + .init( + send: target != nil, + recieve: target != nil && receivedData.isEmpty == false, + accept: false, + error: nil + ) + } - /// The socket's security level. - private(set) var securityLevel: SecurityLevel = .sdp + func securityLevel() throws(POSIXError) -> Bluetooth.SecurityLevel { + _securityLevel + } + private var _securityLevel: Bluetooth.SecurityLevel = .sdp + /// Attempts to change the socket's security level. - func setSecurityLevel(_ securityLevel: SecurityLevel) async throws { - self.securityLevel = securityLevel + func setSecurityLevel(_ securityLevel: SecurityLevel) throws(POSIXError) { + _securityLevel = securityLevel } /// Target socket. private weak var target: TestL2CAPSocket? - fileprivate(set) var receivedData = [Data]() + fileprivate(set) var receivedData = [Foundation.Data]() - private(set) var cache = [Data]() + private(set) var cache = [Foundation.Data]() // MARK: - Initialization - private init( + init( address: BluetoothAddress = .zero, + destination: Bluetooth.BluetoothAddress, name: String ) { self.address = address + self.destination = destination self.name = name - var continuation: L2CAPSocketEventStream.Continuation! - self.event = L2CAPSocketEventStream { - continuation = $0 - } - self.eventContinuation = continuation } - // MARK: - Methods - - func close() async { - + deinit { + close() } - func accept() async throws -> TestL2CAPSocket { - // sleep until a client socket is created - while (await Cache.shared.pendingClients[address] ?? []).isEmpty { - try await Task.sleep(nanoseconds: 10_000_000) - } - let client = await Cache.shared.dequeue(server: address)! - let newConnection = TestL2CAPSocket(address: client.address, name: "Server connection") - // connect sockets - await newConnection.connect(to: client) - await client.connect(to: newConnection) - return newConnection + // MARK: - Methods + + func close() { + target = nil + target?.target = nil } /// Write to the socket. - func send(_ data: Data) async throws { + func send(_ data: Data) throws(POSIXError) { print("L2CAP Socket: \(name) will send \(data.count) bytes") guard let target = self.target else { throw POSIXError(.ECONNRESET) } - await target.receive(data) - eventContinuation.yield(.didWrite(data.count)) + target.receive(data) } /// Reads from the socket. - func recieve(_ bufferSize: Int) async throws -> Data { + func receive(_ bufferSize: Int) throws(POSIXError) -> Data { print("L2CAP Socket: \(name) will read \(bufferSize) bytes") - while self.receivedData.isEmpty { - guard self.target != nil - else { throw POSIXError(.ECONNRESET) } - try await Task.sleep(nanoseconds: 100_000_000) + guard self.target != nil + else { throw POSIXError(.ECONNRESET) } + + guard self.receivedData.isEmpty == false else { + throw POSIXError(.EAGAIN) } let data = self.receivedData.removeFirst() cache.append(data) - eventContinuation.yield(.didRead(data.count)) return data } fileprivate func receive(_ data: Data) { receivedData.append(data) - print("L2CAP Socket: \(name) recieved \([UInt8](data))") - eventContinuation.yield(.read) + print("L2CAP Socket: \(name) received \([UInt8](data))") } internal func connect(to socket: TestL2CAPSocket) { self.target = socket } } + #endif