├── Assets ├── PaintCode.pcvd ├── images │ └── _280pxTuxsvg2.png ├── PureSwiftBluetoothLinux.png └── PureSwiftBluetoothLinux.svg ├── Sources ├── CBluetoothLinuxTest │ ├── include │ │ ├── shim.h │ │ ├── module.modulemap │ │ ├── IOCTLTestsHelper.h │ │ └── MathTestsHelper.h │ └── dummy.c ├── CBluetoothLinux │ ├── include │ │ ├── module.modulemap │ │ └── bluetooth.h │ └── dummy.c └── BluetoothLinux │ ├── BNEP │ ├── BNEP.swift │ └── IOCTL │ │ └── BNEPIO.swift │ ├── CMTP │ ├── CMTP.swift │ └── IOCTL │ │ └── CMTPIO.swift │ ├── SCO │ ├── SCOSocketOption.swift │ └── SCOSocket.swift │ ├── IOControl │ └── DeviceDown.swift │ ├── HIDP │ ├── HIDP.swift │ └── IOCTL │ │ └── HIDPIO.swift │ ├── RFCOMM │ ├── RFCOMMSocket.swift │ ├── RFCOMMFlag.swift │ ├── RFCOMMLinkMode.swift │ ├── RFCOMMState.swift │ ├── RFCOMMDevice.swift │ ├── IOCTL │ │ ├── RFCOMMGetDeviceInformation.swift │ │ ├── RFCOMMReleaseDevice.swift │ │ ├── RFCOMMIO.swift │ │ ├── RFCOMMGetDeviceList.swift │ │ └── RFCOMMCreateDevice.swift │ ├── RFCOMMSocketAddress.swift │ └── RFCOMMSocketOption.swift │ ├── HCI │ ├── HCIChannel.swift │ ├── HCIDeviceEvent.swift │ ├── HCIDeviceFlag.swift │ ├── HCISocketOption.swift │ ├── HCIPacketType.swift │ ├── DeviceCommand.swift │ ├── HCIDeviceOptions.swift │ ├── DevicePollEvent.swift │ ├── HCIControllerType.swift │ ├── IOCTL │ │ ├── HCIDeviceUp.swift │ │ ├── HCIDeviceDown.swift │ │ ├── HCIDeviceInformation.swift │ │ ├── HCIDeviceList.swift │ │ ├── HostControllerIO.swift │ │ └── HCIScan.swift │ ├── SocketOptions │ │ └── HCIFilterSocketOption.swift │ ├── HCISocketAddress.swift │ ├── HCIBusType.swift │ ├── DeviceRequest.swift │ ├── HostController.swift │ └── HCIFileDescriptor.swift │ ├── SocketAddress.swift │ ├── LinkMode.swift │ ├── Extensions │ └── Sequence.swift │ ├── L2CAP │ ├── L2CAPFileDescriptor.swift │ ├── L2CAPSocketOption.swift │ ├── L2CAPSocketAddress.swift │ └── L2CAPSocket.swift │ ├── SocketOptionLevel.swift │ ├── Internal │ ├── CSocketAddress.swift │ ├── SocketDescriptor.swift │ ├── Darwin.swift │ ├── IOCTL.swift │ └── CInterop.swift │ ├── SocketOption.swift │ ├── BluetoothLinux.docc │ └── Documentation.md │ ├── AddressType.swift │ ├── SocketProtocol.swift │ ├── SocketOptions │ └── SecuritySocketOption.swift │ └── Error.swift ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .github ├── pull_request_template.md └── workflows │ └── swift.yml ├── LICENSE ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ ├── node-debian.sh │ └── common-debian.sh ├── README.md ├── Package.swift └── Tests └── BluetoothLinuxTests ├── BluetoothLinuxTests.swift └── L2CAPTests.swift /Assets/PaintCode.pcvd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureSwift/BluetoothLinux/HEAD/Assets/PaintCode.pcvd -------------------------------------------------------------------------------- /Sources/CBluetoothLinuxTest/include/shim.h: -------------------------------------------------------------------------------- 1 | #import "MathTestsHelper.h" 2 | #import "IOCTLTestsHelper.h" 3 | -------------------------------------------------------------------------------- /Assets/images/_280pxTuxsvg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureSwift/BluetoothLinux/HEAD/Assets/images/_280pxTuxsvg2.png -------------------------------------------------------------------------------- /Assets/PureSwiftBluetoothLinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureSwift/BluetoothLinux/HEAD/Assets/PureSwiftBluetoothLinux.png -------------------------------------------------------------------------------- /Sources/CBluetoothLinux/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CBluetoothLinux [system] { 2 | header "bluetooth.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinux/dummy.c: -------------------------------------------------------------------------------- 1 | // To avoid SPM warnings: 2 | // 3 | // warning: module 'CBluetoothLinux' does not contain any sources. 4 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinuxTest/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CBluetoothLinuxTest [system] { 2 | header "shim.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinuxTest/dummy.c: -------------------------------------------------------------------------------- 1 | // To avoid SPM warnings: 2 | // 3 | // warning: module 'CBluetoothLinuxTests' does not contain any sources. 4 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/BNEP/BNEP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/CMTP/CMTP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SCO/SCOSocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/IOControl/DeviceDown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceDown.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HIDP/HIDP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HIDP.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/* 2 | Packages/* 3 | Carthage/* 4 | Headers/* 5 | 6 | Xcode/BluetoothLinux.xcodeproj/project.xcworkspace/xcuserdata 7 | BluetoothLinux.xcodeproj 8 | 9 | build/ 10 | Docs/ 11 | 12 | # SwiftPM 13 | Package.resolved 14 | .swiftpm 15 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMM.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Bluetooth RFCOMM Socket 11 | public actor RFCOMMSocket { 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SCO/SCOSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCOSocket.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | import SystemPackage 10 | 11 | /// Bluetooth SCO Socket 12 | public actor SCOSocket { 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "Test", 8 | "program": "./.build/debug/BluetoothLinuxPackageTests.xctest", 9 | "preLaunchTask": "swift-build-tests" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Issue** 2 | 3 | Fixes #1. 4 | 5 | **What does this PR Do?** 6 | 7 | Description of the changes in this pull request. 8 | 9 | **Where should the reviewer start?** 10 | 11 | `main.swift` 12 | 13 | **Sweet giphy showing how you feel about this PR** 14 | 15 | ![Giphy](https://media.giphy.com/media/rkDXJA9GoWR2/giphy.gif) 16 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIChannel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIChannel.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | /// HCI Channel 9 | public enum HCIChannel: UInt16, Sendable { 10 | 11 | case raw = 0 12 | case user = 1 13 | case monitor = 2 14 | case control = 3 15 | case logging = 4 16 | } 17 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIDeviceEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIDeviceEvent.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | /// HCI dev events 9 | public enum HCIDeviceEvent: CInt { 10 | 11 | case register = 1 12 | case unregister = 2 13 | case up = 3 14 | case down = 4 15 | case suspend = 5 16 | case resume = 6 17 | } 18 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SocketAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothSocketAddress.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | /// Bluetooth Linux Socket Address protocol 13 | public protocol BluetoothSocketAddress: SocketAddress where ProtocolID == BluetoothSocketProtocol { 14 | 15 | static var protocolID: BluetoothSocketProtocol { get } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIDeviceFlag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIDeviceFlag.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | 10 | /// HCI device flags 11 | public enum HCIDeviceFlag: Int32, BitMaskOption, CaseIterable { 12 | 13 | case up 14 | case initialized 15 | case running 16 | 17 | case passiveScan 18 | case interactiveScan 19 | case authenticated 20 | case encrypt 21 | case inquiry 22 | 23 | case raw 24 | } 25 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMFlag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | 10 | /// RFCOMM Flags 11 | @frozen 12 | public enum RFCOMMFlag: UInt32, CaseIterable, BitMaskOption { 13 | 14 | case reuseDLC = 0x01 // RFCOMM_REUSE_DLC 15 | case releaseOnHangup = 0x02 // RFCOMM_RELEASE_ONHUP 16 | case hangupNow = 0x04 // RFCOMM_HANGUP_NOW 17 | case serialAttached = 0x08 // RFCOMM_TTY_ATTACHED 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMLinkMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMLinkMode.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | 10 | /// RFCOMM Link Mode 11 | @frozen 12 | public enum RFCOMMLinkMode: UInt16, CaseIterable, BitMaskOption { 13 | 14 | case master = 0x0001 15 | case authenticated = 0x0002 16 | case encrypted = 0x0004 17 | case trusted = 0x0008 18 | case reliable = 0x0010 19 | case secure = 0x0020 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinux/include/bluetooth.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Swift Bluetooth Linux Stack 4 | * MIT License 5 | * PureSwift 6 | * 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | /** 13 | Sets the HCI bit. 14 | @param bit The bit to set. 15 | @param destination The targeted value. 16 | */ 17 | static inline void swift_bluetooth_hci_set_bit(int bit, void *destination) 18 | __attribute__((swift_name("HCISetBit(_:_:)"))) 19 | { 20 | *((uint32_t *) destination + (bit >> 5)) |= (1 << (bit & 31)); 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "swift-build", 6 | "type": "shell", 7 | "command": "swift build", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "swift-build-tests", 12 | "type": "process", 13 | "command": "swift", 14 | "group": "build", 15 | "args": [ 16 | "build", 17 | "--build-tests" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /Sources/BluetoothLinux/LinkMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkMode.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 1/15/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum LinkMode: UInt16 { 12 | 13 | case accept = 0x8000 14 | case master = 0x0001 15 | case authenticated = 0x0002 16 | case encrypted = 0x0004 17 | case trusted = 0x0008 18 | case reliable = 0x0010 19 | case secure = 0x0020 20 | } 21 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Extensions/Sequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | internal extension Sequence { 9 | 10 | @usableFromInline 11 | func _buildDescription() -> String { 12 | var string = "[" 13 | for element in self { 14 | if _slowPath(string.count == 1) { 15 | string += "\(element)" 16 | } else { 17 | string += ", \(element)" 18 | } 19 | } 20 | string += "]" 21 | return string 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCISocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCISocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import BluetoothHCI 10 | import Socket 11 | 12 | /// Bluetooth HCI Socket Option Identifier 13 | public enum HCISocketOption: Int32, SocketOptionID { 14 | 15 | @_alwaysEmitIntoClient 16 | public static var optionLevel: SocketOptionLevel { .hostControllerInterface } 17 | 18 | case dataDirection = 1 19 | case filter = 2 20 | case timeStamp = 3 21 | } 22 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIPacketType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIPacketType.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import BluetoothHCI 9 | 10 | /// HCI Packet types 11 | public enum HCIPacketType: UInt8 { 12 | 13 | case command = 0x01 14 | case acl = 0x02 15 | case sco = 0x03 16 | case event = 0x04 17 | case vendor = 0xff 18 | } 19 | 20 | internal extension HCIEventHeader { 21 | 22 | static var maximumSize: Int { 260 } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/L2CAP/L2CAPFileDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAPFileDescriptor.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import BluetoothHCI 10 | import Socket 11 | 12 | internal extension SocketDescriptor { 13 | 14 | /// Creates an L2CAP socket binded to the specified address. 15 | @usableFromInline 16 | static func l2cap( 17 | _ address: L2CAPSocketAddress, 18 | _ flags: SocketFlags 19 | ) throws(Errno) -> SocketDescriptor { 20 | try bluetooth( 21 | .l2cap, 22 | bind: address, 23 | flags: flags 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMState.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | /// RFCOMM State 9 | @frozen 10 | public enum RFCOMMState: UInt16, CaseIterable, Codable { 11 | 12 | case unknown = 0x00 13 | case connected = 0x01 14 | case clean = 0x02 15 | case bound = 0x03 16 | case listening = 0x04 17 | case connecting = 0x05 18 | case connecting2 = 0x06 // FIXME: Duplicate connecting definitions 19 | case config = 0x07 20 | case disconnecting = 0x08 21 | case closed = 0x09 22 | } 23 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/DeviceCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceCommand.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/2/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BluetoothHCI 11 | import SystemPackage 12 | 13 | public extension HostController { 14 | 15 | func deviceCommand(_ command: T) async throws { 16 | try await socket.sendCommand(command) 17 | } 18 | 19 | func deviceCommand(_ commandParameter: T) async throws { 20 | try await socket.sendCommand( 21 | T.command, 22 | parameter: commandParameter.data 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | linux-swift: 8 | name: Linux 9 | runs-on: ubuntu-20.04 10 | container: swift:6.0.2-jammy 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Swift Version 15 | run: swift --version 16 | - name: Build (Debug) 17 | run: swift build -c debug 18 | - name: Build (Release) 19 | run: swift build -c release 20 | - name: Test (Debug) 21 | run: swift test --configuration debug --enable-code-coverage 22 | - name: Archive Build artifacts 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: swiftpm-build-ubuntu-x86_64 26 | path: .build/*/*.xctest 27 | - name: Coverage Report 28 | uses: maxep/spm-lcov-action@0.3.1 29 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SocketOptionLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketOptionLevel.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Socket 9 | 10 | internal extension SocketOptionLevel { 11 | 12 | /// Bluetooth HCI Socket Option Level 13 | @_alwaysEmitIntoClient 14 | static var hostControllerInterface: SocketOptionLevel { SocketOptionLevel(rawValue: 0) } 15 | 16 | /// Bluetooth L2CAP Socket Option Level 17 | @_alwaysEmitIntoClient 18 | static var l2cap: SocketOptionLevel { SocketOptionLevel(rawValue: 6) } 19 | 20 | /// Bluetooth SCO Socket Option Level 21 | @_alwaysEmitIntoClient 22 | static var sco: SocketOptionLevel { SocketOptionLevel(rawValue: 17) } 23 | 24 | /// Bluetooth RFCOMM Socket Option Level 25 | @_alwaysEmitIntoClient 26 | static var rfcomm: SocketOptionLevel { SocketOptionLevel(rawValue: 18) } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIDeviceOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIDeviceOptions.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | 10 | /// HCI Device Options 11 | public struct HCIDeviceOptions: RawRepresentable, Equatable, Hashable { 12 | 13 | public let rawValue: UInt32 14 | 15 | public init(rawValue: UInt32) { 16 | self.rawValue = rawValue 17 | } 18 | } 19 | 20 | public extension HCIDeviceOptions { 21 | 22 | var flags: BitMaskOptionSet { 23 | var options = BitMaskOptionSet() 24 | HCIDeviceFlag.allCases.forEach { 25 | if contains($0) { 26 | options.insert($0) 27 | } 28 | } 29 | return options 30 | } 31 | 32 | func contains(_ flag: HCIDeviceFlag) -> Bool { 33 | return (self.rawValue + (UInt32(bitPattern: flag.rawValue) >> 5)) & (1 << (UInt32(bitPattern: flag.rawValue) & 31)) != 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/DevicePollEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DevicePollEvent.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/27/18. 6 | // 7 | 8 | import Foundation 9 | import Bluetooth 10 | import BluetoothHCI 11 | import SystemPackage 12 | 13 | public extension HostController { 14 | 15 | func receive(_ eventType: Event.Type) async throws -> Event where Event: HCIEventParameter, Event.HCIEventType == HCIGeneralEvent { 16 | var newFilter = HCISocketOption.Filter() 17 | newFilter.setPacketType(.event) 18 | newFilter.setEvent(Event.event) 19 | return try await socket.fileDescriptor.setFilter(newFilter) { 20 | let readData = try await socket.read(HCIEventHeader.maximumSize) 21 | let eventData = Data(readData[(1 + HCIEventHeader.length) ..< readData.count]) // create unsafe data pointer 22 | guard let eventParameter = Event.init(data: eventData) 23 | else { throw BluetoothHostControllerError.garbageResponse(eventData) } 24 | return eventParameter 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Internal/CSocketAddress.swift: -------------------------------------------------------------------------------- 1 | import Socket 2 | 3 | @usableFromInline 4 | internal protocol CSocketAddress { 5 | 6 | static var family: SocketAddressFamily { get } 7 | } 8 | 9 | internal extension CSocketAddress { 10 | 11 | @usableFromInline 12 | func withUnsafePointer( 13 | _ body: (UnsafePointer, UInt32) throws -> Result 14 | ) rethrows -> Result { 15 | return try Swift.withUnsafeBytes(of: self) { 16 | return try body($0.baseAddress!.assumingMemoryBound(to: CInterop.SocketAddress.self), UInt32(MemoryLayout.size)) 17 | } 18 | } 19 | 20 | @usableFromInline 21 | mutating func withUnsafeMutablePointer( 22 | _ body: (UnsafeMutablePointer, UInt32) throws -> Result 23 | ) rethrows -> Result { 24 | return try Swift.withUnsafeMutableBytes(of: &self) { 25 | return try body($0.baseAddress!.assumingMemoryBound(to: CInterop.SocketAddress.self), UInt32(MemoryLayout.size)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMDevice.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | 11 | /// RFCOMM Device Information 12 | @frozen 13 | public struct RFCOMMDevice: Equatable, Hashable { 14 | 15 | public let id: HostController.ID 16 | 17 | public var flags: BitMaskOptionSet 18 | 19 | public var state: RFCOMMState 20 | 21 | public var source: BluetoothAddress 22 | 23 | public var destination: BluetoothAddress 24 | 25 | public var channel: UInt8 26 | } 27 | 28 | extension RFCOMMDevice: Identifiable { } 29 | 30 | internal extension RFCOMMDevice { 31 | 32 | @usableFromInline 33 | init(_ cValue: CInterop.RFCOMMDeviceInformation) { 34 | self.id = .init(rawValue: cValue.id) 35 | self.flags = .init(rawValue: cValue.flags) 36 | self.state = .init(rawValue: cValue.state) ?? .unknown 37 | self.source = cValue.source 38 | self.destination = cValue.source 39 | self.channel = cValue.channel 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Swift version: 2 | ARG VARIANT=6.0.2-jammy 3 | FROM swift:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="false" 9 | 10 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=vscode 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | COPY library-scripts/common-debian.sh /tmp/library-scripts/ 15 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 16 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 17 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/library-scripts 18 | 19 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 20 | ARG NODE_VERSION="none" 21 | ENV NVM_DIR=/usr/local/share/nvm 22 | ENV NVM_SYMLINK_CURRENT=true \ 23 | PATH=${NVM_DIR}/current/bin:${PATH} 24 | COPY library-scripts/node-debian.sh /tmp/library-scripts/ 25 | RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ 26 | && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 27 | 28 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Internal/SocketDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileDescriptor.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | import Bluetooth 10 | import BluetoothHCI 11 | import SystemPackage 12 | import Socket 13 | 14 | internal extension SocketDescriptor { 15 | 16 | @usableFromInline 17 | static func bluetooth( 18 | _ socketProtocol: BluetoothSocketProtocol, 19 | flags: SocketFlags = [.closeOnExec] 20 | ) throws(Errno) -> SocketDescriptor { 21 | return try self.init( 22 | socketProtocol, 23 | flags: flags 24 | ) 25 | } 26 | 27 | @usableFromInline 28 | static func bluetooth( 29 | _ socketProtocol: BluetoothSocketProtocol, 30 | bind address: Address, 31 | flags: SocketFlags = [.closeOnExec] 32 | ) throws(Errno) -> SocketDescriptor { 33 | return try self.init(socketProtocol, bind: address, flags: flags) 34 | } 35 | 36 | @usableFromInline 37 | func setNonblocking(retryOnInterrupt: Bool = true) throws(Errno) { 38 | var flags = try getStatus(retryOnInterrupt: retryOnInterrupt) 39 | flags.insert(.nonBlocking) 40 | try setStatus(flags, retryOnInterrupt: retryOnInterrupt) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIControllerType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIControllerType.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | /// HCI controller types 9 | @frozen 10 | public struct HCIControllerType: RawRepresentable, Equatable, Hashable { 11 | 12 | public let rawValue: CInt 13 | 14 | public init(rawValue: CInt) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | private init(_ raw: CInt) { 19 | self.init(rawValue: raw) 20 | } 21 | } 22 | 23 | // MARK: - Definitions 24 | 25 | public extension HCIControllerType { 26 | 27 | /// Bluetooth Primary / BREDR controller type 28 | static var primary: HCIControllerType { HCIControllerType(0x00) } // Also known as BREDR 29 | 30 | /// Bluetooth AMP controller type 31 | static var amp: HCIControllerType { HCIControllerType(0x01) } 32 | } 33 | 34 | // MARK: - Definitions 35 | 36 | extension HCIControllerType: CustomStringConvertible, CustomDebugStringConvertible { 37 | 38 | public var description: String { 39 | switch self { 40 | case .primary: 41 | return "Primary" 42 | case .amp: 43 | return "AMP" 44 | default: 45 | return "Unknown \(rawValue)" 46 | } 47 | } 48 | 49 | public var debugDescription: String { 50 | description 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import Socket 10 | 11 | /// Bluetooth Socket Options 12 | public enum BluetoothSocketOption: CInt, SocketOptionID { 13 | 14 | @_alwaysEmitIntoClient 15 | public static var optionLevel: SocketOptionLevel { .bluetooth } 16 | 17 | /// Bluetooth Security socket option 18 | case security = 4 // BT_SECURITY 19 | 20 | /// Bluetooth defer setup socket option 21 | case deferSetup = 7 // BT_DEFER_SETUP 22 | 23 | /// Bluetooth flushable socket option 24 | case flushable = 8 25 | 26 | /// Bluetooth power socket option 27 | case power = 9 28 | 29 | /// Bluetooth Channel Policy socket option 30 | case channelPolicy = 10 // BT_CHANNEL_POLICY 31 | 32 | /// Bluetooth Voice options 33 | case voice = 11 // BT_VOICE 34 | 35 | /// Bluetooth Socket Send MTU 36 | case sendMTU = 12 // BT_SNDMTU 37 | 38 | /// Bluetooth Socket Receive MTU 39 | case receiveMTU = 13 // BT_RCVMTU 40 | 41 | /// Bluetooth Phy 42 | case phy = 14 // BT_PHY 43 | 44 | /// Bluetooth Mode 45 | case mode = 15 // BT_MODE 46 | 47 | /// Bluetooth Packet Status 48 | case packetStatus = 16 // BT_PKT_STATUS 49 | } 50 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/CMTP/IOCTL/CMTPIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CMTPIO.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | /// Bluetooth CMTP `ioctl` requests 12 | @frozen 13 | public enum CMTPIO: Hashable, CaseIterable, IOControlID { 14 | 15 | case addConnection 16 | case removeConnection 17 | case getConnectionList 18 | case getConnectionInfo 19 | 20 | public init?(rawValue: UInt) { 21 | guard let value = Self.allCases.first(where: { $0.rawValue == rawValue }) else { 22 | return nil 23 | } 24 | self = value 25 | } 26 | 27 | public var rawValue: UInt { 28 | switch self { 29 | case .addConnection: return _CMTPCONNADD 30 | case .removeConnection: return _CMTPCONNDEL 31 | case .getConnectionList: return _CMTPGETCONNLIST 32 | case .getConnectionInfo: return _CMTPGETCONNINFO 33 | } 34 | } 35 | } 36 | 37 | @_alwaysEmitIntoClient 38 | internal var _CMTPCONNADD: CUnsignedLong { _IOW("C", 200, CInt.self) } 39 | @_alwaysEmitIntoClient 40 | internal var _CMTPCONNDEL: CUnsignedLong { _IOW("C", 201, CInt.self) } 41 | @_alwaysEmitIntoClient 42 | internal var _CMTPGETCONNLIST: CUnsignedLong { _IOR("C", 210, CInt.self) } 43 | @_alwaysEmitIntoClient 44 | internal var _CMTPGETCONNINFO: CUnsignedLong { _IOR("C", 211, CInt.self) } 45 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/BluetoothLinux.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``BluetoothLinux`` 2 | 3 | Pure Swift Bluetooth Stack for Linux. 4 | 5 | ## Overview 6 | 7 | Provides a variety of APIs for interacting with different Sockets and IOCTL that Linux provides for Bluetooth. 8 | 9 | This library does not require the official [BlueZ](https://www.bluez.org) userland library, instead it communicates directly with the Linux kernel Bluetooth subsystem. 10 | 11 | ## Topics 12 | 13 | ### Bluetooth Linux subsystem 14 | 15 | - ``BluetoothSocketProtocol`` 16 | - ``BluetoothSocketAddress`` 17 | - ``BluetoothSocketOption`` 18 | - ``AddressType`` 19 | - ``LinkMode`` 20 | 21 | ### Host Controller Interface 22 | 23 | - ``HostController`` 24 | - ``HostControllerIO`` 25 | - ``HCISocketAddress`` 26 | - ``HCISocketOption`` 27 | - ``HCIBusType`` 28 | - ``HCIControllerType`` 29 | - ``HCIChannel`` 30 | - ``HCIDeviceOptions`` 31 | - ``HCIDeviceEvent`` 32 | - ``HCIDeviceFlag`` 33 | - ``HCIPacketType`` 34 | 35 | ### L2CAP 36 | 37 | - ``L2CAPSocket`` 38 | - ``L2CAPSocketAddress`` 39 | - ``L2CAPSocketOption`` 40 | 41 | ### RFCOMM 42 | 43 | - ``RFCOMMIO`` 44 | - ``RFCOMMSocket`` 45 | - ``RFCOMMSocketAddress`` 46 | - ``RFCOMMSocketOption`` 47 | - ``RFCOMMDevice`` 48 | - ``RFCOMMState`` 49 | - ``RFCOMMFlag`` 50 | - ``RFCOMMLinkMode`` 51 | 52 | ### SCO 53 | 54 | - ``SCOSocket`` 55 | 56 | ### HIDP 57 | 58 | - ``HIDPIO`` 59 | 60 | ### CMTP 61 | 62 | - ``CMTPIO`` 63 | 64 | ### BNEP 65 | 66 | - ``BNEPIO`` 67 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/swift 3 | { 4 | "name": "Swift", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a Swift version 9 | "VARIANT": "6.0.2-jammy", 10 | // Options 11 | "NODE_VERSION": "lts/*", 12 | "UPGRADE_PACKAGES": "true" 13 | } 14 | }, 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined" 19 | ], 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | // Configure properties specific to VS Code. 24 | "vscode": { 25 | // Set *default* container specific settings.json values on container create. 26 | "settings": { 27 | "lldb.library": "/usr/lib/liblldb.so" 28 | }, 29 | 30 | // Add the IDs of extensions you want installed when the container is created. 31 | "extensions": [ 32 | "sswg.swift-lang" 33 | ] 34 | } 35 | }, 36 | 37 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 38 | // "forwardPorts": [], 39 | 40 | // Use 'postCreateCommand' to run commands after the container is created. 41 | // "postCreateCommand": "", 42 | 43 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 44 | "remoteUser": "vscode" 45 | } 46 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HCIDeviceUp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIDeviceUp.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | public extension HostControllerIO { 12 | 13 | struct DeviceUp: IOControlInteger { 14 | 15 | @_alwaysEmitIntoClient 16 | public static var id: HostControllerIO { .deviceUp } 17 | 18 | public var device: HostController.ID 19 | 20 | public init(device: HostController.ID) { 21 | self.device = device 22 | } 23 | 24 | @_alwaysEmitIntoClient 25 | public var intValue: Int32 { 26 | return Int32(device.rawValue) 27 | } 28 | } 29 | } 30 | 31 | // MARK: - File Descriptor 32 | 33 | internal extension SocketDescriptor { 34 | 35 | @usableFromInline 36 | func deviceUp(for id: HostController.ID) throws(Errno) { 37 | try inputOutput(HostControllerIO.DeviceUp(device: id)) 38 | } 39 | } 40 | 41 | // MARK: - Host Controller 42 | 43 | public extension HostController { 44 | 45 | /// Open and initialize HCI device. 46 | static func enable(device id: HostController.ID) throws(Errno) { 47 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 48 | try fileDescriptor.closeAfter { () throws(Errno) -> () in 49 | try fileDescriptor.deviceUp(for: id) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HIDP/IOCTL/HIDPIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HIDPIO.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | /// Bluetooth HIDP `ioctl` requests 12 | @frozen 13 | public enum HIDPIO: Hashable, CaseIterable, IOControlID { 14 | 15 | case addConnection 16 | case removeConnection 17 | case getConnectionList 18 | case getConnectionInfo 19 | 20 | public init?(rawValue: UInt) { 21 | guard let value = Self.allCases.first(where: { $0.rawValue == rawValue }) else { 22 | return nil 23 | } 24 | self = value 25 | } 26 | 27 | public var rawValue: UInt { 28 | switch self { 29 | case .addConnection: return _HIDPCONNADD 30 | case .removeConnection: return _HIDPCONNDEL 31 | case .getConnectionList: return _HIDPGETCONNLIST 32 | case .getConnectionInfo: return _HIDPGETCONNINFO 33 | } 34 | } 35 | } 36 | 37 | @_alwaysEmitIntoClient 38 | internal var _HIDPCONNADD: CUnsignedLong { _IOW("H", 200, CInt.self) } 39 | 40 | @_alwaysEmitIntoClient 41 | internal var _HIDPCONNDEL: CUnsignedLong { _IOW("H", 201, CInt.self) } 42 | 43 | @_alwaysEmitIntoClient 44 | internal var _HIDPGETCONNLIST: CUnsignedLong { _IOR("H", 210, CInt.self) } 45 | 46 | @_alwaysEmitIntoClient 47 | internal var _HIDPGETCONNINFO: CUnsignedLong { _IOR("H", 211, CInt.self) } 48 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HCIDeviceDown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceDown.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | public extension HostControllerIO { 12 | 13 | struct DeviceDown: IOControlInteger { 14 | 15 | @_alwaysEmitIntoClient 16 | public static var id: HostControllerIO { .deviceDown } 17 | 18 | public var device: HostController.ID 19 | 20 | public init(device: HostController.ID) { 21 | self.device = device 22 | } 23 | 24 | @_alwaysEmitIntoClient 25 | public var intValue: Int32 { 26 | return Int32(device.rawValue) 27 | } 28 | } 29 | } 30 | 31 | // MARK: - File Descriptor 32 | 33 | internal extension SocketDescriptor { 34 | 35 | @usableFromInline 36 | func deviceDown(for id: HostController.ID) throws(Errno) { 37 | try inputOutput(HostControllerIO.DeviceDown(device: id)) 38 | } 39 | } 40 | 41 | // MARK: - Host Controller 42 | 43 | public extension HostController { 44 | 45 | /// Disable the HCI device. 46 | static func disable(device id: HostController.ID) throws(Errno) { 47 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 48 | try fileDescriptor.closeAfter { () throws(Errno) -> () in 49 | try fileDescriptor.deviceDown(for: id) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/AddressType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressType.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 2/28/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Bluetooth 10 | import BluetoothHCI 11 | 12 | /// Bluetooth Address type 13 | /// 14 | /// - SeeAlso: [Ten Important Differences between Bluetooth BR/EDR and Bluetooth Smart](http://blog.bluetooth.com/ten-important-differences-between-bluetooth-bredr-and-bluetooth-smart/) 15 | @frozen 16 | public enum AddressType: UInt8 { 17 | 18 | /// Bluetooth Basic Rate/Enhanced Data Rate 19 | case bredr = 0x00 20 | case lowEnergyPublic = 0x01 21 | case lowEnergyRandom = 0x02 22 | 23 | @_alwaysEmitIntoClient 24 | public init() { self = .bredr } 25 | } 26 | 27 | public extension AddressType { 28 | 29 | /// Initialize with LE address type. 30 | @_alwaysEmitIntoClient 31 | init(lowEnergy addressType: LowEnergyAddressType) { 32 | 33 | switch addressType { 34 | case .public, 35 | .publicIdentity: 36 | self = .lowEnergyPublic 37 | case .random, 38 | .randomIdentity: 39 | self = .lowEnergyRandom 40 | } 41 | } 42 | 43 | /// Whether the Bluetooth address type is LE. 44 | @_alwaysEmitIntoClient 45 | var isLowEnergy: Bool { 46 | 47 | switch self { 48 | case .lowEnergyPublic, 49 | .lowEnergyRandom: 50 | return true 51 | case .bredr: 52 | return false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinuxTest/include/IOCTLTestsHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // IOCTLTestsHelper.h 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/3/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #ifdef __linux__ 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | /* HCI ioctl defines */ 16 | #define HCIDEVUP _IOW('H', 201, int) 17 | #define HCIDEVDOWN _IOW('H', 202, int) 18 | #define HCIDEVRESET _IOW('H', 203, int) 19 | #define HCIDEVRESTAT _IOW('H', 204, int) 20 | 21 | #define HCIGETDEVLIST _IOR('H', 210, int) 22 | #define HCIGETDEVINFO _IOR('H', 211, int) 23 | #define HCIGETCONNLIST _IOR('H', 212, int) 24 | #define HCIGETCONNINFO _IOR('H', 213, int) 25 | #define HCIGETAUTHINFO _IOR('H', 215, int) 26 | 27 | #define HCISETRAW _IOW('H', 220, int) 28 | #define HCISETSCAN _IOW('H', 221, int) 29 | #define HCISETAUTH _IOW('H', 222, int) 30 | #define HCISETENCRYPT _IOW('H', 223, int) 31 | #define HCISETPTYPE _IOW('H', 224, int) 32 | #define HCISETLINKPOL _IOW('H', 225, int) 33 | #define HCISETLINKMODE _IOW('H', 226, int) 34 | #define HCISETACLMTU _IOW('H', 227, int) 35 | #define HCISETSCOMTU _IOW('H', 228, int) 36 | 37 | #define HCIBLOCKADDR _IOW('H', 230, int) 38 | #define HCIUNBLOCKADDR _IOW('H', 231, int) 39 | 40 | #define HCIINQUIRY _IOR('H', 240, int) 41 | 42 | static int hci_ioctl_list[] = { 43 | HCIDEVUP, 44 | HCIDEVDOWN, 45 | HCIDEVRESET, 46 | HCIDEVRESTAT, 47 | HCIGETDEVLIST, 48 | HCIGETDEVINFO, 49 | HCIGETCONNLIST, 50 | HCIGETCONNINFO, 51 | HCIGETAUTHINFO 52 | }; 53 | #else 54 | static int hci_ioctl_list[] = {}; 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/BNEP/IOCTL/BNEPIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BNEPIO.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | /// Bluetooth BNEP `ioctl` requests 12 | @frozen 13 | public enum BNEPIO: Hashable, CaseIterable, IOControlID { 14 | 15 | case addConnection 16 | case removeConnection 17 | case getConnectionList 18 | case getConnectionInfo 19 | case getSupportedFeatures 20 | 21 | public init?(rawValue: UInt) { 22 | guard let value = Self.allCases.first(where: { $0.rawValue == rawValue }) else { 23 | return nil 24 | } 25 | self = value 26 | } 27 | 28 | public var rawValue: UInt { 29 | switch self { 30 | case .addConnection: return _BNEPCONNADD 31 | case .removeConnection: return _BNEPCONNDEL 32 | case .getConnectionList: return _BNEPGETCONNLIST 33 | case .getConnectionInfo: return _BNEPGETCONNINFO 34 | case .getSupportedFeatures: return _BNEPGETSUPPFEAT 35 | } 36 | } 37 | } 38 | 39 | @_alwaysEmitIntoClient 40 | var _BNEPCONNADD: CUnsignedLong { _IOW("B", 200, CInt.self) } 41 | 42 | @_alwaysEmitIntoClient 43 | var _BNEPCONNDEL: CUnsignedLong { _IOW("B", 201, CInt.self) } 44 | 45 | @_alwaysEmitIntoClient 46 | var _BNEPGETCONNLIST: CUnsignedLong { _IOR("B", 210, CInt.self) } 47 | 48 | @_alwaysEmitIntoClient 49 | var _BNEPGETCONNINFO: CUnsignedLong { _IOR("B", 211, CInt.self) } 50 | 51 | @_alwaysEmitIntoClient 52 | var _BNEPGETSUPPFEAT: CUnsignedLong { _IOR("B", 212, CInt.self) } 53 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SocketProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothProtocol.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/1/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import SystemPackage 10 | import Socket 11 | 12 | /// Bluetooth Socket Protocol 13 | public enum BluetoothSocketProtocol: Int32, Sendable, Codable, CaseIterable { 14 | 15 | /// Bluetooth L2CAP (Logical link control and adaptation protocol) 16 | case l2cap = 0 17 | 18 | /// Bluetooth HCI protocol (Host Controller Interface) 19 | case hci = 1 20 | 21 | /// Bluetooth SCO protocol (Synchronous Connection Oriented Link) 22 | case sco = 2 23 | 24 | /// Bluetooth RFCOMM protocol (Radio frequency communication) 25 | case rfcomm = 3 26 | 27 | /// Bluetooth BNEP (network encapsulation protocol) 28 | case bnep = 4 29 | 30 | /// CAPI Message Transport Protocol 31 | case cmtp = 5 32 | 33 | /// HIDP (Human Interface Device Protocol) is a transport layer for HID reports. 34 | case hidp = 6 35 | 36 | /// Audio/video data transport protocol 37 | case avdtp = 7 38 | } 39 | 40 | extension BluetoothSocketProtocol: SocketProtocol { 41 | 42 | @_alwaysEmitIntoClient 43 | public static var family: SocketAddressFamily { .bluetooth } 44 | 45 | @_alwaysEmitIntoClient 46 | public var type: SocketType { 47 | switch self { 48 | case .l2cap: return .sequencedPacket 49 | case .hci: return .raw 50 | case .sco: return .sequencedPacket 51 | case .rfcomm: return .stream 52 | case .bnep: return .raw 53 | case .cmtp: return .raw 54 | case .hidp: return .raw 55 | case .avdtp: return .raw 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/SocketOptions/SecuritySocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecuritySocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension BluetoothSocketOption { 13 | 14 | struct Security: Equatable, Hashable, SocketOption { 15 | 16 | @_alwaysEmitIntoClient 17 | public static var id: BluetoothSocketOption { .security } 18 | 19 | internal private(set) var bytes: CInterop.BluetoothSocketSecurity 20 | 21 | @usableFromInline 22 | internal init(_ bytes: CInterop.BluetoothSocketSecurity) { 23 | self.bytes = bytes 24 | } 25 | 26 | /// 27 | public init(level: SecurityLevel = .sdp, keySize: UInt8 = 0) { 28 | self.init(CInterop.BluetoothSocketSecurity()) 29 | self.bytes.level = level.rawValue 30 | self.bytes.key_size = keySize 31 | } 32 | 33 | public var level: SecurityLevel { 34 | return SecurityLevel(rawValue: bytes.level)! 35 | } 36 | 37 | public var keySize: UInt8 { 38 | return bytes.key_size 39 | } 40 | 41 | public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 42 | return try Swift.withUnsafeBytes(of: bytes, body) 43 | } 44 | 45 | public static func withUnsafeBytes( 46 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 47 | ) rethrows -> Self where Error: Swift.Error { 48 | var value = CInterop.BluetoothSocketSecurity() 49 | try Swift.withUnsafeMutableBytes(of: &value, body) 50 | return Self.init(value) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/IOCTL/RFCOMMGetDeviceInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMGetDeviceInformation.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension RFCOMMIO { 13 | 14 | /// RFCOMM Get Device Information 15 | struct GetDeviceInformation: Equatable, Hashable, IOControlValue { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var id: RFCOMMIO { .getDeviceInfo } 19 | 20 | @usableFromInline 21 | internal private(set) var bytes: CInterop.RFCOMMDeviceInformation 22 | 23 | @usableFromInline 24 | internal init(_ bytes: CInterop.RFCOMMDeviceInformation) { 25 | self.bytes = bytes 26 | } 27 | 28 | @_alwaysEmitIntoClient 29 | public init(id: HostController.ID) { 30 | self.init(CInterop.RFCOMMDeviceInformation(id: id.rawValue)) 31 | } 32 | 33 | @_alwaysEmitIntoClient 34 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 35 | try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in 36 | try body(buffer.baseAddress!) 37 | } 38 | } 39 | } 40 | } 41 | 42 | public extension RFCOMMIO.GetDeviceInformation { 43 | 44 | @_alwaysEmitIntoClient 45 | var device: RFCOMMDevice { 46 | return RFCOMMDevice(bytes) 47 | } 48 | } 49 | 50 | 51 | // MARK: - File Descriptor 52 | 53 | internal extension SocketDescriptor { 54 | 55 | @usableFromInline 56 | func rfcommGetDevice( 57 | id: HostController.ID 58 | ) throws -> RFCOMMDevice { 59 | var request = RFCOMMIO.GetDeviceInformation(id: id) 60 | try inputOutput(&request) 61 | return request.device 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/IOCTL/RFCOMMReleaseDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMReleaseDevice.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension RFCOMMIO { 13 | 14 | /// RFCOMM Release Device 15 | struct ReleaseDevice: Equatable, Hashable, IOControlValue { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var id: RFCOMMIO { .releaseDevice } 19 | 20 | @usableFromInline 21 | internal private(set) var bytes: CInterop.RFCOMMDeviceRequest 22 | 23 | @usableFromInline 24 | internal init(_ bytes: CInterop.RFCOMMDeviceRequest) { 25 | self.bytes = bytes 26 | } 27 | 28 | @_alwaysEmitIntoClient 29 | public init( 30 | id: HostController.ID, 31 | flags: BitMaskOptionSet 32 | ) { 33 | self.init(CInterop.RFCOMMDeviceRequest( 34 | device: id.rawValue, 35 | flags: flags.rawValue, 36 | source: .zero, 37 | destination: .zero, 38 | channel: 0x00) 39 | ) 40 | } 41 | 42 | @_alwaysEmitIntoClient 43 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 44 | try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in 45 | try body(buffer.baseAddress!) 46 | } 47 | } 48 | } 49 | } 50 | 51 | public extension RFCOMMIO.ReleaseDevice { 52 | 53 | @_alwaysEmitIntoClient 54 | var id: HostController.ID { 55 | return .init(rawValue: bytes.device) 56 | } 57 | 58 | @_alwaysEmitIntoClient 59 | var flags: BitMaskOptionSet { 60 | return .init(rawValue: bytes.flags) 61 | } 62 | } 63 | 64 | // MARK: - File Descriptor 65 | 66 | internal extension SocketDescriptor { 67 | 68 | @usableFromInline 69 | func rfcommReleaseDevice( 70 | id: HostController.ID, 71 | flags: BitMaskOptionSet = [] 72 | ) throws { 73 | var request = RFCOMMIO.ReleaseDevice( 74 | id: id, 75 | flags: flags 76 | ) 77 | try inputOutput(&request) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Internal/Darwin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Darwin.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 2/28/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import SystemPackage 10 | import Socket 11 | 12 | #if !os(Linux) 13 | #warning("This module will only run on Linux") 14 | 15 | @usableFromInline 16 | internal func stub(function: StaticString = #function) -> Never { 17 | fatalError("\(function) not implemented. This code only runs on Linux.") 18 | } 19 | 20 | internal extension SocketOptionLevel { 21 | 22 | @_alwaysEmitIntoClient 23 | static var bluetooth: SocketOptionLevel { stub() } 24 | } 25 | 26 | internal extension SocketAddressFamily { 27 | 28 | @usableFromInline 29 | static var bluetooth: SocketAddressFamily { stub() } 30 | } 31 | 32 | internal extension SocketDescriptor { 33 | 34 | init( 35 | _ protocolID: T, 36 | flags: SocketFlags, 37 | retryOnInterrupt: Bool = true 38 | ) throws(Errno) { 39 | stub() 40 | } 41 | 42 | init( 43 | _ protocolID: Address.ProtocolID, 44 | bind address: Address, 45 | flags: SocketFlags, 46 | retryOnInterrupt: Bool = true 47 | ) throws(Errno) { 48 | stub() 49 | } 50 | } 51 | 52 | @usableFromInline 53 | internal struct SocketFlags: OptionSet, Hashable, Codable { 54 | 55 | /// The raw C file events. 56 | @_alwaysEmitIntoClient 57 | public let rawValue: CInt 58 | 59 | /// Create a strongly-typed file events from a raw C value. 60 | @_alwaysEmitIntoClient 61 | public init(rawValue: CInt) { self.rawValue = rawValue } 62 | 63 | @_alwaysEmitIntoClient 64 | private init(_ raw: CInt) { self.init(rawValue: numericCast(raw)) } 65 | } 66 | 67 | extension SocketFlags { 68 | 69 | /// Set the `O_NONBLOCK` file status flag on the open file description referred to by the new file 70 | /// descriptor. Using this flag saves extra calls to `fcntl()` to achieve the same result. 71 | public static var nonBlocking: SocketFlags { stub() } 72 | 73 | /// Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor. 74 | public static var closeOnExec: SocketFlags { stub() } 75 | } 76 | 77 | #endif 78 | 79 | #if os(Android) 80 | #warning("Android does not use BlueZ kernel module") 81 | #endif 82 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMSocketAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMSocketAddress.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | import Bluetooth 11 | 12 | /// RFCOMM Socket Address 13 | @frozen 14 | public struct RFCOMMSocketAddress: Equatable, Hashable, Sendable { 15 | 16 | // MARK: - Properties 17 | 18 | public var address: BluetoothAddress 19 | 20 | public var channel: UInt8 21 | 22 | // MARK: - Initialization 23 | 24 | public init(address: BluetoothAddress, 25 | channel: UInt8) { 26 | 27 | self.address = address 28 | self.channel = channel 29 | } 30 | } 31 | 32 | extension RFCOMMSocketAddress: BluetoothSocketAddress { 33 | 34 | @_alwaysEmitIntoClient 35 | public static var protocolID: BluetoothSocketProtocol { .rfcomm } 36 | 37 | /// Unsafe pointer closure 38 | @_alwaysEmitIntoClient 39 | public func withUnsafePointer( 40 | _ body: (UnsafePointer, UInt32) throws(Error) -> Result 41 | ) rethrows -> Result where Error: Swift.Error { 42 | try bytes.withUnsafePointer(body) 43 | } 44 | 45 | @_alwaysEmitIntoClient 46 | public static func withUnsafePointer( 47 | _ body: (UnsafeMutablePointer, UInt32) throws(Error) -> () 48 | ) rethrows -> Self where Error: Swift.Error { 49 | var bytes = CInterop.RFCOMMSocketAddress() 50 | try bytes.withUnsafeMutablePointer(body) 51 | return Self.init(bytes) 52 | } 53 | 54 | public static func withUnsafePointer( 55 | _ pointer: UnsafeMutablePointer 56 | ) -> Self { 57 | return pointer.withMemoryRebound(to: CInterop.RFCOMMSocketAddress.self, capacity: 1) { pointer in 58 | Self.init(pointer.pointee) 59 | } 60 | } 61 | } 62 | 63 | internal extension RFCOMMSocketAddress { 64 | 65 | @usableFromInline 66 | init(_ bytes: CInterop.RFCOMMSocketAddress) { 67 | self.init( 68 | address: bytes.address, 69 | channel: bytes.channel 70 | ) 71 | } 72 | 73 | @usableFromInline 74 | var bytes: CInterop.RFCOMMSocketAddress { 75 | CInterop.RFCOMMSocketAddress( 76 | address: address, 77 | channel: channel 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/SocketOptions/HCIFilterSocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIFilterSocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import BluetoothHCI 10 | import SystemPackage 11 | import Socket 12 | 13 | public extension HCISocketOption { 14 | 15 | /// HCI Filter Socket Option 16 | struct Filter: SocketOption, Sendable { 17 | 18 | @_alwaysEmitIntoClient 19 | public static var id: HCISocketOption { .filter } 20 | 21 | @usableFromInline 22 | internal var bytes: CInterop.HCIFilterSocketOption 23 | 24 | @usableFromInline 25 | internal init(_ bytes: CInterop.HCIFilterSocketOption) { 26 | self.bytes = bytes 27 | } 28 | 29 | @_alwaysEmitIntoClient 30 | public init() { 31 | self.init(CInterop.HCIFilterSocketOption()) 32 | } 33 | 34 | @_alwaysEmitIntoClient 35 | public var typeMask: UInt32 { 36 | get { return bytes.typeMask } 37 | set { bytes.typeMask = newValue } 38 | } 39 | 40 | @_alwaysEmitIntoClient 41 | public var opcode: UInt16 { 42 | get { return bytes.opcode } 43 | set { bytes.opcode = newValue } 44 | } 45 | 46 | @_alwaysEmitIntoClient 47 | public mutating func setPacketType(_ type: HCIPacketType) { 48 | bytes.setPacketType(type) 49 | } 50 | 51 | @_alwaysEmitIntoClient 52 | public mutating func setEvent(_ event: T) { 53 | bytes.setEvent(event.rawValue) 54 | } 55 | 56 | @_alwaysEmitIntoClient 57 | public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 58 | return try Swift.withUnsafeBytes(of: bytes) { bufferPointer in 59 | try body(bufferPointer) 60 | } 61 | } 62 | 63 | @_alwaysEmitIntoClient 64 | public static func withUnsafeBytes( 65 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 66 | ) rethrows -> Self where Error: Swift.Error { 67 | var value = self.init() 68 | try Swift.withUnsafeMutableBytes(of: &value.bytes, body) 69 | return value 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![BluetoothLinux](https://github.com/PureSwift/BluetoothLinux/raw/master/Assets/PureSwiftBluetoothLinux.png) 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![Platform][platform-badge]][platform-url] 5 | [![Release][release-badge]][release-url] 6 | [![License][mit-badge]][mit-url] 7 | 8 | Pure Swift Bluetooth Stack for Linux 9 | 10 | Does not require [BlueZ](https://www.bluez.org) userland library, communicates directly with the Linux kernel Bluetooth subsystem. 11 | 12 | ## Usage 13 | 14 | ```swift 15 | import Bluetooth 16 | import BluetoothLinux 17 | 18 | guard let hostController = await BluetoothLinux.HostController.default 19 | else { fatalError("No Bluetooth adapters found") } 20 | let uuid = UUID(rawValue: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")! 21 | let beacon = AppleBeacon(uuid: uuid, major: 1, minor: 1, rssi: -29) 22 | do { try await hostController.iBeacon(beacon) } 23 | catch { print("Error enabling iBeacon: \(error)") } 24 | ``` 25 | 26 | ## Installation 27 | 28 | ### Swift Package Manager 29 | 30 | ```swift 31 | import PackageDescription 32 | 33 | let package = Package( 34 | dependencies: [ 35 | .package( 36 | url: "https://github.com/PureSwift/BluetoothLinux.git", 37 | .branch("master") 38 | ) 39 | ] 40 | ) 41 | ``` 42 | 43 | ## Documentation 44 | 45 | Read the documentation [here](http://pureswift.github.io/BluetoothLinux/documentation/bluetoothlinux/). 46 | Documentation can be generated with [DocC](https://github.com/apple/swift-docc). 47 | 48 | ## See Also 49 | 50 | - [Bluetooth](https://github.com/PureSwift/Bluetooth) - Pure Swift Bluetooth Definitions. 51 | - [GATT](https://github.com/PureSwift/GATT) - Bluetooth Generic Attribute Profile (GATT) for Swift. 52 | - [Netlink](https://github.com/PureSwift/Netlink) - Swift library for communicating with Linux Kernel Netlink subsystem. 53 | 54 | License 55 | ------- 56 | 57 | **BluetoothLinux** is released under the MIT license. See LICENSE for details. 58 | 59 | [swift-badge]: https://img.shields.io/badge/Swift-6.0-orange.svg?style=flat 60 | [swift-url]: https://swift.org 61 | [platform-badge]: https://img.shields.io/badge/platform-linux-lightgrey.svg 62 | [platform-url]: https://swift.org 63 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 64 | [mit-url]: https://tldrlegal.com/license/mit-license 65 | [release-badge]: https://img.shields.io/github/release/PureSwift/BluetoothLinux.svg 66 | [release-url]: https://github.com/PureSwift/BluetoothLinux/releases 67 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/IOCTL/RFCOMMIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMIO.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | /// Bluetooth RFCOMM `ioctl` requests 12 | @frozen 13 | public struct RFCOMMIO: RawRepresentable, Hashable, Codable, IOControlID { 14 | 15 | /// The raw C value. 16 | public let rawValue: UInt 17 | 18 | /// Creates a strongly-typed file handle from a raw C value. 19 | public init?(rawValue: UInt) { 20 | let value = RFCOMMIO(rawValue) 21 | guard Self._allCases.keys.contains(value) 22 | else { return nil } 23 | self = value 24 | } 25 | 26 | @_alwaysEmitIntoClient 27 | private init(_ raw: UInt) { 28 | self.rawValue = raw 29 | } 30 | } 31 | 32 | extension RFCOMMIO: CaseIterable { 33 | 34 | public static var allCases: [RFCOMMIO] { 35 | return [RFCOMMIO](_allCases.keys) 36 | } 37 | } 38 | 39 | public extension RFCOMMIO { 40 | 41 | @_alwaysEmitIntoClient 42 | static var createDevice: RFCOMMIO { IOW("R", 200, CInt.self) } 43 | 44 | @_alwaysEmitIntoClient 45 | static var releaseDevice: RFCOMMIO { IOW("R", 201, CInt.self) } 46 | 47 | @_alwaysEmitIntoClient 48 | static var getDeviceList: RFCOMMIO { IOR("R", 210, CInt.self) } 49 | 50 | @_alwaysEmitIntoClient 51 | static var getDeviceInfo: RFCOMMIO { IOR("R", 211, CInt.self) } 52 | } 53 | 54 | extension RFCOMMIO: CustomStringConvertible, CustomDebugStringConvertible { 55 | 56 | @_alwaysEmitIntoClient 57 | internal static var _allCases: [RFCOMMIO: String] { 58 | return [ 59 | .createDevice: ".createDevice", 60 | .releaseDevice: ".releaseDevice", 61 | .getDeviceList: ".getDeviceList", 62 | .getDeviceInfo: ".getDeviceInfo" 63 | ] 64 | } 65 | 66 | public var description: String { 67 | return Self._allCases[self] ?? rawValue.description 68 | } 69 | 70 | public var debugDescription: String { 71 | return description 72 | } 73 | } 74 | 75 | internal extension RFCOMMIO { 76 | 77 | @_alwaysEmitIntoClient 78 | static func IOW(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> RFCOMMIO { 79 | return RFCOMMIO(_IOW(type, nr, size)) 80 | } 81 | 82 | @_alwaysEmitIntoClient 83 | static func IOR(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> RFCOMMIO { 84 | return RFCOMMIO(_IOR(type, nr, size)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCISocketAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCISocketAddress.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | import Bluetooth 11 | 12 | /// Bluetooth HCI Socket Address 13 | @frozen 14 | public struct HCISocketAddress: Equatable, Hashable, Sendable { 15 | 16 | // MARK: - Properties 17 | 18 | /// HCI device identifier 19 | public var device: HostController.ID 20 | 21 | /// Channel identifier 22 | public var channel: HCIChannel 23 | 24 | // MARK: - Initialization 25 | 26 | /// Initialize with device and channel identifiers. 27 | @_alwaysEmitIntoClient 28 | public init( 29 | device: HostController.ID = .none, 30 | channel: HCIChannel = .raw 31 | ) { 32 | self.device = device 33 | self.channel = channel 34 | } 35 | } 36 | 37 | internal extension HCISocketAddress { 38 | 39 | @usableFromInline 40 | init(_ bytes: CInterop.HCISocketAddress) { 41 | self.init( 42 | device: HostController.ID(rawValue: bytes.device), 43 | channel: HCIChannel(rawValue: bytes.channel) ?? .raw 44 | ) 45 | } 46 | 47 | @usableFromInline 48 | var bytes: CInterop.HCISocketAddress { 49 | CInterop.HCISocketAddress( 50 | device: device.rawValue, 51 | channel: channel.rawValue 52 | ) 53 | } 54 | } 55 | 56 | extension HCISocketAddress: BluetoothSocketAddress { 57 | 58 | @_alwaysEmitIntoClient 59 | public static var protocolID: BluetoothSocketProtocol { .hci } 60 | 61 | /// Unsafe pointer closure 62 | @_alwaysEmitIntoClient 63 | public func withUnsafePointer( 64 | _ body: (UnsafePointer, UInt32) throws(Error) -> Result 65 | ) rethrows -> Result where Error: Swift.Error { 66 | try bytes.withUnsafePointer(body) 67 | } 68 | 69 | @_alwaysEmitIntoClient 70 | public static func withUnsafePointer( 71 | _ body: (UnsafeMutablePointer, UInt32) throws(Error) -> () 72 | ) rethrows -> Self where Error: Swift.Error { 73 | var bytes = CInterop.HCISocketAddress() 74 | try bytes.withUnsafeMutablePointer(body) 75 | return Self.init(bytes) 76 | } 77 | 78 | public static func withUnsafePointer( 79 | _ pointer: UnsafeMutablePointer 80 | ) -> Self { 81 | return pointer.withMemoryRebound(to: CInterop.HCISocketAddress.self, capacity: 1) { pointer in 82 | Self.init(pointer.pointee) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | import class Foundation.ProcessInfo 4 | 5 | // force building as dynamic library 6 | let dynamicLibrary = ProcessInfo.processInfo.environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] != nil 7 | let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrary ? .dynamic : nil 8 | 9 | var package = Package( 10 | name: "BluetoothLinux", 11 | platforms: [ 12 | .macOS(.v10_15), 13 | .iOS(.v13), 14 | .watchOS(.v6), 15 | .tvOS(.v13), 16 | ], 17 | products: [ 18 | .library( 19 | name: "BluetoothLinux", 20 | type: libraryType, 21 | targets: ["BluetoothLinux"] 22 | ) 23 | ], 24 | dependencies: [ 25 | .package( 26 | url: "https://github.com/PureSwift/Bluetooth.git", 27 | branch: "master" 28 | ), 29 | .package( 30 | url: "https://github.com/PureSwift/Socket.git", 31 | branch: "main" 32 | ) 33 | ], 34 | targets: [ 35 | .target( 36 | name: "BluetoothLinux", 37 | dependencies: [ 38 | .product( 39 | name: "Bluetooth", 40 | package: "Bluetooth" 41 | ), 42 | .product( 43 | name: "BluetoothHCI", 44 | package: "Bluetooth" 45 | ), 46 | .product( 47 | name: "Socket", 48 | package: "Socket" 49 | ), 50 | "CBluetoothLinux" 51 | ] 52 | ), 53 | .target( 54 | name: "CBluetoothLinux" 55 | ), 56 | .target( 57 | name: "CBluetoothLinuxTest" 58 | ), 59 | .testTarget( 60 | name: "BluetoothLinuxTests", 61 | dependencies: [ 62 | "BluetoothLinux", 63 | "CBluetoothLinuxTest", 64 | .product( 65 | name: "BluetoothGATT", 66 | package: "Bluetooth" 67 | ), 68 | .product( 69 | name: "BluetoothGAP", 70 | package: "Bluetooth" 71 | ) 72 | ], 73 | swiftSettings: [.swiftLanguageMode(.v5)] 74 | ) 75 | ] 76 | ) 77 | 78 | // SwiftPM command plugins are only supported by Swift version 5.6 and later. 79 | #if swift(>=5.6) 80 | let buildDocs = ProcessInfo.processInfo.environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] != nil 81 | if buildDocs { 82 | package.dependencies += [ 83 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 84 | ] 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/CBluetoothLinuxTest/include/MathTestsHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // MathTestsHelper.h 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/3/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | struct hci_filter { 12 | uint32_t type_mask; 13 | uint32_t event_mask[2]; 14 | uint16_t opcode; 15 | }; 16 | 17 | #define HCI_FLT_TYPE_BITS 31 18 | #define HCI_FLT_EVENT_BITS 63 19 | #define HCI_FLT_OGF_BITS 63 20 | #define HCI_FLT_OCF_BITS 127 21 | 22 | /* HCI Packet types */ 23 | #define HCI_COMMAND_PKT 0x01 24 | #define HCI_ACLDATA_PKT 0x02 25 | #define HCI_SCODATA_PKT 0x03 26 | #define HCI_EVENT_PKT 0x04 27 | #define HCI_VENDOR_PKT 0xff 28 | 29 | static inline void hci_set_bit(int nr, void *addr) 30 | { 31 | *((uint32_t *) addr + (nr >> 5)) |= (1 << (nr & 31)); 32 | } 33 | 34 | static inline void hci_clear_bit(int nr, void *addr) 35 | { 36 | *((uint32_t *) addr + (nr >> 5)) &= ~(1 << (nr & 31)); 37 | } 38 | 39 | static inline int hci_test_bit(int nr, void *addr) 40 | { 41 | return *((uint32_t *) addr + (nr >> 5)) & (1 << (nr & 31)); 42 | } 43 | 44 | /* HCI filter tools */ 45 | 46 | static inline void hci_filter_set_ptype(int t, struct hci_filter *f) 47 | { 48 | hci_set_bit((t == HCI_VENDOR_PKT) ? 0 : (t & HCI_FLT_TYPE_BITS), &f->type_mask); 49 | } 50 | static inline void hci_filter_clear_ptype(int t, struct hci_filter *f) 51 | { 52 | hci_clear_bit((t == HCI_VENDOR_PKT) ? 0 : (t & HCI_FLT_TYPE_BITS), &f->type_mask); 53 | } 54 | static inline int hci_filter_test_ptype(int t, struct hci_filter *f) 55 | { 56 | return hci_test_bit((t == HCI_VENDOR_PKT) ? 0 : (t & HCI_FLT_TYPE_BITS), &f->type_mask); 57 | } 58 | static inline void hci_filter_all_ptypes(struct hci_filter *f) 59 | { 60 | memset((void *) &f->type_mask, 0xff, sizeof(f->type_mask)); 61 | } 62 | static inline void hci_filter_set_event(int e, struct hci_filter *f) 63 | { 64 | hci_set_bit((e & HCI_FLT_EVENT_BITS), &f->event_mask); 65 | } 66 | static inline void hci_filter_clear_event(int e, struct hci_filter *f) 67 | { 68 | hci_clear_bit((e & HCI_FLT_EVENT_BITS), &f->event_mask); 69 | } 70 | static inline int hci_filter_test_event(int e, struct hci_filter *f) 71 | { 72 | return hci_test_bit((e & HCI_FLT_EVENT_BITS), &f->event_mask); 73 | } 74 | static inline void hci_filter_all_events(struct hci_filter *f) 75 | { 76 | memset((void *) f->event_mask, 0xff, sizeof(f->event_mask)); 77 | } 78 | static inline void hci_filter_set_opcode(int opcode, struct hci_filter *f) 79 | { 80 | f->opcode = opcode; 81 | } 82 | static inline void hci_filter_clear_opcode(struct hci_filter *f) 83 | { 84 | f->opcode = 0; 85 | } 86 | static inline int hci_filter_test_opcode(int opcode, struct hci_filter *f) 87 | { 88 | return (f->opcode == opcode); 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIBusType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIBusType.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | /// HCI bus types 9 | @frozen 10 | public struct HCIBusType: RawRepresentable, Equatable, Hashable, Sendable { 11 | 12 | public let rawValue: CInt 13 | 14 | public init(rawValue: CInt) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | private init(_ raw: CInt) { 19 | self.init(rawValue: raw) 20 | } 21 | } 22 | 23 | // MARK: - Definitions 24 | 25 | public extension HCIBusType { 26 | 27 | /// Bluetooth Virtual Bus Type 28 | static var virtual: HCIBusType { HCIBusType(0) } 29 | 30 | /// Bluetooth USB controller type 31 | static var usb: HCIBusType { HCIBusType(1) } 32 | 33 | /// Bluetooth PCCARD controller type 34 | static var pcCard: HCIBusType { HCIBusType(2) } 35 | 36 | /// Bluetooth UART controller type 37 | static var uart: HCIBusType { HCIBusType(3) } 38 | 39 | /// Bluetooth RS232 controller type 40 | static var rs232: HCIBusType { HCIBusType(4) } 41 | 42 | /// Bluetooth PCI controller type 43 | static var pci: HCIBusType { HCIBusType(5) } 44 | 45 | /// Bluetooth SDIO controller type 46 | static var sdio: HCIBusType { HCIBusType(6) } 47 | 48 | /// Bluetooth SPI controller type 49 | static var spi: HCIBusType { HCIBusType(7) } 50 | 51 | /// Bluetooth I2C controller type 52 | static var i2c: HCIBusType { HCIBusType(8) } 53 | 54 | /// Bluetooth SMD controller type 55 | static var smd: HCIBusType { HCIBusType(9) } 56 | 57 | /// Bluetooth VIRTIO controller type 58 | static var virtio: HCIBusType { HCIBusType(10) } 59 | } 60 | 61 | // MARK: - Definitions 62 | 63 | extension HCIBusType: CustomStringConvertible, CustomDebugStringConvertible { 64 | 65 | public var description: String { 66 | switch self { 67 | case .virtual: 68 | return "Virtual" 69 | case .usb: 70 | return "USB" 71 | case .pcCard: 72 | return "PCCARD" 73 | case .uart: 74 | return "UART" 75 | case .rs232: 76 | return "RS232" 77 | case .pci: 78 | return "PCI" 79 | case .sdio: 80 | return "SDIO" 81 | case .spi: 82 | return "SPI" 83 | case .i2c: 84 | return "I2C" 85 | case .smd: 86 | return "SMD" 87 | case .virtio: 88 | return "VIRTIO" 89 | default: 90 | return "Unknown \(rawValue)" 91 | } 92 | } 93 | 94 | public var debugDescription: String { 95 | description 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import BluetoothHCI 10 | 11 | public extension Errno { 12 | 13 | init(_ error: HCIError) { 14 | switch error { 15 | case .unknownCommand: 16 | self = .badMessage // EBADRQC 17 | case .noConnection: 18 | self = .socketNotConnected 19 | case .hardwareFailure: 20 | self = .ioError 21 | case .pageTimeout: 22 | self = .hostIsDown 23 | case .authenticationFailure: 24 | self = .permissionDenied 25 | case .keyMissing: 26 | self = .invalidArgument 27 | case .memoryFull: 28 | self = .noMemory 29 | case .connectionTimeout: 30 | self = .timedOut 31 | case .maxConnections, 32 | .maxSCOConnections: 33 | self = .tooManyLinks; 34 | case .aclConnectionExists: 35 | self = .alreadyInProcess; 36 | case .commandDisallowed, 37 | .differentTransactionCollision, 38 | .roleSwitchPending: 39 | self = .resourceBusy; 40 | case .rejectedLimitedResources, 41 | .rejectedAddress, 42 | .qosRejected: 43 | self = .connectionRefused; 44 | case .hostTimeout: 45 | self = .timedOut; 46 | case .unsupportedFeature, 47 | .requestedQoSNotSupported, 48 | .pairingWithUnitKeyNotSupported, 49 | .channelClassificationNotSupported, 50 | .unsupportedLMPParameterValue, 51 | .parameterOutOfMandatoryRange, 52 | .qosUnacceptableParameter: 53 | self = .notSupportedOnSocket; 54 | case .invalidParameters, 55 | .reservedSlotViolation: 56 | self = .invalidArgument; 57 | case .remoteUserEndedConnection, 58 | .remoteLowResources, 59 | .remotePowerOff: 60 | self = .connectionReset; 61 | case .connectionTerminated: 62 | self = .connectionAbort 63 | case .repeatedAttempts: 64 | self = .tooManySymbolicLinkLevels 65 | case .rejectedSecurity, 66 | .pairingNotAllowed, 67 | .insufficientSecurity: 68 | self = .permissionDenied; 69 | case .unsupportedRemoteFeature: 70 | self = .protocolNotSupported; 71 | case .scoOffsetRejected: 72 | self = .connectionRefused; 73 | case .unknownLMPPDU, 74 | .invalidLMPParameters, 75 | .lmpErrorTransactionCollision, 76 | .lmpPDUNotAllowed, 77 | .encryptionModeNotAcceptable: 78 | self = .protocolError; 79 | default: 80 | self = .noFunction; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HCIDeviceInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIDeviceInformation.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 17/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension HostControllerIO { 13 | 14 | struct DeviceInformation: IOControlValue { 15 | 16 | @_alwaysEmitIntoClient 17 | public static var id: HostControllerIO { .getDeviceInfo } 18 | 19 | @usableFromInline 20 | internal private(set) var bytes: CInterop.HCIDeviceInformation 21 | 22 | @usableFromInline 23 | internal init(_ bytes: CInterop.HCIDeviceInformation) { 24 | self.bytes = bytes 25 | } 26 | 27 | public init(request id: HostController.ID) { 28 | self.init(CInterop.HCIDeviceInformation(id: id.rawValue)) 29 | } 30 | 31 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 32 | try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in 33 | try body(buffer.baseAddress!) 34 | } 35 | } 36 | } 37 | } 38 | 39 | public extension HostControllerIO.DeviceInformation { 40 | 41 | var id: HostController.ID { 42 | return .init(rawValue: bytes.id) 43 | } 44 | 45 | var name: String { 46 | return bytes._name 47 | } 48 | 49 | var address: BluetoothAddress { 50 | return BluetoothAddress(bytes: bytes.address) 51 | } 52 | 53 | var flags: HCIDeviceOptions { 54 | return .init(rawValue: bytes.flags) 55 | } 56 | 57 | var type: HCIControllerType { 58 | return HCIControllerType(rawValue: CInt((bytes.type & 0x30) >> 4)) 59 | } 60 | 61 | var busType: HCIBusType { 62 | return HCIBusType(rawValue: CInt(bytes.type & 0x0f)) 63 | } 64 | } 65 | 66 | 67 | // MARK: - File Descriptor 68 | 69 | internal extension SocketDescriptor { 70 | 71 | @usableFromInline 72 | func deviceInformation(for id: HostController.ID) throws(Errno) -> HostControllerIO.DeviceInformation { 73 | var request = HostControllerIO.DeviceInformation(request: id) 74 | try inputOutput(&request) 75 | return request 76 | } 77 | } 78 | 79 | // MARK: - Host Controller 80 | 81 | public extension HostController { 82 | 83 | /// Get device information. 84 | static func deviceInformation(for id: HostController.ID) throws(Errno) -> HostControllerIO.DeviceInformation { 85 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 86 | return try fileDescriptor.closeAfter { () throws(Errno) -> HostControllerIO.DeviceInformation in 87 | try fileDescriptor.deviceInformation(for: id) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/IOCTL/RFCOMMGetDeviceList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMGetDeviceList.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 27/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension RFCOMMIO { 13 | 14 | /// RFCOMM Get Device List 15 | struct GetDeviceList: Equatable, Hashable, IOControlValue { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var id: RFCOMMIO { .getDeviceList } 19 | 20 | @_alwaysEmitIntoClient 21 | public static var maxLimit: Int { 256 } 22 | 23 | public var limit: Int 24 | 25 | public private(set) var response: [RFCOMMDevice] 26 | 27 | public init(limit: Int = Self.maxLimit) { 28 | precondition(limit <= Self.maxLimit, "Only \(Self.maxLimit) maximum devices is allowed") 29 | self.limit = limit 30 | self.response = [] 31 | } 32 | 33 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 34 | 35 | let bufferSize = MemoryLayout.size 36 | + (MemoryLayout.size * self.limit) 37 | 38 | let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) 39 | defer { buffer.deallocate() } 40 | 41 | buffer.withMemoryRebound(to: CInterop.RFCOMMDeviceListRequest.self, capacity: 1) { 42 | $0.pointee.count = numericCast(self.limit) 43 | } 44 | 45 | // call ioctl 46 | let result = try body(buffer) 47 | 48 | let resultCount = buffer.withMemoryRebound(to: CInterop.RFCOMMDeviceListRequest.self, capacity: 1) { 49 | Int($0.pointee.count) 50 | } 51 | 52 | self.response.removeAll(keepingCapacity: true) 53 | self.response.reserveCapacity(resultCount) 54 | 55 | for index in 0 ..< resultCount { 56 | let offset = MemoryLayout.size + (MemoryLayout.size * index) 57 | buffer.advanced(by: offset).withMemoryRebound(to: CInterop.RFCOMMDeviceInformation.self, capacity: 1) { 58 | let element = RFCOMMDevice($0.pointee) 59 | self.response.append(element) 60 | } 61 | } 62 | 63 | return result 64 | } 65 | } 66 | } 67 | 68 | // MARK: - File Descriptor 69 | 70 | internal extension SocketDescriptor { 71 | 72 | @usableFromInline 73 | func rfcommListDevices( 74 | limit: Int = RFCOMMIO.GetDeviceList.maxLimit 75 | ) throws -> [RFCOMMDevice] { 76 | var request = RFCOMMIO.GetDeviceList(limit: limit) 77 | try inputOutput(&request) 78 | return request.response 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Internal/IOCTL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IOCTL.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 3/1/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | /// #define _IOC(dir,type,nr,size) \ 10 | /// (((dir) << _IOC_DIRSHIFT) | \ 11 | /// ((type) << _IOC_TYPESHIFT) | \ 12 | /// ((nr) << _IOC_NRSHIFT) | \ 13 | /// ((size) << _IOC_SIZESHIFT)) 14 | @usableFromInline 15 | internal func _IOC( 16 | _ direction: IOCDirection, 17 | _ type: IOCType, 18 | _ nr: CInt, 19 | _ size: CInt 20 | ) -> CUnsignedLong { 21 | 22 | let dir = CInt(direction.rawValue) 23 | let dirValue = dir << _DIRSHIFT 24 | let typeValue = type.rawValue << _TYPESHIFT 25 | let nrValue = nr << _NRSHIFT 26 | let sizeValue = size << _SIZESHIFT 27 | let value = CLong(dirValue | typeValue | nrValue | sizeValue) 28 | return CUnsignedLong(bitPattern: value) 29 | } 30 | 31 | @usableFromInline 32 | internal func _IOW(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> CUnsignedLong { 33 | return _IOC(.write, type, nr, _TYPECHECK(size)) 34 | } 35 | 36 | @usableFromInline 37 | internal func _IOR(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> CUnsignedLong { 38 | return _IOC(.read, type, nr, _TYPECHECK(size)) 39 | } 40 | 41 | @usableFromInline 42 | internal func _TYPECHECK(_ type: T.Type) -> CInt { 43 | return CInt(MemoryLayout.size) 44 | } 45 | 46 | @usableFromInline 47 | internal enum IOCDirection: CUnsignedInt { 48 | 49 | case none = 0 50 | case write = 1 51 | case read = 2 52 | } 53 | 54 | @usableFromInline 55 | internal struct IOCType: RawRepresentable, Equatable, Hashable { 56 | 57 | public let rawValue: CInt 58 | 59 | public init(rawValue: CInt) { 60 | self.rawValue = rawValue 61 | } 62 | } 63 | 64 | extension IOCType: ExpressibleByUnicodeScalarLiteral { 65 | 66 | public init(unicodeScalarLiteral character: Unicode.Scalar) { 67 | self.init(rawValue: CInt(character.value)) 68 | } 69 | } 70 | 71 | @usableFromInline 72 | var _NRBITS: CInt { CInt(8) } 73 | 74 | @usableFromInline 75 | var _TYPEBITS: CInt { CInt(8) } 76 | 77 | @usableFromInline 78 | var _SIZEBITS: CInt { CInt(14) } 79 | 80 | @usableFromInline 81 | var _DIRBITS: CInt { CInt(2) } 82 | 83 | @usableFromInline 84 | var _NRMASK: CInt { CInt((1 << _NRBITS)-1) } 85 | 86 | @usableFromInline 87 | var _TYPEMASK: CInt { CInt((1 << _TYPEBITS)-1) } 88 | 89 | @usableFromInline 90 | var _SIZEMASK: CInt { CInt((1 << _SIZEBITS)-1) } 91 | 92 | @usableFromInline 93 | var _DIRMASK: CInt { CInt((1 << _DIRBITS)-1) } 94 | 95 | @usableFromInline 96 | var _NRSHIFT: CInt { CInt(0) } 97 | 98 | @usableFromInline 99 | var _TYPESHIFT: CInt { CInt(_NRSHIFT+_NRBITS) } 100 | 101 | @usableFromInline 102 | var _SIZESHIFT: CInt { CInt(_TYPESHIFT+_TYPEBITS) } 103 | 104 | @usableFromInline 105 | var _DIRSHIFT: CInt { CInt(_SIZESHIFT+_SIZEBITS) } 106 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/RFCOMMSocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMSocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import Socket 10 | 11 | /// RFCOMM Socket Options 12 | @frozen 13 | public enum RFCOMMSocketOption: CInt, SocketOptionID { 14 | 15 | @_alwaysEmitIntoClient 16 | public static var optionLevel: SocketOptionLevel { .rfcomm } 17 | 18 | /// RFCOMM Connection Info 19 | case connectionInfo = 0x02 // RFCOMM_CONNINFO 20 | 21 | /// RFCOMM LM 22 | case linkMode = 0x03 // RFCOMM_LM 23 | } 24 | 25 | public extension RFCOMMSocketOption { 26 | 27 | /// L2CAP Connection Info 28 | @frozen 29 | struct ConnectionInfo: SocketOption { 30 | 31 | @_alwaysEmitIntoClient 32 | public static var id: RFCOMMSocketOption { .connectionInfo } 33 | 34 | public private(set) var handle: UInt16 35 | public private(set) var deviceClass: (UInt8, UInt8, UInt8) 36 | 37 | public init() { 38 | self.handle = 0 39 | self.deviceClass = (0,0,0) 40 | } 41 | 42 | public func withUnsafeBytes(_ pointer: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 43 | return try Swift.withUnsafeBytes(of: self) { bufferPointer in 44 | try pointer(bufferPointer) 45 | } 46 | } 47 | 48 | public static func withUnsafeBytes( 49 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 50 | ) rethrows -> Self where Error: Swift.Error { 51 | var value = self.init() 52 | try Swift.withUnsafeMutableBytes(of: &value, body) 53 | return value 54 | } 55 | } 56 | } 57 | 58 | public extension RFCOMMSocketOption { 59 | 60 | /// L2CAP Connection Info 61 | @frozen 62 | struct LinkMode: SocketOption { 63 | 64 | @_alwaysEmitIntoClient 65 | public static var id: RFCOMMSocketOption { .connectionInfo } 66 | 67 | public var linkMode: BitMaskOptionSet 68 | 69 | public init(linkMode: BitMaskOptionSet = []) { 70 | self.linkMode = linkMode 71 | } 72 | 73 | public func withUnsafeBytes(_ pointer: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 74 | return try Swift.withUnsafeBytes(of: linkMode.rawValue) { bufferPointer in 75 | try pointer(bufferPointer) 76 | } 77 | } 78 | 79 | public static func withUnsafeBytes( 80 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 81 | ) rethrows -> Self where Error: Swift.Error { 82 | var rawValue: UInt16 = 0 83 | try Swift.withUnsafeMutableBytes(of: &rawValue, body) 84 | return self.init(linkMode: .init(rawValue: rawValue)) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/RFCOMM/IOCTL/RFCOMMCreateDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RFCOMMCreateDevice.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 26/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension RFCOMMIO { 13 | 14 | /// RFCOMM Create Device 15 | struct CreateDevice: Equatable, Hashable, IOControlValue { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var id: RFCOMMIO { .createDevice } 19 | 20 | @usableFromInline 21 | internal private(set) var bytes: CInterop.RFCOMMDeviceRequest 22 | 23 | @usableFromInline 24 | internal init(_ bytes: CInterop.RFCOMMDeviceRequest) { 25 | self.bytes = bytes 26 | } 27 | 28 | @_alwaysEmitIntoClient 29 | public init( 30 | id: HostController.ID, 31 | flags: BitMaskOptionSet, 32 | source: BluetoothAddress, 33 | destination: BluetoothAddress, 34 | channel: UInt8 = 1 35 | ) { 36 | self.init(CInterop.RFCOMMDeviceRequest( 37 | device: id.rawValue, 38 | flags: flags.rawValue, 39 | source: source, 40 | destination: destination, 41 | channel: channel) 42 | ) 43 | } 44 | 45 | @_alwaysEmitIntoClient 46 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 47 | try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in 48 | try body(buffer.baseAddress!) 49 | } 50 | } 51 | } 52 | } 53 | 54 | public extension RFCOMMIO.CreateDevice { 55 | 56 | @_alwaysEmitIntoClient 57 | var id: HostController.ID { 58 | return .init(rawValue: bytes.device) 59 | } 60 | 61 | @_alwaysEmitIntoClient 62 | var flags: BitMaskOptionSet { 63 | return .init(rawValue: bytes.flags) 64 | } 65 | 66 | @_alwaysEmitIntoClient 67 | var source: BluetoothAddress { 68 | return bytes.source 69 | } 70 | 71 | @_alwaysEmitIntoClient 72 | var destination: BluetoothAddress { 73 | return bytes.destination 74 | } 75 | 76 | @_alwaysEmitIntoClient 77 | var channel: UInt8 { 78 | return bytes.channel 79 | } 80 | } 81 | 82 | 83 | // MARK: - File Descriptor 84 | 85 | internal extension SocketDescriptor { 86 | 87 | @usableFromInline 88 | func rfcommCreateDevice( 89 | id: HostController.ID, 90 | flags: BitMaskOptionSet = [], 91 | source: BluetoothAddress, 92 | destination: BluetoothAddress, 93 | channel: UInt8 = 1 94 | ) throws { 95 | var request = RFCOMMIO.CreateDevice( 96 | id: id, 97 | flags: flags, 98 | source: source, 99 | destination: destination, 100 | channel: channel 101 | ) 102 | try inputOutput(&request) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/L2CAP/L2CAPSocketOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAPSocketOption.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | 11 | /// L2CAP Socket Options 12 | public enum L2CAPSocketOption: CInt, SocketOptionID { 13 | 14 | public static var optionLevel: SocketOptionLevel { .l2cap } 15 | 16 | /// L2CAP Socket Options 17 | case options = 0x01 18 | 19 | /// L2CAP Connection Info 20 | case connectionInfo = 0x02 21 | 22 | /// L2CAP Link Mode 23 | case linkMode = 0x03 24 | } 25 | 26 | public extension L2CAPSocketOption { 27 | 28 | struct Options: Equatable, Hashable, SocketOption, Sendable { 29 | 30 | public static var id: L2CAPSocketOption { .options } 31 | 32 | public var outputMaximumTransmissionUnit: UInt16 // omtu 33 | public var inputMaximumTransmissionUnit: UInt16 // imtu 34 | public var flushTo: UInt16 // flush_to 35 | public var mode: UInt8 36 | public var fcs: UInt8 37 | public var maxTransmission: UInt8 // max_tx 38 | public var transmissionWindowSize: UInt8 // txwin_size 39 | 40 | public init() { 41 | self.outputMaximumTransmissionUnit = 0 42 | self.inputMaximumTransmissionUnit = 0 43 | self.flushTo = 0 44 | self.mode = 0 45 | self.fcs = 0 46 | self.maxTransmission = 0 47 | self.transmissionWindowSize = 0 48 | } 49 | 50 | public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 51 | return try Swift.withUnsafeBytes(of: self) { bufferPointer in 52 | try body(bufferPointer) 53 | } 54 | } 55 | 56 | public static func withUnsafeBytes( 57 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 58 | ) rethrows -> Self where Error: Swift.Error { 59 | var value = self.init() 60 | try Swift.withUnsafeMutableBytes(of: &value, body) 61 | return value 62 | } 63 | } 64 | } 65 | 66 | public extension L2CAPSocketOption { 67 | 68 | /// L2CAP Connection Info 69 | struct ConnectionInfo: SocketOption, Sendable { 70 | 71 | public static var id: L2CAPSocketOption { .connectionInfo } 72 | 73 | public private(set) var handle: UInt16 74 | public private(set) var deviceClass: (UInt8, UInt8, UInt8) 75 | 76 | public init() { 77 | self.handle = 0 78 | self.deviceClass = (0,0,0) 79 | } 80 | 81 | public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error { 82 | return try Swift.withUnsafeBytes(of: self) { bufferPointer in 83 | try body(bufferPointer) 84 | } 85 | } 86 | 87 | public static func withUnsafeBytes( 88 | _ body: (UnsafeMutableRawBufferPointer) throws(Error) -> () 89 | ) rethrows -> Self where Error: Swift.Error { 90 | var value = self.init() 91 | try Swift.withUnsafeMutableBytes(of: &value, body) 92 | return value 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/L2CAP/L2CAPSocketAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAPSocketAddress.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import SystemPackage 9 | import Socket 10 | import Bluetooth 11 | import BluetoothHCI 12 | 13 | /// Bluetooth L2CAP Socket 14 | @frozen 15 | public struct L2CAPSocketAddress: Equatable, Hashable, BluetoothSocketAddress, Sendable { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var protocolID: BluetoothSocketProtocol { .l2cap } 19 | 20 | // MARK: - Properties 21 | 22 | /// Bluetooth address 23 | public var address: BluetoothAddress 24 | 25 | /// Bluetooth address type 26 | public var addressType: AddressType? 27 | 28 | /// Protocol Service Multiplexer 29 | public var protocolServiceMultiplexer: ProtocolServiceMultiplexer? 30 | 31 | /// Channel 32 | public var channel: ChannelIdentifier 33 | 34 | // MARK: - Initialization 35 | 36 | /// Initialize with device and channel identifiers. 37 | public init( 38 | address: BluetoothAddress, 39 | addressType: AddressType? = nil, 40 | protocolServiceMultiplexer: ProtocolServiceMultiplexer? = nil, 41 | channel: ChannelIdentifier 42 | ) { 43 | self.address = address 44 | self.addressType = addressType 45 | self.protocolServiceMultiplexer = protocolServiceMultiplexer 46 | self.channel = channel 47 | } 48 | 49 | public static var none: L2CAPSocketAddress { 50 | return L2CAPSocketAddress( 51 | address: .zero, 52 | addressType: nil, 53 | protocolServiceMultiplexer: nil, 54 | channel: 0 55 | ) 56 | } 57 | 58 | @_alwaysEmitIntoClient 59 | public init( 60 | lowEnergy address: BluetoothAddress, 61 | isRandom: Bool 62 | ) { 63 | self.init( 64 | address: address, 65 | addressType: isRandom ? .lowEnergyRandom : .lowEnergyPublic, 66 | protocolServiceMultiplexer: nil, 67 | channel: .att 68 | ) 69 | } 70 | 71 | internal init(_ address: CInterop.L2CAPSocketAddress) { 72 | self.init( 73 | address: .init(littleEndian: address.l2_bdaddr), 74 | addressType: .init(rawValue: address.l2_bdaddr_type), 75 | protocolServiceMultiplexer: .init(rawValue: UInt8(UInt16(littleEndian: address.l2_psm))), 76 | channel: .init(rawValue: .init(littleEndian: address.l2_cid)) 77 | ) 78 | } 79 | 80 | public func withUnsafePointer( 81 | _ body: (UnsafePointer, UInt32) throws(Error) -> Result 82 | ) rethrows -> Result where Error: Swift.Error { 83 | var value = CInterop.L2CAPSocketAddress() 84 | value.l2_bdaddr = address.littleEndian 85 | // FIXME: PSM enum should be UInt16 not UInt8 86 | value.l2_psm = UInt16(protocolServiceMultiplexer?.rawValue ?? 0).littleEndian 87 | value.l2_cid = channel.rawValue.littleEndian 88 | value.l2_bdaddr_type = addressType?.rawValue ?? 0 89 | return try value.withUnsafePointer(body) 90 | } 91 | 92 | public static func withUnsafePointer( 93 | _ body: (UnsafeMutablePointer, UInt32) throws(Error) -> () 94 | ) rethrows -> Self where Error: Swift.Error { 95 | var value = CInterop.L2CAPSocketAddress() 96 | try value.withUnsafeMutablePointer(body) 97 | return Self.init(value) 98 | } 99 | 100 | public static func withUnsafePointer( 101 | _ pointer: UnsafeMutablePointer 102 | ) -> Self { 103 | return pointer.withMemoryRebound(to: CInterop.L2CAPSocketAddress.self, capacity: 1) { pointer in 104 | Self.init(pointer.pointee) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/BluetoothLinuxTests/BluetoothLinuxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothLinuxTests.swift 3 | // BluetoothLinuxTests 4 | // 5 | // Created by Alsey Coleman Miller on 4/18/19. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import Bluetooth 11 | import BluetoothHCI 12 | import CBluetoothLinux 13 | import CBluetoothLinuxTest 14 | @testable import BluetoothLinux 15 | 16 | final class BluetoothLinuxTests: XCTestCase { 17 | 18 | func testHCISetBit() { 19 | 20 | let event = HCIGeneralEvent.commandStatus.rawValue 21 | let bit = (CInt(event) & HCI_FLT_EVENT_BITS) 22 | var cDestination: UInt32 = 0 23 | CBluetoothLinuxTest.hci_set_bit(bit, &cDestination) 24 | var swiftDestination: UInt32 = 0 25 | CBluetoothLinux.HCISetBit(bit, &swiftDestination) 26 | 27 | XCTAssert(cDestination == swiftDestination, "\(cDestination) == \(swiftDestination)") 28 | XCTAssert(swiftDestination == 32768, "\(swiftDestination)") 29 | XCTAssert(cDestination == 32768, "\(cDestination)") 30 | } 31 | 32 | func testHCIFilterSetPacketType() { 33 | 34 | var swiftFilter = HCISocketOption.Filter() 35 | swiftFilter.setPacketType(.event) 36 | 37 | var cFilter = hci_filter() 38 | hci_filter_set_ptype(HCI_EVENT_PKT, &cFilter) 39 | 40 | XCTAssert(swiftFilter.typeMask == cFilter.type_mask, "\(swiftFilter.typeMask) == \(cFilter.type_mask)") 41 | XCTAssert(cFilter.type_mask == 16) 42 | } 43 | 44 | func testHCIFilterSetEvent() { 45 | 46 | let event = HCIGeneralEvent.commandComplete 47 | 48 | var swiftFilter = HCISocketOption.Filter() 49 | 50 | swiftFilter.setEvent(HCIGeneralEvent.commandStatus) 51 | swiftFilter.setEvent(HCIGeneralEvent.commandComplete) 52 | swiftFilter.setEvent(HCIGeneralEvent.lowEnergyMeta) 53 | swiftFilter.setEvent(event) 54 | 55 | var cFilter = hci_filter() 56 | hci_filter_set_event(CInt(HCIGeneralEvent.commandStatus.rawValue), &cFilter) 57 | hci_filter_set_event(CInt(HCIGeneralEvent.commandComplete.rawValue), &cFilter) 58 | hci_filter_set_event(CInt(HCIGeneralEvent.lowEnergyMeta.rawValue), &cFilter) 59 | hci_filter_set_event(CInt(event.rawValue), &cFilter) 60 | 61 | XCTAssert(swiftFilter.bytes.eventMask.0 == cFilter.event_mask.0 && swiftFilter.bytes.eventMask.1 == cFilter.event_mask.1, 62 | "\(swiftFilter.bytes.eventMask) == \(cFilter.event_mask)") 63 | 64 | swiftFilter = .init() 65 | swiftFilter.bytes.setEvent(HCIGeneralEvent.commandStatus.rawValue, 66 | HCIGeneralEvent.commandComplete.rawValue, 67 | HCIGeneralEvent.lowEnergyMeta.rawValue, 68 | event.rawValue) 69 | 70 | //XCTAssert(swiftFilter.eventMask.0 == cFilter.event_mask.0 && swiftFilter.eventMask.1 == cFilter.event_mask.1, "\(swiftFilter.eventMask) == \(cFilter.event_mask)") 71 | } 72 | 73 | #if os(Linux) 74 | func testIOCTLConstants() { 75 | 76 | let swiftDefinitionList: [HostControllerIO] = [ 77 | .deviceUp, 78 | .deviceDown, 79 | .deviceReset, 80 | .deviceRestat, 81 | .getDeviceList, 82 | .getDeviceInfo, 83 | .getConnectionList, 84 | .getConnectionInfo, 85 | .getAuthenticationInfo 86 | ] 87 | 88 | XCTAssertEqual(swiftDefinitionList.count, 9) 89 | withUnsafePointer(to: hci_ioctl_list) { 90 | $0.withMemoryRebound(to: Int32.self, capacity: swiftDefinitionList.count) { (cListPointer) in 91 | for (index, swiftDefinition) in swiftDefinitionList.enumerated() { 92 | let cDefinition = CUnsignedLong(bitPattern: CLong(cListPointer[index])) 93 | guard swiftDefinition.rawValue == cDefinition else { 94 | XCTFail("\(swiftDefinition) == \(cDefinition) at definition \(index + 1)") 95 | return 96 | } 97 | } 98 | } 99 | } 100 | } 101 | #endif 102 | } 103 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HCIDeviceList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceList.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import SystemPackage 10 | import Socket 11 | 12 | public extension HostControllerIO { 13 | 14 | /// HCI Device List 15 | struct DeviceList: IOControlValue { 16 | 17 | @_alwaysEmitIntoClient 18 | public static var id: HostControllerIO { .getDeviceList } 19 | 20 | @usableFromInline 21 | internal private(set) var bytes: CInterop.HCIDeviceList 22 | 23 | @usableFromInline 24 | internal init(_ bytes: CInterop.HCIDeviceList) { 25 | self.bytes = bytes 26 | } 27 | 28 | public init(request count: Int = CInterop.HCIDeviceList.capacity) { 29 | self.init(.request(count: numericCast(count))) 30 | } 31 | 32 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 33 | try Swift.withUnsafeMutableBytes(of: &bytes) { buffer in 34 | try body(buffer.baseAddress!) 35 | } 36 | } 37 | } 38 | } 39 | 40 | // MARK: - CustomStringConvertible 41 | 42 | extension HostControllerIO.DeviceList: CustomStringConvertible, CustomDebugStringConvertible { 43 | 44 | public var description: String { 45 | return _buildDescription() 46 | } 47 | 48 | public var debugDescription: String { 49 | return description 50 | } 51 | } 52 | 53 | // MARK: - RandomAccessCollection 54 | 55 | extension HostControllerIO.DeviceList: RandomAccessCollection { 56 | 57 | public func makeIterator() -> IndexingIterator { 58 | return IndexingIterator(_elements: self) 59 | } 60 | 61 | public subscript (index: Int) -> Element { 62 | return Element(bytes[index]) 63 | } 64 | 65 | public var count: Int { 66 | return bytes.count 67 | } 68 | 69 | /// The start `Index`. 70 | public var startIndex: Int { 71 | return 0 72 | } 73 | 74 | /// The end `Index`. 75 | /// 76 | /// This is the "one-past-the-end" position, and will always be equal to the `count`. 77 | public var endIndex: Int { 78 | return count 79 | } 80 | 81 | public func index(before i: Int) -> Int { 82 | return i - 1 83 | } 84 | 85 | public func index(after i: Int) -> Int { 86 | return i + 1 87 | } 88 | 89 | public subscript(bounds: Range) -> Slice { 90 | return Slice(base: self, bounds: bounds) 91 | } 92 | } 93 | 94 | // MARK: - Supporting Types 95 | 96 | public extension HostControllerIO.DeviceList { 97 | 98 | /// HCI Device 99 | struct Element: Equatable, Hashable { 100 | 101 | public let id: HostController.ID 102 | 103 | public let options: HCIDeviceOptions 104 | 105 | @usableFromInline 106 | internal init(_ bytes: CInterop.HCIDeviceList.Element) { 107 | self.id = .init(rawValue: bytes.id) 108 | self.options = .init(rawValue: bytes.options) 109 | } 110 | } 111 | } 112 | 113 | // MARK: - File Descriptor 114 | 115 | internal extension SocketDescriptor { 116 | 117 | /// List all HCI devices. 118 | @usableFromInline 119 | func deviceList(count: Int = CInterop.HCIDeviceList.capacity) throws(Errno) -> HostControllerIO.DeviceList { 120 | var deviceList = HostControllerIO.DeviceList(request: count) 121 | try inputOutput(&deviceList) 122 | return deviceList 123 | } 124 | } 125 | 126 | // MARK: - Host Controller 127 | 128 | public extension HostController { 129 | 130 | /// Get device information. 131 | static func deviceList(count: Int = CInterop.HCIDeviceList.capacity) throws(Errno) -> HostControllerIO.DeviceList { 132 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 133 | return try fileDescriptor.closeAfter { () throws(Errno) -> HostControllerIO.DeviceList in 134 | try fileDescriptor.deviceList(count: count) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/DeviceRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceRequest.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 1/3/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Bluetooth 11 | import BluetoothHCI 12 | 13 | public extension HostController { 14 | 15 | /// Send an HCI command with parameters to the controller and waits for a response. 16 | func deviceRequest ( 17 | _ commandParameter: CP, 18 | _ eventParameterType: EP.Type, 19 | timeout: HCICommandTimeout = .default 20 | ) async throws -> EP { 21 | 22 | let command = CP.command 23 | let parameterData = commandParameter.data 24 | let responseData = try await socket.sendRequest( 25 | command: command, 26 | commandParameterData: parameterData, 27 | event: EP.event.rawValue, 28 | eventParameterLength: EP.length, 29 | timeout: timeout 30 | ) 31 | guard let eventParameter = EP(data: responseData) 32 | else { throw BluetoothHostControllerError.garbageResponse(responseData) } 33 | 34 | return eventParameter 35 | } 36 | 37 | /// Send an HCI command to the controller and waits for a response. 38 | func deviceRequest( 39 | _ command: C, 40 | _ eventParameterType: EP.Type, 41 | timeout: HCICommandTimeout = .default 42 | ) async throws -> EP where C : HCICommand, EP : HCIEventParameter { 43 | 44 | let data = try await socket.sendRequest( 45 | command: command, 46 | event: EP.event.rawValue, 47 | eventParameterLength: EP.length, 48 | timeout: timeout 49 | ) 50 | 51 | guard let eventParameter = EP(data: data) 52 | else { throw BluetoothHostControllerError.garbageResponse(data) } 53 | 54 | return eventParameter 55 | } 56 | 57 | /// Send a command to the controller and wait for response. 58 | func deviceRequest( 59 | _ command: C, 60 | timeout: HCICommandTimeout = .default 61 | ) async throws { 62 | 63 | let data = try await socket.sendRequest( 64 | command: command, 65 | eventParameterLength: 1, 66 | timeout: timeout 67 | ) 68 | 69 | guard let statusByte = data.first 70 | else { fatalError("Missing status byte!") } 71 | 72 | guard statusByte == 0x00 73 | else { throw HCIError(rawValue: statusByte)! } 74 | 75 | } 76 | 77 | func deviceRequest( 78 | _ commandParameter: CP, 79 | timeout: HCICommandTimeout = .default 80 | ) async throws { 81 | 82 | let data = try await socket.sendRequest( 83 | command: CP.command, 84 | commandParameterData: commandParameter.data, 85 | eventParameterLength: 1, 86 | timeout: timeout 87 | ) 88 | 89 | guard let statusByte = data.first 90 | else { fatalError("Missing status byte!") } 91 | 92 | guard statusByte == 0x00 93 | else { throw HCIError(rawValue: statusByte)! } 94 | } 95 | 96 | func deviceRequest ( 97 | _ commandReturnType : Return.Type, 98 | timeout: HCICommandTimeout = .default 99 | ) async throws -> Return { 100 | 101 | let data = try await socket.sendRequest( 102 | command: commandReturnType.command, 103 | eventParameterLength: commandReturnType.length + 1, // status code + parameters 104 | timeout: timeout 105 | ) 106 | 107 | guard let statusByte = data.first 108 | else { fatalError("Missing status byte!") } 109 | 110 | guard statusByte == 0x00 111 | else { throw HCIError(rawValue: statusByte)! } 112 | 113 | guard let response = Return(data: Data(data.suffix(from: 1))) 114 | else { throw BluetoothHostControllerError.garbageResponse(Data(data)) } 115 | 116 | return response 117 | } 118 | 119 | /// Sends a command to the device and waits for a response with return parameter values. 120 | func deviceRequest ( 121 | _ commandParameter: CP, 122 | _ commandReturnType : Return.Type, 123 | timeout: HCICommandTimeout = .default 124 | ) async throws -> Return { 125 | 126 | assert(CP.command.opcode == Return.command.opcode) 127 | 128 | let data = try await socket.sendRequest( 129 | command: commandReturnType.command, 130 | commandParameterData: commandParameter.data, 131 | eventParameterLength: commandReturnType.length + 1, 132 | timeout: timeout 133 | ) 134 | 135 | guard let statusByte = data.first 136 | else { fatalError("Missing status byte!") } 137 | 138 | guard statusByte == 0x00 139 | else { throw HCIError(rawValue: statusByte)! } 140 | 141 | guard let response = Return(data: Data(data.suffix(from: 1))) 142 | else { throw BluetoothHostControllerError.garbageResponse(Data(data)) } 143 | 144 | return response 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/BluetoothLinuxTests/L2CAPTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAPTests.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 4/10/23. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import Bluetooth 11 | import BluetoothHCI 12 | import BluetoothGATT 13 | import BluetoothGAP 14 | import SystemPackage 15 | @testable import BluetoothLinux 16 | 17 | final class L2CAPTests: XCTestCase { 18 | 19 | func testServerConnection() async throws { 20 | guard ProcessInfo.processInfo.environment["SWIFT_BLUETOOTH_HARDWARE_TEST_SERVER"] != nil else { 21 | return 22 | } 23 | guard let controller = await BluetoothLinux.HostController.default else { 24 | XCTFail() 25 | return 26 | } 27 | do { 28 | do { try await controller.enableLowEnergyAdvertising(false) } 29 | catch HCIError.commandDisallowed { /* ignore */ } 30 | 31 | let encoder = GAPDataEncoder.self 32 | let advertisingData = encoder.encode( 33 | GAPShortLocalName(name: "Test") 34 | ) 35 | try await controller.setLowEnergyAdvertisingData(advertisingData) 36 | 37 | do { try await controller.enableLowEnergyAdvertising() } 38 | catch HCIError.commandDisallowed { /* ignore */ } 39 | } 40 | catch { 41 | NSLog("Unable to enable advertising. \(error)") 42 | } 43 | NSLog("Enabled advertising") 44 | let address = try await controller.readDeviceAddress() 45 | NSLog("Will create server socket \(address)") 46 | let serverSocket = try BluetoothLinux.L2CAPSocket.Server.lowEnergyServer( 47 | address: address 48 | ) 49 | NSLog("Created server socket") 50 | while serverSocket.status.accept == false { 51 | try await Task.sleep(nanoseconds: 10_000) 52 | if let error = serverSocket.status.error { 53 | throw error 54 | } 55 | } 56 | let newConnection = try serverSocket.accept() 57 | NSLog("Server Connected") 58 | let service = GATTAttribute.Service( 59 | uuid: .deviceInformation, 60 | isPrimary: true, 61 | characteristics: [ 62 | GATTAttribute.Characteristic( 63 | uuid: GATTManufacturerNameString.uuid, 64 | value: GATTManufacturerNameString(rawValue: "PureSwift").data, 65 | permissions: [.read], 66 | properties: [.read], 67 | descriptors: [] 68 | ), 69 | GATTAttribute.Characteristic( 70 | uuid: GATTModelNumber.uuid, 71 | value: GATTModelNumber(rawValue: "SolarInverter1,1").data, 72 | permissions: [.read], 73 | properties: [.read], 74 | descriptors: [] 75 | ), 76 | GATTAttribute.Characteristic( 77 | uuid: GATTHardwareRevisionString.uuid, 78 | value: GATTHardwareRevisionString(rawValue: "1.0.0").data, 79 | permissions: [.read], 80 | properties: [.read], 81 | descriptors: [] 82 | ), 83 | GATTAttribute.Characteristic( 84 | uuid: GATTFirmwareRevisionString.uuid, 85 | value: GATTFirmwareRevisionString(rawValue: "1.0.1").data, 86 | permissions: [.read], 87 | properties: [.read], 88 | descriptors: [] 89 | ), 90 | ] 91 | ) 92 | let service2 = GATTAttribute.Service( 93 | uuid: .savantSystems, 94 | isPrimary: true, 95 | characteristics: [ 96 | GATTAttribute.Characteristic( 97 | uuid: .savantSystems2, 98 | value: GATTManufacturerNameString(rawValue: "PureSwift").data, 99 | permissions: [.read, .write], 100 | properties: [.read, .write], 101 | descriptors: [] 102 | ) 103 | ] 104 | ) 105 | let batteryService = GATTAttribute.Service( 106 | uuid: .batteryService, 107 | isPrimary: true, 108 | characteristics: [ 109 | GATTAttribute.Characteristic( 110 | uuid: .batteryService, 111 | value: GATTBatteryLevel(level: .init(rawValue: 95)!).data, 112 | permissions: [.read], 113 | properties: [.read], 114 | descriptors: [] 115 | ) 116 | ] 117 | ) 118 | let database = GATTDatabase(services: [service, service2, batteryService]) 119 | var logs = [String]() 120 | let server = GATTServer( 121 | socket: newConnection, 122 | maximumTransmissionUnit: .max, 123 | maximumPreparedWrites: 1000, 124 | database: database, 125 | log: { 126 | NSLog("Server: \($0)") 127 | logs.append($0) 128 | } 129 | ) 130 | try await Task.sleep(nanoseconds: 100_000) 131 | do { 132 | while true { 133 | try server.run() 134 | } 135 | } 136 | catch { 137 | print(error) 138 | } 139 | withExtendedLifetime(server) { 140 | _ = $0 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HostControllerIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostControllerIO.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Socket 9 | 10 | /// Bluetooth HCI `ioctl` requests 11 | @frozen 12 | public struct HostControllerIO: RawRepresentable, Hashable, Codable, IOControlID { 13 | 14 | /// The raw C value. 15 | public let rawValue: UInt 16 | 17 | /// Creates a strongly-typed file handle from a raw C value. 18 | public init?(rawValue: UInt) { 19 | let value = HostControllerIO(rawValue) 20 | guard Self._allCases.keys.contains(value) 21 | else { return nil } 22 | self = value 23 | } 24 | 25 | @_alwaysEmitIntoClient 26 | private init(_ raw: UInt) { 27 | self.rawValue = raw 28 | } 29 | } 30 | 31 | extension HostControllerIO: CaseIterable { 32 | 33 | public static var allCases: [HostControllerIO] { 34 | return [HostControllerIO](_allCases.keys) 35 | } 36 | } 37 | 38 | public extension HostControllerIO { 39 | 40 | @_alwaysEmitIntoClient 41 | static var deviceUp: HostControllerIO { IOW("H", 201, CInt.self) } 42 | 43 | @_alwaysEmitIntoClient 44 | static var deviceDown: HostControllerIO { IOW("H", 202, CInt.self)} 45 | 46 | @_alwaysEmitIntoClient 47 | static var deviceReset: HostControllerIO { IOW("H", 203, CInt.self) } 48 | 49 | @_alwaysEmitIntoClient 50 | static var deviceRestat: HostControllerIO { IOW("H", 204, CInt.self) } 51 | 52 | @_alwaysEmitIntoClient 53 | static var getDeviceList: HostControllerIO { IOR("H", 210, CInt.self) } 54 | 55 | @_alwaysEmitIntoClient 56 | static var getDeviceInfo: HostControllerIO { IOR("H", 211, CInt.self) } 57 | 58 | @_alwaysEmitIntoClient 59 | static var getConnectionList: HostControllerIO { IOR("H", 212, CInt.self) } 60 | 61 | @_alwaysEmitIntoClient 62 | static var getConnectionInfo: HostControllerIO { IOR("H", 213, CInt.self) } 63 | 64 | @_alwaysEmitIntoClient 65 | static var getAuthenticationInfo: HostControllerIO { IOR("H", 215, CInt.self) } 66 | 67 | @_alwaysEmitIntoClient 68 | static var setRaw: HostControllerIO { IOW("H", 220, CInt.self) } 69 | 70 | @_alwaysEmitIntoClient 71 | static var setScan: HostControllerIO { IOW("H", 221, CInt.self) } 72 | 73 | @_alwaysEmitIntoClient 74 | static var setAuthentication: HostControllerIO { IOW("H", 222, CInt.self) } 75 | 76 | @_alwaysEmitIntoClient 77 | static var setEncrypt: HostControllerIO { IOW("H", 223, CInt.self) } 78 | 79 | @_alwaysEmitIntoClient 80 | static var setPacketType: HostControllerIO { IOW("H", 224, CInt.self) } 81 | 82 | @_alwaysEmitIntoClient 83 | static var setLinkPolicy: HostControllerIO { IOW("H", 225, CInt.self) } 84 | 85 | @_alwaysEmitIntoClient 86 | static var setLinkMode: HostControllerIO { IOW("H", 226, CInt.self) } 87 | 88 | @_alwaysEmitIntoClient 89 | static var setACLMTU: HostControllerIO { IOW("H", 227, CInt.self) } 90 | 91 | @_alwaysEmitIntoClient 92 | static var setSCOMTU: HostControllerIO { IOW("H", 228, CInt.self) } 93 | 94 | @_alwaysEmitIntoClient 95 | static var blockAddress: HostControllerIO { IOW("H", 230, CInt.self) } 96 | 97 | @_alwaysEmitIntoClient 98 | static var unblockAddress: HostControllerIO { IOW("H", 231, CInt.self) } 99 | 100 | @_alwaysEmitIntoClient 101 | static var inquiry: HostControllerIO { IOR("H", 240, CInt.self) } 102 | } 103 | 104 | extension HostControllerIO: CustomStringConvertible, CustomDebugStringConvertible { 105 | 106 | @_alwaysEmitIntoClient 107 | internal static var _allCases: [HostControllerIO: String] { 108 | return [ 109 | .deviceUp: ".deviceUp", 110 | .deviceDown: ".deviceDown", 111 | .deviceReset: ".deviceReset", 112 | .deviceRestat: ".deviceRestat", 113 | .getDeviceList: ".getDeviceList", 114 | .getDeviceInfo: ".getDeviceInfo", 115 | .getConnectionList: ".getConnectionList", 116 | .getConnectionInfo: ".getConnectionInfo", 117 | .getAuthenticationInfo: ".getAuthenticationInfo", 118 | .setRaw: ".setRaw", 119 | .setScan: ".setScan", 120 | .setAuthentication: ".setAuthentication", 121 | .setEncrypt: ".setEncrypt", 122 | .setPacketType: ".setPacketType", 123 | .setLinkPolicy: ".setLinkPolicy", 124 | .setLinkMode: ".setLinkMode", 125 | .setACLMTU: ".setACLMTU", 126 | .setSCOMTU: ".setSCOMTU", 127 | .blockAddress: ".blockAddress", 128 | .unblockAddress: ".unblockAddress", 129 | .inquiry: ".inquiry", 130 | ] 131 | } 132 | 133 | public var description: String { 134 | return Self._allCases[self] ?? rawValue.description 135 | } 136 | 137 | public var debugDescription: String { 138 | return description 139 | } 140 | } 141 | 142 | internal extension HostControllerIO { 143 | 144 | @_alwaysEmitIntoClient 145 | static func IOW(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> HostControllerIO { 146 | return HostControllerIO(_IOW(type, nr, size)) 147 | } 148 | 149 | @_alwaysEmitIntoClient 150 | static func IOR(_ type: IOCType, _ nr: CInt, _ size: T.Type) -> HostControllerIO { 151 | return HostControllerIO(_IOR(type, nr, size)) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/node-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] 11 | 12 | export NVM_DIR=${1:-"/usr/local/share/nvm"} 13 | export NODE_VERSION=${2:-"lts"} 14 | USERNAME=${3:-"automatic"} 15 | UPDATE_RC=${4:-"true"} 16 | INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" 17 | export NVM_VERSION="0.38.0" 18 | 19 | set -e 20 | 21 | if [ "$(id -u)" -ne 0 ]; then 22 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 23 | exit 1 24 | fi 25 | 26 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 27 | rm -f /etc/profile.d/00-restore-env.sh 28 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 29 | chmod +x /etc/profile.d/00-restore-env.sh 30 | 31 | # Determine the appropriate non-root user 32 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 33 | USERNAME="" 34 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 35 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 36 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 37 | USERNAME=${CURRENT_USER} 38 | break 39 | fi 40 | done 41 | if [ "${USERNAME}" = "" ]; then 42 | USERNAME=root 43 | fi 44 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 45 | USERNAME=root 46 | fi 47 | 48 | updaterc() { 49 | if [ "${UPDATE_RC}" = "true" ]; then 50 | echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." 51 | if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then 52 | echo -e "$1" >> /etc/bash.bashrc 53 | fi 54 | if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then 55 | echo -e "$1" >> /etc/zsh/zshrc 56 | fi 57 | fi 58 | } 59 | 60 | # Function to run apt-get if needed 61 | apt_get_update_if_needed() 62 | { 63 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 64 | echo "Running apt-get update..." 65 | apt-get update 66 | else 67 | echo "Skipping apt-get update." 68 | fi 69 | } 70 | 71 | # Checks if packages are installed and installs them if not 72 | check_packages() { 73 | if ! dpkg -s "$@" > /dev/null 2>&1; then 74 | apt_get_update_if_needed 75 | apt-get -y install --no-install-recommends "$@" 76 | fi 77 | } 78 | 79 | # Ensure apt is in non-interactive to avoid prompts 80 | export DEBIAN_FRONTEND=noninteractive 81 | 82 | # Install dependencies 83 | check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr 84 | 85 | # Install yarn 86 | if type yarn > /dev/null 2>&1; then 87 | echo "Yarn already installed." 88 | else 89 | # Import key safely (new method rather than deprecated apt-key approach) and install 90 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg 91 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list 92 | apt-get update 93 | apt-get -y install --no-install-recommends yarn 94 | fi 95 | 96 | # Adjust node version if required 97 | if [ "${NODE_VERSION}" = "none" ]; then 98 | export NODE_VERSION= 99 | elif [ "${NODE_VERSION}" = "lts" ]; then 100 | export NODE_VERSION="lts/*" 101 | fi 102 | 103 | # Create a symlink to the installed version for use in Dockerfile PATH statements 104 | export NVM_SYMLINK_CURRENT=true 105 | 106 | # Install the specified node version if NVM directory already exists, then exit 107 | if [ -d "${NVM_DIR}" ]; then 108 | echo "NVM already installed." 109 | if [ "${NODE_VERSION}" != "" ]; then 110 | su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" 111 | fi 112 | exit 0 113 | fi 114 | 115 | # Create nvm group, nvm dir, and set sticky bit 116 | if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then 117 | groupadd -r nvm 118 | fi 119 | umask 0002 120 | usermod -a -G nvm ${USERNAME} 121 | mkdir -p ${NVM_DIR} 122 | chown :nvm ${NVM_DIR} 123 | chmod g+s ${NVM_DIR} 124 | su ${USERNAME} -c "$(cat << EOF 125 | set -e 126 | umask 0002 127 | # Do not update profile - we'll do this manually 128 | export PROFILE=/dev/null 129 | ls -lah /home/${USERNAME}/.nvs || : 130 | curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash 131 | source ${NVM_DIR}/nvm.sh 132 | if [ "${NODE_VERSION}" != "" ]; then 133 | nvm alias default ${NODE_VERSION} 134 | fi 135 | nvm clear-cache 136 | EOF 137 | )" 2>&1 138 | # Update rc files 139 | if [ "${UPDATE_RC}" = "true" ]; then 140 | updaterc "$(cat < /dev/null 2>&1; then 153 | to_install="${to_install} make" 154 | fi 155 | if ! type gcc > /dev/null 2>&1; then 156 | to_install="${to_install} gcc" 157 | fi 158 | if ! type g++ > /dev/null 2>&1; then 159 | to_install="${to_install} g++" 160 | fi 161 | if ! type python3 > /dev/null 2>&1; then 162 | to_install="${to_install} python3-minimal" 163 | fi 164 | if [ ! -z "${to_install}" ]; then 165 | apt_get_update_if_needed 166 | apt-get -y install ${to_install} 167 | fi 168 | fi 169 | 170 | echo "Done!" 171 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HostController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostController.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 12/6/15. 6 | // Copyright © 2015 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @_implementationOnly import CBluetoothLinux 11 | import BluetoothHCI 12 | import SystemPackage 13 | import Socket 14 | 15 | /// Manages connection / communication to the underlying Bluetooth hardware. 16 | public final class HostController: BluetoothHostControllerInterface { 17 | 18 | // MARK: - Properties 19 | 20 | /// The device identifier of the Bluetooth controller. 21 | public let id: ID 22 | 23 | /// HCI device name. 24 | public let name: String 25 | 26 | /// Bluetooth device address. 27 | public let address: BluetoothAddress 28 | 29 | /// HCI Controller type. 30 | public let type: HCIControllerType 31 | 32 | /// Bus type. 33 | public let busType: HCIBusType 34 | 35 | /// Internal file descriptor for HCI socket 36 | @usableFromInline 37 | internal let socket: Socket 38 | 39 | // MARK: - Initizalization 40 | 41 | deinit { 42 | let socket = self.socket 43 | Task(priority: .high) { 44 | await socket.close() 45 | } 46 | } 47 | 48 | /// Initialize and open socket. 49 | private init( 50 | _ device: HostControllerIO.DeviceInformation 51 | ) async throws { 52 | let socketAddress = HCISocketAddress( 53 | device: device.id, 54 | channel: .raw 55 | ) 56 | let fileDescriptor = try SocketDescriptor.hci(socketAddress, flags: [.closeOnExec, .nonBlocking]) 57 | self.id = device.id 58 | self.name = device.name 59 | self.address = device.address 60 | self.busType = device.busType 61 | self.type = device.type 62 | self.socket = await Socket(fileDescriptor: fileDescriptor) 63 | } 64 | 65 | /// Attempt to initialize an Bluetooth controller 66 | public convenience init(id: ID) async throws { 67 | let deviceInfo = try Self.deviceInformation(for: id) 68 | try await self.init(deviceInfo) 69 | } 70 | 71 | /// Initializes the Bluetooth controller with the specified address. 72 | public convenience init(address: BluetoothAddress) async throws { 73 | // open socket to query devices with ioctl()` 74 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 75 | let deviceInfo = try fileDescriptor.closeAfter { () throws(Errno) -> HostControllerIO.DeviceInformation in 76 | let deviceList = try fileDescriptor.deviceList() 77 | for device in deviceList { 78 | let deviceInfo = try fileDescriptor.deviceInformation(for: device.id) 79 | guard deviceInfo.address == address else { 80 | continue 81 | } 82 | return deviceInfo 83 | } 84 | throw Errno.noSuchAddressOrDevice 85 | } 86 | // initialize with new file descriptor 87 | try await self.init(deviceInfo) 88 | } 89 | } 90 | 91 | // MARK: - CustomStringConvertible 92 | 93 | extension HostController: CustomStringConvertible, CustomDebugStringConvertible { 94 | 95 | public var description: String { 96 | "HostController(id: \(id), name: \(name), address: \(address))" 97 | } 98 | 99 | public var debugDescription: String { 100 | "HostController(id: \(id), name: \(name), address: \(address), socket: \(socket.fileDescriptor.rawValue)" 101 | } 102 | } 103 | 104 | // MARK: - Controllers 105 | 106 | internal extension HostController { 107 | 108 | static nonisolated(unsafe) private(set) var cachedControllers = [ID: HostController]() 109 | 110 | @usableFromInline 111 | static func loadDevices() throws -> HostControllerIO.DeviceList { 112 | let fileDescriptor = try SocketDescriptor.bluetooth(.hci, flags: [.closeOnExec]) 113 | defer { try? fileDescriptor.close() } 114 | return try fileDescriptor.deviceList() 115 | } 116 | 117 | @discardableResult 118 | static func reloadControllers() async throws -> [HostController] { 119 | let cachedDevices = cachedControllers.keys 120 | // load current devices 121 | let devices = try loadDevices() 122 | .lazy 123 | .map { $0.id } 124 | // initialize new controllers 125 | let newDevices = devices 126 | .filter { cachedDevices.contains($0) == false } 127 | for id in newDevices { 128 | do { 129 | let hostController = try await HostController(id: id) 130 | cachedControllers[id] = hostController 131 | } 132 | catch { 133 | assertionFailure("Unable to load Bluetooth HCI device \(id.rawValue). \(error)") 134 | continue 135 | } 136 | } 137 | // remove invalid controllers 138 | let oldDevices = cachedDevices 139 | .filter { devices.contains($0) == false } 140 | for id in oldDevices { 141 | cachedControllers[id] = nil 142 | } 143 | // return sorted array 144 | return cachedControllers 145 | .values 146 | .sorted { $0.id.rawValue < $1.id.rawValue } 147 | } 148 | } 149 | 150 | public extension HostController { 151 | 152 | static var controllers: [HostController] { 153 | get async { 154 | do { 155 | return try await reloadControllers() 156 | } 157 | catch { 158 | assertionFailure("Unable to fetch Bluetooth HCI devices. \(error)") 159 | return [] 160 | } 161 | } 162 | } 163 | } 164 | 165 | // MARK: - Supporting Types 166 | 167 | public extension HostController { 168 | 169 | typealias Error = BluetoothHostControllerError 170 | } 171 | 172 | public extension HostController { 173 | 174 | @frozen 175 | struct ID: RawRepresentable, Equatable, Hashable, Codable { 176 | 177 | public let rawValue: UInt16 178 | 179 | @_alwaysEmitIntoClient 180 | public init(rawValue: UInt16) { 181 | self.rawValue = rawValue 182 | } 183 | } 184 | } 185 | 186 | extension HostController.ID: CustomStringConvertible, CustomDebugStringConvertible { 187 | 188 | public var description: String { 189 | rawValue.description 190 | } 191 | 192 | public var debugDescription: String { 193 | description 194 | } 195 | } 196 | 197 | public extension HostController.ID { 198 | 199 | @_alwaysEmitIntoClient 200 | static var none: HostController.ID { 201 | return .init(rawValue: 0xffff) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/IOCTL/HCIScan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scan.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 12/6/15. 6 | // Copyright © 2015 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BluetoothHCI 11 | @_implementationOnly import CBluetoothLinux 12 | import SystemPackage 13 | import Socket 14 | 15 | // MARK: - Methods 16 | 17 | public extension HostController { 18 | 19 | /// Scans for nearby Classic Bluetooth devices. 20 | /// 21 | /// - Parameter duration: The duration of the scan. The actual duration lasts for at most 1.28 * ``duration`` seconds 22 | /// 23 | /// - Parameter scanLimit: The maximum amount of devices to scan. 24 | /// 25 | /// - Parameter deviceClass: Device class to filter results by. 26 | /// 27 | /// - Parameter options: Array of ```ScanOption```. 28 | func scan(duration: Int = 8, 29 | limit: Int = 255, 30 | deviceClass: (UInt8, UInt8, UInt8)? = nil, 31 | options: BitMaskOptionSet = []) throws -> [InquiryResult] { 32 | 33 | assert(duration > 0, "Scan must be longer than 0 seconds") 34 | assert(limit > 0, "Must scan at least one device") 35 | assert(limit <= 255, "Cannot be larger than UInt8.max") 36 | 37 | return try socket.fileDescriptor.inquiry( 38 | device: id, 39 | duration: UInt8(duration), 40 | limit: UInt8(limit), 41 | deviceClass: deviceClass, 42 | options: options 43 | ) 44 | } 45 | 46 | /* 47 | /// Requests the remote device for its user-friendly name. 48 | func requestDeviceName(deviceAddress: Address, timeout: Int = 0) throws -> String? { 49 | 50 | let maxNameLength = 248 51 | 52 | var address = bdaddr_t() 53 | 54 | let nameBuffer = UnsafeMutablePointer.init(allocatingCapacity: maxNameLength) 55 | defer { nameBuffer.deallocateCapacity(maxNameLength) } 56 | 57 | guard hci_read_remote_name(internalSocket, &address, CInt(maxNameLength), nameBuffer, CInt(timeout)) == CInt(0) 58 | else { throw POSIXError.fromErrno() } 59 | 60 | let name = String.fromCString(nameBuffer) 61 | 62 | return name 63 | }*/ 64 | } 65 | 66 | public extension HostController { 67 | 68 | /// Options for scanning Bluetooth devices 69 | enum ScanOption: UInt16, BitMaskOption { 70 | 71 | /// The cache of previously detected devices is flushed before performing the current inquiry. 72 | /// Otherwise, if flags is set to 0, then the results of previous inquiries may be returned, 73 | /// even if the devices aren't in range anymore. 74 | case flushCache = 0x0001 75 | } 76 | 77 | struct InquiryResult { 78 | 79 | /// Device Address 80 | public let address: BluetoothAddress 81 | 82 | public let pscanRepMode: UInt8 83 | 84 | public let pscanPeriodMode: UInt8 85 | 86 | public let pscanMode: UInt8 87 | 88 | public let deviceClass: (UInt8, UInt8, UInt8) 89 | 90 | public let clockOffset: UInt16 91 | } 92 | } 93 | 94 | // MARK: - IOCTL 95 | 96 | public extension HostControllerIO { 97 | 98 | struct Inquiry: IOControlValue { 99 | 100 | @_alwaysEmitIntoClient 101 | public static var id: HostControllerIO { .inquiry } 102 | 103 | public var device: HostController.ID 104 | 105 | public var duration: UInt8 106 | 107 | public var limit: UInt8 108 | 109 | public var deviceClass: (UInt8, UInt8, UInt8)? 110 | 111 | public var options: BitMaskOptionSet 112 | 113 | public private(set) var response: [HostController.InquiryResult] 114 | 115 | public init( 116 | device: HostController.ID, 117 | duration: UInt8 = 8, 118 | limit: UInt8 = 255, 119 | deviceClass: (UInt8, UInt8, UInt8)? = nil, 120 | options: BitMaskOptionSet = [] 121 | ) { 122 | self.device = device 123 | self.duration = duration 124 | self.limit = limit 125 | self.deviceClass = deviceClass 126 | self.options = options 127 | self.response = [] 128 | } 129 | 130 | public mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result { 131 | 132 | let bufferSize = MemoryLayout.size 133 | + (MemoryLayout.size * Int(limit)) 134 | 135 | let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) 136 | defer { buffer.deallocate() } 137 | 138 | buffer.withMemoryRebound(to: CInterop.HCIInquiryRequest.self, capacity: 1) { 139 | $0.pointee.id = self.device.rawValue 140 | $0.pointee.lap = self.deviceClass ?? (0x33, 0x8b, 0x9e) 141 | $0.pointee.flags = self.options.rawValue 142 | $0.pointee.responseCount = self.limit 143 | $0.pointee.length = self.duration 144 | } 145 | 146 | // call ioctl 147 | let result = try body(buffer) 148 | 149 | let resultCount = buffer.withMemoryRebound(to: CInterop.HCIInquiryRequest.self, capacity: 1) { 150 | Int($0.pointee.responseCount) 151 | } 152 | 153 | self.response.removeAll(keepingCapacity: true) 154 | self.response.reserveCapacity(resultCount) 155 | 156 | for index in 0 ..< resultCount { 157 | let offset = MemoryLayout.size + (MemoryLayout.size * index) 158 | buffer.advanced(by: offset).withMemoryRebound(to: CInterop.HCIInquiryResult.self, capacity: 1) { 159 | let element = HostController.InquiryResult( 160 | address: $0.pointee.address, 161 | pscanRepMode: $0.pointee.pscanRepMode, 162 | pscanPeriodMode: $0.pointee.pscanPeriodMode, 163 | pscanMode: $0.pointee.pscanMode, 164 | deviceClass: $0.pointee.deviceClass, 165 | clockOffset: $0.pointee.clockOffset 166 | ) 167 | self.response.append(element) 168 | } 169 | } 170 | 171 | return result 172 | } 173 | } 174 | } 175 | 176 | // MARK: - File Descriptor 177 | 178 | internal extension SocketDescriptor { 179 | 180 | @usableFromInline 181 | func inquiry( 182 | device id: HostController.ID, 183 | duration: UInt8 = 8, 184 | limit: UInt8 = 255, 185 | deviceClass: (UInt8, UInt8, UInt8)? = nil, 186 | options: BitMaskOptionSet = [] 187 | ) throws -> [HostController.InquiryResult] { 188 | var inquiry = HostControllerIO.Inquiry( 189 | device: id, 190 | duration: duration, 191 | limit: limit, 192 | deviceClass: deviceClass, 193 | options: options 194 | ) 195 | try inputOutput(&inquiry) 196 | return inquiry.response 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/HCI/HCIFileDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCIFileDescriptor.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Foundation 9 | import Bluetooth 10 | import BluetoothHCI 11 | import SystemPackage 12 | import Socket 13 | 14 | internal extension Socket { 15 | 16 | /// Sends an HCI command without waiting for an event. 17 | @usableFromInline 18 | func sendCommand( 19 | _ command: Command, 20 | parameter parameterData: Data = Data() 21 | ) async throws { 22 | // build data buffer to write 23 | assert(parameterData.count <= UInt8.max) 24 | let header = HCICommandHeader( 25 | command: command, 26 | parameterLength: UInt8(parameterData.count) 27 | ) 28 | let dataLength = 1 + HCICommandHeader.length + parameterData.count 29 | var data = Data(capacity: dataLength) 30 | data.append(HCIPacketType.command.rawValue) 31 | data.append(header.data) 32 | if parameterData.isEmpty == false { 33 | data.append(parameterData) 34 | } 35 | assert(data.count == dataLength) 36 | // write data to socket 37 | try await write(data) 38 | } 39 | 40 | /// Sends an HCI command and waits for expected event parameter data. 41 | @usableFromInline 42 | func sendRequest ( 43 | command: Command, 44 | commandParameterData: Data = Data(), 45 | event: UInt8 = 0, 46 | eventParameterLength: Int = 0, 47 | timeout: HCICommandTimeout = .default 48 | ) async throws -> Data { 49 | 50 | // initialize variables 51 | let opcodePacked = command.opcode.littleEndian 52 | 53 | // configure new filter 54 | var newFilter = HCISocketOption.Filter() 55 | newFilter.typeMask = 16 56 | //newFilter.setPacketType(.Event) 57 | newFilter.setEvent(HCIGeneralEvent.commandStatus) 58 | newFilter.setEvent(HCIGeneralEvent.commandComplete) 59 | newFilter.setEvent(HCIGeneralEvent.lowEnergyMeta) 60 | newFilter.bytes.setEvent(event) 61 | newFilter.opcode = opcodePacked 62 | 63 | return try await fileDescriptor.setFilter(newFilter) { () throws -> (Data) in 64 | 65 | // send command 66 | try await sendCommand(command, parameter: commandParameterData) 67 | 68 | // retrieve data... 69 | var attempts = 10 70 | while attempts > 0 { 71 | 72 | // decrement attempts 73 | attempts -= 1 74 | 75 | // attempt to read 76 | let eventBuffer = try await read(HCIEventHeader.maximumSize) 77 | assert(eventBuffer.isEmpty == false, "No HCI event read") 78 | let actualBytesRead = eventBuffer.count 79 | let headerData = Data(eventBuffer[1 ..< 1 + HCIEventHeader.length]) 80 | let eventData = Data(eventBuffer[(1 + HCIEventHeader.length) ..< actualBytesRead]) 81 | 82 | guard let eventHeader = HCIEventHeader(data: headerData) 83 | else { throw BluetoothHostControllerError.garbageResponse(headerData) } 84 | 85 | switch eventHeader.event { 86 | 87 | case .commandStatus: 88 | 89 | let parameterData = Data(eventData.prefix(min(eventData.count, HCICommandStatus.length))) 90 | 91 | guard let parameter = HCICommandStatus(data: parameterData) 92 | else { throw BluetoothHostControllerError.garbageResponse(parameterData) } 93 | 94 | /// must be command status for sent command 95 | guard parameter.opcode == opcodePacked else { continue } 96 | 97 | /// 98 | guard event == HCIGeneralEvent.commandStatus.rawValue else { 99 | 100 | switch parameter.status { 101 | 102 | case let .error(error): 103 | throw error 104 | 105 | case .success: 106 | break 107 | } 108 | 109 | break 110 | } 111 | 112 | // success! 113 | let dataLength = min(eventData.count, eventParameterLength) 114 | return Data(eventData.suffix(dataLength)) 115 | 116 | case .commandComplete: 117 | 118 | let parameterData = Data(eventData.prefix(min(eventData.count, HCICommandComplete.length))) 119 | 120 | guard let parameter = HCICommandComplete(data: parameterData) 121 | else { throw BluetoothHostControllerError.garbageResponse(parameterData) } 122 | 123 | guard parameter.opcode == opcodePacked else { continue } 124 | 125 | // success! 126 | let commandCompleteParameterLength = HCICommandComplete.length 127 | let data = eventData.suffix(eventParameterLength) 128 | 129 | let dataLength = max(data.count, commandCompleteParameterLength) 130 | return Data(data.suffix(dataLength)) 131 | 132 | case .remoteNameRequestComplete: 133 | 134 | guard eventHeader.event.rawValue == event else { break } 135 | 136 | let parameterData = Data(eventData.prefix(min(eventData.count, HCIRemoteNameRequestComplete.length))) 137 | 138 | guard let parameter = HCIRemoteNameRequestComplete(data: parameterData) 139 | else { throw BluetoothHostControllerError.garbageResponse(parameterData) } 140 | 141 | if commandParameterData.isEmpty == false { 142 | 143 | guard let commandParameter = HCIRemoteNameRequest(data: commandParameterData) 144 | else { fatalError("HCI Command 'RemoteNameRequest' was sent, but the event parameter data does not correspond to 'RemoteNameRequestParameter'") } 145 | 146 | // must be different, for some reason 147 | guard commandParameter.address == parameter.address else { continue } 148 | } 149 | 150 | // success! 151 | let dataLength = min(eventData.count, eventParameterLength) 152 | return Data(eventData.suffix(dataLength)) 153 | 154 | case .lowEnergyMeta: 155 | 156 | let parameterData = eventData 157 | 158 | guard let metaParameter = HCILowEnergyMetaEvent(data: parameterData) 159 | else { throw BluetoothHostControllerError.garbageResponse(parameterData) } 160 | 161 | // LE event should match 162 | guard metaParameter.subevent.rawValue == event 163 | else { continue } 164 | 165 | // success! 166 | return metaParameter.eventData 167 | 168 | // all other events 169 | default: 170 | 171 | guard eventHeader.event.rawValue == event else { break } 172 | 173 | let dataLength = min(eventData.count, eventParameterLength) 174 | return Data(eventData.suffix(dataLength)) 175 | } 176 | } 177 | 178 | // throw timeout error 179 | throw Errno.timedOut 180 | } 181 | } 182 | } 183 | 184 | internal extension SocketDescriptor { 185 | 186 | /// Creates an HCI socket binded to the specified address. 187 | @usableFromInline 188 | static func hci( 189 | _ address: HCISocketAddress, 190 | flags: SocketFlags = [.closeOnExec] 191 | ) throws -> SocketDescriptor { 192 | try bluetooth( 193 | .hci, 194 | bind: address, 195 | flags: flags 196 | ) 197 | } 198 | 199 | func setFilter(_ newFilter: HCISocketOption.Filter, _ body: () async throws -> (R)) async throws -> R { 200 | let oldFilter = try getSocketOption(HCISocketOption.Filter.self) 201 | try setSocketOption(newFilter) 202 | let result: R 203 | do { result = try await body() } 204 | catch let error { 205 | // restore filter 206 | do { try setSocketOption(oldFilter) } 207 | catch let restoreError { 208 | throw BluetoothHostControllerError.couldNotRestoreFilter(error, restoreError) 209 | } 210 | throw error 211 | } 212 | // restore filter on success 213 | try setSocketOption(oldFilter) 214 | return result 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Assets/PureSwiftBluetoothLinux.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | BluetoothLinux 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/L2CAP/L2CAPSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L2CAP.swift 3 | // BluetoothLinux 4 | // 5 | // Created by Alsey Coleman Miller on 2/28/16. 6 | // Copyright © 2016 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Bluetooth 11 | import BluetoothHCI 12 | @_implementationOnly import CBluetoothLinux 13 | import SystemPackage 14 | import Socket 15 | 16 | /// L2CAP Bluetooth socket 17 | public struct L2CAPSocket: Sendable { 18 | 19 | // MARK: - Properties 20 | 21 | /// Internal socket file descriptor 22 | @usableFromInline 23 | internal let fileDescriptor: SocketDescriptor 24 | 25 | /// L2CAP Socket address 26 | public let address: L2CAPSocketAddress 27 | 28 | // MARK: - Initialization 29 | 30 | internal init( 31 | fileDescriptor: SocketDescriptor, 32 | address: L2CAPSocketAddress 33 | ) { 34 | self.fileDescriptor = fileDescriptor 35 | self.address = address 36 | } 37 | 38 | /// Create a new L2CAP socket with the specified address. 39 | public init(address: L2CAPSocketAddress) throws(Errno) { 40 | self.fileDescriptor = try .l2cap(address, [.closeOnExec, .nonBlocking]) 41 | self.address = address 42 | } 43 | 44 | /// Create a new L2CAP socket on the `HostController` with the specified identifier. 45 | public init( 46 | hostController: HostController, 47 | type: AddressType? = nil, 48 | protocolServiceMultiplexer: ProtocolServiceMultiplexer? = nil, 49 | channel: ChannelIdentifier 50 | ) async throws { 51 | let deviceAddress = try await hostController.readDeviceAddress() 52 | let socketAddress = L2CAPSocketAddress( 53 | address: deviceAddress, 54 | addressType: type, 55 | protocolServiceMultiplexer: protocolServiceMultiplexer, 56 | channel: channel 57 | ) 58 | try self.init(address: socketAddress) 59 | } 60 | 61 | /// Creates a server socket for an L2CAP connection. 62 | public static func lowEnergyServer( 63 | address: BluetoothAddress, 64 | isRandom: Bool = false, 65 | backlog: Int = Socket.maxBacklog 66 | ) throws(Errno) -> Self { 67 | let address = L2CAPSocketAddress( 68 | lowEnergy: address, 69 | isRandom: isRandom 70 | ) 71 | let fileDescriptor = try SocketDescriptor.l2cap(address, [.closeOnExec, .nonBlocking]) 72 | try fileDescriptor.closeIfThrows { () throws(Errno) -> () in 73 | try fileDescriptor.listen(backlog: backlog) 74 | } 75 | return Self.init(fileDescriptor: fileDescriptor, address: address) 76 | } 77 | 78 | /// Creates a server socket for an L2CAP connection. 79 | public static func lowEnergyServer( 80 | hostController: HostController, 81 | isRandom: Bool = false, 82 | backlog: Int = Socket.maxBacklog 83 | ) async throws -> Self { 84 | let address = try await hostController.readDeviceAddress() 85 | return try lowEnergyServer( 86 | address: address, 87 | isRandom: isRandom, 88 | backlog: backlog 89 | ) 90 | } 91 | 92 | /// Creates a new socket connected to the remote address specified. 93 | public static func lowEnergyClient( 94 | address: BluetoothAddress, 95 | destination: BluetoothAddress, 96 | isRandom: Bool 97 | ) throws(Errno) -> Self { 98 | try lowEnergyClient( 99 | address: address, 100 | destination: destination, 101 | type: isRandom ? .random : .public 102 | ) 103 | } 104 | 105 | /// Creates a client socket for an L2CAP connection. 106 | public static func lowEnergyClient( 107 | address localAddress: BluetoothAddress, 108 | destination destinationAddress: BluetoothAddress, 109 | type destinationAddressType: LowEnergyAddressType 110 | ) throws(Errno) -> Self { 111 | let localSocketAddress = L2CAPSocketAddress( 112 | address: localAddress, 113 | addressType: nil, 114 | protocolServiceMultiplexer: nil, 115 | channel: .att 116 | ) 117 | let destinationSocketAddress = L2CAPSocketAddress( 118 | address: destinationAddress, 119 | addressType: AddressType(lowEnergy: destinationAddressType), 120 | protocolServiceMultiplexer: .att, 121 | channel: .att 122 | ) 123 | let fileDescriptor = try SocketDescriptor.l2cap(localSocketAddress, [.closeOnExec, .nonBlocking]) 124 | try? fileDescriptor.connect(to: destinationSocketAddress) // ignore result, async socket always throws 125 | return Self.init(fileDescriptor: fileDescriptor, address: localSocketAddress) 126 | } 127 | 128 | /// Creates a client socket for an L2CAP connection. 129 | public static func lowEnergyClient( 130 | address localAddress: BluetoothAddress, 131 | destination: HCILEAdvertisingReport.Report 132 | ) throws(Errno) -> Self { 133 | try lowEnergyClient( 134 | address: localAddress, 135 | destination: destination.address, 136 | type: destination.addressType 137 | ) 138 | } 139 | 140 | // MARK: - Methods 141 | 142 | /// Close socket. 143 | public func close() { 144 | try? fileDescriptor.close() 145 | } 146 | 147 | /// Attempt to accept an incoming connection. 148 | public func accept() throws(Errno) -> Self { 149 | let (fileDescriptor, address) = try self.fileDescriptor.accept(L2CAPSocketAddress.self) 150 | return Self.init( 151 | fileDescriptor: fileDescriptor, 152 | address: address 153 | ) 154 | } 155 | 156 | /// Write to the socket. 157 | public func send(_ data: Data) throws(Errno) -> Int { 158 | do { 159 | return try data.withUnsafeBytes { (bytes) throws(Errno) -> Int in 160 | try fileDescriptor.write(bytes) 161 | } 162 | } 163 | catch { 164 | throw error as! Errno // TODO: Foundation doesnt support typed error yet 165 | } 166 | } 167 | 168 | /// Reads from the socket. 169 | public func receive(_ length: Int) throws(Errno) -> Data { 170 | do { 171 | var data = Data(count: length) 172 | let bytesRead = try data.withUnsafeMutableBytes { (bytes) throws(Errno) -> Int in 173 | try fileDescriptor.read(into: bytes) 174 | } 175 | if bytesRead < length { 176 | data = data.prefix(bytesRead) 177 | } 178 | return data 179 | } 180 | catch { 181 | throw error as! Errno // TODO: Foundation doesnt support typed error yet 182 | } 183 | } 184 | 185 | public var security: BluetoothSocketOption.Security { 186 | get throws(Errno) { 187 | try fileDescriptor.getSocketOption(BluetoothSocketOption.Security.self) 188 | } 189 | } 190 | 191 | /// Attempts to change the socket's security level. 192 | public func setSecurity(_ security: BluetoothSocketOption.Security) throws(Errno) { 193 | try fileDescriptor.setSocketOption(security) 194 | } 195 | 196 | /// Attempt to get L2CAP socket options. 197 | public var options: L2CAPSocketOption.Options { 198 | get throws(Errno) { 199 | return try fileDescriptor.getSocketOption(L2CAPSocketOption.Options.self) 200 | } 201 | } 202 | 203 | public func setOptions(_ options: L2CAPSocketOption.Options) throws(Errno) { 204 | try fileDescriptor.setSocketOption(options) 205 | } 206 | } 207 | 208 | // MARK: - Supporting Types 209 | 210 | // MARK: - Server 211 | 212 | public extension L2CAPSocket { 213 | 214 | struct Server: Bluetooth.L2CAPServer, Sendable { 215 | 216 | // MARK: Properties 217 | 218 | @usableFromInline 219 | internal let socket: BluetoothLinux.L2CAPSocket 220 | 221 | /// Socket address 222 | public var address: BluetoothAddress { 223 | socket.address.address 224 | } 225 | 226 | /// Socket status 227 | public var status: L2CAPSocketStatus { 228 | let events: FileEvents 229 | let errno: Errno? 230 | do { 231 | events = try socket.fileDescriptor.poll(for: [.read, .error, .hangup, .invalidRequest]) 232 | if events.contains(.error) { 233 | errno = .connectionReset 234 | } else if events.contains(.invalidRequest) { 235 | errno = .badFileDescriptor 236 | } else if events.contains(.hangup) { 237 | errno = .socketShutdown 238 | } else { 239 | errno = nil 240 | } 241 | } 242 | catch { 243 | errno = error 244 | events = [] 245 | } 246 | return .init( 247 | send: false, 248 | recieve: false, 249 | accept: events.contains(.read) && errno == nil, 250 | error: errno 251 | ) 252 | } 253 | 254 | // MARK: Initialization 255 | 256 | internal init(socket: BluetoothLinux.L2CAPSocket) { 257 | self.socket = socket 258 | } 259 | 260 | public static func lowEnergyServer( 261 | address: BluetoothAddress, 262 | isRandom: Bool = false, 263 | backlog: Int = Socket.maxBacklog 264 | ) throws(Errno) -> L2CAPSocket.Server { 265 | let socket = try L2CAPSocket.lowEnergyServer( 266 | address: address, 267 | isRandom: isRandom, 268 | backlog: backlog 269 | ) 270 | return Self.init(socket: socket) 271 | } 272 | 273 | // MARK: Methods 274 | 275 | /// Close socket. 276 | public func close() { 277 | socket.close() 278 | } 279 | 280 | public func accept() throws(Errno) -> L2CAPSocket.Connection { 281 | let socket = try self.socket.accept() 282 | return .init(socket: socket, destination: self.socket.address.address) 283 | } 284 | } 285 | } 286 | 287 | // MARK: - Connection 288 | 289 | public extension L2CAPSocket { 290 | 291 | struct Connection: Bluetooth.L2CAPConnection, Sendable { 292 | 293 | // MARK: Properties 294 | 295 | @usableFromInline 296 | internal let socket: BluetoothLinux.L2CAPSocket 297 | 298 | public let destination: BluetoothAddress 299 | 300 | /// Socket address 301 | public var address: BluetoothAddress { 302 | socket.address.address 303 | } 304 | 305 | /// Socket status 306 | public var status: L2CAPSocketStatus { 307 | let events: FileEvents 308 | let errno: Errno? 309 | do { 310 | events = try socket.fileDescriptor.poll(for: [.read, .write, .error, .hangup, .invalidRequest]) 311 | if events.contains(.error) { 312 | errno = .connectionReset 313 | } else if events.contains(.invalidRequest) { 314 | errno = .badFileDescriptor 315 | } else if events.contains(.hangup) { 316 | errno = .socketShutdown 317 | } else { 318 | errno = nil 319 | } 320 | } 321 | catch { 322 | errno = error 323 | events = [] 324 | } 325 | return .init( 326 | send: events.contains(.write) && errno == nil, 327 | recieve: events.contains(.read) && errno == nil, 328 | accept: false, 329 | error: errno 330 | ) 331 | } 332 | 333 | // MARK: Initialization 334 | 335 | internal init( 336 | socket: BluetoothLinux.L2CAPSocket, 337 | destination: BluetoothAddress 338 | ) { 339 | self.socket = socket 340 | self.destination = destination 341 | } 342 | 343 | public static func lowEnergyClient( 344 | address: BluetoothAddress, 345 | destination: BluetoothAddress, 346 | isRandom: Bool = false 347 | ) throws(Errno) -> Self { 348 | let socket = try L2CAPSocket.lowEnergyClient( 349 | address: address, 350 | destination: destination, 351 | isRandom: isRandom 352 | ) 353 | return .init(socket: socket, destination: destination) 354 | } 355 | 356 | // MARK: Methods 357 | 358 | /// Close socket. 359 | public func close() { 360 | socket.close() 361 | } 362 | 363 | /// Write to the socket. 364 | public func send(_ data: Data) throws(Errno) { 365 | _ = try socket.send(data) 366 | } 367 | 368 | /// Reads from the socket. 369 | public func receive(_ bufferSize: Int) throws(Errno) -> Data { 370 | try socket.receive(bufferSize) 371 | } 372 | 373 | /// Attempts to change the socket's security level. 374 | public func setSecurityLevel(_ securityLevel: SecurityLevel) throws(Errno) { 375 | var security = try socket.security 376 | security = .init(level: securityLevel, keySize: security.keySize) 377 | try socket.setSecurity(security) 378 | } 379 | 380 | /// Get security level 381 | public func securityLevel() throws(Errno) -> SecurityLevel { 382 | try socket.security.level 383 | } 384 | } 385 | } 386 | 387 | internal extension L2CAPSocket { 388 | 389 | enum ConnectionResult: UInt16 { 390 | 391 | case success = 0x0000 392 | case pending = 0x0001 393 | case badPSM = 0x0002 394 | case secBlock = 0x0003 395 | case noMemory = 0x0004 396 | } 397 | 398 | enum ConnectionStatus: UInt16 { 399 | 400 | case noInfo = 0x0000 401 | case authenticationPending = 0x0001 402 | case authorizationPending = 0x0002 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /Sources/BluetoothLinux/Internal/CInterop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CInterop.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 16/10/21. 6 | // 7 | 8 | import Bluetooth 9 | import BluetoothHCI 10 | import SystemPackage 11 | import Socket 12 | @_implementationOnly import CBluetoothLinux 13 | 14 | public extension CInterop { 15 | 16 | /// `sockaddr_hci` 17 | struct HCISocketAddress: Equatable, Hashable { 18 | 19 | public var family: CInterop.SocketAddressFamily //sa_family_t() 20 | public var device: UInt16 21 | public var channel: UInt16 22 | 23 | public init( 24 | device: UInt16 = 0, 25 | channel: UInt16 = 0 26 | ) { 27 | self.family = .init(CInterop.HCISocketAddress.family.rawValue) 28 | self.device = device 29 | self.channel = channel 30 | } 31 | } 32 | } 33 | 34 | extension CInterop.HCISocketAddress: CSocketAddress { 35 | 36 | @usableFromInline 37 | static var family: SocketAddressFamily { .bluetooth } 38 | } 39 | 40 | public extension CInterop { 41 | 42 | /// `sockaddr_l2` L2CAP socket address (not packed) 43 | struct L2CAPSocketAddress: Equatable, Hashable { 44 | let l2_family: SocketAddressFamily 45 | var l2_psm: CUnsignedShort = 0 46 | var l2_bdaddr: BluetoothAddress = .zero 47 | var l2_cid: CUnsignedShort = 0 48 | var l2_bdaddr_type: UInt8 = 0 49 | init() { 50 | self.l2_family = SocketAddressFamily(Self.family.rawValue) 51 | } 52 | } 53 | } 54 | 55 | extension CInterop.L2CAPSocketAddress: CSocketAddress { 56 | 57 | @usableFromInline 58 | static var family: SocketAddressFamily { .bluetooth } 59 | } 60 | 61 | public extension CInterop { 62 | 63 | /// `sockaddr_rc` RFCOMM socket address 64 | struct RFCOMMSocketAddress: Equatable, Hashable { 65 | 66 | public let family: CInterop.SocketAddressFamily 67 | public var address: BluetoothAddress 68 | public var channel: UInt8 69 | 70 | public init(address: BluetoothAddress = .zero, channel: UInt8 = 0) { 71 | self.family = .init(Self.family.rawValue) 72 | self.address = address 73 | self.channel = channel 74 | } 75 | } 76 | } 77 | 78 | extension CInterop.RFCOMMSocketAddress: CSocketAddress { 79 | 80 | @usableFromInline 81 | static var family: SocketAddressFamily { .bluetooth } 82 | } 83 | 84 | public extension CInterop { 85 | 86 | /// `sockaddr_sco` SCO Socket Address 87 | struct SCOSocketAddress: Equatable, Hashable { 88 | public let family: CInterop.SocketAddressFamily 89 | public var address: BluetoothAddress 90 | public init(address: BluetoothAddress = .zero) { 91 | self.family = .init(Self.family.rawValue) 92 | self.address = address 93 | } 94 | } 95 | } 96 | 97 | extension CInterop.SCOSocketAddress: CSocketAddress { 98 | 99 | @usableFromInline 100 | static var family: SocketAddressFamily { .bluetooth } 101 | } 102 | 103 | public extension CInterop { 104 | 105 | /// `l2cap_conninfo` L2CAP Connection Information 106 | struct L2CAPConnectionInfo { 107 | public var handle: UInt16 108 | public var deviceClass: (UInt8, UInt8, UInt8) 109 | public init(handle: UInt16 = 0, 110 | deviceClass: (UInt8, UInt8, UInt8) = (0,0,0)) { 111 | self.handle = handle 112 | self.deviceClass = deviceClass 113 | } 114 | } 115 | } 116 | 117 | public extension CInterop { 118 | 119 | /// `l2cap_options` L2CAP Socket Options 120 | struct L2CAPSocketOptions { 121 | 122 | public var outputMaximumTransmissionUnit: UInt16 // omtu 123 | public var inputMaximumTransmissionUnit: UInt16 // imtu 124 | public var flushTo: UInt16 // flush_to 125 | public var mode: UInt8 126 | public var fcs: UInt8 127 | public var maxTX: UInt8 // max_tx 128 | public var txwinSize: UInt8 // txwin_size 129 | 130 | public init() { 131 | self.outputMaximumTransmissionUnit = 0 132 | self.inputMaximumTransmissionUnit = 0 133 | self.flushTo = 0 134 | self.mode = 0 135 | self.fcs = 0 136 | self.maxTX = 0 137 | self.txwinSize = 0 138 | } 139 | } 140 | } 141 | 142 | public extension CInterop { 143 | 144 | /// `bt_power` Bluetooth power options 145 | struct BluetoothPower { 146 | public var forceActive: UInt8 147 | public init(forceActive: UInt8 = 0) { 148 | self.forceActive = forceActive 149 | } 150 | }; 151 | } 152 | 153 | public extension CInterop { 154 | 155 | /// `bt_security` Bluetooth security level (not packed) 156 | struct BluetoothSocketSecurity: Equatable, Hashable { 157 | var level: UInt8 = 0 158 | var key_size: UInt8 = 0 159 | init() { } 160 | } 161 | } 162 | 163 | public extension CInterop { 164 | 165 | /// `rfcomm_conninfo` RFCOMM Connection Information 166 | struct RFCOMMConnectionInfo { 167 | public var handle: UInt16 168 | public var deviceClass: (UInt8, UInt8, UInt8) 169 | public init(handle: UInt16 = 0, 170 | deviceClass: (UInt8, UInt8, UInt8) = (0,0,0)) { 171 | self.handle = handle 172 | self.deviceClass = deviceClass 173 | } 174 | } 175 | } 176 | 177 | public extension CInterop { 178 | 179 | /// `rfcomm_dev_req` 180 | struct RFCOMMDeviceRequest: Equatable, Hashable { 181 | 182 | /// int16_t dev_id; 183 | public let device: UInt16 184 | 185 | /// uint32_t flags; 186 | public var flags: UInt32 187 | 188 | /// bdaddr_t src; 189 | public var source: BluetoothAddress 190 | 191 | /// bdaddr_t dst; 192 | public var destination: BluetoothAddress 193 | 194 | /// uint8_t channel; 195 | public var channel: UInt8 196 | 197 | public init( 198 | device: UInt16, 199 | flags: UInt32, 200 | source: BluetoothAddress, 201 | destination: BluetoothAddress, 202 | channel: UInt8 203 | ) { 204 | self.device = device 205 | self.flags = flags 206 | self.source = source 207 | self.destination = destination 208 | self.channel = channel 209 | } 210 | } 211 | } 212 | 213 | public extension CInterop { 214 | 215 | /// `rfcomm_dev_info` 216 | struct RFCOMMDeviceInformation: Equatable, Hashable { 217 | 218 | /// int16_t id; 219 | public let id: UInt16 220 | 221 | /// uint32_t flags; 222 | public var flags: UInt32 223 | 224 | /// uint16_t state; 225 | public var state: UInt16 226 | 227 | /// bdaddr_t src; 228 | public var source: BluetoothAddress 229 | 230 | /// bdaddr_t dst; 231 | public var destination: BluetoothAddress 232 | 233 | /// uint8_t channel; 234 | public var channel: UInt8 235 | 236 | public init( 237 | id: UInt16, 238 | flags: UInt32, 239 | state: UInt16, 240 | source: BluetoothAddress, 241 | destination: BluetoothAddress, 242 | channel: UInt8 243 | ) { 244 | self.id = id 245 | self.flags = flags 246 | self.state = state 247 | self.source = source 248 | self.destination = destination 249 | self.channel = channel 250 | } 251 | 252 | @usableFromInline 253 | internal init(id: UInt16) { 254 | self.id = id 255 | self.flags = 0 256 | self.state = 0 257 | self.source = .zero 258 | self.destination = .zero 259 | self.channel = 0 260 | } 261 | } 262 | } 263 | 264 | public extension CInterop { 265 | 266 | /// `rfcomm_dev_list_req` 267 | struct RFCOMMDeviceListRequest { 268 | 269 | public var count: UInt16 270 | 271 | public init(count: UInt16) { 272 | self.count = count 273 | } 274 | } 275 | } 276 | 277 | public extension CInterop { 278 | 279 | /// `sco_conninfo` SCO Connection Information 280 | struct SCOConnectionInfo { 281 | 282 | public var handle: UInt16 283 | public var deviceClass: (UInt8, UInt8, UInt8) 284 | 285 | public init(handle: UInt16 = 0, 286 | deviceClass: (UInt8, UInt8, UInt8) = (0,0,0)) { 287 | self.handle = handle 288 | self.deviceClass = deviceClass 289 | } 290 | } 291 | } 292 | 293 | public extension CInterop { 294 | 295 | /// `hci_dev_list_req` 296 | struct HCIDeviceList { 297 | 298 | /// uint16_t dev_num; 299 | public private(set) var numberOfDevices: UInt16 300 | 301 | /// struct hci_dev_req dev_req[0]; /* hci_dev_req structures */ 302 | /// 16 elements 303 | public private(set) var list: (Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element, Element) 304 | 305 | public init() { 306 | self.numberOfDevices = 0 307 | self.list = (Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element(), Element()) 308 | } 309 | 310 | @usableFromInline 311 | internal static func request(count: UInt16 = UInt16(Self.capacity)) -> Self { 312 | var value = self.init() 313 | value.numberOfDevices = count 314 | return value 315 | } 316 | } 317 | } 318 | 319 | public extension CInterop.HCIDeviceList { 320 | 321 | /// `hci_dev_req` 322 | struct Element { 323 | 324 | /// uint16_t dev_id; 325 | public var id: UInt16 326 | 327 | /// uint32_t dev_opt; 328 | public var options: UInt32 329 | 330 | public init(id: UInt16, options: UInt32) { 331 | self.id = id 332 | self.options = options 333 | } 334 | 335 | internal init() { 336 | self.id = 0 337 | self.options = 0 338 | } 339 | } 340 | } 341 | 342 | extension CInterop.HCIDeviceList: Collection { 343 | 344 | public subscript (index: Int) -> Element { 345 | 346 | assert(index < Self.capacity, "HCIDeviceList can only contain up to \(Self.capacity) devices") 347 | 348 | switch index { 349 | case 0: return list.0 350 | case 1: return list.1 351 | case 2: return list.2 352 | case 3: return list.3 353 | case 4: return list.4 354 | case 5: return list.5 355 | case 6: return list.6 356 | case 7: return list.7 357 | case 8: return list.8 358 | case 9: return list.9 359 | case 10: return list.10 360 | case 11: return list.11 361 | case 12: return list.12 362 | case 13: return list.13 363 | case 14: return list.14 364 | case 15: return list.15 365 | default: fatalError("Invalid index \(index)") 366 | } 367 | } 368 | 369 | public static var capacity: Int { 370 | return 16 371 | } 372 | 373 | public var count: Int { 374 | return Int(numberOfDevices) 375 | } 376 | 377 | /// The start `Index`. 378 | public var startIndex: Int { 379 | return 0 380 | } 381 | 382 | /// The end `Index`. 383 | /// 384 | /// This is the "one-past-the-end" position, and will always be equal to the `count`. 385 | public var endIndex: Int { 386 | return count 387 | } 388 | 389 | public func index(before i: Int) -> Int { 390 | return i - 1 391 | } 392 | 393 | public func index(after i: Int) -> Int { 394 | return i + 1 395 | } 396 | 397 | } 398 | 399 | extension CInterop.HCIDeviceList: RandomAccessCollection { 400 | 401 | public subscript(bounds: Range) -> Slice { 402 | return Slice(base: self, bounds: bounds) 403 | } 404 | 405 | public func makeIterator() -> IndexingIterator { 406 | return IndexingIterator(_elements: self) 407 | } 408 | } 409 | 410 | public extension CInterop { 411 | 412 | /// `hci_inquiry_req` 413 | struct HCIInquiryRequest { 414 | 415 | /// uint16_t dev_id; 416 | public var id: UInt16 = 0 417 | 418 | /// uint16_t flags; 419 | public var flags: UInt16 = 0 420 | 421 | /// uint8_t lap[3]; 422 | public var lap: (UInt8, UInt8, UInt8) = (0,0,0) 423 | 424 | /// uint8_t length; 425 | public var length: UInt8 = 0 426 | 427 | /// uint8_t num_rsp; 428 | public var responseCount: UInt8 = 0 429 | 430 | public init() { } 431 | } 432 | } 433 | 434 | public extension CInterop { 435 | 436 | struct HCIInquiryResult { 437 | 438 | /// Device Address 439 | public let address: BluetoothAddress 440 | 441 | public let pscanRepMode: UInt8 442 | 443 | public let pscanPeriodMode: UInt8 444 | 445 | public let pscanMode: UInt8 446 | 447 | public let deviceClass: (UInt8, UInt8, UInt8) 448 | 449 | public let clockOffset: UInt16 450 | 451 | public init() { 452 | self.address = .zero 453 | self.pscanRepMode = 0 454 | self.pscanPeriodMode = 0 455 | self.pscanMode = 0 456 | self.deviceClass = (0, 0, 0) 457 | self.clockOffset = 0 458 | } 459 | } 460 | } 461 | 462 | public extension CInterop { 463 | 464 | /// `hci_dev_info` 465 | struct HCIDeviceInformation { 466 | 467 | /// uint16_t dev_id; 468 | public let id: UInt16 469 | 470 | /// char name[8]; 471 | public var name: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0, 0, 0, 0) 472 | 473 | /// bdaddr_t bdaddr; 474 | public var address: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0,0,0,0,0,0) 475 | 476 | /// uint32_t flags; 477 | public var flags: UInt32 = 0 478 | 479 | /// uint8_t type; 480 | public var type: UInt8 = 0 481 | 482 | /// uint8_t features[8]; 483 | public var features: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0) 484 | 485 | /// uint32_t pkt_type; 486 | public var packetType: UInt32 = 0 487 | 488 | /// uint32_t link_policy; 489 | public var linkPolicy: UInt32 = 0 490 | 491 | /// uint32_t link_mode; 492 | public var linkMode: UInt32 = 0 493 | 494 | /// uint16_t acl_mtu; 495 | public var aclMaximumTransmissionUnit: UInt16 = 0 496 | 497 | /// uint16_t acl_pkts; 498 | public var aclPacketSize: UInt16 = 0 499 | 500 | /// uint16_t sco_mtu; 501 | public var scoMaximumTransmissionUnit: UInt16 = 0 502 | 503 | /// uint16_t sco_pkts; 504 | public var scoPacketSize: UInt16 = 0 505 | 506 | /// struct hci_dev_stats stat; 507 | public var statistics: HCIDeviceStatistics = HCIDeviceStatistics() 508 | 509 | internal init(id: UInt16) { 510 | self.id = id 511 | } 512 | } 513 | } 514 | 515 | internal extension CInterop.HCIDeviceInformation { 516 | 517 | var _name: String { 518 | return withUnsafePointer(to: name) { 519 | $0.withMemoryRebound(to: CChar.self, capacity: 8) { 520 | return String(cString: $0) 521 | } 522 | } 523 | } 524 | } 525 | 526 | public extension CInterop { 527 | 528 | struct HCIDeviceStatistics: Equatable, Hashable { 529 | 530 | /// uint32_t err_rx; 531 | public let errorRX: UInt32 = 0 532 | 533 | /// uint32_t err_tx; 534 | public let errorTX: UInt32 = 0 535 | 536 | /// uint32_t cmd_tx; 537 | public let commandTX: UInt32 = 0 538 | 539 | /// uint32_t evt_rx; 540 | public let eventRX: UInt32 = 0 541 | 542 | /// uint32_t acl_tx; 543 | public let alcTX: UInt32 = 0 544 | 545 | /// uint32_t acl_rx; 546 | public let alcRX: UInt32 = 0 547 | 548 | /// uint32_t sco_tx; 549 | public let scoTX: UInt32 = 0 550 | 551 | /// uint32_t sco_rx; 552 | public let scoRX: UInt32 = 0 553 | 554 | /// uint32_t byte_rx; 555 | public let byteRX: UInt32 = 0 556 | 557 | /// uint32_t byte_tx; 558 | public let byteTX: UInt32 = 0 559 | 560 | internal init() { } 561 | } 562 | } 563 | 564 | public extension CInterop { 565 | 566 | struct HCIFilterSocketOption: Sendable { 567 | 568 | public var typeMask: UInt32 = 0 569 | 570 | public var eventMask: (UInt32, UInt32) = (0, 0) 571 | 572 | public var opcode: UInt16 = 0 573 | 574 | // Explicit padding for Linux kernel 6.8+ compatibility 575 | // The kernel expects 16 bytes but the struct is naturally 14 bytes 576 | private var _padding: UInt16 = 0 577 | 578 | public init() { } 579 | } 580 | } 581 | 582 | internal extension CInterop.HCIFilterSocketOption { 583 | 584 | @usableFromInline 585 | mutating func setPacketType(_ type: HCIPacketType) { 586 | let bit = type == .vendor ? 0 : CInt(type.rawValue) & 31 587 | HCISetBit(bit, &typeMask) 588 | } 589 | 590 | @usableFromInline 591 | mutating func setEvent(_ event: UInt8) { 592 | let bit = (CInt(event) & 63) 593 | HCISetBit(bit, &eventMask.0) 594 | } 595 | 596 | @usableFromInline 597 | mutating func setEvent( 598 | _ event1: UInt8, 599 | _ event2: UInt8, 600 | _ event3: UInt8, 601 | _ event4: UInt8 602 | ) { 603 | eventMask.0 = 0 604 | eventMask.0 += UInt32(event4) << 0o30 605 | eventMask.0 += UInt32(event3) << 0o20 606 | eventMask.0 += UInt32(event2) << 0o10 607 | eventMask.0 += UInt32(event1) << 0o00 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust[0-9] \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | --------------------------------------------------------------------------------