diff --git a/esp32-ble-peripheral-sdk/CMakeLists.txt b/esp32-ble-peripheral-sdk/CMakeLists.txt new file mode 100644 index 0000000..bdc0558 --- /dev/null +++ b/esp32-ble-peripheral-sdk/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.29) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(main) diff --git a/esp32-ble-peripheral-sdk/README.md b/esp32-ble-peripheral-sdk/README.md new file mode 100644 index 0000000..217a196 --- /dev/null +++ b/esp32-ble-peripheral-sdk/README.md @@ -0,0 +1,39 @@ +# esp32-ble-peripheral-sdk + +This example demonstrates how to integrate with the ESP-IDF SDK via CMake and how to use the the SDK to advertise as a Bluetooth iBeacon from Swift. This example is specifically made for the RISC-V MCUs from ESP32 (the Xtensa MCUs are not currently supported by Swift). + +## Requirements + +- Set up the [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/) development environment. Follow the steps in the [ESP32-C6 "Get Started" guide](https://docs.espressif.com/projects/esp-idf/en/v5.2/esp32c6/get-started/index.html). + - Make sure you specifically set up development for the RISC-V ESP32-C6, and not the Xtensa based products. + +- Before trying to use Swift with the ESP-IDF SDK, make sure your environment works and can build the provided C/C++ sample projects, in particular: + - Try building and running the "get-started/blink" example from ESP-IDF written in C. + +## Building + +- Make sure you have a recent nightly Swift toolchain that has Embedded Swift support. +- If needed, run export.sh to get access to the idf.py script from ESP-IDF. +- Specify the nightly toolchain to be used via the `TOOLCHAINS` environment variable and the target board type by using `idf.py set-target`. +``` console +$ cd esp32-ble-peripheral-sdk +$ export TOOLCHAINS=... +$ . /export.sh +$ idf.py set-target esp32c6 +$ idf.py build +``` + +## Running + +- Connect the Esp32-C6-Bug board over a USB cable to your Mac. Alternatively you can just connect external LED to GPIO pin 8 on any other board. +- Connect RX pin of USB-UART converter to TX0 pin of your board if you need serial ouput. You may also need to connect GND converter pin to the GND pin of the board. +- Use `idf.py` to upload the firmware and to run it: + +```console +$ idf.py flash +``` + +- Find the peripheral advertised as `ESP32-C6 XX:XX:XX:XX:XX:XX` in a Bluetooth scanner app like LightBlue or nRF Connect. + +![LightBlue](assets/images/lightblue.jpg) +![nRF Connect](assets/images/nrfconnect.jpg) diff --git a/esp32-ble-peripheral-sdk/assets/images/lightblue.png b/esp32-ble-peripheral-sdk/assets/images/lightblue.png new file mode 100644 index 0000000..19d294c Binary files /dev/null and b/esp32-ble-peripheral-sdk/assets/images/lightblue.png differ diff --git a/esp32-ble-peripheral-sdk/assets/images/nrfconnect.jpeg b/esp32-ble-peripheral-sdk/assets/images/nrfconnect.jpeg new file mode 100644 index 0000000..6034e27 Binary files /dev/null and b/esp32-ble-peripheral-sdk/assets/images/nrfconnect.jpeg differ diff --git a/esp32-ble-peripheral-sdk/main/ATTAttributePermissions.swift b/esp32-ble-peripheral-sdk/main/ATTAttributePermissions.swift new file mode 100644 index 0000000..3f1dbba --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/ATTAttributePermissions.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// ATT attribute permission bitfield values. Permissions are grouped as +/// "Access", "Encryption", "Authentication", and "Authorization". A bitmask of +/// permissions is a byte that encodes a combination of these. +@frozen +public struct ATTAttributePermissions: OptionSet, Equatable, Hashable, Sendable { + + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension ATTAttributePermissions: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: UInt8) { + self.rawValue = value + } +} + +// MARK: - CustomStringConvertible + +extension ATTAttributePermissions: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + "0x" + rawValue.toHexadecimal() + } + + /// A textual representation of the file permissions, suitable for debugging. + public var debugDescription: String { self.description } +} + +// MARK: - Options + +public extension ATTAttributePermissions { + + // Access + static var read: ATTAttributePermissions { 0x01 } + static var write: ATTAttributePermissions { 0x02 } + + // Encryption + static var encrypt: ATTAttributePermissions { [.readEncrypt, .writeEncrypt] } + static var readEncrypt: ATTAttributePermissions { 0x04 } + static var writeEncrypt: ATTAttributePermissions { 0x08 } + + // The following have no effect on Darwin + + // Authentication + static var authentication: ATTAttributePermissions { [.readAuthentication, .writeAuthentication] } + static var readAuthentication: ATTAttributePermissions { 0x10 } + static var writeAuthentication: ATTAttributePermissions { 0x20 } + + // Authorization + static var authorized: ATTAttributePermissions { 0x40 } + static var noAuthorization: ATTAttributePermissions { 0x80 } +} diff --git a/esp32-ble-peripheral-sdk/main/ATTError.swift b/esp32-ble-peripheral-sdk/main/ATTError.swift new file mode 100644 index 0000000..78789b3 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/ATTError.swift @@ -0,0 +1,225 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** + The possible errors returned by a GATT server (a remote peripheral) during Bluetooth low energy ATT transactions. + + These error constants are based on the Bluetooth ATT error codes, defined in the Bluetooth 4.0 specification. + For more information about these errors, see the Bluetooth 4.0 specification, Volume 3, Part F, Section 3.4.1.1. + */ +@frozen +public enum ATTError: UInt8, Error { + + /// Invalid Handle + /// + /// The attribute handle given was not valid on this server. + case invalidHandle = 0x01 + + /// Read Not Permitted + /// + /// The attribute cannot be read. + case readNotPermitted = 0x02 + + /// Write Not Permitted + /// + /// The attribute cannot be written. + case writeNotPermitted = 0x03 + + /// Invalid PDU + /// + /// The attribute PDU was invalid. + case invalidPDU = 0x04 + + /// Insufficient Authentication + /// + /// The attribute requires authentication before it can be read or written. + case insufficientAuthentication = 0x05 + + /// Request Not Supported + /// + /// Attribute server does not support the request received from the client. + case requestNotSupported = 0x06 + + /// Invalid Offset + /// + /// Offset specified was past the end of the attribute. + case invalidOffset = 0x07 + + /// Insufficient Authorization + /// + /// The attribute requires authorization before it can be read or written. + case insufficientAuthorization = 0x08 + + /// Prepare Queue Full + /// + /// Too many prepare writes have been queued. + case prepareQueueFull = 0x09 + + /// Attribute Not Found + /// + /// No attribute found within the given attribute handle range. + case attributeNotFound = 0x0A + + /// Attribute Not Long + /// + /// The attribute cannot be read or written using the *Read Blob Request*. + case attributeNotLong = 0x0B + + /// Insufficient Encryption Key Size + /// + /// The *Encryption Key Size* used for encrypting this link is insufficient. + case insufficientEncryptionKeySize = 0x0C + + /// Invalid Attribute Value Length + /// + /// The attribute value length is invalid for the operation. + case invalidAttributeValueLength = 0x0D + + /// Unlikely Error + /// + /// The attribute request that was requested has encountered an error that was unlikely, + /// and therefore could not be completed as requested. + case unlikelyError = 0x0E + + /// Insufficient Encryption + /// + /// The attribute requires encryption before it can be read or written. + case insufficientEncryption = 0x0F + + /// Unsupported Group Type + /// + /// The attribute type is not a supported grouping attribute as defined by a higher layer specification. + case unsupportedGroupType = 0x10 + + /// Insufficient Resources + /// + /// Insufficient Resources to complete the request. + case insufficientResources = 0x11 +} + +// MARK: - CustomStringConvertible + +extension ATTError: CustomStringConvertible { + + public var description: String { + return name + } +} + +// MARK: - Description Values + +public extension ATTError { + + var name: String { + + switch self { + case .invalidHandle: + return "Invalid Handle" + case .readNotPermitted: + return "Read Not Permitted" + case .writeNotPermitted: + return "Write Not Permitted" + case .invalidPDU: + return "Invalid PDU" + case .insufficientAuthentication: + return "Insufficient Authentication" + case .requestNotSupported: + return "Request Not Supported" + case .invalidOffset: + return "Invalid Offset" + case .insufficientAuthorization: + return "Insufficient Authorization" + case .prepareQueueFull: + return "Prepare Queue Full" + case .attributeNotFound: + return "Attribute Not Found" + case .attributeNotLong: + return "Attribute Not Long" + case .insufficientEncryptionKeySize: + return "Insufficient Encryption Key Size" + case .invalidAttributeValueLength: + return "Invalid Attribute Value Length" + case .unlikelyError: + return "Unlikely Error" + case .insufficientEncryption: + return "Insufficient Encryption" + case .unsupportedGroupType: + return "Unsupported Group Type" + case .insufficientResources: + return "Insufficient Resources" + } + } + + #if !hasFeature(Embedded) + var errorDescription: String { + + switch self { + case .invalidHandle: + return "The attribute handle given was not valid on this server." + case .readNotPermitted: + return "The attribute cannot be read." + case .writeNotPermitted: + return "The attribute cannot be written." + case .invalidPDU: + return "The attribute PDU was invalid." + case .insufficientAuthentication: + return "The attribute requires authentication before it can be read or written." + case .requestNotSupported: + return "Attribute server does not support the request received from the client." + case .invalidOffset: + return "Offset specified was past the end of the attribute." + case .insufficientAuthorization: + return "The attribute requires authorization before it can be read or written." + case .prepareQueueFull: + return "Too many prepare writes have been queued." + case .attributeNotFound: + return "No attribute found within the given attri- bute handle range." + case .attributeNotLong: + return "The attribute cannot be read using the Read Blob Request." + case .insufficientEncryptionKeySize: + return "The Encryption Key Size used for encrypting this link is insufficient." + case .invalidAttributeValueLength: + return "The attribute value length is invalid for the operation." + case .unlikelyError: + return "The attribute request that was requested has encountered an error that was unlikely, and therefore could not be completed as requested." + case .insufficientEncryption: + return "The attribute requires encryption before it can be read or written." + case .unsupportedGroupType: + return "The attribute type is not a supported grouping attribute as defined by a higher layer specification." + case .insufficientResources: + return "Insufficient Resources to complete the request." + } + } + #endif +} + +// MARK: - CustomNSError + +#if canImport(Foundation) +extension ATTError: CustomNSError { + + public static var errorDomain: String { + return "org.pureswift.Bluetooth.ATTError" + } + + public var errorCode: Int { + return Int(rawValue) + } + + public var errorUserInfo: [String: Any] { + + return [ + NSLocalizedDescriptionKey: name, + NSLocalizedFailureReasonErrorKey: errorDescription + ] + } +} +#endif diff --git a/esp32-ble-peripheral-sdk/main/BluetoothAddress.swift b/esp32-ble-peripheral-sdk/main/BluetoothAddress.swift new file mode 100644 index 0000000..733d1d7 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/BluetoothAddress.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Bluetooth address. +@frozen +public struct BluetoothAddress: Comparable, Sendable { + + // MARK: - Properties + + /// Underlying address bytes (host endianess). + public var bytes: ByteValue + + // MARK: - Initialization + + /// Initialize with the specifed bytes (in host endianess). + public init(bytes: ByteValue = (0, 0, 0, 0, 0, 0)) { + self.bytes = bytes + } +} + +public extension BluetoothAddress { + + /// The minimum representable value in this type. + static var min: BluetoothAddress { BluetoothAddress(bytes: (.min, .min, .min, .min, .min, .min)) } + + /// The maximum representable value in this type. + static var max: BluetoothAddress { BluetoothAddress(bytes: (.max, .max, .max, .max, .max, .max)) } + + /// A zero address. + static var zero: BluetoothAddress { BluetoothAddress(bytes: (.zero, .zero, .zero, .zero, .zero, .zero)) } +} + +// MARK: - Equatable + +extension BluetoothAddress: Equatable { + + public static func == (lhs: BluetoothAddress, rhs: BluetoothAddress) -> Bool { + return lhs.bytes.0 == rhs.bytes.0 + && lhs.bytes.1 == rhs.bytes.1 + && lhs.bytes.2 == rhs.bytes.2 + && lhs.bytes.3 == rhs.bytes.3 + && lhs.bytes.4 == rhs.bytes.4 + && lhs.bytes.5 == rhs.bytes.5 + } +} + +// MARK: - Hashable + +extension BluetoothAddress: Hashable { + + public func hash(into hasher: inout Hasher) { + Swift.withUnsafeBytes(of: bytes) { hasher.combine(bytes: $0) } + } +} + +// MARK: - ByteValue + +extension BluetoothAddress: ByteValue { + + /// Raw Bluetooth Address 6 byte (48 bit) value. + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public static var bitWidth: Int { 48 } +} + +// MARK: - Byte Swap + +extension BluetoothAddress: ByteSwap { + + /// A representation of this address with the byte order swapped. + public var byteSwapped: BluetoothAddress { + return BluetoothAddress(bytes: (bytes.5, bytes.4, bytes.3, bytes.2, bytes.1, bytes.0)) + } +} + +// MARK: - RawRepresentable + +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 + guard characters.count == 17, + let separator = ":".utf8.first + else { return nil } + + var bytes: ByteValue = (0, 0, 0, 0, 0, 0) + + let components = characters.split(whereSeparator: { $0 == separator }) + + guard components.count == 6 + else { return nil } + + for (index, subsequence) in components.enumerated() { + + guard subsequence.count == 2, + let byte = UInt8(hexadecimal: subsequence) + else { return nil } + + withUnsafeMutablePointer(to: &bytes) { + $0.withMemoryRebound(to: UInt8.self, capacity: 6) { + $0.advanced(by: index).pointee = byte + } + } + } + + self.init(bigEndian: BluetoothAddress(bytes: bytes)) + } + + /// Convert a Bluetooth Address to its big endian string representation (e.g. `00:1A:7D:DA:71:13`). + public var rawValue: String { + let bytes = self.bigEndian.bytes + return bytes.0.toHexadecimal() + + ":" + bytes.1.toHexadecimal() + + ":" + bytes.2.toHexadecimal() + + ":" + bytes.3.toHexadecimal() + + ":" + bytes.4.toHexadecimal() + + ":" + bytes.5.toHexadecimal() + } +} + +// MARK: - CustomStringConvertible + +extension BluetoothAddress: CustomStringConvertible { + + public var description: String { rawValue } +} + +// MARK: - Data + +extension BluetoothAddress: DataConvertible { + + public init?(data: Data) { + guard data.count == Self.length + else { return nil } + self.bytes = (data[0], data[1], data[2], data[3], data[4], data[5]) + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension BluetoothAddress: Codable { } +#endif diff --git a/esp32-ble-peripheral-sdk/main/BluetoothUUID.swift b/esp32-ble-peripheral-sdk/main/BluetoothUUID.swift new file mode 100644 index 0000000..95859f4 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/BluetoothUUID.swift @@ -0,0 +1,387 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Foundation) +import Foundation +#endif + +/// Bluetooth UUID +@frozen +public enum BluetoothUUID: Hashable, Sendable { + + case bit16(UInt16) + case bit32(UInt32) + case bit128(UInt128) +} + +public extension BluetoothUUID { + + /// Creates a random 128-bit Bluetooth UUID. + init() { + self.init(uuid: UUID()) + } +} + +// MARK: - Equatable + +extension BluetoothUUID: Equatable { + + public static func == (lhs: BluetoothUUID, rhs: BluetoothUUID) -> Bool { + switch (lhs, rhs) { + case let (.bit16(lhsValue), .bit16(rhsValue)): + return lhsValue == rhsValue + case let (.bit32(lhsValue), .bit32(rhsValue)): + return lhsValue == rhsValue + case let (.bit128(lhsValue), .bit128(rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } +} + +// MARK: - CustomStringConvertible + +extension BluetoothUUID: CustomStringConvertible { + + public var description: String { + #if !os(WASI) && !hasFeature(Embedded) + if let name = self.name { + return "\(rawValue) (\(name))" + } else { + return rawValue + } + #else + return rawValue + #endif + } +} + +// MARK: - LosslessStringConvertible + +extension BluetoothUUID: LosslessStringConvertible { + + public init?(_ string: String) { + #if !os(WASI) && !hasFeature(Embedded) + var rawValue = string + var name: String? + // Find UUID name + let components = string.split( + maxSplits: 1, + omittingEmptySubsequences: true, + whereSeparator: { $0 == " " } + ) + if components.count == 2 { + rawValue = String(components[0]) + name = String(components[1]) + // remove parenthesis + if name?.first == "(", name?.last == ")" { + name?.removeFirst() + name?.removeLast() + } + } + self.init(rawValue: rawValue) + // validate name + if let name { + guard name == self.name else { + return nil + } + } + #else + self.init(rawValue: string) + #endif + } +} + +// MARK: - RawRepresentable + +extension BluetoothUUID: RawRepresentable { + + /// Initialize from a UUID string (in big endian representation). + /// + /// - Example: "60F14FE2-F972-11E5-B84F-23E070D5A8C7", "000000A8", "00A8" + public init?(rawValue: String) { + + switch rawValue.utf8.count { + + case 4: + + guard let value = UInt16(hexadecimal: rawValue) + else { return nil } + self = .bit16(value) + + case 8: + + guard let value = UInt32(hexadecimal: rawValue) + else { return nil } + self = .bit32(value) + + case 36: + + guard let uuid = UInt128(uuidString: rawValue) + else { return nil } + self = .bit128(uuid) + + default: + return nil + } + } + + public var rawValue: String { + switch self { + case let .bit16(value): + return value.toHexadecimal() + case let .bit32(value): + return value.toHexadecimal() + case let .bit128(value): + return value.uuidString + } + } +} + +// MARK: - Data + +extension BluetoothUUID: DataConvertible { + + public init?(data: Data) { + + guard let length = Length(rawValue: data.count) + else { return nil } + + switch length { + + // 16 bit + case .bit16: + + let value = UInt16(bytes: (data[0], data[1])) + self = .bit16(value) + + // 32 bit + case .bit32: + + let value = UInt32(bytes: (data[0], data[1], data[2], data[3])) + self = .bit32(value) + + // 128 bit + case .bit128: + + let value = UInt128(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15])) + self = .bit128(value) + } + } + + public var dataLength: Int { + length.rawValue + } + + public func append(to data: inout Data) where Data : DataContainer { + switch self { + case let .bit16(value): + data += value + case let .bit32(value): + data += value + case let .bit128(value): + data += value + } + } +} + +internal extension BluetoothUUID { + + /// Number of bytes to represent Bluetooth UUID. + enum Length: Int { + + case bit16 = 2 + case bit32 = 4 + case bit128 = 16 + } + + private var length: Length { + + switch self { + case .bit16: return .bit16 + case .bit32: return .bit32 + case .bit128: return .bit128 + } + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension BluetoothUUID: Codable { } +#endif + +// MARK: - Byte Swap + +extension BluetoothUUID: ByteSwap { + + /// A representation of this Bluetooth UUID with the byte order swapped. + public var byteSwapped: BluetoothUUID { + + switch self { + case let .bit16(value): return .bit16(value.byteSwapped) + case let .bit32(value): return .bit32(value.byteSwapped) + case let .bit128(value): return .bit128(value.byteSwapped) + } + } +} + +// MARK: - UInt128 Conversion + +public extension BluetoothUUID { + + /// Bluetooth Base UUID (big endian) + internal static var baseUUID: UInt128 { return UInt128(bytes: (0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB)) } + +} + +public extension UInt128 { + + /// Forceably convert `BluetoothUUID` to `UInt128` value. + init(_ bluetoothUUID: BluetoothUUID) { + + switch bluetoothUUID { + + case let .bit16(value): + + let bytes = value.bigEndian.bytes + var bigEndianValue = BluetoothUUID.baseUUID + + bigEndianValue.bytes.2 = bytes.0 + bigEndianValue.bytes.3 = bytes.1 + + self = UInt128(bigEndian: bigEndianValue) + + case let .bit32(value): + + let bytes = value.bigEndian.bytes + var bigEndianValue = BluetoothUUID.baseUUID + + bigEndianValue.bytes.0 = bytes.0 + bigEndianValue.bytes.1 = bytes.1 + bigEndianValue.bytes.2 = bytes.2 + bigEndianValue.bytes.3 = bytes.3 + + self = UInt128(bigEndian: bigEndianValue) + + case let .bit128(value): + + self = value + } + } +} + +public extension BluetoothUUID { + + /// Forceably convert `BluetoothUUID` to `UInt128` value. + var bit128: BluetoothUUID { + let value = UInt128(self) + return .bit128(value) + } +} + +internal extension UUID { + + @inline(__always) + func bluetoothPrefix() -> (UInt8, UInt8, UInt8, UInt8)? { + + // big endian + let baseUUID = BluetoothUUID.baseUUID.bytes + + guard bytes.4 == baseUUID.4, + bytes.5 == baseUUID.5, + bytes.6 == baseUUID.6, + bytes.7 == baseUUID.7, + bytes.8 == baseUUID.8, + bytes.9 == baseUUID.9, + bytes.10 == baseUUID.10, + bytes.11 == baseUUID.11, + bytes.12 == baseUUID.12, + bytes.13 == baseUUID.13, + bytes.14 == baseUUID.14, + bytes.15 == baseUUID.15 + else { return nil } + + return (bytes.0, bytes.1, bytes.2, bytes.3) + } +} + +public extension UInt16 { + + /// Attempt to extract Bluetooth 16-bit UUID from standard 128-bit UUID. + init?(bluetooth uuid: UUID) { + + guard let prefixBytes = uuid.bluetoothPrefix(), + prefixBytes.0 == 0, + prefixBytes.1 == 0 + else { return nil } + + self.init(bigEndian: UInt16(bytes: (prefixBytes.2, prefixBytes.3))) + } +} + +public extension UInt32 { + + /// Attempt to extract Bluetooth 32-bit UUID from standard 128-bit UUID. + init?(bluetooth uuid: UUID) { + + guard let prefixBytes = uuid.bluetoothPrefix() + else { return nil } + + self.init(bigEndian: UInt32(bytes: (prefixBytes.0, prefixBytes.1, prefixBytes.2, prefixBytes.3))) + } +} + +// MARK: - NSUUID Conversion + +public extension BluetoothUUID { + + /// Initialize from a `Foundation.UUID`. + init(uuid: UUID) { + self = .bit128(UInt128(uuid: uuid)) + } +} + +public extension UUID { + + /// Initialize and convert from a Bluetooth UUID. + init(bluetooth uuid: BluetoothUUID) { + self.init(UInt128(uuid)) + } +} + +// MARK: - CoreBluetooth + +#if canImport(CoreBluetooth) +import CoreBluetooth + +public extension BluetoothUUID { + + init(_ coreBluetooth: CBUUID) { + + guard let uuid = BluetoothUUID(data: coreBluetooth.data) + else { fatalError("Could not create Bluetooth UUID from \(coreBluetooth)") } + + // CBUUID is always big endian + self.init(bigEndian: uuid) + } +} + +public extension CBUUID { + + convenience init(_ bluetoothUUID: BluetoothUUID) { + self.init(data: Data(bluetoothUUID.bigEndian)) + } +} + +#endif diff --git a/esp32-ble-peripheral-sdk/main/BridgingHeader.h b/esp32-ble-peripheral-sdk/main/BridgingHeader.h new file mode 100644 index 0000000..ecaeded --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/BridgingHeader.h @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "sdkconfig.h" +#include "nimble/ble.h" +#include "nimble/transport.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "esp_bt.h" +#include "esp_task.h" +#include "esp_nimble_cfg.h" +#include "esp_log.h" +//#include "nvs_flash.h" +#include "esp_bt.h" +#include "os/os.h" +#include "led_strip.h" + +#ifndef MYNEWT_VAL_BLE_LL_WHITELIST_SIZE +#define MYNEWT_VAL_BLE_LL_WHITELIST_SIZE CONFIG_BT_NIMBLE_WHITELIST_SIZE +#endif + +// Private functions +int ble_uuid_length(const ble_uuid_t *uuid); \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/ByteSwap.swift b/esp32-ble-peripheral-sdk/main/ByteSwap.swift new file mode 100644 index 0000000..94c40eb --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/ByteSwap.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A Bluetooth value that is stored in the CPU native endianess format. +public protocol ByteSwap { + + /// A representation of this integer with the byte order swapped. + var byteSwapped: Self { get } +} + +public extension ByteSwap { + + /// Creates an instance from its little-endian representation, changing the + /// byte order if necessary. + /// + /// - Parameter value: A value to use as the little-endian representation of + /// the new instance. + init(littleEndian value: Self) { + #if _endian(little) + self = value + #else + self = value.byteSwapped + #endif + } + + /// Creates an instance from its big-endian representation, changing the byte + /// order if necessary. + /// + /// - Parameter value: A value to use as the big-endian representation of the + /// new instance. + init(bigEndian value: Self) { + #if _endian(big) + self = value + #else + self = value.byteSwapped + #endif + } + + /// The little-endian representation of this value. + /// + /// If necessary, the byte order of this value is reversed from the typical + /// byte order of this address. On a little-endian platform, for any + /// address `x`, `x == x.littleEndian`. + var littleEndian: Self { + #if _endian(little) + return self + #else + return byteSwapped + #endif + } + + /// The big-endian representation of this value. + /// + /// If necessary, the byte order of this value is reversed from the typical + /// byte order of this address. On a big-endian platform, for any + /// address `x`, `x == x.bigEndian`. + var bigEndian: Self { + #if _endian(big) + return self + #else + return byteSwapped + #endif + } +} diff --git a/esp32-ble-peripheral-sdk/main/ByteValue.swift b/esp32-ble-peripheral-sdk/main/ByteValue.swift new file mode 100644 index 0000000..9d41ba8 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/ByteValue.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Stores a primitive value. +/// +/// Useful for Swift wrappers for primitive byte types. +public protocol ByteValue: Equatable { + + associatedtype ByteValue + + /// Returns the the primitive byte type. + var bytes: ByteValue { get } + + /// Initializes with the primitive the primitive byte type. + init(bytes: ByteValue) + + /// The number of bits used for the underlying binary representation of values of this type. + static var bitWidth: Int { get } +} + +// MARK: - Data Convertible + +public extension ByteValue { + + /// Size of value in bytes. + static var length: Int { bitWidth / 8 } + + @inline(__always) + func withUnsafeBytes(_ body: (UnsafeBufferPointer) throws -> R) rethrows -> R { + return try Swift.withExtendedLifetime(self) { + try Swift.withUnsafeBytes(of: bytes) { rawBuffer in + return try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in + return try body(buffer) + } + } + } + } +} + +public extension ByteValue where Self: DataConvertible { + + /// Append data representation into buffer. + func append(to data: inout Data) { + withUnsafeBytes { buffer in + data += buffer + } + } + + var dataLength: Int { + Self.length + } +} + +// MARK: - Equatable + +extension ByteValue where Self: Equatable { + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.withUnsafeBytes { (b1) in + rhs.withUnsafeBytes { (b2) in + b1.elementsEqual(b2) + } + } + } +} + +// MARK: - Hashable + +extension ByteValue where Self: Hashable { + + public func hash(into hasher: inout Hasher) { + Swift.withUnsafeBytes(of: bytes) { hasher.combine(bytes: $0) } + } +} + +// MARK: - Comparable + +extension ByteValue where Self: Comparable { + + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.withUnsafeBytes { (b1) in + rhs.withUnsafeBytes { (b2) in + _memcmp( + UnsafeRawPointer(b1.baseAddress), + UnsafeRawPointer(b2.baseAddress), + Self.length + ) < 0 + } + } + } + + public static func > (lhs: Self, rhs: Self) -> Bool { + lhs.withUnsafeBytes { (b1) in + rhs.withUnsafeBytes { (b2) in + _memcmp( + UnsafeRawPointer(b1.baseAddress), + UnsafeRawPointer(b2.baseAddress), + Self.length + ) > 0 + } + } + } +} + +// MARK: - CustomStringConvertible + +extension ByteValue where Self: CustomStringConvertible { + + public var description: String { + withUnsafeBytes { + "0x" + $0.toHexadecimal() + } + } +} diff --git a/esp32-ble-peripheral-sdk/main/CMakeLists.txt b/esp32-ble-peripheral-sdk/main/CMakeLists.txt new file mode 100644 index 0000000..103aea7 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/CMakeLists.txt @@ -0,0 +1,98 @@ +# Register the app as an IDF component +idf_component_register( + SRCS /dev/null # We don't have any C++ sources + PRIV_INCLUDE_DIRS "." + LDFRAGMENTS "linker.lf" + REQUIRES bt driver +) + +idf_build_get_property(target IDF_TARGET) +idf_build_get_property(arch IDF_TARGET_ARCH) + +if("${arch}" STREQUAL "xtensa") + message(FATAL_ERROR "Not supported target: ${target}") +endif() + +if(${target} STREQUAL "esp32c2" OR ${target} STREQUAL "esp32c3") + set(march_flag "rv32imc_zicsr_zifencei") + set(mabi_flag "ilp32") +elseif(${target} STREQUAL "esp32p4") + set(march_flag "rv32imafc_zicsr_zifencei") + set(mabi_flag "ilp32f") +else() + set(march_flag "rv32imac_zicsr_zifencei") + set(mabi_flag "ilp32") +endif() + +# Clear the default COMPILE_OPTIONS which include a lot of C/C++ specific compiler flags that the Swift compiler will not accept +get_target_property(var ${COMPONENT_LIB} COMPILE_OPTIONS) +set_target_properties(${COMPONENT_LIB} PROPERTIES COMPILE_OPTIONS "") + +# Compute -Xcc flags to set up the C and C++ header search paths for Swift (for bridging header). +set(SWIFT_INCLUDES) +foreach(dir ${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}) + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-Xcc ") + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-I${dir} ") +endforeach() +foreach(dir ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-Xcc ") + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-I${dir} ") +endforeach() + +# Swift compiler flags to build in Embedded Swift mode, optimize for size, choose the right ISA, ABI, etc. +target_compile_options(${COMPONENT_LIB} PUBLIC "$<$:SHELL: + -target riscv32-none-none-eabi + -Xfrontend -function-sections -enable-experimental-feature Embedded -wmo -parse-as-library -Osize + -Xcc -march=${march_flag} -Xcc -mabi=${mabi_flag} + + -pch-output-dir /tmp + -Xfrontend -enable-single-module-llvm-emission + + ${SWIFT_INCLUDES} + + -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h + >") + +# Enable Swift support in CMake, force Whole Module builds (required by Embedded Swift), and use "CMAKE_Swift_COMPILER_WORKS" to +# skip the trial compilations which don't (yet) correctly work when cross-compiling. +set(CMAKE_Swift_COMPILER_WORKS YES) +set(CMAKE_Swift_COMPILATION_MODE_DEFAULT wholemodule) +set(CMAKE_Swift_COMPILATION_MODE wholemodule) +enable_language(Swift) + +# List of Swift source files to build. +target_sources(${COMPONENT_LIB} + PRIVATE + Main.swift + Led.swift + LedStrip.swift + Error.swift + NimBLE.swift + BluetoothAddress.swift + BluetoothUUID.swift + ByteValue.swift + ByteSwap.swift + CompanyIdentifier.swift + LowEnergyAddressType.swift + LowEnergyAdvertisingData.swift + Data.swift + String.swift + System.swift + Hexadecimal.swift + Encoder.swift + GAPData.swift + GAPDataType.swift + GAPFlags.swift + GAPShortLocalName.swift + GAPManufacturerSpecificData.swift + UInt128.swift + UUID.swift + iBeacon.swift + Integer.swift + ATTError.swift + ATTAttributePermissions.swift + GATTAttributes.swift + GATTCharacteristicProperties.swift + Peer.swift + PeripheralProtocol.swift +) diff --git a/esp32-ble-peripheral-sdk/main/CompanyIdentifier.swift b/esp32-ble-peripheral-sdk/main/CompanyIdentifier.swift new file mode 100644 index 0000000..20fa3a7 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/CompanyIdentifier.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Company identifiers are unique numbers assigned by the Bluetooth SIG to member companies requesting one. +/// +/// Each Bluetooth SIG member assigned a Company Identifier may use the assigned value for any/all of the following: +/// +/// * LMP_CompID (refer to the Bluetooth® Core Specification) +/// * Company Identifier Code used in Manufacturer Specific Data type used for EIR and Advertising Data Types (refer to CSSv1 or later) +/// * Company ID for vendor specific codecs (refer to Vol. 2, Part E, of the Bluetooth Core Specification, v4.1 or later) +/// * As the lower 16 bits of the Vendor ID for designating Vendor Specific A2DP Codecs (refer to the A2DP v1.3 or later +/// * VendorID Attribute in Device ID service record (when VendorIDSourceAttribute equals 0x0001, refer toDevice ID Profile) +/// * 802.11_PAL_Company_Identifier (refer to Bluetooth Core Specification v3.0 + HS or later) +/// * TCS Company ID (refer to Telephony Control Protocol [[WITHDRAWN](https://www.bluetooth.com/specifications)]) +/// +/// Each of the adopted specifications listed above can be found on the [Adopted Specifications Page](https://www.bluetooth.com/specifications) +/// unless it is otherwise indicated as withdrawn. +/// +/// - SeeAlso: [Company Identifiers](https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers) +public struct CompanyIdentifier: RawRepresentable, Equatable, Hashable, Sendable { + + public var rawValue: UInt16 + + public init(rawValue: UInt16) { + self.rawValue = rawValue + } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension CompanyIdentifier: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: UInt16) { + self.init(rawValue: value) + } +} + +// MARK: - CustomStringConvertible + +extension CompanyIdentifier: CustomStringConvertible { + + public var description: String { + return rawValue.description + } +} diff --git a/esp32-ble-peripheral-sdk/main/Data.swift b/esp32-ble-peripheral-sdk/main/Data.swift new file mode 100644 index 0000000..01ad4a5 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Data.swift @@ -0,0 +1,197 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Data container type. +public protocol DataContainer: RandomAccessCollection where Self.Index == Int, Self.Element == UInt8, Self: Hashable, Self: Sendable { + + init() + + init (_ collection: C) where C.Element == UInt8 + + mutating func reserveCapacity(_ capacity: Int) + + subscript(index: Int) -> UInt8 { get } + + mutating func append(_ newElement: UInt8) + + mutating func append(_ pointer: UnsafePointer, count: Int) + + mutating func append (contentsOf bytes: C) where C.Element == UInt8 + + static func += (lhs: inout Self, rhs: UInt8) + + static func += (lhs: inout Self, rhs: C) where C.Element == UInt8 + + /// Return a new copy of the data in a specified range. + /// + /// - parameter range: The range to copy. + func subdata(in range: Range) -> Self +} + +extension LowEnergyAdvertisingData: DataContainer { + + public mutating func reserveCapacity(_ capacity: Int) { + // does nothing + } + + public func subdata(in range: Range) -> LowEnergyAdvertisingData { + var data = LowEnergyAdvertisingData() + data.length = UInt8(range.count) + for (newIndex, oldIndex) in range.enumerated() { + data[newIndex] = self[oldIndex] + } + return data + } +} + +extension Array: DataContainer where Self.Element == UInt8 { + + public static func += (lhs: inout Array, rhs: UInt8) { + lhs.append(rhs) + } + + public mutating func append(_ pointer: UnsafePointer, count: Int) { + let newCapacity = self.count + count + self.reserveCapacity(newCapacity) + for index in 0 ..< count { + self.append(pointer[index]) + } + } + + public func subdata(in range: Range) -> [UInt8] { + .init(self[range]) + } +} + +// MARK: - DataConvertible + +/// Can be converted into data. +public protocol DataConvertible { + + /// Initialize from data. + init?(data: Data) + + /// Append data representation into buffer. + func append(to data: inout Data) + + /// Length of value when encoded into data. + var dataLength: Int { get } +} + +public extension DataConvertible { + + /// Append data representation into buffer. + static func += (data: inout Data, value: Self) { + value.append(to: &data) + } +} + +public extension Array where Element: DataConvertible { + + /// Append data representation into buffer. + static func += (data: inout T, value: Self) { + value.forEach { data += $0 } + } +} + +public extension DataContainer { + + /// Initialize data with contents of value. + init (_ value: T) { + let length = value.dataLength + self.init() + self.reserveCapacity(length) + self += value + assert(self.count == length) + } + + mutating func append (_ value: T) { + self += value + } +} + +// MARK: - UnsafeDataConvertible + +/// Internal Data casting protocol +internal protocol UnsafeDataConvertible { } + +extension UnsafeDataConvertible { + + var unsafeDataLength: Int { + MemoryLayout.size + } + + func unsafeAppend (to data: inout T) { + let length = unsafeDataLength + withUnsafePointer(to: self) { + $0.withMemoryRebound(to: UInt8.self, capacity: length) { + data.append($0, count: length) + } + } + } +} + +extension UInt16: UnsafeDataConvertible { } +extension UInt32: UnsafeDataConvertible { } +extension UInt64: UnsafeDataConvertible { } +extension UInt128: UnsafeDataConvertible { } +extension BluetoothAddress: UnsafeDataConvertible { } + +extension UInt16: DataConvertible { + + public init?(data: Data) { + guard data.count == MemoryLayout.size else { + return nil + } + self.init(bytes: (data[0], data[1])) + } + + public func append(to data: inout Data) { + unsafeAppend(to: &data) + } + + /// Length of value when encoded into data. + public var dataLength: Int { unsafeDataLength } +} + +extension UInt32: DataConvertible { + + public init?(data: Data) { + guard data.count == MemoryLayout.size else { + return nil + } + self.init(bytes: (data[0], data[1], data[2], data[3])) + } + + public func append(to data: inout Data) { + unsafeAppend(to: &data) + } + + /// Length of value when encoded into data. + public var dataLength: Int { unsafeDataLength } +} + +extension UInt64: DataConvertible { + + public init?(data: Data) { + guard data.count == MemoryLayout.size else { + return nil + } + self.init(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7])) + } + + public func append(to data: inout Data) { + unsafeAppend(to: &data) + } + + /// Length of value when encoded into data. + public var dataLength: Int { unsafeDataLength } +} diff --git a/esp32-ble-peripheral-sdk/main/Encoder.swift b/esp32-ble-peripheral-sdk/main/Encoder.swift new file mode 100644 index 0000000..3a7d067 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Encoder.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// GAP Data Encoder +public struct GAPDataEncoder { + + // MARK: - Methods + + internal static func encode( + _ value: T, + length: Int? = nil, + to data: inout Data + ) where T: GAPData { + let length = length ?? value.dataLength // try to use precalculated length + data += UInt8(length + 1) + data += T.dataType.rawValue + value.append(to: &data) + } +} + +// Generic specializations + +public extension GAPDataEncoder { + + static func encode(_ value: T) -> Data { + var data = Data() + data.reserveCapacity(value.dataLength + 2) + Self.encode(value, to: &data) + return data + } + + static func encode(_ value0: T0, _ value1: T1) -> Data { + var data = Data() + let length = value0.dataLength + + value1.dataLength + + (2 * 2) + data.reserveCapacity(length) + Self.encode(value0, to: &data) + Self.encode(value1, to: &data) + return data + } + + static func encode(_ value0: T0, _ value1: T1, _ value2: T2) -> Data { + var data = Data() + let length = value0.dataLength + + value1.dataLength + + value2.dataLength + + (2 * 3) + data.reserveCapacity(length) + Self.encode(value0, to: &data) + Self.encode(value1, to: &data) + Self.encode(value2, to: &data) + return data + } + + static func encode(_ value0: T0, _ value1: T1, _ value2: T2, _ value3: T3) -> Data { + var data = Data() + let length = value0.dataLength + + value1.dataLength + + value2.dataLength + + value3.dataLength + + (2 * 4) + data.reserveCapacity(length) + Self.encode(value0, to: &data) + Self.encode(value1, to: &data) + Self.encode(value2, to: &data) + Self.encode(value3, to: &data) + return data + } +} diff --git a/esp32-ble-peripheral-sdk/main/Error.swift b/esp32-ble-peripheral-sdk/main/Error.swift new file mode 100644 index 0000000..12089ee --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Error.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// ESP32 error codes +public enum ESPError: Int32, Error, Sendable { + + case unknown = -1 // ESP_FAIL + case noMemory = 0x101 // ESP_ERR_NO_MEM + case invalidArg = 0x102 // ESP_ERR_INVALID_ARG + case invalidState = 0x103 // ESP_ERR_INVALID_STATE + case invalidSize = 0x104 // ESP_ERR_INVALID_SIZE + case notFound = 0x105 // ESP_ERR_NOT_FOUND + case notSupported = 0x106 // ESP_ERR_NOT_SUPPORTED + case timeout = 0x107 // ESP_ERR_TIMEOUT + case invalidResponse = 0x108 // ESP_ERR_INVALID_RESPONSE + case invalidCrc = 0x109 // ESP_ERR_INVALID_CRC + case invalidVersion = 0x10a // ESP_ERR_INVALID_VERSION + case invalidMac = 0x10b // ESP_ERR_INVALID_MAC + case notFinished = 0x10c // ESP_ERR_NOT_FINISHED + case notAllowed = 0x10d // ESP_ERR_NOT_ALLOWED + + // NVS related error codes + case nvsBase = 0x1100 // ESP_ERR_NVS_BASE + case nvsNotInitialized = 0x1101 // ESP_ERR_NVS_NOT_INITIALIZED + case nvsNotFound = 0x1102 // ESP_ERR_NVS_NOT_FOUND + case nvsTypeMismatch = 0x1103 // ESP_ERR_NVS_TYPE_MISMATCH + case nvsReadOnly = 0x1104 // ESP_ERR_NVS_READ_ONLY + case nvsNotEnoughSpace = 0x1105 // ESP_ERR_NVS_NOT_ENOUGH_SPACE + case nvsInvalidName = 0x1106 // ESP_ERR_NVS_INVALID_NAME + case nvsInvalidHandle = 0x1107 // ESP_ERR_NVS_INVALID_HANDLE + case nvsRemoveFailed = 0x1108 // ESP_ERR_NVS_REMOVE_FAILED + case nvsKeyTooLong = 0x1109 // ESP_ERR_NVS_KEY_TOO_LONG + case nvsPageFull = 0x110a // ESP_ERR_NVS_PAGE_FULL + case nvsInvalidState = 0x110b // ESP_ERR_NVS_INVALID_STATE + + // ULP related error codes + case ulpBase = 0x1200 // ESP_ERR_ULP_BASE + case ulpSizeTooBig = 0x1201 // ESP_ERR_ULP_SIZE_TOO_BIG + case ulpInvalidLoadAddr = 0x1202 // ESP_ERR_ULP_INVALID_LOAD_ADDR + case ulpDuplicateLabel = 0x1203 // ESP_ERR_ULP_DUPLICATE_LABEL + case ulpUndefinedLabel = 0x1204 // ESP_ERR_ULP_UNDEFINED_LABEL + + // OTA related error codes + case otaBase = 0x1500 // ESP_ERR_OTA_BASE + case otaPartitionConflict = 0x1501 // ESP_ERR_OTA_PARTITION_CONFLICT + case otaValidateFailed = 0x1503 // ESP_ERR_OTA_VALIDATE_FAILED + + // Wi-Fi related error codes + case wifiBase = 0x3000 // ESP_ERR_WIFI_BASE + case wifiNotInit = 0x3001 // ESP_ERR_WIFI_NOT_INIT + case wifiNotStarted = 0x3002 // ESP_ERR_WIFI_NOT_STARTED + case wifiNotStopped = 0x3003 // ESP_ERR_WIFI_NOT_STOPPED + case wifiIf = 0x3004 // ESP_ERR_WIFI_IF + case wifiMode = 0x3005 // ESP_ERR_WIFI_MODE + + // Mesh network related error codes + case meshBase = 0x4000 // ESP_ERR_MESH_BASE + case meshNotInit = 0x4002 // ESP_ERR_MESH_NOT_INIT + case meshNotStart = 0x4004 // ESP_ERR_MESH_NOT_START + case meshNoMemory = 0x4007 // ESP_ERR_MESH_NO_MEMORY + + // Networking error codes + case espNetifBase = 0x5000 // ESP_ERR_ESP_NETIF_BASE + case espNetifInvalidParams = 0x5001 // ESP_ERR_ESP_NETIF_INVALID_PARAMS + + // Flash error codes + case flashBase = 0x6000 // ESP_ERR_FLASH_BASE + case flashOpFail = 0x6001 // ESP_ERR_FLASH_OP_FAIL + case flashNotInitialized = 0x6003 // ESP_ERR_FLASH_NOT_INITIALISED + + // HTTP error codes + case httpBase = 0x7000 // ESP_ERR_HTTP_BASE + case httpMaxRedirect = 0x7001 // ESP_ERR_HTTP_MAX_REDIRECT + case httpConnect = 0x7002 // ESP_ERR_HTTP_CONNECT + + // TLS error codes + case espTlsBase = 0x8000 // ESP_ERR_ESP_TLS_BASE + case espTlsCannotResolveHost = 0x8001 // ESP_ERR_ESP_TLS_CANNOT_RESOLVE_HOSTNAME + + // Hardware crypto error codes + case hwCryptoBase = 0xc000 // ESP_ERR_HW_CRYPTO_BASE + case hwCryptoDsHmacFail = 0xc001 // ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL + + // Memory protection error codes + case memprotBase = 0xd000 // ESP_ERR_MEMPROT_BASE + case memprotMemoryTypeInvalid = 0xd001 // ESP_ERR_MEMPROT_MEMORY_TYPE_INVALID + + // TCP transport error codes + case tcpTransportBase = 0xe000 // ESP_ERR_TCP_TRANSPORT_BASE + case tcpTransportConnectionTimeout = 0xe001 // ESP_ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT + + // NVS secure error codes + case nvsSecBase = 0xf000 // ESP_ERR_NVS_SEC_BASE + case nvsSecHmacKeyNotFound = 0xf001 // ESP_ERR_NVS_SEC_HMAC_KEY_NOT_FOUND + case nvsSecHmacKeyGenerationFailed = 0xf003 // ESP_ERR_NVS_SEC_HMAC_KEY_GENERATION_FAILED +} + +extension ESPError: CustomStringConvertible { + + public var description: String { + "0x" + rawValue.toHexadecimal() + } +} + +/// NimBLE error codes +public struct NimBLEError: Error, RawRepresentable, Equatable, Hashable, Sendable { + + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } +} + +internal extension Int32 { + + func throwsError() throws(NimBLEError) { + guard self == 0 else { + throw NimBLEError(rawValue: self) + } + } + + func throwsESPError() throws(ESPError) { + guard self == 0 else { + throw ESPError(rawValue: self) ?? .unknown + } + } +} \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/GAPData.swift b/esp32-ble-peripheral-sdk/main/GAPData.swift new file mode 100644 index 0000000..56dc57a --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GAPData.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** +Generic Access Profile + +- SeeAlso: +[Generic Access Profile](https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile) +*/ +public protocol GAPData { + + /// Generic Access Profile data type. + static var dataType: GAPDataType { get } + + /// Initialize from data. + init?(data: Data) + + /// Append data representation into buffer. + func append(to data: inout Data) + + /// Length of value when encoded into data. + var dataLength: Int { get } +} diff --git a/esp32-ble-peripheral-sdk/main/GAPDataType.swift b/esp32-ble-peripheral-sdk/main/GAPDataType.swift new file mode 100644 index 0000000..55ad670 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GAPDataType.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Generic Access Profile Data Type +/// +/// ​​Assigned numbers are used in GAP for inquiry response, EIR data type values, manufacturer-specific data, +/// advertising data, low energy UUIDs and appearance characteristics, and class of device. +/// +/// - SeeAlso: +/// [Generic Access Profile](https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile) +public struct GAPDataType: RawRepresentable, Equatable, Hashable, Sendable { + + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + +// MARK: - Defined Types + +public extension GAPDataType { + + /// Flags + /// + /// **Reference**: + /// + /// Bluetooth Core Specification Vol. 3, Part C, section 8.1.3 (v2.1 + EDR, 3.0 + HS and 4.0) + /// + /// Bluetooth Core Specification Vol. 3, Part C, sections 11.1.3 and 18.1 (v4.0) + /// + /// Core Specification Supplement, Part A, section 1.3 + static var flags: GAPDataType { 0x01 } + + /// Shortened Local Name + static var shortLocalName: GAPDataType { 0x08 } + + /// Complete Local Name + static var completeLocalName: GAPDataType { 0x09 } + + /// Manufacturer Specific Data + static var manufacturerSpecificData: GAPDataType { 0xFF } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension GAPDataType: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: UInt8) { + self.rawValue = value + } +} + +// MARK: - CustomStringConvertible + +extension GAPDataType: CustomStringConvertible { + + public var description: String { + rawValue.description + } +} \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/GAPFlags.swift b/esp32-ble-peripheral-sdk/main/GAPFlags.swift new file mode 100644 index 0000000..637f5c7 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GAPFlags.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** + 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. + */ +public struct GAPFlags: GAPData, Equatable, Hashable, OptionSet, Sendable { + + public static var dataType: GAPDataType { .flags } + + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + +public extension GAPFlags { + + init?(data: Data) where Data : DataContainer { + guard data.count == 1 + else { return nil } + self.init(rawValue: data[0]) + } + + func append(to data: inout Data) where Data : DataContainer { + data += self + } + + var dataLength: Int { + 1 + } +} + +// MARK: - DataConvertible + +extension GAPFlags: DataConvertible { + + static func += (data: inout T, value: GAPFlags) { + data += value.rawValue + } +} + +// MARK: - CustomStringConvertible + +extension GAPFlags: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + rawValue.description + } + + public var debugDescription: String { self.description } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension GAPFlags: ExpressibleByIntegerLiteral { + + public init(integerLiteral rawValue: RawValue) { + self.init(rawValue: rawValue) + } +} + +// MARK: - ExpressibleByArrayLiteral + +extension GAPFlags: ExpressibleByArrayLiteral { } + +// MARK: - Constants + +public extension GAPFlags { + + /** + LE Limited Discoverable Mode + + - Note: Limited Discoverable Mode is used to suggest that the device should have a high priority to scanning devices and often the advertising interval used when in this mode is faster than when in the General Discoverable Mode. A device will be in Limited Discoverable Mode for a limited time only and the core specification recommends this be no more than one minute. A device whose Flags field indicates it is not discoverable just means scanning devices should ignore it. + + - SeeAlso: [Bluetooth Advertising Works](https://blog.bluetooth.com/advertising-works-part-2) + */ + static var lowEnergyLimitedDiscoverableMode: GAPFlags { 0b00000001 } + + /// LE General Discoverable Mode + /// + /// Use general discoverable mode to advertise indefinitely. + static var lowEnergyGeneralDiscoverableMode: GAPFlags { 0b00000010 } + + /// BR/EDR Not Supported. + /// + /// Bit 37 of LMP Feature Mask Definitions (Page 0) + static var notSupportedBREDR: GAPFlags { 0b00000100 } + + /// Simultaneous LE and BR/EDR to Same Device Capable (Controller). + /// + /// Bit 49 of LMP Feature Mask Definitions (Page 0) + static var simultaneousController: GAPFlags { 0b00001000 } + + /// Simultaneous LE and BR/EDR to Same Device Capable (Host). + /// + /// Bit 66 of LMP Feature Mask Definitions (Page 1) + static var simultaneousHost: GAPFlags { 0b00010000 } +} diff --git a/esp32-ble-peripheral-sdk/main/GAPManufacturerSpecificData.swift b/esp32-ble-peripheral-sdk/main/GAPManufacturerSpecificData.swift new file mode 100644 index 0000000..5e8f1ff --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GAPManufacturerSpecificData.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** + The Manufacturer Specific data type is used for manufacturer specific data. + The first two data octets shall contain a company identifier code from the Assigned Numbers - Company Identifiers document. + The interpretation of any other octets within the data shall be defined by the manufacturer specified by the company identifier. + + Size: 2 or more octets + The first 2 octets contain the Company Identifier Code followed by additional manufacturer specific data + */ +public struct GAPManufacturerSpecificData : GAPData, Equatable, Hashable { + + /// GAP Data Type + public static var dataType: GAPDataType { return .manufacturerSpecificData } + + /// Company Identifier + public var companyIdentifier: CompanyIdentifier + + /// Additional Data. + public var additionalData: AdditionalData + + /// Initialize with company identifier and additional data. + public init(companyIdentifier: CompanyIdentifier, + additionalData: AdditionalData = AdditionalData()) { + + self.companyIdentifier = companyIdentifier + self.additionalData = additionalData + } + + public init?(data: Data) where Data : DataContainer { + + guard data.count >= 2 + else { return nil } + + self.companyIdentifier = CompanyIdentifier(rawValue: UInt16(littleEndian: UInt16(bytes: (data[0], data[1])))) + if data.count > 2 { + self.additionalData = AdditionalData(data[2 ..< data.count]) + } else { + self.additionalData = AdditionalData() + } + } + + public func append(to data: inout Data) where Data : DataContainer { + data += self.companyIdentifier.rawValue.littleEndian + data += self.additionalData + } + + public var dataLength: Int { + return 2 + additionalData.count + } +} + +// MARK: - CustomStringConvertible + +extension GAPManufacturerSpecificData: CustomStringConvertible { + + public var description: String { + return "(\(companyIdentifier)) \(additionalData.toHexadecimal())" + } +} + +// MARK: - DataConvertible + +extension GAPManufacturerSpecificData: DataConvertible { + + static func += (data: inout T, value: GAPManufacturerSpecificData) { + value.append(to: &data) + } +} diff --git a/esp32-ble-peripheral-sdk/main/GAPShortLocalName.swift b/esp32-ble-peripheral-sdk/main/GAPShortLocalName.swift new file mode 100644 index 0000000..35808de --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GAPShortLocalName.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** + GAP Shortened Local Name + + The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the device. The Local Name data type value indicates if the name is complete or shortened. If the name is shortened, the complete name can be read using the remote name request procedure over BR/EDR or by reading the device name characteristic after the connection has been established using GATT. + + A shortened name shall only contain contiguous characters from the beginning of the full name. For example, if the device name is ‘BT_Device_Name’ then the shortened name could be ‘BT_Device’ or ‘BT_Dev’. + */ +public struct GAPShortLocalName: GAPData, Equatable, Hashable, Sendable { + + public static var dataType: GAPDataType { .shortLocalName } + + public var name: String + + public init(name: String) { + self.name = name + } +} + +public extension GAPShortLocalName { + + init?(data: Data) { + + guard let rawValue = String(utf8: data) + else { return nil } + + self.init(name: rawValue) + } + + func append(to data: inout Data) { + data += name.utf8 + } + + var dataLength: Int { + return name.utf8.count + } +} + +// MARK: - CustomStringConvertible + +extension GAPShortLocalName: CustomStringConvertible { + + public var description: String { + return name + } +} + +// MARK: - ExpressibleByStringLiteral + +extension GAPShortLocalName: ExpressibleByStringLiteral { + + public init(stringLiteral value: String) { + self.init(name: value) + } +} diff --git a/esp32-ble-peripheral-sdk/main/GATTAttributes.swift b/esp32-ble-peripheral-sdk/main/GATTAttributes.swift new file mode 100644 index 0000000..fbf80f4 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GATTAttributes.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// GATT Attribute +@frozen +public enum GATTAttribute : Equatable, Hashable, Sendable { + + case service(Service) + case include(Include) + case characteristic(Characteristic) + case descriptor(Descriptor) +} + +public extension GATTAttribute { + + typealias Permissions = ATTAttributePermissions + + /// GATT Service + struct Service: Equatable, Hashable, Sendable { + + public var uuid: BluetoothUUID + + public var isPrimary: Bool + + public var characteristics: [Characteristic] + + public var includedServices: [Include] + + public init( + uuid: BluetoothUUID, + isPrimary: Bool = true, + characteristics: [Characteristic] = [], + includedServices: [Include] = [] + ) { + self.uuid = uuid + self.characteristics = characteristics + self.isPrimary = isPrimary + self.includedServices = includedServices + } + } + + /// GATT Include Declaration + struct Include: Equatable, Hashable, Sendable { + + /// Included service handle + public var serviceHandle: UInt16 + + /// End group handle + public var endGroupHandle: UInt16 + + /// Included Service UUID + public var serviceUUID: BluetoothUUID + + public init( + serviceHandle: UInt16, + endGroupHandle: UInt16, + serviceUUID: BluetoothUUID + ) { + self.serviceHandle = serviceHandle + self.endGroupHandle = endGroupHandle + self.serviceUUID = serviceUUID + } + } + + /// GATT Characteristic + struct Characteristic: Equatable, Hashable, Sendable { + + public typealias Properties = GATTCharacteristicProperties + + public var uuid: BluetoothUUID + + public var value: Data + + public var permissions: Permissions + + public var properties: Properties + + public var descriptors: [Descriptor] + + public init( + uuid: BluetoothUUID, + value: Data = Data(), + permissions: Permissions = [.read], + properties: Properties = [.read], + descriptors: [Descriptor] = [] + ) { + self.uuid = uuid + self.value = value + self.permissions = permissions + self.descriptors = descriptors + self.properties = properties + } + } + + /// GATT Characteristic Descriptor + struct Descriptor: Equatable, Hashable, Sendable { + + public var uuid: BluetoothUUID + + public var value: Data + + public var permissions: Permissions + + public init( + uuid: BluetoothUUID = BluetoothUUID(), + value: Data = Data(), + permissions: Permissions = [.read] + ) { + self.uuid = uuid + self.value = value + self.permissions = permissions + } + } +} diff --git a/esp32-ble-peripheral-sdk/main/GATTCharacteristicProperties.swift b/esp32-ble-peripheral-sdk/main/GATTCharacteristicProperties.swift new file mode 100644 index 0000000..e552de7 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/GATTCharacteristicProperties.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// GATT Characteristic Properties Bitfield valuess +public struct GATTCharacteristicProperties: OptionSet, Hashable, Sendable { + + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension GATTCharacteristicProperties: ExpressibleByIntegerLiteral { + + public init(integerLiteral value: UInt8) { + self.rawValue = value + } +} + +// MARK: CustomStringConvertible + +extension GATTCharacteristicProperties: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + "0x" + rawValue.toHexadecimal() + } + + /// A textual representation of the file permissions, suitable for debugging. + public var debugDescription: String { self.description } +} + +// MARK: - Options + +public extension GATTCharacteristicProperties { + + static var broadcast: GATTCharacteristicProperties { 0x01 } + static var read: GATTCharacteristicProperties { 0x02 } + static var writeWithoutResponse: GATTCharacteristicProperties { 0x04 } + static var write: GATTCharacteristicProperties { 0x08 } + static var notify: GATTCharacteristicProperties { 0x10 } + static var indicate: GATTCharacteristicProperties { 0x20 } + + /// Characteristic supports write with signature + static var signedWrite: GATTCharacteristicProperties { 0x40 } // BT_GATT_CHRC_PROP_AUTH + + static var extendedProperties: GATTCharacteristicProperties { 0x80 } +} diff --git a/esp32-ble-peripheral-sdk/main/Hexadecimal.swift b/esp32-ble-peripheral-sdk/main/Hexadecimal.swift new file mode 100644 index 0000000..08ada87 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Hexadecimal.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +internal extension FixedWidthInteger { + + func toHexadecimal() -> String { + let length = MemoryLayout.size * 2 + var string: String + #if hasFeature(Embedded) + string = "" + string.reserveCapacity(length) + self.bigEndian.bytes.forEach { byte in + string.append(String(format: "%02X", length: 2, byte)!) + } + #else // Non-Embedded builds use Swift StdLib + string = String(self, radix: 16, uppercase: true) + // Add Zero padding + while string.utf8.count < length { + string = "0" + string + } + #endif + assert(string.utf8.count == length) + #if !hasFeature(Embedded) + assert(string == string.uppercased(), "String should be uppercased") + #endif + return string + } +} + +internal extension Collection where Element: FixedWidthInteger { + + func toHexadecimal() -> String { + let length = count * MemoryLayout.size * 2 + var string = "" + string.reserveCapacity(length) + string = reduce(into: string) { $0 += $1.toHexadecimal() } + assert(string.utf8.count == length) + return string + } +} + +internal extension FixedWidthInteger { + + init?(parse string: S, radix: Self) { + #if !hasFeature(Embedded) + let string = string.uppercased() + #endif + self.init(utf8: string.utf8, radix: radix) + } + + init?(hexadecimal string: S) { + guard string.utf8.count == MemoryLayout.size * 2 else { + return nil + } + #if hasFeature(Embedded) || DEBUG + guard let value = Self(parse: string, radix: 16) else { + return nil + } + self.init(value) + #else + 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 + } +} \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/Integer.swift b/esp32-ble-peripheral-sdk/main/Integer.swift new file mode 100644 index 0000000..c466c4f --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Integer.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +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) + } +} + +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) + } +} + +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) + } +} + +internal extension BinaryInteger { + + @inlinable + var bytes: [UInt8] { + var mutableValueCopy = self + return withUnsafeBytes(of: &mutableValueCopy) { Array($0) } + } +} \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/Led.swift b/esp32-ble-peripheral-sdk/main/Led.swift new file mode 100644 index 0000000..7970b80 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Led.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// A simple "overlay" to provide nicer APIs in Swift +struct Led { + var ledPin: gpio_num_t + init(gpioPin: Int) { + ledPin = gpio_num_t(Int32(gpioPin)) + + guard gpio_reset_pin(ledPin) == ESP_OK else { + fatalError("cannot reset led") + } + + guard gpio_set_direction(ledPin, GPIO_MODE_OUTPUT) == ESP_OK else { + fatalError("cannot reset led") + } + } + func setLed(_ value:Bool) { + let level: UInt32 = value ? 1 : 0 + gpio_set_level(ledPin, level) + } +} diff --git a/esp32-ble-peripheral-sdk/main/LedStrip.swift b/esp32-ble-peripheral-sdk/main/LedStrip.swift new file mode 100644 index 0000000..a085e12 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/LedStrip.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// A simple "overlay" to provide nicer APIs in Swift +struct LedStrip { + private let handle: led_strip_handle_t + init(gpioPin: Int, maxLeds: Int) { + var handle = led_strip_handle_t(bitPattern: 0) + var stripConfig = led_strip_config_t( + strip_gpio_num: Int32(gpioPin), + max_leds: UInt32(maxLeds), + led_pixel_format: LED_PIXEL_FORMAT_GRB, + led_model: LED_MODEL_WS2812, + flags: .init(invert_out: 0) + ) + var spiConfig = led_strip_spi_config_t( + clk_src: SPI_CLK_SRC_DEFAULT, + spi_bus: SPI2_HOST, + flags: .init(with_dma: 1) + ) + guard led_strip_new_spi_device(&stripConfig, &spiConfig, &handle) == ESP_OK else { + fatalError("cannot configure spi device") + } + self.handle = handle! + } + + struct Color { + var r, g, b: UInt8 + static var white = Color(r: 255, g: 255, b: 255) + static var lightWhite = Color(r: 16, g: 16, b: 16) + static var lightRandom: Color { + Color(r: .random(in: 0...16), g: .random(in: 0...16), b: .random(in: 0...16)) + } + static var off = Color(r: 0, g: 0, b: 0) + } + + func setPixel(index: Int, color: Color) { + led_strip_set_pixel(handle, UInt32(index), UInt32(color.r), UInt32(color.g), UInt32(color.b)) + } + + func refresh() { led_strip_refresh(handle) } + + func clear() { led_strip_clear(handle) } +} diff --git a/esp32-ble-peripheral-sdk/main/LowEnergyAddressType.swift b/esp32-ble-peripheral-sdk/main/LowEnergyAddressType.swift new file mode 100644 index 0000000..2cb2cf1 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/LowEnergyAddressType.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Bluetooth Low Energy Address type +public enum LowEnergyAddressType: UInt8 { + + /// Public Device Address + case `public` = 0x00 + + /// Random Device Address + case random = 0x01 + + /// Public Identity Address (Corresponds to peer’s Resolvable Private Address). + /// + /// This value shall only be used by the Host if either the Host or the + /// Controller does not support the LE Set Privacy Mode command. + /// + /// - Note: Requires Bluetooth 5.0 + case publicIdentity = 0x02 + + /// Random (static) Identity Address (Corresponds to peer’s Resolvable Private Address). + /// + /// This value shall only be used by a Host if either the Host or the Controller does + /// not support the LE Set Privacy Mode command. + /// + /// - Note: Requires Bluetooth 5.0 + case randomIdentity = 0x03 + + /// Default Low Energy Address type (`.public`). + public init() { self = .public } +} diff --git a/esp32-ble-peripheral-sdk/main/LowEnergyAdvertisingData.swift b/esp32-ble-peripheral-sdk/main/LowEnergyAdvertisingData.swift new file mode 100644 index 0000000..400694b --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/LowEnergyAdvertisingData.swift @@ -0,0 +1,322 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Bluetooth Low Energy Advertising Data +public struct LowEnergyAdvertisingData: Sendable { + + public typealias Element = UInt8 + + // MARK: - ByteValue + + /// Raw Bluetooth Low Energy Advertising Data 31 byte value. + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + // MARK: - Properties + + public var length: UInt8 { + didSet { precondition(length <= 31, "LE Advertising Data can only less than or equal to 31 octets") } + } + + public var bytes: ByteValue + + // MARK: - Initialization + + public init(length: UInt8, bytes: ByteValue) { + + precondition(length <= 31, "LE Advertising Data can only less than or equal to 31 octets") + self.bytes = bytes + self.length = length + } + + public init() { + + self.length = 0 + self.bytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + } +} + +public extension LowEnergyAdvertisingData { + + static var capacity: Int { return 31 } +} + +public extension LowEnergyAdvertisingData { + + /// Unsafe data access. + func withUnsafePointer (_ block: (UnsafePointer) throws -> Result) rethrows -> Result { + return try Swift.withUnsafePointer(to: bytes) { + try $0.withMemoryRebound(to: UInt8.self, capacity: LowEnergyAdvertisingData.capacity) { + try block($0) + } + } + } +} + +public extension LowEnergyAdvertisingData { + + init(data: Data) { + self.init(data) + } + + init (_ collection: C) where C.Element == UInt8 { + let length = collection.count + precondition(length <= 31) + self.init() + self.length = UInt8(length) + collection.enumerated().forEach { + self[$0.offset] = $0.element + } + } +} + +public extension LowEnergyAdvertisingData { + + mutating func append(_ byte: UInt8) { + assert(count < 31) + self[count] = byte + self.length += 1 + } + + static func += (data: inout LowEnergyAdvertisingData, byte: UInt8) { + data.append(byte) + } + + mutating func append (contentsOf bytes: C) where C.Element == UInt8 { + assert(count + bytes.count <= LowEnergyAdvertisingData.capacity) + for (index, byte) in bytes.enumerated() { + self[count + index] = byte + } + self.length += UInt8(bytes.count) + } + + static func += (data: inout LowEnergyAdvertisingData, bytes: C) where C.Element == UInt8 { + data.append(contentsOf: bytes) + } + + mutating func append(_ pointer: UnsafePointer, count: Int) { + assert(self.count + count <= LowEnergyAdvertisingData.capacity) + for index in 0 ..< count { + self[self.count + index] = pointer.advanced(by: index).pointee + } + self.length += UInt8(count) + } +} + +// MARK: - Equatable + +extension LowEnergyAdvertisingData: Equatable { + + public static func == (lhs: LowEnergyAdvertisingData, rhs: LowEnergyAdvertisingData) -> Bool { + return lhs.length == rhs.length && + lhs.bytes.0 == rhs.bytes.0 && + lhs.bytes.1 == rhs.bytes.1 && + lhs.bytes.2 == rhs.bytes.2 && + lhs.bytes.3 == rhs.bytes.3 && + lhs.bytes.4 == rhs.bytes.4 && + lhs.bytes.5 == rhs.bytes.5 && + lhs.bytes.6 == rhs.bytes.6 && + lhs.bytes.7 == rhs.bytes.7 && + lhs.bytes.8 == rhs.bytes.8 && + lhs.bytes.9 == rhs.bytes.9 && + lhs.bytes.10 == rhs.bytes.10 && + lhs.bytes.11 == rhs.bytes.11 && + lhs.bytes.12 == rhs.bytes.12 && + lhs.bytes.13 == rhs.bytes.13 && + lhs.bytes.14 == rhs.bytes.14 && + lhs.bytes.15 == rhs.bytes.15 && + lhs.bytes.16 == rhs.bytes.16 && + lhs.bytes.17 == rhs.bytes.17 && + lhs.bytes.18 == rhs.bytes.18 && + lhs.bytes.19 == rhs.bytes.19 && + lhs.bytes.20 == rhs.bytes.20 && + lhs.bytes.21 == rhs.bytes.21 && + lhs.bytes.22 == rhs.bytes.22 && + lhs.bytes.23 == rhs.bytes.23 && + lhs.bytes.24 == rhs.bytes.24 && + lhs.bytes.25 == rhs.bytes.25 && + lhs.bytes.26 == rhs.bytes.26 && + lhs.bytes.27 == rhs.bytes.27 && + lhs.bytes.28 == rhs.bytes.28 && + lhs.bytes.29 == rhs.bytes.29 && + lhs.bytes.30 == rhs.bytes.30 + } +} + +// MARK: - Hashable + +extension LowEnergyAdvertisingData: Hashable { + + public func hash(into hasher: inout Hasher) { + length.hash(into: &hasher) + withUnsafeBytes(of: bytes) { hasher.combine(bytes: $0) } + } +} + +// MARK: - CustomStringConvertible + +extension LowEnergyAdvertisingData: CustomStringConvertible { + + public var description: String { + return toHexadecimal() + } +} + +// MARK: - ExpressibleByArrayLiteral + +extension LowEnergyAdvertisingData: ExpressibleByArrayLiteral { + + public init(arrayLiteral elements: UInt8...) { + precondition(elements.count <= 31) + self.init(elements) + } +} + +// MARK: - Sequence + +extension LowEnergyAdvertisingData: Sequence { + + public func makeIterator() -> IndexingIterator { + return IndexingIterator(_elements: self) + } +} + +// MARK: - Collection + +extension LowEnergyAdvertisingData: MutableCollection { + + public var count: Int { + return Int(length) + } + + public func index(after index: Int) -> Int { + return index + 1 + } + + public var startIndex: Int { + return 0 + } + + public var endIndex: Int { + return count + } + + /// Get the byte at the specified index. + public subscript (index: Int) -> UInt8 { + + get { + + switch index { + case 0: return bytes.0 + case 1: return bytes.1 + case 2: return bytes.2 + case 3: return bytes.3 + case 4: return bytes.4 + case 5: return bytes.5 + case 6: return bytes.6 + case 7: return bytes.7 + case 8: return bytes.8 + case 9: return bytes.9 + case 10: return bytes.10 + case 11: return bytes.11 + case 12: return bytes.12 + case 13: return bytes.13 + case 14: return bytes.14 + case 15: return bytes.15 + case 16: return bytes.16 + case 17: return bytes.17 + case 18: return bytes.18 + case 19: return bytes.19 + case 20: return bytes.20 + case 21: return bytes.21 + case 22: return bytes.22 + case 23: return bytes.23 + case 24: return bytes.24 + case 25: return bytes.25 + case 26: return bytes.26 + case 27: return bytes.27 + case 28: return bytes.28 + case 29: return bytes.29 + case 30: return bytes.30 + default: + fatalError("Invalid index") + } + } + + mutating set { + + switch index { + case 0: bytes.0 = newValue + case 1: bytes.1 = newValue + case 2: bytes.2 = newValue + case 3: bytes.3 = newValue + case 4: bytes.4 = newValue + case 5: bytes.5 = newValue + case 6: bytes.6 = newValue + case 7: bytes.7 = newValue + case 8: bytes.8 = newValue + case 9: bytes.9 = newValue + case 10: bytes.10 = newValue + case 11: bytes.11 = newValue + case 12: bytes.12 = newValue + case 13: bytes.13 = newValue + case 14: bytes.14 = newValue + case 15: bytes.15 = newValue + case 16: bytes.16 = newValue + case 17: bytes.17 = newValue + case 18: bytes.18 = newValue + case 19: bytes.19 = newValue + case 20: bytes.20 = newValue + case 21: bytes.21 = newValue + case 22: bytes.22 = newValue + case 23: bytes.23 = newValue + case 24: bytes.24 = newValue + case 25: bytes.25 = newValue + case 26: bytes.26 = newValue + case 27: bytes.27 = newValue + case 28: bytes.28 = newValue + case 29: bytes.29 = newValue + case 30: bytes.30 = newValue + default: + fatalError("Invalid index") + } + } + } +} + +// MARK: - RandomAccessCollection + +extension LowEnergyAdvertisingData: RandomAccessCollection { + + public subscript(bounds: Range) -> Slice { + return Slice(base: self, bounds: bounds) + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension LowEnergyAdvertisingData: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data.self) + guard data.count <= LowEnergyAdvertisingData.capacity else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of bytes (\(data.count).")) + } + self.init(data: data) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(Data(self)) + } +} +#endif diff --git a/esp32-ble-peripheral-sdk/main/Main.swift b/esp32-ble-peripheral-sdk/main/Main.swift new file mode 100644 index 0000000..f979c83 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Main.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@_cdecl("app_main") +func app_main() { + print("Hello from Swift on ESP32-C6!") + + let ledStrip: LedStrip = LedStrip(gpioPin: 8, maxLeds: 1) + ledStrip.clear() + + var bluetooth: NimBLE + do { + try nvs_flash_init().throwsESPError() + bluetooth = try NimBLE() + bluetooth.log = { print($0) } + } + catch { + print("Bluetooth init failed \(error)") + return + } + + do { + var server = bluetooth.server + let service = GATTAttribute<[UInt8]>.Service( + uuid: .bit16(0x180A), + isPrimary: true, + characteristics: [ + .init( + uuid: .bit16(0x2A00), + value: Array("ESP32-C6".utf8), + permissions: .read, + properties: [.read] + ), + .init( + uuid: .bit16(0x2A29), + value: Array("Test Inc.".utf8), + permissions: [.read], + properties: [.read], + descriptors: [ + .init(uuid: .bit16(0x2901), value: Array("Manufacturer Name String".utf8), permissions: .read) + ] + ) + ] + ) + let ledStateCharacteristicUUID = BluetoothUUID(rawValue: "D8936E7C-F254-4F3F-9A66-D57B3D346F09")! + let service2 = GATTAttribute<[UInt8]>.Service( + uuid: .bit16(0xFEA9), + isPrimary: true, + characteristics: [ + .init( + uuid: ledStateCharacteristicUUID, + value: [0x00], + permissions: [.read, .write], + properties: [.read, .write], + descriptors: [ + .init(uuid: .bit16(0x2901), value: Array("LED State".utf8), permissions: .read), + ] + ) + ] + ) + try server.set(services: [service, service2]) + server.dump() + + server.willWrite = { write in + switch write.uuid { + case ledStateCharacteristicUUID: + guard write.newValue.count == 1 else { + return .writeNotPermitted + } + switch write.newValue[0] { + case 0, 1: + return nil + default: + return .writeNotPermitted + } + default: + return nil + } + } + + server.didWrite = { write in + switch write.uuid { + case ledStateCharacteristicUUID: + let newState = write.value.first != 0 + print("Light State: \(newState)") + ledStrip.setPixel(index: 0, color: newState ? .lightWhite : .off) + ledStrip.refresh() + default: + break + } + } + + while bluetooth.hostController.isEnabled == false { + vTaskDelay(500 / (1000 / UInt32(configTICK_RATE_HZ))) + } + + // read address + let address = try bluetooth.hostController.address() + print("Bluetooth address: \(address)") + + // Estimote iBeacon B9407F30-F5F8-466E-AFF9-25556B57FE6D + // Major 0x01 Minor 0x01 + guard let uuid = UUID(uuidString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D") else { + fatalError("Invalid UUID string") + } + let beacon = AppleBeacon(uuid: uuid, major: 0x01, minor: 0x01, rssi: -10) + let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] + let advertisement = LowEnergyAdvertisingData(beacon: beacon, flags: flags) + try bluetooth.gap.setAdvertisement(advertisement) + + // set scan response + let name = GAPShortLocalName(name: "ESP32-C6 " + address.description) + let scanResponse: LowEnergyAdvertisingData = GAPDataEncoder.encode(name) + try bluetooth.gap.setScanResponse(scanResponse) + print("Advertisement name: \(name)") + + try bluetooth.gap.startAdvertising() + } + catch { + print("Bluetooth error") + } + + while bluetooth.hostController.isEnabled { + vTaskDelay(500 / (1000 / UInt32(configTICK_RATE_HZ))) + } + + _ = bluetooth // retain +} diff --git a/esp32-ble-peripheral-sdk/main/NimBLE.swift b/esp32-ble-peripheral-sdk/main/NimBLE.swift new file mode 100644 index 0000000..71e8e6d --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/NimBLE.swift @@ -0,0 +1,826 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +public struct NimBLE: ~Copyable { + + internal let context: UnsafeMutablePointer + + public init() throws(ESPError) { + try nimble_port_init().throwsESPError() + nimble_port_freertos_init(nimble_host_task) + // Allocate context on heap for callbacks + context = .allocate(capacity: 1) + context.pointee = Context() + } + + deinit { + context.deinitialize(count: 1) + context.deallocate() + nimble_port_freertos_deinit() + } + + public var log: (@Sendable (String) -> ())? { + get { context.pointee.log } + set { context.pointee.log = newValue } + } +} + +internal extension NimBLE { + + struct Context { + + var log: (@Sendable (String) -> ())? + + var gap = GAP.Context() + + var gattServer = GATTServer.Context() + + + } +} + +@_silgen_name("nvs_flash_init") +internal func nvs_flash_init() -> Int32 + +@_silgen_name("nimble_port_init") +internal func nimble_port_init() -> Int32 + +@_silgen_name("nimble_port_run") +internal func nimble_port_run() -> Int32 + +@_silgen_name("nimble_port_freertos_init") +internal func nimble_port_freertos_init(_ function: (UnsafeMutableRawPointer) -> ()) + +@_silgen_name("nimble_port_freertos_deinit") +internal func nimble_port_freertos_deinit() + +internal func nimble_host_task(_ parameters: UnsafeMutableRawPointer) { + print("BLE Host Task Started") + nimble_port_run() + nimble_port_freertos_deinit() +} + +public extension NimBLE { + + var hostController: HostController { HostController(context: context) } +} + +public struct HostController { + + internal let context: UnsafeMutablePointer + + public var isEnabled: Bool { + ble_hs_is_enabled() == 1 + } + + public func address( + type: LowEnergyAddressType = .public + ) throws(NimBLEError) -> BluetoothAddress { + var address = BluetoothAddress.zero + try withUnsafeMutablePointer(to: &address) { + $0.withMemoryRebound(to: UInt8.self, capacity: 6) { + ble_hs_id_copy_addr(type.rawValue, $0, nil) + } + }.throwsError() + return address + } +} + +public extension NimBLE { + + var gap: GAP { + GAP(context: context) + } +} + +/// NimBLE GAP interface. +public struct GAP { + + internal struct Context { + + var advertisment = LowEnergyAdvertisingData() + + var scanResponse = LowEnergyAdvertisingData() + } + + internal let context: UnsafeMutablePointer + + /// Indicates whether an advertisement procedure is currently in progress. + public var isAdvertising: Bool { + ble_gap_adv_active() == 1 + } + + /// Start advertising + public func startAdvertising( + addressType: LowEnergyAddressType = .public, + address: BluetoothAddress? = nil, + parameters: ble_gap_adv_params = ble_gap_adv_params(conn_mode: UInt8(BLE_GAP_CONN_MODE_UND), disc_mode: UInt8(BLE_GAP_DISC_MODE_GEN), itvl_min: 0, itvl_max: 0, channel_map: 0, filter_policy: 0, high_duty_cycle: 0) + ) throws(NimBLEError) { + var address = ble_addr_t( + type: 0, + val: (address ?? .zero).bytes + ) + var parameters = parameters + try ble_gap_adv_start(addressType.rawValue, &address, BLE_HS_FOREVER, ¶meters, _gap_callback, context).throwsError() + } + + /// Stops the currently-active advertising procedure. + public func stopAdvertising() throws(NimBLEError) { + try ble_gap_adv_stop().throwsError() + } + + public var advertisementData: LowEnergyAdvertisingData { + context.pointee.gap.advertisment + } + + /// Configures the data to include in subsequent advertisements. + public func setAdvertisement(_ data: LowEnergyAdvertisingData) throws(NimBLEError) { + context.pointee.gap.advertisment = data + try context.pointee.gap.advertisment.withUnsafePointer { + ble_gap_adv_set_data($0, Int32(data.length)) + }.throwsError() + } + + public var scanResponse: LowEnergyAdvertisingData { + context.pointee.gap.scanResponse + } + + /// Configures the data to include in subsequent scan responses. + public func setScanResponse(_ data: LowEnergyAdvertisingData) throws(NimBLEError) { + context.pointee.gap.scanResponse = data + try context.pointee.gap.scanResponse.withUnsafePointer { + ble_gap_adv_rsp_set_data($0, Int32(data.length)) + }.throwsError() + } + + public func connection(for handle: UInt16) throws(NimBLEError) -> ble_gap_conn_desc { + var connection = ble_gap_conn_desc() + try ble_gap_conn_find(handle, &connection).throwsError() + return connection + } +} + +internal func _gap_callback(event: UnsafeMutablePointer?, context contextPointer: UnsafeMutableRawPointer?) -> Int32 { + guard let context = contextPointer?.assumingMemoryBound(to: NimBLE.Context.self), + let event else { + return 0 + } + let log = context.pointee.log + switch Int32(event.pointee.type) { + case BLE_GAP_EVENT_CONNECT: + let handle = event.pointee.connect.conn_handle + log?("Connected - Handle \(handle)") + case BLE_GAP_EVENT_DISCONNECT: + let handle = event.pointee.connect.conn_handle + log?("Disconnected - Handle \(handle)") + do { + try GAP(context: context).startAdvertising() + } + catch { + log?("Unable to advertise") + } + default: + break + } + + return 0 +} + +public extension NimBLE { + + var server: GATTServer { GATTServer(context: context) } +} + + +/// NimBLE GATT Server interface. +public struct GATTServer { + + internal struct Context { + + var services = [GATTAttribute<[UInt8]>.Service]() + + var servicesBuffer = [ble_gatt_svc_def]() + + var characteristicsBuffers = [[ble_gatt_chr_def]]() + + var descriptorBuffers = [[[ble_gatt_dsc_def]]]() + + var buffers = [[UInt8]]() + + var characteristicValueHandles = [[UInt16]]() + + /// Callback to handle GATT read requests. + var willRead: ((GATTReadRequest) -> ATTError?)? + + /// Callback to handle GATT write requests. + var willWrite: ((GATTWriteRequest) -> ATTError?)? + + /// Callback to handle post-write actions for GATT write requests. + var didWrite: ((GATTWriteConfirmation) -> ())? + } + + // MARK: - Properties + + internal let context: UnsafeMutablePointer + + /// Callback to handle GATT read requests. + public var willRead: ((GATTReadRequest) -> ATTError?)? { + get { context.pointee.gattServer.willRead } + set { context.pointee.gattServer.willRead = newValue } + } + + /// Callback to handle GATT write requests. + public var willWrite: ((GATTWriteRequest) -> ATTError?)? { + get { context.pointee.gattServer.willWrite } + set { context.pointee.gattServer.willWrite = newValue } + } + + /// Callback to handle post-write actions for GATT write requests. + public var didWrite: ((GATTWriteConfirmation) -> ())? { + get { context.pointee.gattServer.didWrite } + set { context.pointee.gattServer.didWrite = newValue } + } + + // MARK: - Methods + + internal func start() throws(NimBLEError) { + try ble_gatts_start().throwsError() + } + + /// Attempts to add the specified service to the GATT database. + public func set(services: [GATTAttribute<[UInt8]>.Service]) throws(NimBLEError) -> [[UInt16]] { + removeAllServices() + var cServices = [ble_gatt_svc_def].init(repeating: .init(), count: services.count + 1) + var characteristicsBuffers = [[ble_gatt_chr_def]].init(repeating: [], count: services.count) + var buffers = [[UInt8]]() + var valueHandles = [[UInt16]].init(repeating: [], count: services.count) + var descriptorBuffers = [[[ble_gatt_dsc_def]]].init(repeating: [], count: services.count) + for (serviceIndex, service) in services.enumerated() { + // set type + cServices[serviceIndex].type = service.isPrimary ? UInt8(BLE_GATT_SVC_TYPE_PRIMARY) : UInt8(BLE_GATT_SVC_TYPE_SECONDARY) + // set uuid + let serviceUUID = ble_uuid_any_t(service.uuid) + withUnsafeBytes(of: serviceUUID) { + let buffer = [UInt8]($0) + buffers.append(buffer) + buffer.withUnsafeBytes { + cServices[serviceIndex].uuid = .init(OpaquePointer($0.baseAddress)) + } + } + assert(ble_uuid_any_t(cServices[serviceIndex].uuid) == serviceUUID) + assert(serviceUUID.dataLength == service.uuid.dataLength) + var characteristicHandles = [UInt16](repeating: 0, count: service.characteristics.count) + descriptorBuffers[serviceIndex] = .init(repeating: [], count: service.characteristics.count) + // add characteristics + var cCharacteristics = [ble_gatt_chr_def].init(repeating: .init(), count: service.characteristics.count + 1) + for (characteristicIndex, characteristic) in service.characteristics.enumerated() { + // set flags + cCharacteristics[characteristicIndex].flags = ble_gatt_chr_flags(characteristic.properties.rawValue) + // set access callback + cCharacteristics[characteristicIndex].access_cb = _ble_gatt_access + cCharacteristics[characteristicIndex].arg = .init(context) + // set UUID + let characteristicUUID = ble_uuid_any_t(characteristic.uuid) + withUnsafeBytes(of: characteristicUUID) { + let buffer = [UInt8]($0) + buffers.append(buffer) + buffer.withUnsafeBytes { + cCharacteristics[characteristicIndex].uuid = .init(OpaquePointer($0.baseAddress)) + } + } + // set handle + characteristicHandles[characteristicIndex] = 0x0000 + characteristicHandles.withUnsafeBufferPointer { + cCharacteristics[characteristicIndex].val_handle = .init(mutating: $0.baseAddress?.advanced(by: characteristicIndex)) + } + // descriptors + var cDescriptors = [ble_gatt_dsc_def].init(repeating: .init(), count: characteristic.descriptors.count + 1) + for (descriptorIndex, descriptor) in characteristic.descriptors.enumerated() { + // set flags + cDescriptors[descriptorIndex].att_flags = .init(descriptor.permissions.rawValue) + // set access callback + cDescriptors[descriptorIndex].access_cb = _ble_gatt_access + cDescriptors[descriptorIndex].arg = .init(context) + // set UUID + let descriptorUUID = ble_uuid_any_t(descriptor.uuid) + withUnsafeBytes(of: descriptorUUID) { + let buffer = [UInt8]($0) + buffers.append(buffer) + buffer.withUnsafeBytes { + cDescriptors[descriptorIndex].uuid = .init(OpaquePointer($0.baseAddress)) + } + } + } + cDescriptors.withUnsafeMutableBufferPointer { + cCharacteristics[characteristicIndex].descriptors = $0.baseAddress + } + descriptorBuffers[serviceIndex][characteristicIndex] = cDescriptors // retain buffer + } + cCharacteristics.withUnsafeBufferPointer { + cServices[serviceIndex].characteristics = $0.baseAddress + } + characteristicsBuffers[serviceIndex] = cCharacteristics + valueHandles[serviceIndex] = characteristicHandles + } + // queue service registration + try ble_gatts_count_cfg(cServices).throwsError() + try ble_gatts_add_svcs(cServices).throwsError() + // register services + try start() + // store buffers + cServices.removeLast() // nil terminator + self.context.pointee.gattServer.servicesBuffer = cServices + self.context.pointee.gattServer.characteristicsBuffers = characteristicsBuffers + self.context.pointee.gattServer.descriptorBuffers = descriptorBuffers + self.context.pointee.gattServer.buffers = buffers + self.context.pointee.gattServer.services = services + self.context.pointee.gattServer.characteristicValueHandles = valueHandles + // get handles + return valueHandles + } + + /// Removes the service with the specified handle. + public func remove(service: UInt16) { + // iterate all services and find the specified handle + } + + /// Clears the local GATT database. + public func removeAllServices() { + ble_gatts_reset() + self.context.pointee.gattServer.services.removeAll(keepingCapacity: false) + self.context.pointee.gattServer.buffers.removeAll(keepingCapacity: false) + self.context.pointee.gattServer.services.removeAll(keepingCapacity: false) + self.context.pointee.gattServer.characteristicsBuffers.removeAll(keepingCapacity: false) + self.context.pointee.gattServer.characteristicValueHandles.removeAll(keepingCapacity: false) + self.context.pointee.gattServer.descriptorBuffers.removeAll(keepingCapacity: false) + } + + public func dump() { + ble_gatts_show_local() + } +} + +internal extension GATTServer.Context { + + func descriptor(for pointer: UnsafePointer) -> GATTAttribute<[UInt8]>.Descriptor? { + for (serviceIndex, service) in services.enumerated() { + for (characteristicIndex, characteristic) in service.characteristics.enumerated() { + for (descriptorIndex, descriptor) in characteristic.descriptors.enumerated() { + guard descriptorBuffers[serviceIndex][characteristicIndex].withUnsafeBufferPointer({ + $0.baseAddress?.advanced(by: descriptorIndex) == pointer + }) else { continue } + return descriptor + } + } + } + return nil + } + + func characteristic(for handle: UInt16) -> GATTAttribute<[UInt8]>.Characteristic? { + for (serviceIndex, service) in services.enumerated() { + for (characteristicIndex, characteristic) in service.characteristics.enumerated() { + guard characteristicsBuffers[serviceIndex][characteristicIndex].val_handle.pointee == handle else { + continue + } + return characteristic + } + } + return nil + } + + @discardableResult + mutating func didWriteCharacteristic(_ newValue: [UInt8], for handle: UInt16) -> Bool { + for (serviceIndex, service) in services.enumerated() { + for (characteristicIndex, _) in service.characteristics.enumerated() { + guard characteristicsBuffers[serviceIndex][characteristicIndex].val_handle.pointee == handle else { + continue + } + services[serviceIndex].characteristics[characteristicIndex].value = newValue + return true + } + } + return false + } + + @discardableResult + mutating func didWriteDescriptor(_ newValue: [UInt8], for pointer: UnsafePointer) -> Bool { + for (serviceIndex, service) in services.enumerated() { + for (characteristicIndex, characteristic) in service.characteristics.enumerated() { + for (descriptorIndex, _) in characteristic.descriptors.enumerated() { + guard descriptorBuffers[serviceIndex][characteristicIndex].withUnsafeBufferPointer({ + $0.baseAddress?.advanced(by: descriptorIndex) == pointer + }) else { continue } + services[serviceIndex].characteristics[characteristicIndex].descriptors[descriptorIndex].value = newValue + return true + } + } + } + return false + } +} + +internal extension NimBLE.Context { + + func readCharacteristic( + handle attributeHandle: UInt16, + connection: ble_gap_conn_desc, + accessContext: borrowing GATTServer.AttributeAccessContext + ) throws(ATTError) { + guard let characteristic = gattServer.characteristic(for: attributeHandle) else { + throw .unlikelyError + } + let address = BluetoothAddress(bytes: connection.peer_ota_addr.val) + assert(address != .zero) + log?("[\(address)] Read characteristic \(characteristic.uuid) - Handle 0x\(attributeHandle.toHexadecimal())") + let central = Central(id: address) + let maximumUpdateValueLength = 20 // TODO: Get MTU + let offset = accessContext.offset + let data = characteristic.value + // ask delegate + let request = GATTReadRequest( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: characteristic.uuid, + handle: attributeHandle, + value: data, + offset: offset + ) + if let error = gattServer.willRead?(request) { + throw error + } + // respond with data + var memoryBuffer = accessContext.memoryBuffer + memoryBuffer.append(contentsOf: data) + } + + func readDescriptor( + handle attributeHandle: UInt16, + connection: ble_gap_conn_desc, + accessContext: borrowing GATTServer.AttributeAccessContext + ) throws(ATTError) { + guard let descriptor = gattServer.descriptor(for: accessContext.pointer.pointee.dsc) else { + throw .unlikelyError + } + let address = BluetoothAddress(bytes: connection.peer_ota_addr.val) + assert(address != .zero) + log?("[\(address)] Read descriptor \(descriptor.uuid) - Handle 0x\(attributeHandle.toHexadecimal())") + let central = Central(id: address) + let maximumUpdateValueLength = 20 // TODO: Get MTU + let offset = accessContext.offset + let data = descriptor.value + // ask delegate + let request = GATTReadRequest( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: descriptor.uuid, + handle: attributeHandle, + value: data, + offset: offset + ) + if let error = gattServer.willRead?(request) { + throw error + } + // return data + var memoryBuffer = accessContext.memoryBuffer + memoryBuffer.append(contentsOf: data) + } + + mutating func writeCharacteristic( + handle attributeHandle: UInt16, + connection: ble_gap_conn_desc, + accessContext: borrowing GATTServer.AttributeAccessContext + ) throws(ATTError) { + guard let characteristic = gattServer.characteristic(for: attributeHandle), + let newValue = try? [UInt8](accessContext.memoryBuffer) else { + throw .unlikelyError + } + let address = BluetoothAddress(bytes: connection.peer_ota_addr.val) + assert(address != .zero) + log?("[\(address)] Write characteristic \(characteristic.uuid) - Handle 0x\(attributeHandle.toHexadecimal())") + let central = Central(id: address) + let maximumUpdateValueLength = 20 // TODO: Get MTU + let oldValue = characteristic.value + + // ask delegate + let request = GATTWriteRequest( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: characteristic.uuid, + handle: attributeHandle, + value: oldValue, + newValue: newValue + ) + // ask delegate + if let error = gattServer.willWrite?(request) { + throw error + } + // update value + let isValidAttribute = gattServer.didWriteCharacteristic(newValue, for: attributeHandle) + assert(isValidAttribute) + // confirmation + let confirmation = GATTWriteConfirmation( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: characteristic.uuid, + handle: attributeHandle, + value: newValue + ) + gattServer.didWrite?(confirmation) + } + + mutating func writeDescriptor( + handle attributeHandle: UInt16, + connection: ble_gap_conn_desc, + accessContext: borrowing GATTServer.AttributeAccessContext + ) throws(ATTError) { + guard let descriptor = gattServer.descriptor(for: accessContext.pointer.pointee.dsc), + let newValue = try? [UInt8](accessContext.memoryBuffer) else { + throw .unlikelyError + } + let address = BluetoothAddress(bytes: connection.peer_ota_addr.val) + assert(address != .zero) + log?("[\(address)] Write descriptor \(descriptor.uuid) - Handle 0x\(attributeHandle.toHexadecimal())") + let central = Central(id: address) + let maximumUpdateValueLength = 20 // TODO: Get MTU + let oldValue = descriptor.value + + // ask delegate + let request = GATTWriteRequest( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: descriptor.uuid, + handle: attributeHandle, + value: oldValue, + newValue: newValue + ) + // ask delegate + if let error = gattServer.willWrite?(request) { + throw error + } + // update value + let isValidAttribute = gattServer.didWriteDescriptor(newValue, for: accessContext.pointer.pointee.dsc) + assert(isValidAttribute) + // confirmation + let confirmation = GATTWriteConfirmation( + central: central, + maximumUpdateValueLength: maximumUpdateValueLength, + uuid: descriptor.uuid, + handle: attributeHandle, + value: newValue + ) + gattServer.didWrite?(confirmation) + } +} + +// typedef int ble_gatt_access_fn(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +internal func _ble_gatt_access( + connectionHandle: UInt16, + attributeHandle: UInt16, + accessContext accessContextPointer: UnsafeMutablePointer?, + context contextPointer: UnsafeMutableRawPointer? +) -> CInt { + guard let context = contextPointer?.assumingMemoryBound(to: NimBLE.Context.self), + let accessContextPointer = accessContextPointer, + let connection = try? GAP(context: context).connection(for: connectionHandle) else { + return BLE_ATT_ERR_UNLIKELY + } + let accessContext = GATTServer.AttributeAccessContext(accessContextPointer) + do { + switch accessContext.operationType { + case BLE_GATT_ACCESS_OP_READ_CHR: + // read characteristic + try context.pointee.readCharacteristic( + handle: attributeHandle, + connection: connection, + accessContext: accessContext + ) + + case BLE_GATT_ACCESS_OP_WRITE_CHR: + try context.pointee.writeCharacteristic( + handle: attributeHandle, + connection: connection, + accessContext: accessContext + ) + case BLE_GATT_ACCESS_OP_READ_DSC: + // read descriptor + try context.pointee.readDescriptor( + handle: attributeHandle, + connection: connection, + accessContext: accessContext + ) + case BLE_GATT_ACCESS_OP_WRITE_DSC: + try context.pointee.writeDescriptor( + handle: attributeHandle, + connection: connection, + accessContext: accessContext + ) + default: + assertionFailure("Unknown operation") + return CInt(ATTError.unlikelyError.rawValue) + } + } + catch { + return CInt(error.rawValue) + } + return 0 +} + +internal extension GATTServer { + + struct AttributeAccessContext: ~Copyable { + + let pointer: UnsafeMutablePointer + + init(_ pointer: UnsafeMutablePointer) { + self.pointer = pointer + } + + var memoryBuffer: MemoryBuffer { + MemoryBuffer(pointer.pointee.om, retain: false) + } + + var operationType: Int32 { + Int32(pointer.pointee.op) + } + + var offset: Int { + 0 + } + } +} + +/// NimBLE Memory Buffer +public struct MemoryBuffer: ~Copyable { + + var pointer: UnsafeMutablePointer + + let retain: Bool + + public init(_ other: borrowing MemoryBuffer) { + guard let pointer = r_os_mbuf_dup(other.pointer) else { + fatalError("Unable to duplicate buffer") + } + self.init(pointer, retain: true) + } + + init(_ pointer: UnsafeMutablePointer, retain: Bool) { + self.pointer = pointer + self.retain = retain + } + + deinit { + if retain { + r_os_mbuf_free(pointer) + } + } + + public mutating func append(_ pointer: UnsafeRawPointer, count: UInt16) throws(NimBLEError) { + try r_os_mbuf_append(self.pointer, pointer, count).throwsError() + } + + public mutating func append(_ pointer: UnsafePointer, count: Int) { + do { try append(UnsafeRawPointer(pointer), count: UInt16(count)) } + catch { + //fatalError("Unable to append to buffer") + } + } + + public mutating func append (contentsOf bytes: C) where C.Element == UInt8 { + guard bytes.isEmpty == false else { + return + } + bytes.withContiguousStorageIfAvailable { + append($0.baseAddress!, count: $0.count) + } + } + + public var count: Int { + Int(r_os_mbuf_len(self.pointer)) + } +} + +public extension [UInt8] { + + init(_ memoryBuffer: borrowing MemoryBuffer) throws(NimBLEError) { + let length = memoryBuffer.count + guard length > 0 else { + self.init() + return + } + var outLength: UInt16 = 0 + self.init(repeating: 0, count: length) + try self.withUnsafeMutableBytes { + ble_hs_mbuf_to_flat(memoryBuffer.pointer, $0.baseAddress, UInt16(length), &outLength) + }.throwsError() + assert(outLength == length) + assert(self.count == length) + } +} + + +internal extension ble_uuid_any_t { + + init(_ uuid: BluetoothUUID) { + switch uuid { + case .bit16(let value): + self.init(u16: .init(uuid: value)) + case .bit32(let value): + self.init(u32: .init(uuid: value)) + case .bit128(let value): + self.init(u128: .init(uuid: value)) + } + } + + init(_ string: String) throws(NimBLEError) { + self.init() + try withUnsafeMutablePointer(to: &self) { uuidBuffer in + string.withCString { cString in + ble_uuid_from_str(uuidBuffer, cString) + } + }.throwsError() + } + + init(buffer: UnsafeRawBufferPointer) throws(NimBLEError) { + self.init() + try ble_uuid_init_from_buf( + &self, + buffer.baseAddress, + buffer.count + ).throwsError() + } + + init(_ pointer: UnsafePointer) { + self.init() + ble_uuid_copy(&self, pointer) + } +} + +extension ble_uuid_any_t: @retroactive CustomStringConvertible { + + public var description: String { + withUnsafeBytes(of: self) { uuidBuffer in + var cString = [CChar](repeating: 0, count: 37) + ble_uuid_to_str( + uuidBuffer.assumingMemoryBound(to: ble_uuid_t.self).baseAddress, + &cString + ) + return String(cString: &cString) + } + } +} + +extension ble_uuid_any_t: @retroactive Equatable { + + public static func == (lhs: ble_uuid_any_t, rhs: ble_uuid_any_t) -> Bool { + withUnsafeBytes(of: lhs) { + $0.withMemoryRebound(to: ble_uuid_t.self) { lhsPointer in + withUnsafeBytes(of: rhs) { + $0.withMemoryRebound(to: ble_uuid_t.self) { rhsPointer in + ble_uuid_cmp(lhsPointer.baseAddress, rhsPointer.baseAddress) == 0 + } + } + } + } + } +} + +extension ble_uuid_any_t { //: @retroactive DataConvertible { + + public var dataLength: Int { + let value = withUnsafeBytes(of: self) { + $0.withMemoryRebound(to: ble_uuid_t.self) { + ble_uuid_length($0.baseAddress) + } + } + return Int(value) + } +} + +internal extension ble_uuid16_t { + + init(uuid: UInt16) { + self.init(u: .init(type: UInt8(BLE_UUID_TYPE_16)), value: uuid) + } +} + +internal extension ble_uuid32_t { + + init(uuid: UInt32) { + self.init(u: .init(type: UInt8(BLE_UUID_TYPE_32)), value: uuid) + } +} + +internal extension ble_uuid128_t { + + init(uuid: UInt128) { + self.init(u: .init(type: UInt8(BLE_UUID_TYPE_128)), value: uuid.bytes) + } +} diff --git a/esp32-ble-peripheral-sdk/main/Peer.swift b/esp32-ble-peripheral-sdk/main/Peer.swift new file mode 100644 index 0000000..99d0008 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/Peer.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Bluetooth LE Peer (Central, Peripheral) +public protocol Peer: Hashable, Sendable where ID: Hashable { + + associatedtype ID: Hashable + + /// Unique identifier of the peer. + var id: ID { get } +} + +// MARK: Hashable + +public extension Peer { + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } +} + +// MARK: CustomStringConvertible + +extension Peer where Self: CustomStringConvertible, ID: CustomStringConvertible { + + public var description: String { + return id.description + } +} + +// MARK: - Central + +/// Central Peer +/// +/// Represents a remote central device that has connected to an app implementing the peripheral role on a local device. +public struct Central: Peer, Identifiable, Sendable, CustomStringConvertible { + + public let id: BluetoothAddress + + public init(id: BluetoothAddress) { + self.id = id + } +} + +// MARK: - Peripheral + +/// Peripheral Peer +/// +/// Represents a remote peripheral device that has been discovered. +public struct Peripheral: Peer, Identifiable, Sendable, CustomStringConvertible { + + public let id: BluetoothAddress + + public init(id: BluetoothAddress) { + self.id = id + } +} diff --git a/esp32-ble-peripheral-sdk/main/PeripheralProtocol.swift b/esp32-ble-peripheral-sdk/main/PeripheralProtocol.swift new file mode 100644 index 0000000..d743558 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/PeripheralProtocol.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// GATT Peripheral Manager +/// +/// Implementation varies by operating system. +public protocol PeripheralManager { + + /// Central Peer + /// + /// Represents a remote central device that has connected to an app implementing the peripheral role on a local device. + associatedtype Central: Peer + + associatedtype Data: DataContainer + + associatedtype Error: Swift.Error + + var log: (@Sendable (String) -> ())? { get set } + + /// Start advertising the peripheral and listening for incoming connections. + func start() throws(Error) + + /// Stop the peripheral. + func stop() + + /// A Boolean value that indicates whether the peripheral is advertising data. + var isAdvertising: Bool { get } + + /// Attempts to add the specified service to the GATT database. + /// + /// - Returns: Handle for service declaration and handles for characteristic value handles. + func add(service: GATTAttribute.Service) throws(Error) -> (UInt16, [UInt16]) + + /// Removes the service with the specified handle. + func remove(service: UInt16) + + /// Clears the local GATT database. + func removeAllServices() + + /// Callback to handle GATT read requests. + var willRead: ((GATTReadRequest) -> ATTError?)? { get set } + + /// Callback to handle GATT write requests. + var willWrite: ((GATTWriteRequest) -> ATTError?)? { get set } + + /// Callback to handle post-write actions for GATT write requests. + var didWrite: ((GATTWriteConfirmation) -> ())? { get set } + + /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. + func write(_ newValue: Data, forCharacteristic handle: UInt16) + + /// Modify the value of a characteristic, optionally emiting notifications if configured on the specified connection. + /// + /// Throws error if central is unknown or disconnected. + func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) throws(Error) + + /// Read the value of the characteristic with specified handle. + subscript(characteristic handle: UInt16) -> Data { get } + + /// Read the value of the characteristic with specified handle for the specified connection. + func value(for characteristicHandle: UInt16, central: Central) throws(Error) -> Data +} + +// MARK: - Supporting Types + +public protocol GATTRequest { + + associatedtype Central: Peer + + associatedtype Data: DataContainer + + var central: Central { get } + + var maximumUpdateValueLength: Int { get } + + var uuid: BluetoothUUID { get } + + var handle: UInt16 { get } + + var value: Data { get } +} + +public struct GATTReadRequest : GATTRequest, Equatable, Hashable, Sendable { + + public let central: Central + + public let maximumUpdateValueLength: Int + + public let uuid: BluetoothUUID + + public let handle: UInt16 + + public let value: Data + + public let offset: Int + + public init(central: Central, + maximumUpdateValueLength: Int, + uuid: BluetoothUUID, + handle: UInt16, + value: Data, + offset: Int) { + + self.central = central + self.maximumUpdateValueLength = maximumUpdateValueLength + self.uuid = uuid + self.handle = handle + self.value = value + self.offset = offset + } +} + +public struct GATTWriteRequest : GATTRequest, Equatable, Hashable, Sendable { + + public let central: Central + + public let maximumUpdateValueLength: Int + + public let uuid: BluetoothUUID + + public let handle: UInt16 + + public let value: Data + + public let newValue: Data + + public init(central: Central, + maximumUpdateValueLength: Int, + uuid: BluetoothUUID, + handle: UInt16, + value: Data, + newValue: Data) { + + self.central = central + self.maximumUpdateValueLength = maximumUpdateValueLength + self.uuid = uuid + self.handle = handle + self.value = value + self.newValue = newValue + } +} + +public struct GATTWriteConfirmation : GATTRequest, Equatable, Hashable, Sendable { + + public let central: Central + + public let maximumUpdateValueLength: Int + + public let uuid: BluetoothUUID + + public let handle: UInt16 + + public let value: Data + + public init(central: Central, + maximumUpdateValueLength: Int, + uuid: BluetoothUUID, + handle: UInt16, + value: Data) { + + self.central = central + self.maximumUpdateValueLength = maximumUpdateValueLength + self.uuid = uuid + self.handle = handle + self.value = value + } +} + diff --git a/esp32-ble-peripheral-sdk/main/String.swift b/esp32-ble-peripheral-sdk/main/String.swift new file mode 100644 index 0000000..f630d15 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/String.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#endif + +internal extension String { + + /// Initialize from UTF8 data. + init?(utf8 data: Data) { + #if canImport(Darwin) + // Newer Darwin and other platforms use StdLib parsing + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + self.init(validating: data, as: UTF8.self) + } else { + // Older Darwin uses Foundation + self.init(bytes: data, encoding: .utf8) + } + #else + self.init(validating: data, as: UTF8.self) + #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 +} diff --git a/esp32-ble-peripheral-sdk/main/System.swift b/esp32-ble-peripheral-sdk/main/System.swift new file mode 100644 index 0000000..e18d1e1 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/System.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Bionic) +import Bionic +#endif + +// Declares the required C functions +#if hasFeature(Embedded) +@_silgen_name("memcmp") +internal func _memcmp( + _ p1: UnsafeRawPointer?, + _ p2: UnsafeRawPointer?, + _ size: Int +) -> Int32 +#else +internal func _memcmp( + _ p1: UnsafeRawPointer?, + _ p2: UnsafeRawPointer?, + _ size: Int +) -> Int32 { + memcmp(p1, p2, size) +} +#endif + +#if hasFeature(Embedded) +@_silgen_name("snprintf") +internal func _snprintf_uint8_t( + _ pointer: UnsafeMutablePointer, + _ length: Int, + _ format: UnsafePointer, + _ arg: UInt8 +) -> Int32 +#endif diff --git a/esp32-ble-peripheral-sdk/main/UInt128.swift b/esp32-ble-peripheral-sdk/main/UInt128.swift new file mode 100644 index 0000000..915c3c0 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/UInt128.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// MARK: - ByteValue + +extension UInt128: ByteValue { + + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public var bytes: ByteValue { + @_transparent + get { + unsafeBitCast(self, to: ByteValue.self) + } + + @_transparent + set { + self = .init(bytes: newValue) + } + } + + public init(bytes: ByteValue) { + self = unsafeBitCast(bytes, to: Self.self) + } +} + +// MARK: - Data Convertible + +extension UInt128: DataConvertible { + + public init?(data: Data) { + guard data.count == UInt128.length + else { return nil } + self.init(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15])) + } + + public func append(to data: inout Data) { + unsafeAppend(to: &data) + } + + /// Length of value when encoded into data. + public var dataLength: Int { Self.length } +} + +// MARK: - UUID + +public extension UInt128 { + + init(uuid: UUID) { + /// UUID is always big endian + let bigEndian = UInt128(bytes: uuid.uuid) + self.init(bigEndian: bigEndian) + } +} + +public extension UUID { + + init(_ value: UInt128) { + + // UUID is always stored in big endian bytes + let bytes = value.bigEndian.bytes + + self.init(bytes: (bytes.0, + bytes.1, + bytes.2, + bytes.3, + bytes.4, + bytes.5, + bytes.6, + bytes.7, + bytes.8, + bytes.9, + bytes.10, + bytes.11, + bytes.12, + bytes.13, + bytes.14, + bytes.15)) + } +} + +// MARK: - Backwards compatibility + +internal extension UInt128 { + + var hexadecimal: String { + let bytes = self.bigEndian.bytes + return bytes.0.toHexadecimal() + + bytes.1.toHexadecimal() + + bytes.2.toHexadecimal() + + bytes.3.toHexadecimal() + + bytes.4.toHexadecimal() + + bytes.5.toHexadecimal() + + bytes.6.toHexadecimal() + + bytes.7.toHexadecimal() + + bytes.8.toHexadecimal() + + bytes.9.toHexadecimal() + + bytes.10.toHexadecimal() + + bytes.11.toHexadecimal() + + bytes.12.toHexadecimal() + + bytes.13.toHexadecimal() + + bytes.14.toHexadecimal() + + bytes.15.toHexadecimal() + } +} diff --git a/esp32-ble-peripheral-sdk/main/UUID.swift b/esp32-ble-peripheral-sdk/main/UUID.swift new file mode 100644 index 0000000..0530b1c --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/UUID.swift @@ -0,0 +1,233 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Represents UUID strings, which can be used to uniquely identify types, interfaces, and other items. +public struct UUID: Sendable { + + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public let uuid: ByteValue + + /// Create a UUID from a `uuid_t`. + public init(uuid: ByteValue) { + self.uuid = uuid + } +} + +internal extension UUID { + + static var length: Int { return 16 } + static var stringLength: Int { return 36 } + static var unformattedStringLength: Int { return 32 } +} + +extension UUID { + + public static var bitWidth: Int { return 128 } + + public init(bytes: ByteValue) { + self.init(uuid: bytes) + } + + public var bytes: ByteValue { + get { return uuid } + set { self = UUID(uuid: newValue) } + } +} + +extension UUID { + + /// Create a new UUID with RFC 4122 version 4 random bytes. + public init() { + var uuidBytes: ByteValue = ( + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255) + ) + + // Set the version to 4 (random UUID) + uuidBytes.6 = (uuidBytes.6 & 0x0F) | 0x40 + + // Set the variant to RFC 4122 + uuidBytes.8 = (uuidBytes.8 & 0x3F) | 0x80 + + self.init(uuid: uuidBytes) + } + + @inline(__always) + internal func withUUIDBytes(_ work: (UnsafeBufferPointer) throws -> R) rethrows -> R { + return try withExtendedLifetime(self) { + try withUnsafeBytes(of: uuid) { rawBuffer in + return try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in + return try work(buffer) + } + } + } + } + + /// Create a UUID from a string such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". + /// + /// Returns nil for invalid strings. + public init?(uuidString string: String) { + guard let value = UInt128.bigEndian(uuidString: string) else { + return nil + } + self.init(uuid: value.bytes) + } + + /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + public var uuidString: String { + UInt128(bytes: uuid).bigEndianUUIDString + } +} + +extension UUID: Equatable { + + public static func ==(lhs: UUID, rhs: UUID) -> Bool { + withUnsafeBytes(of: lhs) { lhsPtr in + withUnsafeBytes(of: rhs) { rhsPtr in + let lhsTuple = lhsPtr.loadUnaligned(as: (UInt64, UInt64).self) + let rhsTuple = rhsPtr.loadUnaligned(as: (UInt64, UInt64).self) + return (lhsTuple.0 ^ rhsTuple.0) | (lhsTuple.1 ^ rhsTuple.1) == 0 + } + } + } +} + +extension UUID: Hashable { + + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: uuid) { buffer in + hasher.combine(bytes: buffer) + } + } +} + +extension UUID: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + return uuidString + } + + public var debugDescription: String { + return description + } +} + +extension UUID : Comparable { + + public static func < (lhs: UUID, rhs: UUID) -> Bool { + var leftUUID = lhs.uuid + var rightUUID = rhs.uuid + var result: Int = 0 + var diff: Int = 0 + withUnsafeBytes(of: &leftUUID) { leftPtr in + withUnsafeBytes(of: &rightUUID) { rightPtr in + for offset in (0 ..< MemoryLayout.size).reversed() { + diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - + Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) + // Constant time, no branching equivalent of + // if (diff != 0) { + // result = diff; + // } + result = (result & (((diff - 1) & ~diff) >> 8)) | diff + } + } + } + + return result < 0 + } +} + +fileprivate extension UInt128 { + + static func bigEndian(uuidString string: String) -> UInt128? { + guard string.utf8.count == 36, + let separator = "-".utf8.first else { + return nil + } + 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 hexadecimal = (a + b + c + d + e) + guard hexadecimal.utf8.count == 32 else { + return nil + } + guard let value = UInt128(parse: hexadecimal, radix: 16) else { + return nil + } + return value.bigEndian + } + + /// Generate UUID string, e.g. `0F4DD6A4-0F71-48EF-98A5-996301B868F9` from a value initialized in its big endian order. + var bigEndianUUIDString: String { + + let a = (bytes.0.toHexadecimal() + + bytes.1.toHexadecimal() + + bytes.2.toHexadecimal() + + bytes.3.toHexadecimal()) + + let b = (bytes.4.toHexadecimal() + + bytes.5.toHexadecimal()) + + let c = (bytes.6.toHexadecimal() + + bytes.7.toHexadecimal()) + + let d = (bytes.8.toHexadecimal() + + bytes.9.toHexadecimal()) + + let e = (bytes.10.toHexadecimal() + + bytes.11.toHexadecimal() + + bytes.12.toHexadecimal() + + bytes.13.toHexadecimal() + + bytes.14.toHexadecimal() + + bytes.15.toHexadecimal()) + + return a + "-" + b + "-" + c + "-" + d + "-" + e + } +} + +internal extension UInt128 { + + /// Parse a UUID string. + init?(uuidString string: String) { + guard let bigEndian = Self.bigEndian(uuidString: string) else { + return nil + } + self.init(bigEndian: bigEndian) + } + + /// Generate UUID string, e.g. `0F4DD6A4-0F71-48EF-98A5-996301B868F9`. + var uuidString: String { + self.bigEndian.bigEndianUUIDString + } +} diff --git a/esp32-ble-peripheral-sdk/main/iBeacon.swift b/esp32-ble-peripheral-sdk/main/iBeacon.swift new file mode 100644 index 0000000..17002c0 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/iBeacon.swift @@ -0,0 +1,166 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/** + Apple iBeacon + + iBeacon is an exciting technology enabling new location awareness possibilities for apps. Leveraging Bluetooth Low Energy (BLE), a device with iBeacon technology can be used to establish a region around an object. This allows an iOS device to determine when it has entered or left the region, along with an estimation of proximity to a beacon . There are both hardware and software components to consider when using iBeacon technology, and this document will give an introduction to both, along with suggested uses and best practices to help ensure a highly effective deployment leading to an outstanding user experience. + + Devices with iBeacon technology can be powered using coin cell batteries for a month or longer, or operate for months at a time using larger batteries, or can be powered externally for extended periods of time. iOS devices can also be configured to generate iBeacon advertisements, although this functionality is limited in scope. This would be appropriate for uses such as a Point Of Sale or kiosk application, or for an application that wants to become an iBeacon for a short time while someone is actively using the application. + + - SeeAlso: [Getting Started with iBeacon](https://developer.apple.com/ibeacon/Getting-Started-with-iBeacon.pdf). + */ +public struct AppleBeacon: Equatable, Sendable { + + /// The company that created this specification. + public static var companyIdentifier: CompanyIdentifier { 76 } + + /// The unique ID of the beacons being targeted. + /// + /// Application developers should define a UUID specific to their app and deployment use case. + public var uuid: UUID + + /// The value identifying a group of beacons. + /// + /// Further specifies a specific iBeacon and use case. + /// For example, this could define a sub-region within a larger region defined by the UUID. + public var major: UInt16 + + /// The value identifying a specific beacon within a group. + /// + /// Allows further subdivision of region or use case, specified by the application developer. + public var minor: UInt16 + + /// The received signal strength indicator (RSSI) value (measured in decibels) for the device. + public var rssi: Int8 + + public init(uuid: UUID, + major: UInt16 = 0, + minor: UInt16 = 0, + rssi: Int8) { + + self.uuid = uuid + self.major = major + self.minor = minor + self.rssi = rssi + } +} + +public extension AppleBeacon { + + init?(manufacturerData: GAPManufacturerSpecificData) { + + let data = manufacturerData.additionalData + + guard manufacturerData.companyIdentifier == type(of: self).companyIdentifier, + data.count == type(of: self).additionalDataLength + else { return nil } + + let dataType = data[0] + + guard dataType == type(of: self).appleDataType + else { return nil } + + let length = data[1] + + guard length == type(of: self).length + else { return nil } + + let uuid = UUID(UInt128(bigEndian: UInt128(data: data.subdata(in: 2 ..< 18))!)) + let major = UInt16(bigEndian: UInt16(bytes: (data[18], data[19]))) + let minor = UInt16(bigEndian: UInt16(bytes: (data[20], data[21]))) + let rssi = Int8(bitPattern: data[22]) + + self.init(uuid: uuid, major: major, minor: minor, rssi: rssi) + } +} + +public extension GAPManufacturerSpecificData { + + init(beacon: AppleBeacon) { + var additionalData = AdditionalData() + additionalData.reserveCapacity(AppleBeacon.additionalDataLength) + beacon.appendAdditionalManufacturerData(to: &additionalData) + assert(additionalData.count == AppleBeacon.additionalDataLength) + self.init( + companyIdentifier: AppleBeacon.companyIdentifier, + additionalData: additionalData + ) + } +} + +internal extension AppleBeacon { + + /// Apple iBeacon data type. + static var appleDataType: UInt8 { 0x02 } // iBeacon + + /// The length of the TLV encoded data. + static var length: UInt8 { 0x15 } // length: 21 = 16 byte UUID + 2 bytes major + 2 bytes minor + 1 byte RSSI + + static var additionalDataLength: Int { return Int(length) + 2 } + + func appendAdditionalManufacturerData (to data: inout T) { + + data += type(of: self).appleDataType // tlvPrefix + data += type(of: self).length + data += UInt128(uuid: uuid).bigEndian // uuidBytes + data += major.bigEndian + data += minor.bigEndian + data += UInt8(bitPattern: rssi) + } +} + +public extension LowEnergyAdvertisingData { + + init(beacon: AppleBeacon, + flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR]) { + let manufacturerData = AppleBeacon.ManufacturerData(beacon) // storage on stack + self = GAPDataEncoder.encode(flags, manufacturerData) + } +} + +internal extension AppleBeacon { + + struct ManufacturerData: GAPData { + + static var dataType: GAPDataType { .manufacturerSpecificData } + + internal let beacon: AppleBeacon + + init(_ beacon: AppleBeacon) { + self.beacon = beacon + } + + init?(data: Data) where Data : DataContainer { + + guard let manufacturerData = GAPManufacturerSpecificData(data: data), + let beacon = AppleBeacon(manufacturerData: manufacturerData) + else { return nil } + + self.init(beacon) + } + + var dataLength: Int { return 2 + AppleBeacon.additionalDataLength } + + func append(to data: inout Data) where Data : DataContainer { + data += self + } + } +} + +extension AppleBeacon.ManufacturerData: DataConvertible { + + @usableFromInline + static func += (data: inout Data, value: AppleBeacon.ManufacturerData) where Data : DataContainer { + data += GAPManufacturerSpecificData(companyIdentifier: AppleBeacon.companyIdentifier) + value.beacon.appendAdditionalManufacturerData(to: &data) + } +} diff --git a/esp32-ble-peripheral-sdk/main/idf_component.yml b/esp32-ble-peripheral-sdk/main/idf_component.yml new file mode 100644 index 0000000..4dcbfc4 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/led_strip: "^2.4.1" \ No newline at end of file diff --git a/esp32-ble-peripheral-sdk/main/linker.lf b/esp32-ble-peripheral-sdk/main/linker.lf new file mode 100644 index 0000000..6fdc4a8 --- /dev/null +++ b/esp32-ble-peripheral-sdk/main/linker.lf @@ -0,0 +1,20 @@ +[sections:flash_text_swift] +entries: + .swift_modhash+ + +[sections:dram0_swift] +entries: + .got+ + .got.plt+ + +[scheme:swift_default] +entries: + flash_text_swift -> flash_text + dram0_swift -> dram0_data + +[mapping:swift_default] +archive: * +entries: + * (swift_default); + flash_text_swift -> flash_text SURROUND (swift_text), + dram0_swift -> dram0_data SURROUND (swift_dram0) diff --git a/esp32-ble-peripheral-sdk/sdkconfig.defaults b/esp32-ble-peripheral-sdk/sdkconfig.defaults new file mode 100644 index 0000000..8cfa5f4 --- /dev/null +++ b/esp32-ble-peripheral-sdk/sdkconfig.defaults @@ -0,0 +1,8 @@ +# +# BT config +# +CONFIG_BT_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70 +CONFIG_BT_NIMBLE_EXT_ADV=n