diff --git a/Package.swift b/Package.swift index 5b35f6a..8a18e73 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "UB", platforms: [ - .macOS(.v10_13), + .macOS(.v10_14), ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 5cb28b4..0000000 --- a/Podfile.lock +++ /dev/null @@ -1,17 +0,0 @@ -PODS: - - SwiftProtobuf (1.6.0) - -DEPENDENCIES: - - SwiftProtobuf - - SwiftProtobuf (~> 1.0) - -SPEC REPOS: - https://github.com/cocoapods/specs.git: - - SwiftProtobuf - -SPEC CHECKSUMS: - SwiftProtobuf: e905ca1366405696411aaf174411c4b19c0ef73d - -PODFILE CHECKSUM: 71a4b409966f97ca5391c4af9b0455a118f34687 - -COCOAPODS: 1.7.5 diff --git a/Sources/UB/Extensions/Numeric+Data.swift b/Sources/UB/Extensions/Numeric+Data.swift new file mode 100644 index 0000000..c24be33 --- /dev/null +++ b/Sources/UB/Extensions/Numeric+Data.swift @@ -0,0 +1,9 @@ +import Foundation + +extension Numeric { + var bytes: Data { + return withUnsafePointer(to: self) { + Data(bytes: $0, count: MemoryLayout.size(ofValue: self)) + } + } +} diff --git a/Sources/UB/Node.swift b/Sources/UB/Node.swift index f60fc9d..6dbb64a 100644 --- a/Sources/UB/Node.swift +++ b/Sources/UB/Node.swift @@ -65,7 +65,7 @@ public class Node { } } - // what this does is send a message to anyone that implements a specific service / + // what this does is send a message to anyone that implements a specific service if message.proto.count != 0 { let filtered = peers.filter { $0.services.contains { $0 == message.proto } } if filtered.count > 0 { diff --git a/Sources/UB/Transports/CoreBluetoothTransport.swift b/Sources/UB/Transports/CoreBluetoothTransport.swift index 1224fee..8d4599d 100644 --- a/Sources/UB/Transports/CoreBluetoothTransport.swift +++ b/Sources/UB/Transports/CoreBluetoothTransport.swift @@ -25,7 +25,10 @@ public class CoreBluetoothTransport: NSObject, Transport { // make this nicer, we need this cause we need a reference to the peripheral? private var perp: CBPeripheral? private var centrals = [Addr: CBCentral]() - private var peripherals = [Addr: (peripheral: CBPeripheral, characteristic: CBCharacteristic)]() + + private var streams = [Addr: StreamClient]() + + private var psm: CBL2CAPPSM? /// Initializes a CoreBluetoothTransport with a new CBCentralManager and CBPeripheralManager. public convenience override init() { @@ -54,21 +57,11 @@ public class CoreBluetoothTransport: NSObject, Transport { /// - message: The message to send. /// - to: The recipient address of the message. public func send(message: Data, to: Addr) { - if let peer = peripherals[to] { - return peer.peripheral.writeValue( - message, - for: peer.characteristic, - type: CBCharacteristicWriteType.withoutResponse - ) + guard let stream = streams[to] else { + return } - if let central = centrals[to] { - peripheralManager.updateValue( - message, - for: CoreBluetoothTransport.characteristic, - onSubscribedCentrals: [central] - ) - } + stream.write(message) } /// Listen implements a function to receive messages being sent to a node. @@ -77,7 +70,8 @@ public class CoreBluetoothTransport: NSObject, Transport { } fileprivate func remove(peer: Addr) { - peripherals.removeValue(forKey: peer) + streams.removeValue(forKey: peer) + centrals.removeValue(forKey: peer) peers.removeAll(where: { $0.id == peer }) } @@ -91,6 +85,12 @@ public class CoreBluetoothTransport: NSObject, Transport { centrals[id] = central peers.append(Peer(id: id, services: [UBID]())) } + + fileprivate func add(channel: CBL2CAPChannel) { + let client = L2CAPStreamClient(channel: channel) + client.delegate = self + streams[Addr(channel.peer.identifier.bytes)] = client + } } /// :nodoc: @@ -102,13 +102,17 @@ extension CoreBluetoothTransport: CBPeripheralManagerDelegate { service.characteristics = [CoreBluetoothTransport.characteristic] peripheral.add(service) - peripheral.startAdvertising([ - CBAdvertisementDataServiceUUIDsKey: [CoreBluetoothTransport.ubServiceUUID], - CBAdvertisementDataLocalNameKey: nil, - ]) + peripheral.publishL2CAPChannel(withEncryption: false) } } + public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error _: Error?) { + peripheral.startAdvertising([ + CBAdvertisementDataServiceUUIDsKey: [service.uuid], + CBAdvertisementDataLocalNameKey: nil, + ]) + } + public func peripheralManager(_: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { for request in requests { guard let data = request.value else { @@ -121,24 +125,55 @@ extension CoreBluetoothTransport: CBPeripheralManagerDelegate { } } - public func peripheralManager( - _: CBPeripheralManager, - central: CBCentral, - didSubscribeTo _: CBCharacteristic - ) { + public func peripheralManager(_: CBPeripheralManager, central: CBCentral, didSubscribeTo _: CBCharacteristic) { + guard let psm = psm else { + return + } + add(central: central) + update(value: psm.bytes) } - public func peripheralManager( - _: CBPeripheralManager, - central: CBCentral, - didUnsubscribeFrom _: CBCharacteristic - ) { + public func peripheralManager(_: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom _: CBCharacteristic) { // @todo check that this is the characteristic let id = Addr(central.identifier.bytes) centrals.removeValue(forKey: id) peers.removeAll(where: { $0.id == id }) } + + public func peripheralManager(_: CBPeripheralManager, didPublishL2CAPChannel PSM: CBL2CAPPSM, error _: Error?) { + psm = PSM + + guard centrals.count > 0 else { + return + } + + update(value: PSM.bytes) + } + + public func peripheralManager(_: CBPeripheralManager, didUnpublishL2CAPChannel _: CBL2CAPPSM, error _: Error?) { + // @todo + } + + public func peripheralManager(_: CBPeripheralManager, didOpen channel: CBL2CAPChannel?, error: Error?) { + if error != nil { + // @todo handle + } + + guard let channel = channel else { + return + } + + add(channel: channel) + } + + private func update(value: Data) { + peripheralManager.updateValue( + value, + for: CoreBluetoothTransport.characteristic, + onSubscribedCentrals: Array(centrals.values) + ) + } } /// :nodoc: @@ -182,20 +217,29 @@ extension CoreBluetoothTransport: CBPeripheralDelegate { public func peripheral( _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, - error _: Error? + error: Error? ) { - let id = Addr(peripheral.identifier.bytes) - if peripherals[id] != nil { - return + if error != nil { + // @todo } let characteristics = service.characteristics if let char = characteristics?.first(where: { $0.uuid == CoreBluetoothTransport.receiveCharacteristicUUID }) { - peripherals[id] = (peripheral, char) - peripherals[id]?.peripheral.setNotifyValue(true, for: char) - // @todo we may need to do some handshake to obtain services from a peer. - peers.append(Peer(id: id, services: [UBID]())) + peripheral.setNotifyValue(true, for: char) } + +// let id = Addr(peripheral.identifier.bytes) +// if peripherals[id] != nil { +// return +// } +// +// let characteristics = service.characteristics +// if let char = characteristics?.first(where: { $0.uuid == CoreBluetoothTransport.receiveCharacteristicUUID }) { +// peripherals[id] = (peripheral, char) +// peripherals[id]?.peripheral.setNotifyValue(true, for: char) +// // @todo we may need to do some handshake to obtain services from a peer. +// peers.append(Peer(id: id, services: [UBID]())) +// } } public func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { @@ -207,10 +251,19 @@ extension CoreBluetoothTransport: CBPeripheralDelegate { public func peripheral( _ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, - error _: Error? + error: Error? ) { + if error != nil { + // @todo + } + guard let value = characteristic.value else { return } - delegate?.transport(self, didReceiveData: value, from: Addr(peripheral.identifier.bytes)) + + let psm = value.withUnsafeBytes { + $0.load(as: UInt16.self) + } + + peripheral.openL2CAPChannel(psm) } public func peripheral( @@ -220,4 +273,27 @@ extension CoreBluetoothTransport: CBPeripheralDelegate { ) { // @todo figure out exactly what we will want to do here. } + + public func peripheral(_: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) { + if error != nil { + // @todo handle + } + + guard let channel = channel else { + return + } + + add(channel: channel) + } +} + +/// :nodoc: +extension CoreBluetoothTransport: StreamClientDelegate { + public func client(_ client: StreamClient, didReceiveData data: Data) { + guard let peer = streams.first(where: { $0.value == client })?.key else { + return // @todo log? + } + + delegate?.transport(self, didReceiveData: data, from: peer) + } } diff --git a/Sources/UB/Transports/Stream/L2CAPStreamClient.swift b/Sources/UB/Transports/Stream/L2CAPStreamClient.swift new file mode 100644 index 0000000..db20b95 --- /dev/null +++ b/Sources/UB/Transports/Stream/L2CAPStreamClient.swift @@ -0,0 +1,13 @@ +import CoreBluetooth + +// This class exists because we need a strong reference to the CBL2CAPChannel + +class L2CAPStreamClient: StreamClient { + + let channel: CBL2CAPChannel + + init(channel: CBL2CAPChannel) { + self.channel = channel + super.init(input: channel.inputStream, output: channel.outputStream) + } +} diff --git a/Sources/UB/Transports/Stream/StreamClient.swift b/Sources/UB/Transports/Stream/StreamClient.swift new file mode 100644 index 0000000..6bd91dc --- /dev/null +++ b/Sources/UB/Transports/Stream/StreamClient.swift @@ -0,0 +1,76 @@ +import Foundation + +/// The StreamClient implements generic stream handling for Ultralight Beam transports. +public class StreamClient: NSObject { + /// The delegate for the StreamClient. + weak var delegate: StreamClientDelegate? + + private let input: InputStream + private let output: OutputStream + + /// Initializes a StreamClient with the input and output streams. + /// + /// - Parameters: + /// - input: The input stream. + /// - output: The output stream. + public init(input: InputStream, output: OutputStream) { + self.input = input + self.output = output + + super.init() + + self.input.delegate = self + self.output.delegate = self + } + + /// Writes the contents of provided data to the receiver. + /// + /// - Parameters: + /// - data: Data to write + public func write(_ data: Data) { + guard output.hasSpaceAvailable else { + // @todo error out or some shit? + return + } + + var length = UInt32(data.count).bigEndian + + let bytes = NSMutableData() + bytes.append(&length, length: 4) + bytes.append(data) + + output.write([UInt8](bytes), maxLength: bytes.count) + } +} + +/// :nodoc: +extension StreamClient: StreamDelegate { + public func stream(_: Stream, handle eventCode: Stream.Event) { + // @todo handle all other cases + + switch eventCode { + case .hasBytesAvailable: + read() + default: + return + } + } + + fileprivate func read() { + let length = read(4).withUnsafeBytes { + $0.load(as: UInt32.self) + } + + delegate?.client(self, didReceiveData: read(Int(length.bigEndian))) + } + + private func read(_ length: Int) -> Data { + if length == 0 { + return Data() + } + + var buffer = [UInt8](repeating: 0, count: length) + input.read(&buffer, maxLength: length) + return Data(buffer) + } +} diff --git a/Sources/UB/Transports/Stream/StreamClientDelegate.swift b/Sources/UB/Transports/Stream/StreamClientDelegate.swift new file mode 100644 index 0000000..662929c --- /dev/null +++ b/Sources/UB/Transports/Stream/StreamClientDelegate.swift @@ -0,0 +1,11 @@ +import Foundation + +/// An interface used to handle events on the StreamClient. +protocol StreamClientDelegate: AnyObject { + /// This method is called when a client receives new data. + /// + /// - Parameters: + /// - client: The client which received data. + /// - data: The data that was received. + func client(_ client: StreamClient, didReceiveData data: Data) +}