diff --git a/esp32-ibeacon-sdk/CMakeLists.txt b/esp32-ibeacon-sdk/CMakeLists.txt new file mode 100644 index 0000000..bdc0558 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/README.md b/esp32-ibeacon-sdk/README.md new file mode 100644 index 0000000..c4add45 --- /dev/null +++ b/esp32-ibeacon-sdk/README.md @@ -0,0 +1,39 @@ +# esp32-ibeacon-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-ibeacon-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-ibeacon-sdk/assets/images/lightblue.png b/esp32-ibeacon-sdk/assets/images/lightblue.png new file mode 100644 index 0000000..19d294c Binary files /dev/null and b/esp32-ibeacon-sdk/assets/images/lightblue.png differ diff --git a/esp32-ibeacon-sdk/assets/images/nrfconnect.jpeg b/esp32-ibeacon-sdk/assets/images/nrfconnect.jpeg new file mode 100644 index 0000000..6034e27 Binary files /dev/null and b/esp32-ibeacon-sdk/assets/images/nrfconnect.jpeg differ diff --git a/esp32-ibeacon-sdk/main/BluetoothAddress.swift b/esp32-ibeacon-sdk/main/BluetoothAddress.swift new file mode 100644 index 0000000..5d1bd51 --- /dev/null +++ b/esp32-ibeacon-sdk/main/BluetoothAddress.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// 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. +public struct BluetoothAddress: 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 { return BluetoothAddress(bytes: (.min, .min, .min, .min, .min, .min)) } + + /// The maximum representable value in this type. + static var max: BluetoothAddress { return BluetoothAddress(bytes: (.max, .max, .max, .max, .max, .max)) } + + /// A zero address. + static var zero: BluetoothAddress { return .min } +} + +// MARK: - ByteValue + +extension BluetoothAddress { + + /// Raw Bluetooth Address 6 byte (48 bit) value. + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public static var bitWidth: Int { return 48 } + + public static var length: Int { return 6 } +} + +// 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) { + withUnsafeBytes(of: bytes) { hasher.combine(bytes: $0) } + } +} + +// 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 + +public extension BluetoothAddress { + + init?(data: Data) { + guard data.count == type(of: 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-ibeacon-sdk/main/BridgingHeader.h b/esp32-ibeacon-sdk/main/BridgingHeader.h new file mode 100644 index 0000000..eb92f8f --- /dev/null +++ b/esp32-ibeacon-sdk/main/BridgingHeader.h @@ -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 +// +//===----------------------------------------------------------------------===// + +#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" + +#ifndef MYNEWT_VAL_BLE_LL_WHITELIST_SIZE +#define MYNEWT_VAL_BLE_LL_WHITELIST_SIZE CONFIG_BT_NIMBLE_WHITELIST_SIZE +#endif diff --git a/esp32-ibeacon-sdk/main/ByteSwap.swift b/esp32-ibeacon-sdk/main/ByteSwap.swift new file mode 100644 index 0000000..94c40eb --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/CMakeLists.txt b/esp32-ibeacon-sdk/main/CMakeLists.txt new file mode 100644 index 0000000..2195294 --- /dev/null +++ b/esp32-ibeacon-sdk/main/CMakeLists.txt @@ -0,0 +1,88 @@ +# 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 + Error.swift + NimBLE.swift + Error.swift + BluetoothAddress.swift + ByteSwap.swift + CompanyIdentifier.swift + LowEnergyAddressType.swift + LowEnergyAdvertisingData.swift + Data.swift + String.swift + Hexadecimal.swift + Encoder.swift + GAPData.swift + GAPDataType.swift + GAPFlags.swift + GAPShortLocalName.swift + GAPManufacturerSpecificData.swift + UInt128.swift + UUID.swift + iBeacon.swift + Integer.swift +) diff --git a/esp32-ibeacon-sdk/main/CompanyIdentifier.swift b/esp32-ibeacon-sdk/main/CompanyIdentifier.swift new file mode 100644 index 0000000..20fa3a7 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/Data.swift b/esp32-ibeacon-sdk/main/Data.swift new file mode 100644 index 0000000..76d89c4 --- /dev/null +++ b/esp32-ibeacon-sdk/main/Data.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// 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]) + } +} + +/// Can be converted into data. +internal protocol DataConvertible { + + /// Append data representation into buffer. + static func += (data: inout T, value: Self) + + /// Length of value when encoded into data. + var dataLength: Int { get } +} + +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) + } +} + +extension DataContainer { + + mutating func append (_ value: T) { + self += value + } +} + +// MARK: - UnsafeDataConvertible + +/// Internal Data casting protocol +internal protocol UnsafeDataConvertible: DataConvertible { } + +extension UnsafeDataConvertible { + + @usableFromInline + var dataLength: Int { + return MemoryLayout.size + } + + /// Append data representation into buffer. + static func += (data: inout T, value: Self) { + withUnsafePointer(to: value) { + $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size) { + data.append($0, count: MemoryLayout.size) + } + } + } +} + +extension UInt16: UnsafeDataConvertible { } +extension UInt32: UnsafeDataConvertible { } +extension UInt64: UnsafeDataConvertible { } +extension UInt128: UnsafeDataConvertible { } +extension BluetoothAddress: UnsafeDataConvertible { } \ No newline at end of file diff --git a/esp32-ibeacon-sdk/main/Encoder.swift b/esp32-ibeacon-sdk/main/Encoder.swift new file mode 100644 index 0000000..3a7d067 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/Error.swift b/esp32-ibeacon-sdk/main/Error.swift new file mode 100644 index 0000000..12089ee --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/GAPData.swift b/esp32-ibeacon-sdk/main/GAPData.swift new file mode 100644 index 0000000..56dc57a --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/GAPDataType.swift b/esp32-ibeacon-sdk/main/GAPDataType.swift new file mode 100644 index 0000000..55ad670 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/GAPFlags.swift b/esp32-ibeacon-sdk/main/GAPFlags.swift new file mode 100644 index 0000000..637f5c7 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/GAPManufacturerSpecificData.swift b/esp32-ibeacon-sdk/main/GAPManufacturerSpecificData.swift new file mode 100644 index 0000000..5e8f1ff --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/GAPShortLocalName.swift b/esp32-ibeacon-sdk/main/GAPShortLocalName.swift new file mode 100644 index 0000000..35808de --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/Hexadecimal.swift b/esp32-ibeacon-sdk/main/Hexadecimal.swift new file mode 100644 index 0000000..08ada87 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/Integer.swift b/esp32-ibeacon-sdk/main/Integer.swift new file mode 100644 index 0000000..c466c4f --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/LowEnergyAddressType.swift b/esp32-ibeacon-sdk/main/LowEnergyAddressType.swift new file mode 100644 index 0000000..2cb2cf1 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/LowEnergyAdvertisingData.swift b/esp32-ibeacon-sdk/main/LowEnergyAdvertisingData.swift new file mode 100644 index 0000000..400694b --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/Main.swift b/esp32-ibeacon-sdk/main/Main.swift new file mode 100644 index 0000000..521e6d3 --- /dev/null +++ b/esp32-ibeacon-sdk/main/Main.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 +// +//===----------------------------------------------------------------------===// + +@_cdecl("app_main") +func app_main() { + print("Hello from Swift on ESP32-C6!") + + var bluetooth: NimBLE + do { + bluetooth = try NimBLE() + } + catch { + print("Bluetooth init failed \(error)") + return + } + + do { + // 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) + + // start advertisement + try bluetooth.gap.startAdvertising() + print("Advertisement name: \(name)") + } + catch { + print("Bluetooth error \(error.rawValue)") + } + + let delayMs: UInt32 = 500 + + while true { + vTaskDelay(delayMs / (1000 / UInt32(configTICK_RATE_HZ))) + } +} diff --git a/esp32-ibeacon-sdk/main/NimBLE.swift b/esp32-ibeacon-sdk/main/NimBLE.swift new file mode 100644 index 0000000..e6c6a8e --- /dev/null +++ b/esp32-ibeacon-sdk/main/NimBLE.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +struct NimBLE: ~Copyable { + + init() throws(ESPError) { + try nvs_flash_init().throwsESPError() + try nimble_port_init().throwsESPError() + nimble_port_freertos_init(nimble_host_task) + } + + deinit { + nimble_port_freertos_deinit() + } + + var gap = GAP() + + var hostController = HostController() +} + +@_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() +} + +/// NimBLE GAP interface. +public struct GAP: ~Copyable { + + private(set) var advertisementData = LowEnergyAdvertisingData() + + private(set) var scanResponse = LowEnergyAdvertisingData() + + /// 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_NON), 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, nil).throwsError() + } + + /// Stops the currently-active advertising procedure. + public func stopAdvertising() throws(NimBLEError) { + try ble_gap_adv_stop().throwsError() + } + + /// Configures the data to include in subsequent advertisements. + public mutating func setAdvertisement(_ data: LowEnergyAdvertisingData) throws(NimBLEError) { + advertisementData = data + try advertisementData.withUnsafePointer { + ble_gap_adv_set_data($0, Int32(data.length)) + }.throwsError() + } + + /// Configures the data to include in subsequent scan responses. + public mutating func setScanResponse(_ data: LowEnergyAdvertisingData) throws(NimBLEError) { + scanResponse = data + try scanResponse.withUnsafePointer { + ble_gap_adv_rsp_set_data($0, Int32(data.length)) + }.throwsError() + } +} + +internal func _gap_callback(event: UnsafeMutablePointer?, context: UnsafeMutableRawPointer?) -> Int32 { + return 0 +} + +public struct HostController: ~Copyable { + + 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 + } +} \ No newline at end of file diff --git a/esp32-ibeacon-sdk/main/String.swift b/esp32-ibeacon-sdk/main/String.swift new file mode 100644 index 0000000..57374fd --- /dev/null +++ b/esp32-ibeacon-sdk/main/String.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 hasFeature(Embedded) + self.init(validating: data, as: UTF8.self) + #else + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + self.init(validating: data, as: UTF8.self) + } else { + self.init(bytes: data, encoding: .utf8) + } + #endif + } + + #if hasFeature(Embedded) + // Can't use `CVarArg` in Embedded Swift + init?(format: String, length: Int, _ value: UInt8) { + var cString: [CChar] = .init(repeating: 0, count: length + 1) + guard _snprintf_uint8_t(&cString, cString.count, format, value) >= 0 else { + return nil + } + self.init(cString: cString) + } + #elseif canImport(Darwin) + init?(format: String, length: Int, _ value: T) { + var cString: [CChar] = .init(repeating: 0, count: length + 1) + guard snprintf(ptr: &cString, cString.count, format, value) >= 0 else { + return nil + } + self.init(cString: cString) + } + #endif +} + +#if hasFeature(Embedded) +@_silgen_name("snprintf") +internal func _snprintf_uint8_t(_ pointer: UnsafeMutablePointer, _ length: Int, _ format: UnsafePointer, _ arg: UInt8) -> Int32 +#endif \ No newline at end of file diff --git a/esp32-ibeacon-sdk/main/UInt128.swift b/esp32-ibeacon-sdk/main/UInt128.swift new file mode 100644 index 0000000..d2e0196 --- /dev/null +++ b/esp32-ibeacon-sdk/main/UInt128.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + + 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 + +public extension UInt128 { + + static var length: Int { return 16 } + + 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])) + } +} + +// 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)) + } +} diff --git a/esp32-ibeacon-sdk/main/UUID.swift b/esp32-ibeacon-sdk/main/UUID.swift new file mode 100644 index 0000000..0530b1c --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/iBeacon.swift b/esp32-ibeacon-sdk/main/iBeacon.swift new file mode 100644 index 0000000..17002c0 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/main/idf_component.yml b/esp32-ibeacon-sdk/main/idf_component.yml new file mode 100644 index 0000000..e69de29 diff --git a/esp32-ibeacon-sdk/main/linker.lf b/esp32-ibeacon-sdk/main/linker.lf new file mode 100644 index 0000000..6fdc4a8 --- /dev/null +++ b/esp32-ibeacon-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-ibeacon-sdk/sdkconfig.defaults b/esp32-ibeacon-sdk/sdkconfig.defaults new file mode 100644 index 0000000..8cfa5f4 --- /dev/null +++ b/esp32-ibeacon-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