├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftLinuxBLE │ ├── Characteristic.swift │ ├── DataConvertible.swift │ ├── HostController.swift │ ├── Peripheral.swift │ └── Service.swift └── Tests ├── LinuxMain.swift └── SwiftLinuxBLETests ├── SwiftLinuxBLETests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Bluetooth", 6 | "repositoryURL": "https://github.com/PureSwift/Bluetooth.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "b03524c7a0c7eca048133a9df9dc4bbe727b38e0", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "BluetoothLinux", 15 | "repositoryURL": "https://github.com/PureSwift/BluetoothLinux", 16 | "state": { 17 | "branch": "master", 18 | "revision": "52078ccc6928c8f3e7712bbda052f4a927a2e5ad", 19 | "version": null 20 | } 21 | }, 22 | { 23 | "package": "CRuntime", 24 | "repositoryURL": "https://github.com/wickwirew/CRuntime.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "95f911318d8c885f6fc05e971471f94adfd39405", 28 | "version": "2.1.2" 29 | } 30 | }, 31 | { 32 | "package": "GATT", 33 | "repositoryURL": "https://github.com/PureSwift/GATT", 34 | "state": { 35 | "branch": "master", 36 | "revision": "955a19b2030e08666154f2ee52d7955f56bb6ff5", 37 | "version": null 38 | } 39 | }, 40 | { 41 | "package": "Runtime", 42 | "repositoryURL": "https://github.com/wickwirew/Runtime.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "c167476b07fe8cc65fdf064076a4081c3269d14a", 46 | "version": "2.1.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftLinuxBLE", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftLinuxBLE", 12 | targets: ["SwiftLinuxBLE"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/PureSwift/GATT", .branch("master")), 16 | .package(url: "https://github.com/PureSwift/BluetoothLinux", .branch("master")), 17 | .package(url: "https://github.com/wickwirew/Runtime.git", from: "2.1.0") 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "SwiftLinuxBLE", 24 | dependencies: ["GATT", "BluetoothLinux", "Runtime"]), 25 | .testTarget( 26 | name: "SwiftLinuxBLETests", 27 | dependencies: ["SwiftLinuxBLE"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftLinuxBLE 2 | 3 | SwiftLinuxBLE is a lightweight convenience wrapper for the [PureSwift BluetoothLinux library](https://github.com/PureSwift/BluetoothLinux). 4 | 5 | It enables you to very quickly create a BLE GATT peripheral on linux (for example, on the Raspberry Pi). 6 | 7 | # Usage 8 | 9 | Create a `Service` for each service you want to expose. Use the `@SwiftLinuxBLE.Characteristic` propertyWrapper for each characteristic. The peripheral will automagically track changes to each characteristic and will also write to them. 10 | 11 | ``` 12 | final class TemperatureService : SwiftLinuxBLE.Service { 13 | let uuid = BluetoothUUID(rawValue: "88d738cc-bdd0-485b-b197-b7186ff534e4")! 14 | 15 | // Characteristics 16 | @SwiftLinuxBLE.Characteristic(uuid: BluetoothUUID(), [.read, .notify]) 17 | var temperature = 7.0 18 | 19 | @SwiftLinuxBLE.Characteristic(uuid: BluetoothUUID(), [.write]) 20 | var tx = Data() 21 | 22 | @SwiftLinuxBLE.Characteristic(uuid: BluetoothUUID(), [.read, .notify]) 23 | var rx = Data() 24 | } 25 | ``` 26 | 27 | 2. Create a `Peripheral` class and add each service. 28 | 29 | ``` 30 | public final class ThermometerPeripheral : SwiftLinuxBLE.Peripheral { 31 | 32 | public let peripheral: GATTPeripheral 33 | let name: GAPCompleteLocalName = "Ferment" 34 | let iBeaconUUID = UUID(rawValue: "1DC24957-9DDA-46C4-88D4-3D3640CB3FDA")! 35 | 36 | public var services: [SwiftLinuxBLE.Service] = [] 37 | public var characteristicsByHandle = [UInt16: CharacteristicType]() 38 | 39 | public init(hostController: HostController) throws { 40 | peripheral = try hostController.newPeripheral() 41 | 42 | add(service: TemperatureService()) 43 | 44 | // Start peripheral 45 | try peripheral.start() 46 | print("BLE Peripheral started") 47 | 48 | try advertise(name: name, services: services, iBeaconUUID: iBeaconUUID) 49 | 50 | peripheral.didWrite = didWrite 51 | } 52 | } 53 | ``` 54 | 55 | # Example: 56 | 57 | Check out the [SwiftLinuxGATTServerExample](https://github.com/kevinbrewster/SwiftLinuxGATTServerExample) for a working example. 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Sources/SwiftLinuxBLE/Characteristic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bluetooth 3 | import GATT 4 | import BluetoothLinux 5 | 6 | 7 | public protocol CharacteristicType { 8 | var uuid: BluetoothUUID { get } 9 | var properties: BitMaskOptionSet { get } 10 | var permissions: BitMaskOptionSet { get } 11 | var descriptors: [GATT.Characteristic.Descriptor] { get } 12 | 13 | var data: Data { get set } 14 | //var didSet: (Data) -> Void { get set } 15 | 16 | func didSet(_ observer: @escaping (Data) -> Void) 17 | } 18 | 19 | @propertyWrapper 20 | public class Characteristic : CharacteristicType { 21 | var value: Value 22 | public let uuid: BluetoothUUID 23 | public var properties: BitMaskOptionSet = [.read, .write] 24 | public var permissions: BitMaskOptionSet = [.read, .write] 25 | public let descriptors: [GATT.Characteristic.Descriptor] 26 | 27 | /* 28 | // Default arguments cause segfault in swift 5.1 29 | public init(wrappedValue value: Value, uuid: BluetoothUUID, _ properties: BitMaskOptionSet, _ permissions: BitMaskOptionSet? = nil, _ descriptors: [GATT.Characteristic.Descriptor]? = nil) { 30 | self.value = value 31 | self.uuid = uuid 32 | self.properties = properties 33 | self.permissions = permissions ?? properties.inferredPermissions 34 | // we need this special descriptor to enable notifications! 35 | self.descriptors = descriptors ?? (properties.contains(.notify) ? [GATTClientCharacteristicConfiguration().descriptor] : []) 36 | }*/ 37 | 38 | public init(wrappedValue value: Value, uuid: BluetoothUUID, _ properties: BitMaskOptionSet) { 39 | self.value = value 40 | self.uuid = uuid 41 | self.properties = properties 42 | self.permissions = properties.inferredPermissions 43 | // we need this special descriptor to enable notifications! 44 | self.descriptors = (properties.contains(.notify) ? [GATTClientCharacteristicConfiguration().descriptor] : []) 45 | } 46 | 47 | public var wrappedValue: Value { 48 | get { value } 49 | set { 50 | value = newValue; 51 | for observer in observers { 52 | observer(newValue) 53 | } 54 | } 55 | } 56 | 57 | public var data: Data { 58 | get { return wrappedValue.data } 59 | set { wrappedValue = Value(data: newValue) ?? wrappedValue } 60 | } 61 | 62 | private var observers: [(Value) -> Void] = [] 63 | public func didSet(_ observer: @escaping (Value) -> Void) { 64 | observers += [observer] 65 | } 66 | public func didSet(_ observer: @escaping (Data) -> Void) { 67 | observers += [{ observer($0.data) }] 68 | } 69 | public func didSet(_ observer: @escaping () -> Void) { 70 | observers += [{ _ in observer() }] 71 | } 72 | } 73 | 74 | 75 | extension BitMaskOptionSet where Element == GATT.Characteristic.Property { 76 | var inferredPermissions: BitMaskOptionSet { 77 | let mapping: [GATT.Characteristic.Property: ATTAttributePermission] = [ 78 | .read: .read, 79 | .notify: .read, 80 | .write: .write 81 | ] 82 | var permissions = BitMaskOptionSet() 83 | for (property, permission) in mapping { 84 | if contains(property) { 85 | permissions.insert(permission) 86 | } 87 | } 88 | return permissions 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/SwiftLinuxBLE/DataConvertible.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DataConvertible { 4 | init?(data: Data) 5 | var data: Data { get } 6 | } 7 | extension DataConvertible where Self: ExpressibleByIntegerLiteral{ 8 | public init?(data: Data) { 9 | var value: Self = 0 10 | guard data.count == MemoryLayout.size(ofValue: value) else { return nil } 11 | _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} ) 12 | self = value 13 | } 14 | 15 | public var data: Data { 16 | return withUnsafeBytes(of: self) { Data($0) } 17 | } 18 | } 19 | extension Int : DataConvertible { } 20 | extension Float : DataConvertible { } 21 | extension Double : DataConvertible { } 22 | // add more types here ... 23 | 24 | 25 | 26 | extension String: DataConvertible { 27 | public init?(data: Data) { 28 | self.init(data: data, encoding: .utf8) 29 | } 30 | public var data: Data { 31 | // Note: a conversion to UTF-8 cannot fail. 32 | return Data(self.utf8) 33 | } 34 | } 35 | 36 | extension Data : DataConvertible { 37 | public init?(data: Data) { 38 | self.init(data) 39 | } 40 | public var data: Data { 41 | return self 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftLinuxBLE/HostController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bluetooth 3 | import GATT 4 | import BluetoothLinux 5 | 6 | extension HostController { 7 | public func newPeripheral() throws -> GATTPeripheral { 8 | // Setup peripheral 9 | let address = try readDeviceAddress() 10 | let serverSocket = try L2CAPSocket.lowEnergyServer(controllerAddress: address, isRandom: false, securityLevel: .low) 11 | 12 | let peripheral = GATTPeripheral(controller: self) 13 | peripheral.log = { print("Peripheral Log: \($0)") } 14 | peripheral.newConnection = { 15 | let socket = try serverSocket.waitForConnection() 16 | let central = Central(identifier: socket.address) 17 | print("BLE Peripheral: new connection") 18 | return (socket, central) 19 | } 20 | return peripheral 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftLinuxBLE/Peripheral.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bluetooth 3 | import GATT 4 | import BluetoothLinux 5 | 6 | public protocol Peripheral : class { 7 | var peripheral: GATTPeripheral { get } 8 | var services: [Service] { get set } 9 | var characteristicsByHandle: [UInt16: CharacteristicType] { get set } 10 | } 11 | extension Peripheral { 12 | public func advertise(name: GAPCompleteLocalName, services: [Service], iBeaconUUID: UUID? = nil) throws { 13 | // Advertise services and peripheral name 14 | let serviceUUIDs = GAPIncompleteListOf128BitServiceClassUUIDs(uuids: services.map { UUID(bluetooth: $0.uuid) }) 15 | let encoder = GAPDataEncoder() 16 | let data = try encoder.encodeAdvertisingData(name, serviceUUIDs) 17 | try peripheral.controller.setLowEnergyScanResponse(data, timeout: .default) 18 | print("BLE Advertising started") 19 | 20 | // Setup iBeacon 21 | if let iBeaconUUID = iBeaconUUID { 22 | let rssi: Int8 = 30 23 | let beacon = AppleBeacon(uuid: iBeaconUUID, rssi: rssi) 24 | let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] 25 | try peripheral.controller.iBeacon(beacon, flags: flags, interval: .min, timeout: .default) 26 | } 27 | } 28 | public func add(service: Service) throws { 29 | // Find all the characteristics for the service 30 | let characteristics = Mirror(reflecting: service).children.compactMap { 31 | $0.value as? CharacteristicType 32 | } 33 | 34 | let gattCharacteristics = characteristics.map { 35 | GATT.Characteristic(uuid: $0.uuid, value: $0.data, permissions: $0.permissions, properties: $0.properties, descriptors: $0.descriptors) 36 | } 37 | 38 | let gattService = GATT.Service(uuid: service.uuid, primary: true, characteristics: gattCharacteristics) 39 | let _ = try peripheral.add(service: gattService) 40 | 41 | 42 | for var characteristic in characteristics { 43 | guard let handle = peripheral.characteristics(for: characteristic.uuid).last else { continue } 44 | 45 | print("Characteristic \(characteristic.uuid) with permissions \(characteristic.permissions) and \(characteristic.descriptors.count) descriptors") 46 | 47 | // Register as observer for each characteristic 48 | characteristic.didSet { [weak self] in 49 | NSLog("MyPeripheral: characteristic \(characteristic.uuid) did change with new value \($0)") 50 | self?.peripheral[characteristic: handle] = $0 51 | } 52 | 53 | characteristicsByHandle[handle] = characteristic 54 | 55 | } 56 | services += [service] 57 | } 58 | 59 | public func didWrite(_ confirmation: GATTWriteConfirmation) { 60 | if var characteristic = characteristicsByHandle[confirmation.handle] { 61 | characteristic.data = confirmation.value 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftLinuxBLE/Service.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bluetooth 3 | import GATT 4 | import BluetoothLinux 5 | 6 | public protocol Service : class { 7 | var uuid: BluetoothUUID { get } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftLinuxBLETests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftLinuxBLETests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftLinuxBLETests/SwiftLinuxBLETests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Bluetooth 3 | import GATT 4 | import BluetoothLinux 5 | import Runtime 6 | @testable import SwiftLinuxBLE 7 | 8 | final class TestService : SwiftLinuxBLE.Service { 9 | let uuid = BluetoothUUID() 10 | 11 | // Characteristics 12 | @SwiftLinuxBLE.Characteristic(uuid: BluetoothUUID(), [.read, .notify]) 13 | var temperature = 7.0 14 | } 15 | 16 | 17 | final class SwiftLinuxBLETests: XCTestCase { 18 | func testExample() { 19 | // This is an example of a functional test case. 20 | // Use XCTAssert and related functions to verify your tests produce the correct 21 | // results. 22 | print("TEST") 23 | 24 | 25 | let service = try TestService() 26 | let serviceInfo = try? Runtime.typeInfo(of: type(of: service)) 27 | 28 | //print("x = \(service._temperature)") 29 | print("serviceInfo = \(serviceInfo)") 30 | } 31 | 32 | static var allTests = [ 33 | ("testExample", testExample), 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /Tests/SwiftLinuxBLETests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftLinuxBLETests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------