From 57095e817951bc9fe589a86d07242791b74522db Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:23:50 -0500 Subject: [PATCH 01/12] #139 Deprecate `BitMaskOption` --- Sources/Bluetooth/BitMaskOption.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Bluetooth/BitMaskOption.swift b/Sources/Bluetooth/BitMaskOption.swift index 9591c0f2b..a76c42352 100644 --- a/Sources/Bluetooth/BitMaskOption.swift +++ b/Sources/Bluetooth/BitMaskOption.swift @@ -9,6 +9,7 @@ /// Enum that represents a bit mask flag / option. /// /// Basically `Swift.OptionSet` for enums. +@available(*, deprecated, message: "Use OptionSet instead") public protocol BitMaskOption: RawRepresentable, Hashable, CaseIterable where RawValue: FixedWidthInteger { } public extension Sequence where Element: BitMaskOption { @@ -40,7 +41,7 @@ public extension BitMaskOption { /// Integer-backed array type for `BitMaskOption`. /// /// The elements are packed in the integer with bitwise math and stored on the stack. -@frozen +@available(*, deprecated, message: "Use OptionSet instead") public struct BitMaskOptionSet : RawRepresentable { public typealias RawValue = Element.RawValue From 0056f05c6962554de7b6058a99f383b7b768db88 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:42:55 -0500 Subject: [PATCH 02/12] #139 Add `OptionSet` conformance to `GAPFlags` --- Sources/BluetoothGAP/GAPFlags.swift | 135 +++++++++++++--------------- 1 file changed, 60 insertions(+), 75 deletions(-) diff --git a/Sources/BluetoothGAP/GAPFlags.swift b/Sources/BluetoothGAP/GAPFlags.swift index 421f1d71f..09f2edae4 100644 --- a/Sources/BluetoothGAP/GAPFlags.swift +++ b/Sources/BluetoothGAP/GAPFlags.swift @@ -8,27 +8,47 @@ import Bluetooth -/// GAP Flag +/** + GAP Flag + + The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be omitted. All 0x00 octets after the last non-zero octet shall be omitted from the value transmitted. + + - Note: If the Flags AD type is not present in a non-connectable advertisement, the Flags should be considered as unknown and no assumptions should be made by the scanner. + + Flags used over the LE physical channel are: + + • Limited Discoverable Mode + + • General Discoverable Mode + + • BR/EDR Not Supported + + • Simultaneous LE and BR/EDR to Same Device Capable (Controller) + + • Simultaneous LE and BR/EDR to Same Device Capable (Host) + + The LE Limited Discoverable Mode and LE General Discoverable Mode flags shall be ignored when received over the BR/EDR physical channel. The ‘BR/ EDR Not Supported’ flag shall be set to 0 when sent over the BR/EDR physical channel. + + The Flags field may be zero or more octets long. This allows the Flags field to be extended while using the minimum number of octets within the data packet. + */ @frozen -public struct GAPFlags: GAPData, Equatable, Hashable { +public struct GAPFlags: GAPData, Equatable, Hashable, OptionSet, Sendable { public static var dataType: GAPDataType { .flags } - public var flags: BitMaskOptionSet + public var rawValue: UInt8 - public init(flags: BitMaskOptionSet = 0) { - self.flags = flags + public init(rawValue: UInt8) { + self.rawValue = rawValue } } public extension GAPFlags { init?(data: Data) where Data : Bluetooth.DataContainer { - guard data.count == 1 else { return nil } - - self.flags = BitMaskOptionSet(rawValue: data[0]) + self.init(rawValue: data[0]) } func append(to data: inout Data) where Data : Bluetooth.DataContainer { @@ -45,16 +65,7 @@ public extension GAPFlags { extension GAPFlags: DataConvertible { static func += (data: inout T, value: GAPFlags) { - data += value.flags.rawValue - } -} - -// MARK: - CustomStringConvertible - -extension GAPFlags: CustomStringConvertible { - - public var description: String { - return flags.description + data += value.rawValue } } @@ -62,48 +73,18 @@ extension GAPFlags: CustomStringConvertible { extension GAPFlags: ExpressibleByIntegerLiteral { - public init(integerLiteral rawValue: GAPFlag.RawValue) { - self.init(flags: BitMaskOptionSet(rawValue: rawValue)) + public init(integerLiteral rawValue: RawValue) { + self.init(rawValue: rawValue) } } // MARK: - ExpressibleByArrayLiteral -extension GAPFlags: ExpressibleByArrayLiteral { - - public init(arrayLiteral elements: GAPFlag...) { - - self.init(flags: BitMaskOptionSet(elements)) - } -} - -// MARK: - Supporting Types +extension GAPFlags: ExpressibleByArrayLiteral { } -/** - GAP Flag - - The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be omitted. All 0x00 octets after the last non-zero octet shall be omitted from the value transmitted. - - - Note: If the Flags AD type is not present in a non-connectable advertisement, the Flags should be considered as unknown and no assumptions should be made by the scanner. - - Flags used over the LE physical channel are: - - • Limited Discoverable Mode - - • General Discoverable Mode - - • BR/EDR Not Supported - - • Simultaneous LE and BR/EDR to Same Device Capable (Controller) - - • Simultaneous LE and BR/EDR to Same Device Capable (Host) - - The LE Limited Discoverable Mode and LE General Discoverable Mode flags shall be ignored when received over the BR/EDR physical channel. The ‘BR/ EDR Not Supported’ flag shall be set to 0 when sent over the BR/EDR physical channel. - - The Flags field may be zero or more octets long. This allows the Flags field to be extended while using the minimum number of octets within the data packet. - */ +// MARK: - Constants -public enum GAPFlag: UInt8, BitMaskOption { +public extension GAPFlags { /** LE Limited Discoverable Mode @@ -112,47 +93,51 @@ public enum GAPFlag: UInt8, BitMaskOption { - SeeAlso: [Bluetooth Advertising Works](https://blog.bluetooth.com/advertising-works-part-2) */ - case lowEnergyLimitedDiscoverableMode = 0b00000001 + static var lowEnergyLimitedDiscoverableMode: GAPFlags { 0b00000001 } /// LE General Discoverable Mode /// /// Use general discoverable mode to advertise indefinitely. - case lowEnergyGeneralDiscoverableMode = 0b00000010 + static var lowEnergyGeneralDiscoverableMode: GAPFlags { 0b00000010 } /// BR/EDR Not Supported. /// /// Bit 37 of LMP Feature Mask Definitions (Page 0) - case notSupportedBREDR = 0b00000100 + static var notSupportedBREDR: GAPFlags { 0b00000100 } /// Simultaneous LE and BR/EDR to Same Device Capable (Controller). /// /// Bit 49 of LMP Feature Mask Definitions (Page 0) - case simultaneousController = 0b00001000 + static var simultaneousController: GAPFlags { 0b00001000 } /// Simultaneous LE and BR/EDR to Same Device Capable (Host). /// /// Bit 66 of LMP Feature Mask Definitions (Page 1) - case simultaneousHost = 0b00010000 - - public static let allCases: [GAPFlag] = [ - .lowEnergyLimitedDiscoverableMode, - .lowEnergyGeneralDiscoverableMode, - .notSupportedBREDR, - .simultaneousController, - .simultaneousHost - ] + static var simultaneousHost: GAPFlags { 0b00010000 } } -extension GAPFlag: CustomStringConvertible { +// MARK: - CustomStringConvertible + +extension GAPFlags: CustomStringConvertible, CustomDebugStringConvertible { + #if hasFeature(Embedded) + public var description: String { + rawValue.description + } + #else + @inline(never) public var description: String { - - switch self { - case .lowEnergyLimitedDiscoverableMode: return "LE Limited Discoverable Mode" - case .lowEnergyGeneralDiscoverableMode: return "LE General Discoverable Mode" - case .notSupportedBREDR: return "BR/EDR Not Supported" - case .simultaneousController: return "Simultaneous LE and BR/EDR Controller" - case .simultaneousHost: return "Simultaneous LE and BR/EDR Host" - } + let descriptions: [(GAPFlags, StaticString)] = [ + (.lowEnergyLimitedDiscoverableMode, ".lowEnergyLimitedDiscoverableMode"), + (.lowEnergyGeneralDiscoverableMode, ".lowEnergyGeneralDiscoverableMode"), + (.notSupportedBREDR, ".notSupportedBREDR"), + (.simultaneousController, ".simultaneousController"), + (.simultaneousHost, ".simultaneousHost") + ] + return buildDescription(descriptions) } + #endif + + /// A textual representation of the file permissions, suitable for debugging. + public var debugDescription: String { self.description } } From d63273375575d74e4b9bc875202830e29a686cc1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:43:52 -0500 Subject: [PATCH 03/12] #154 Improve `GAPDataType` Embedded Swift support --- Sources/BluetoothGAP/GAPDataType.swift | 102 ++++++++++++++----------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/Sources/BluetoothGAP/GAPDataType.swift b/Sources/BluetoothGAP/GAPDataType.swift index ff2ff562d..1ed0c7de9 100644 --- a/Sources/BluetoothGAP/GAPDataType.swift +++ b/Sources/BluetoothGAP/GAPDataType.swift @@ -14,7 +14,7 @@ /// - SeeAlso: /// [Generic Access Profile](https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile) @frozen -public struct GAPDataType: RawRepresentable, Equatable, Hashable { +public struct GAPDataType: RawRepresentable, Equatable, Hashable, Sendable { public var rawValue: UInt8 @@ -174,57 +174,67 @@ extension GAPDataType: ExpressibleByIntegerLiteral { // MARK: - CustomStringConvertible +#if hasFeature(Embedded) extension GAPDataType: CustomStringConvertible { - public var name: String? { - return gapDataTypeNames[self] + public var description: String { + rawValue.description } +} +#else +extension GAPDataType: CustomStringConvertible { + @inline(never) public var description: String { return name ?? "GAP Data Type (\(rawValue))" } + + @inline(never) + public var name: String? { + let names: [GAPDataType: String] = [ + .flags: "Flags", + .incompleteListOf16BitServiceClassUUIDs: "Incomplete List of 16-bit Service Class UUIDs", + .completeListOf16CitServiceClassUUIDs: "Complete List of 16-bit Service Class UUIDs", + .incompleteListOf32BitServiceClassUUIDs: "Incomplete List of 32-bit Service Class UUIDs", + .completeListOf32BitServiceClassUUIDs: "Complete List of 32-bit Service Class UUIDs", + .incompleteListOf128BitServiceClassUUIDs: "Incomplete List of 128-bit Service Class UUIDs", + .completeListOf128BitServiceClassUUIDs: "Complete List of 128-bit Service Class UUIDs", + .shortLocalName: "Shortened Local Name", + .completeLocalName: "Complete Local Name", + .txPowerLevel: "Tx Power Level", + .classOfDevice: "Class of Device", + .simplePairingHashC: "Simple Pairing Hash C", + .simplePairingRandomizerR: "Simple Pairing Randomizer R", + .securityManagerTKValue: "Security Manager TK Value", + .securityManagerOutOfBandFlags: "Security Manager Out of Band Flags", + .slaveConnectionIntervalRange: "Slave Connection Interval Range", + .listOf16BitServiceSolicitationUUIDs: "List of 16-bit Service Solicitation UUIDs", + .listOf32BitServiceSolicitationUUIDs: "List of 32-bit Service Solicitation UUIDs", + .listOf128BitServiceSolicitationUUIDs: "List of 128-bit Service Solicitation UUIDs", + .serviceData16BitUUID: "Service Data - 16-bit UUID", + .serviceData32BitUUID: "Service Data - 32-bit UUID", + .serviceData128BitUUID: "Service Data - 128-bit UUID", + .publicTargetAddress: "Public Target Address", + .randomTargetAddress: "Random Target Address", + .appearance: "Appearance", + .advertisingInterval: "Advertising Interval", + .lowEnergyDeviceAddress: "LE Bluetooth Device Address", + .lowEnergyRole: "LE Role", + .lowEnergySecureConnectionsConfirmation: "LE Secure Connections Confirmation Value", + .lowEnergySecureConnectionsRandom: "LE Secure Connections Random Value", + .uri: "URI", + .indoorPositioning: "Indoor Positioning", + .transportDiscoveryData: "Transport Discovery Data", + .lowEnergySupportedFeatures: "LE Supported Features", + .channelMapUpdateIndication: "Channel Map Update Indication", + .pbAdv: "PB-ADV", + .meshMessage: "Mesh Message", + .meshBeacon: "Mesh Beacon", + .informationData3D: "3D Information Data", + .manufacturerSpecificData: "Manufacturer Specific Data" + ] + return names[self] + } } -/// Standard GAP Data Type names -internal let gapDataTypeNames: [GAPDataType: String] = [ - .flags: "Flags", - .incompleteListOf16BitServiceClassUUIDs: "Incomplete List of 16-bit Service Class UUIDs", - .completeListOf16CitServiceClassUUIDs: "Complete List of 16-bit Service Class UUIDs", - .incompleteListOf32BitServiceClassUUIDs: "Incomplete List of 32-bit Service Class UUIDs", - .completeListOf32BitServiceClassUUIDs: "Complete List of 32-bit Service Class UUIDs", - .incompleteListOf128BitServiceClassUUIDs: "Incomplete List of 128-bit Service Class UUIDs", - .completeListOf128BitServiceClassUUIDs: "Complete List of 128-bit Service Class UUIDs", - .shortLocalName: "Shortened Local Name", - .completeLocalName: "Complete Local Name", - .txPowerLevel: "Tx Power Level", - .classOfDevice: "Class of Device", - .simplePairingHashC: "Simple Pairing Hash C", - .simplePairingRandomizerR: "Simple Pairing Randomizer R", - .securityManagerTKValue: "Security Manager TK Value", - .securityManagerOutOfBandFlags: "Security Manager Out of Band Flags", - .slaveConnectionIntervalRange: "Slave Connection Interval Range", - .listOf16BitServiceSolicitationUUIDs: "List of 16-bit Service Solicitation UUIDs", - .listOf32BitServiceSolicitationUUIDs: "List of 32-bit Service Solicitation UUIDs", - .listOf128BitServiceSolicitationUUIDs: "List of 128-bit Service Solicitation UUIDs", - .serviceData16BitUUID: "Service Data - 16-bit UUID", - .serviceData32BitUUID: "Service Data - 32-bit UUID", - .serviceData128BitUUID: "Service Data - 128-bit UUID", - .publicTargetAddress: "Public Target Address", - .randomTargetAddress: "Random Target Address", - .appearance: "Appearance", - .advertisingInterval: "Advertising Interval", - .lowEnergyDeviceAddress: "LE Bluetooth Device Address", - .lowEnergyRole: "LE Role", - .lowEnergySecureConnectionsConfirmation: "LE Secure Connections Confirmation Value", - .lowEnergySecureConnectionsRandom: "LE Secure Connections Random Value", - .uri: "URI", - .indoorPositioning: "Indoor Positioning", - .transportDiscoveryData: "Transport Discovery Data", - .lowEnergySupportedFeatures: "LE Supported Features", - .channelMapUpdateIndication: "Channel Map Update Indication", - .pbAdv: "PB-ADV", - .meshMessage: "Mesh Message", - .meshBeacon: "Mesh Beacon", - .informationData3D: "3D Information Data", - .manufacturerSpecificData: "Manufacturer Specific Data" -] +#endif From de7dfa96f41d65f6338475280793349d4e900129 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:44:05 -0500 Subject: [PATCH 04/12] #139 Add `OptionSet` extensions --- .../BluetoothGAP/Extensions/OptionSet.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Sources/BluetoothGAP/Extensions/OptionSet.swift diff --git a/Sources/BluetoothGAP/Extensions/OptionSet.swift b/Sources/BluetoothGAP/Extensions/OptionSet.swift new file mode 100644 index 000000000..d0761df90 --- /dev/null +++ b/Sources/BluetoothGAP/Extensions/OptionSet.swift @@ -0,0 +1,30 @@ +// +// OptionSet.swift +// Bluetooth +// +// Created by Alsey Coleman Miller on 11/5/24. +// + +extension OptionSet { + @inline(never) + internal func buildDescription( + _ descriptions: [(Element, StaticString)] + ) -> String { + var copy = self + var result = "[" + + for (option, name) in descriptions { + if _slowPath(copy.contains(option)) { + result += name.description + copy.remove(option) + if !copy.isEmpty { result += ", " } + } + } + + if _slowPath(!copy.isEmpty) { + result += "\(Self.self)(rawValue: \(copy.rawValue))" + } + result += "]" + return result + } +} From 428cb61f41c92d67a49dfaf524d7c05fdbdaf5cd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:44:12 -0500 Subject: [PATCH 05/12] Updated unit tests --- Tests/BluetoothTests/GAPTests.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/BluetoothTests/GAPTests.swift b/Tests/BluetoothTests/GAPTests.swift index b27620287..525e5411a 100644 --- a/Tests/BluetoothTests/GAPTests.swift +++ b/Tests/BluetoothTests/GAPTests.swift @@ -26,10 +26,18 @@ final class GAPTests: XCTestCase { let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] XCTAssertEqual(flags, 0b00000110) - XCTAssertEqual(flags.flags, [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR]) - XCTAssertEqual(flags.description, "[LE General Discoverable Mode, BR/EDR Not Supported]") + XCTAssertEqual(flags, [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR]) + XCTAssertEqual(flags.description, "[.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR]") + + let allCases: [GAPFlags] = [ + .lowEnergyLimitedDiscoverableMode, + .lowEnergyGeneralDiscoverableMode, + .notSupportedBREDR, + .simultaneousController, + .simultaneousHost + ] - let allFlags = Array(GAPFlag.allCases).sorted(by: { $0.rawValue < $1.rawValue }) + let allFlags = allCases.sorted(by: { $0.rawValue < $1.rawValue }) allFlags.forEach { XCTAssertFalse($0.description.isEmpty) } } From 8eb1b3dc2ad9eafef3d1291db5f756e0c2575553 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 19:45:11 -0500 Subject: [PATCH 06/12] Updated GitHub CI --- .github/workflows/swift-arm.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift-arm.yml b/.github/workflows/swift-arm.yml index 1325caa23..2ccdbc984 100644 --- a/.github/workflows/swift-arm.yml +++ b/.github/workflows/swift-arm.yml @@ -14,6 +14,7 @@ jobs: uses: actions/checkout@v2 - name: Swift Version run: swift --version - - name: Build + - name: Build Bluetooth run: swift build --triple armv6m-apple-none-macho --configuration release --verbose -Xswiftc -enable-experimental-feature -Xswiftc Embedded -Xswiftc -disable-stack-protector -Xcc -D__MACH__ -Xcc -ffreestanding -Xcc -mcpu=cortex-m0plus -Xcc -mthumb --target Bluetooth - + - name: Build BluetoothGAP + run: swift build --triple armv6m-apple-none-macho --configuration release --verbose -Xswiftc -enable-experimental-feature -Xswiftc Embedded -Xswiftc -disable-stack-protector -Xcc -D__MACH__ -Xcc -ffreestanding -Xcc -mcpu=cortex-m0plus -Xcc -mthumb --target BluetoothGAP From 71dd4586de6f0c9032d3338d7639244f0258f632 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 22:30:23 -0500 Subject: [PATCH 07/12] #154 Improve `UUID` Embedded Swift support --- .../Bluetooth/Extensions/Hexadecimal.swift | 43 ++++++----------- Sources/Bluetooth/Extensions/Integer.swift | 10 +--- Sources/Bluetooth/Extensions/UUID.swift | 46 +++++++++++++------ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/Sources/Bluetooth/Extensions/Hexadecimal.swift b/Sources/Bluetooth/Extensions/Hexadecimal.swift index e48c9592c..231df0bf1 100644 --- a/Sources/Bluetooth/Extensions/Hexadecimal.swift +++ b/Sources/Bluetooth/Extensions/Hexadecimal.swift @@ -30,19 +30,17 @@ internal extension Collection where Element: FixedWidthInteger { } } -internal extension UInt { +internal extension FixedWidthInteger { - init?(parse string: String, radix: UInt) { - let digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - var result = UInt(0) - for digit in string { - #if hasFeature(Embedded) - let character = digit - #else - let character = String(digit).uppercased().first! - #endif + init?(parse string: String, radix: Self) { + #if !hasFeature(Embedded) + let string = string.uppercased() + #endif + let digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".utf8 + var result = Self(0) + for character in string.utf8 { if let stringIndex = digits.enumerated().first(where: { $0.element == character })?.offset { - let val = UInt(stringIndex) + let val = Self(stringIndex) if val >= radix { return nil } @@ -55,31 +53,14 @@ internal extension UInt { } } -internal extension UInt16 { - - init?(hexadecimal string: String) { - guard string.utf8.count == MemoryLayout.size * 2 else { - return nil - } - #if hasFeature(Embedded) || DEBUG - guard let value = UInt(parse: string, radix: 16) else { - return nil - } - self.init(value) - #else - self.init(string, radix: 16) - #endif - } -} - -internal extension UInt32 { +internal extension FixedWidthInteger { init?(hexadecimal string: String) { guard string.utf8.count == MemoryLayout.size * 2 else { return nil } #if hasFeature(Embedded) || DEBUG - guard let value = UInt(parse: string, radix: 16) else { + guard let value = Self(parse: string, radix: 16) else { return nil } self.init(value) @@ -89,6 +70,7 @@ internal extension UInt32 { } } +#if !hasFeature(Embedded) internal extension String.UTF16View.Element { // Convert 0 ... 9, a ... f, A ...F to their decimal value, @@ -143,3 +125,4 @@ internal extension [UInt8] { } } +#endif diff --git a/Sources/Bluetooth/Extensions/Integer.swift b/Sources/Bluetooth/Extensions/Integer.swift index 8df57e786..14004c5c4 100644 --- a/Sources/Bluetooth/Extensions/Integer.swift +++ b/Sources/Bluetooth/Extensions/Integer.swift @@ -14,13 +14,11 @@ internal extension UInt16 { /// Initializes value from two bytes. init(bytes: (UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt16.self) } /// Converts to two bytes. var bytes: (UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8).self) } } @@ -29,13 +27,11 @@ internal extension UInt32 { /// Initializes value from four bytes. init(bytes: (UInt8, UInt8, UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt32.self) } /// Converts to four bytes. var bytes: (UInt8, UInt8, UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self) } } @@ -44,13 +40,11 @@ internal extension UInt64 { /// Initializes value from four bytes. init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt64.self) } /// Converts to eight bytes. var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self) } } @@ -59,7 +53,6 @@ internal extension UInt8 { /// Initialize a byte from 2 bit enums. static func bit2(_ enum1: UInt8, _ enum2: UInt8, _ enum3: UInt8, _ enum4: UInt8) -> UInt8 { - var value: UInt8 = 0 value += enum1 << 6 value += enum2 << 4 @@ -70,7 +63,6 @@ internal extension UInt8 { /// Get 2 bit values from a byte. func bit2() -> (UInt8, UInt8, UInt8, UInt8) { - return (self >> 6, (self << 2) >> 6, (self << 4) >> 6, (self << 6) >> 6) } } @@ -136,4 +128,4 @@ internal extension UInt64 { } } } -#endif \ No newline at end of file +#endif diff --git a/Sources/Bluetooth/Extensions/UUID.swift b/Sources/Bluetooth/Extensions/UUID.swift index 4a5e85e95..cf965f86e 100644 --- a/Sources/Bluetooth/Extensions/UUID.swift +++ b/Sources/Bluetooth/Extensions/UUID.swift @@ -233,27 +233,43 @@ fileprivate extension UInt128 { /// Parse a UUID string and return a value in big endian order. static func bigEndian(uuidString string: String) -> UInt128? { - let separator: Character = "-" - guard string.utf8.count == 36 else { + guard string.utf8.count == 36, + let separator = "-".utf8.first else { return nil } - guard string[string.index(string.startIndex, offsetBy: 8)] == separator, - string[string.index(string.startIndex, offsetBy: 13)] == separator, - string[string.index(string.startIndex, offsetBy: 18)] == separator, - string[string.index(string.startIndex, offsetBy: 23)] == separator + let characters = string.utf8 + guard characters[characters.index(characters.startIndex, offsetBy: 8)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 13)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 18)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 23)] == separator, + let a = String(characters[characters.startIndex ..< characters.index(characters.startIndex, offsetBy: 8)]), + let b = String(characters[characters.index(characters.startIndex, offsetBy: 9) ..< characters.index(characters.startIndex, offsetBy: 13)]), + let c = String(characters[characters.index(characters.startIndex, offsetBy: 14) ..< characters.index(characters.startIndex, offsetBy: 18)]), + let d = String(characters[characters.index(characters.startIndex, offsetBy: 19) ..< characters.index(characters.startIndex, offsetBy: 23)]), + let e = String(characters[characters.index(characters.startIndex, offsetBy: 24) ..< characters.index(characters.startIndex, offsetBy: 36)]) else { return nil } - let a = string[string.startIndex ..< string.index(string.startIndex, offsetBy: 8)] - let b = string[string.index(string.startIndex, offsetBy: 9) ..< string.index(string.startIndex, offsetBy: 13)] - let c = string[string.index(string.startIndex, offsetBy: 14) ..< string.index(string.startIndex, offsetBy: 18)] - let d = string[string.index(string.startIndex, offsetBy: 19) ..< string.index(string.startIndex, offsetBy: 23)] - let e = string[string.index(string.startIndex, offsetBy: 24) ..< string.index(string.startIndex, offsetBy: 36)] let hexadecimal = (a + b + c + d + e) - guard hexadecimal.utf8.count == 32, - let bytes = [UInt8](hexadecimal: hexadecimal), - let value = UInt128(data: bytes) else { + guard hexadecimal.utf8.count == 32 else { return nil } - return value + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + guard let value = UInt128(hexadecimal: hexadecimal) else { + return nil + } + return value.bigEndian + } else { + #if hasFeature(Embedded) + // should never be executed + assertionFailure() + return nil + #else + guard let bytes = [UInt8](hexadecimal: hexadecimal), + let value = UInt128(data: bytes) else { + return nil + } + return value + #endif + } } /// Generate UUID string, e.g. `0F4DD6A4-0F71-48EF-98A5-996301B868F9` from a value initialized in its big endian order. From 91caf4c459d08ef0a1ab998a796f86c6b6b02a21 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 22:30:44 -0500 Subject: [PATCH 08/12] Updated unit tests --- Tests/BluetoothTests/UInt128Tests.swift | 21 +++++++++++++++ Tests/BluetoothTests/UInt24Tests.swift | 34 ++++++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Tests/BluetoothTests/UInt128Tests.swift b/Tests/BluetoothTests/UInt128Tests.swift index a3d488522..e8e5ba47c 100644 --- a/Tests/BluetoothTests/UInt128Tests.swift +++ b/Tests/BluetoothTests/UInt128Tests.swift @@ -37,6 +37,27 @@ final class UInt128Tests: XCTestCase { XCTAssertNotEqual(Bluetooth.UInt128.max.hashValue, 0) } + func testHexadecimal() { + + var testData: [(UInt128, String)] = [ + (.min, "00000000000000000000000000000000"), + (.max, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + ] + + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + testData += [ + (0x60F14FE2F97211E5B84F23E070D5A8C7, "60F14FE2F97211E5B84F23E070D5A8C7") + ] + } + + for (value, hexadecimal) in testData { + XCTAssertEqual(value.hexadecimal, hexadecimal) + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + XCTAssertEqual(UInt128(parse: hexadecimal, radix: 16), value) + } + } + } + func testExpressibleByIntegerLiteral() { guard #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) else { diff --git a/Tests/BluetoothTests/UInt24Tests.swift b/Tests/BluetoothTests/UInt24Tests.swift index 58e5b7042..68cd78abf 100644 --- a/Tests/BluetoothTests/UInt24Tests.swift +++ b/Tests/BluetoothTests/UInt24Tests.swift @@ -38,16 +38,36 @@ final class UInt24Tests: XCTestCase { XCTAssertNotEqual(UInt24.max.hashValue, 0) } + func testHexadecimal() { + + let testData: [(UInt24, String)] = [ + (.zero, "000000"), + (0x000000, "000000"), + (0x000001, "000001"), + (0x000020, "000020"), + (0xABCDEF, "ABCDEF"), + (16777215, "FFFFFF"), + (0xFFFFFF, "FFFFFF"), + (.max, "FFFFFF") + ] + + for (value, hexadecimal) in testData { + XCTAssertEqual(String(UInt32(value).toHexadecimal().suffix(6)), hexadecimal) + XCTAssertEqual(UInt32(hexadecimal, radix: 16), UInt32(value)) + XCTAssertEqual(UInt32(parse: hexadecimal, radix: 16), UInt32(value)) + } + } + func testExpressibleByIntegerLiteral() { let values: [(UInt24, String)] = [ - (.zero, "000000"), - (0x000000, "000000"), - (0x000001, "000001"), - (0x000020, "000020"), - (0xABCDEF, "ABCDEF"), - (16777215, "FFFFFF"), - (0xFFFFFF, "FFFFFF") + (.zero, "000000"), + (0x000000, "000000"), + (0x000001, "000001"), + (0x000020, "000020"), + (0xABCDEF, "ABCDEF"), + (16777215, "FFFFFF"), + (0xFFFFFF, "FFFFFF") ] values.forEach { XCTAssertEqual($0.description, $1) } From 825e3369d70553c2a56cc798d294a8add9df00d2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 22:45:52 -0500 Subject: [PATCH 09/12] #154 Improve `BluetoothAddress` Embedded Swift support --- Sources/Bluetooth/Address.swift | 34 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Sources/Bluetooth/Address.swift b/Sources/Bluetooth/Address.swift index cd2f80ce8..a25c01c24 100644 --- a/Sources/Bluetooth/Address.swift +++ b/Sources/Bluetooth/Address.swift @@ -86,27 +86,29 @@ extension BluetoothAddress: ByteSwap { // MARK: - RawRepresentable -#if !hasFeature(Embedded) extension BluetoothAddress: RawRepresentable { /// Initialize a Bluetooth Address from its big endian string representation (e.g. `00:1A:7D:DA:71:13`). public init?(rawValue: String) { // verify string length - guard rawValue.utf8.count == 17 + let characters = rawValue.utf8 + guard characters.count == 17, + let separator = ":".utf8.first else { return nil } var bytes: ByteValue = (0, 0, 0, 0, 0, 0) - let components = rawValue.split(whereSeparator: { $0 == ":" }) + let components = characters.split(whereSeparator: { $0 == separator }) guard components.count == 6 else { return nil } - for (index, string) in components.enumerated() { + for (index, subsequence) in components.enumerated() { - guard string.utf8.count == 2, - let byte = UInt8(string, radix: 16) + guard subsequence.count == 2, + let string = String(subsequence), + let byte = UInt8(hexadecimal: string) else { return nil } withUnsafeMutablePointer(to: &bytes) { @@ -121,19 +123,6 @@ extension BluetoothAddress: RawRepresentable { /// Convert a Bluetooth Address to its big endian string representation (e.g. `00:1A:7D:DA:71:13`). public var rawValue: String { - _description - } -} -#endif - -// MARK: - CustomStringConvertible - -extension BluetoothAddress: CustomStringConvertible { - - public var description: String { _description } - - /// Convert a Bluetooth Address to its big endian string representation (e.g. `00:1A:7D:DA:71:13`). - internal var _description: String { let bytes = self.bigEndian.bytes return bytes.0.toHexadecimal() + ":" + bytes.1.toHexadecimal() @@ -144,6 +133,13 @@ extension BluetoothAddress: CustomStringConvertible { } } +// MARK: - CustomStringConvertible + +extension BluetoothAddress: CustomStringConvertible { + + public var description: String { rawValue } +} + // MARK: - Data public extension BluetoothAddress { From c36b49606141eebd84c54af4abfb17ff9ecce2a0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Nov 2024 23:05:34 -0500 Subject: [PATCH 10/12] #154 Improve `BluetoothAddress` Embedded Swift support --- Sources/Bluetooth/Address.swift | 8 ++- .../Bluetooth/Extensions/Hexadecimal.swift | 53 ++++++++++++------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Sources/Bluetooth/Address.swift b/Sources/Bluetooth/Address.swift index a25c01c24..329922918 100644 --- a/Sources/Bluetooth/Address.swift +++ b/Sources/Bluetooth/Address.swift @@ -90,6 +90,11 @@ extension BluetoothAddress: RawRepresentable { /// Initialize a Bluetooth Address from its big endian string representation (e.g. `00:1A:7D:DA:71:13`). public init?(rawValue: String) { + self.init(rawValue) + } + + /// Initialize a Bluetooth Address from its big endian string representation (e.g. `00:1A:7D:DA:71:13`). + internal init?(_ rawValue: S) { // verify string length let characters = rawValue.utf8 @@ -107,8 +112,7 @@ extension BluetoothAddress: RawRepresentable { for (index, subsequence) in components.enumerated() { guard subsequence.count == 2, - let string = String(subsequence), - let byte = UInt8(hexadecimal: string) + let byte = UInt8(hexadecimal: subsequence) else { return nil } withUnsafeMutablePointer(to: &bytes) { diff --git a/Sources/Bluetooth/Extensions/Hexadecimal.swift b/Sources/Bluetooth/Extensions/Hexadecimal.swift index 231df0bf1..3513df0ed 100644 --- a/Sources/Bluetooth/Extensions/Hexadecimal.swift +++ b/Sources/Bluetooth/Extensions/Hexadecimal.swift @@ -32,30 +32,14 @@ internal extension Collection where Element: FixedWidthInteger { internal extension FixedWidthInteger { - init?(parse string: String, radix: Self) { + init?(parse string: S, radix: Self) { #if !hasFeature(Embedded) let string = string.uppercased() #endif - let digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".utf8 - var result = Self(0) - for character in string.utf8 { - if let stringIndex = digits.enumerated().first(where: { $0.element == character })?.offset { - let val = Self(stringIndex) - if val >= radix { - return nil - } - result = result * radix + val - } else { - return nil - } - } - self = result + self.init(utf8: string.utf8, radix: radix) } -} - -internal extension FixedWidthInteger { - init?(hexadecimal string: String) { + init?(hexadecimal string: S) { guard string.utf8.count == MemoryLayout.size * 2 else { return nil } @@ -68,6 +52,37 @@ internal extension FixedWidthInteger { self.init(string, radix: 16) #endif } + + init?(hexadecimal utf8: C) where C: Collection, C.Element == UInt8 { + guard utf8.count == MemoryLayout.size * 2 else { + return nil + } + guard let value = Self(utf8: utf8, radix: 16) else { + return nil + } + self.init(value) + } + + /// Expects uppercase UTF8 data. + init?(utf8: C, radix: Self) where C: Collection, C.Element == UInt8 { + #if !hasFeature(Embedded) && DEBUG + assert(String(decoding: utf8, as: UTF8.self) == String(decoding: utf8, as: UTF8.self).uppercased(), "Expected uppercase string") + #endif + let digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".utf8 + var result = Self(0) + for character in utf8 { + if let stringIndex = digits.enumerated().first(where: { $0.element == character })?.offset { + let val = Self(stringIndex) + if val >= radix { + return nil + } + result = result * radix + val + } else { + return nil + } + } + self = result + } } #if !hasFeature(Embedded) From 6883c2c99c28bdc32f4a4a7c80dd2f3e01e12c20 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 6 Nov 2024 01:11:29 -0500 Subject: [PATCH 11/12] #154 Fixed `FixedWidthInteger.toHexadecimal()` support for Embedded Swift --- .../Bluetooth/Extensions/Hexadecimal.swift | 22 ++++++++++++--- Sources/Bluetooth/Extensions/String.swift | 28 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Sources/Bluetooth/Extensions/Hexadecimal.swift b/Sources/Bluetooth/Extensions/Hexadecimal.swift index 3513df0ed..7273fd093 100644 --- a/Sources/Bluetooth/Extensions/Hexadecimal.swift +++ b/Sources/Bluetooth/Extensions/Hexadecimal.swift @@ -9,12 +9,26 @@ internal extension FixedWidthInteger { func toHexadecimal() -> String { - - var string = String(self, radix: 16) - while string.utf8.count < (MemoryLayout.size * 2) { + let length = MemoryLayout.size * 2 + var string: String + #if hasFeature(Embedded) || (canImport(Darwin) && DEBUG) + string = "" + string.reserveCapacity(length) + self.bigEndian.bytes.forEach { byte in + string.append(String(format: "%02X", length: 2, byte)!) + } + #else // Linux and non-Embedded release builds use Swift StdLib + string = String(self, radix: 16, uppercase: true) + // Add Zero padding + while string.utf8.count < length { string = "0" + string } - return string.uppercased() + #endif + assert(string.utf8.count == length) + #if !hasFeature(Embedded) + assert(string == string.uppercased(), "String should be uppercased") + #endif + return string } } diff --git a/Sources/Bluetooth/Extensions/String.swift b/Sources/Bluetooth/Extensions/String.swift index bd905fd23..b7e5252ff 100644 --- a/Sources/Bluetooth/Extensions/String.swift +++ b/Sources/Bluetooth/Extensions/String.swift @@ -5,6 +5,10 @@ // Created by Alsey Coleman Miller on 11/4/24. // +#if canImport(Darwin) +import Darwin +#endif + internal extension String { /// Initialize from UTF8 data. @@ -19,4 +23,28 @@ internal extension String { } #endif } + + #if hasFeature(Embedded) + // Can't use `CVarArg` in Embedded Swift + init?(format: String, length: Int, _ value: UInt8) { + var cString: [CChar] = .init(repeating: 0, count: length + 1) + guard _snprintf_uint8_t(&cString, cString.count, format, value) >= 0 else { + return nil + } + self.init(cString: cString) + } + #elseif canImport(Darwin) + init?(format: String, length: Int, _ value: T) { + var cString: [CChar] = .init(repeating: 0, count: length + 1) + guard snprintf(ptr: &cString, cString.count, format, value) >= 0 else { + return nil + } + self.init(cString: cString) + } + #endif } + +#if hasFeature(Embedded) +@_silgen_name("snprintf") +internal func _snprintf_uint8_t(_ pointer: UnsafeMutablePointer, _ length: Int, _ format: UnsafePointer, _ arg: UInt8) -> Int32 +#endif From 2cd18c5e31552ee0824568f2fb9af6d6f4559e3e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 6 Nov 2024 01:11:46 -0500 Subject: [PATCH 12/12] Add `BinaryInteger.bytes` extension --- Sources/Bluetooth/Extensions/Integer.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/Bluetooth/Extensions/Integer.swift b/Sources/Bluetooth/Extensions/Integer.swift index 14004c5c4..be9ad8b92 100644 --- a/Sources/Bluetooth/Extensions/Integer.swift +++ b/Sources/Bluetooth/Extensions/Integer.swift @@ -67,6 +67,15 @@ internal extension UInt8 { } } +internal extension BinaryInteger { + + @inlinable + var bytes: [UInt8] { + var mutableValueCopy = self + return withUnsafeBytes(of: &mutableValueCopy) { Array($0) } + } +} + #if canImport(Foundation) internal extension UInt64 {