From 11d310d005559b5440a54de2157afa94bc2f1100 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 6 Nov 2024 01:44:31 -0500 Subject: [PATCH] Enable Bluetooth iBeacon advertisement for Pico W --- pico-w-blink-sdk/Bluetooth.swift | 125 +++++++ pico-w-blink-sdk/BluetoothAddress.swift | 160 +++++++++ pico-w-blink-sdk/BridgingHeader.h | 2 + pico-w-blink-sdk/ByteSwap.swift | 72 ++++ pico-w-blink-sdk/CMakeLists.txt | 106 +++++- pico-w-blink-sdk/CYW43.swift | 27 ++ pico-w-blink-sdk/CompanyIdentifier.swift | 53 +++ pico-w-blink-sdk/Data.swift | 129 +++++++ pico-w-blink-sdk/Encoder.swift | 78 +++++ pico-w-blink-sdk/Error.swift | 77 +++++ pico-w-blink-sdk/GAPData.swift | 31 ++ pico-w-blink-sdk/GAPDataType.swift | 69 ++++ pico-w-blink-sdk/GAPFlags.swift | 128 +++++++ .../GAPManufacturerSpecificData.swift | 78 +++++ pico-w-blink-sdk/GAPShortLocalName.swift | 65 ++++ pico-w-blink-sdk/GPIO.swift | 31 ++ pico-w-blink-sdk/Hexadecimal.swift | 103 ++++++ pico-w-blink-sdk/Integer.swift | 58 ++++ .../LowEnergyAdvertisingData.swift | 322 ++++++++++++++++++ pico-w-blink-sdk/Main.swift | 99 +++++- pico-w-blink-sdk/String.swift | 54 +++ pico-w-blink-sdk/UInt128.swift | 83 +++++ pico-w-blink-sdk/UUID.swift | 233 +++++++++++++ pico-w-blink-sdk/iBeacon.swift | 166 +++++++++ pico-w-blink-sdk/include/btstack_config.h | 58 ++++ 25 files changed, 2392 insertions(+), 15 deletions(-) create mode 100644 pico-w-blink-sdk/Bluetooth.swift create mode 100644 pico-w-blink-sdk/BluetoothAddress.swift create mode 100644 pico-w-blink-sdk/ByteSwap.swift create mode 100644 pico-w-blink-sdk/CYW43.swift create mode 100644 pico-w-blink-sdk/CompanyIdentifier.swift create mode 100644 pico-w-blink-sdk/Data.swift create mode 100644 pico-w-blink-sdk/Encoder.swift create mode 100644 pico-w-blink-sdk/Error.swift create mode 100644 pico-w-blink-sdk/GAPData.swift create mode 100644 pico-w-blink-sdk/GAPDataType.swift create mode 100644 pico-w-blink-sdk/GAPFlags.swift create mode 100644 pico-w-blink-sdk/GAPManufacturerSpecificData.swift create mode 100644 pico-w-blink-sdk/GAPShortLocalName.swift create mode 100644 pico-w-blink-sdk/GPIO.swift create mode 100644 pico-w-blink-sdk/Hexadecimal.swift create mode 100644 pico-w-blink-sdk/Integer.swift create mode 100644 pico-w-blink-sdk/LowEnergyAdvertisingData.swift create mode 100644 pico-w-blink-sdk/String.swift create mode 100644 pico-w-blink-sdk/UInt128.swift create mode 100644 pico-w-blink-sdk/UUID.swift create mode 100644 pico-w-blink-sdk/iBeacon.swift create mode 100644 pico-w-blink-sdk/include/btstack_config.h diff --git a/pico-w-blink-sdk/Bluetooth.swift b/pico-w-blink-sdk/Bluetooth.swift new file mode 100644 index 0000000..8f98f1c --- /dev/null +++ b/pico-w-blink-sdk/Bluetooth.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension CYW43 { + + final class Bluetooth { + + static let shared = Bluetooth() + + private var callbackRegistration = btstack_packet_callback_registration_t() + + fileprivate(set) var state: State = .off + + private(set) var address: BluetoothAddress = .zero + + var advertisement = LowEnergyAdvertisingData() { + didSet { + let length = advertisement.length + advertisementBuffer = [UInt8](advertisement) + // data is not copied, pointer has to stay valid + gap_advertisements_set_data(length, &advertisementBuffer) + } + } + + private var advertisementBuffer = [UInt8]() + + var scanResponse = LowEnergyAdvertisingData() { + didSet { + let length = scanResponse.length + scanResponseBuffer = [UInt8](scanResponse) + // data is not copied, pointer has to stay valid + gap_scan_response_set_data(length, &scanResponseBuffer) + } + } + + private var scanResponseBuffer = [UInt8]() + + var isAdvertising = false { + didSet { + gap_advertisements_enable(isAdvertising ? 1 : 0) + } + } + + private init() { + // register for callbacks + callbackRegistration.callback = _bluetooth_packet_handler + hci_add_event_handler(&callbackRegistration) + } + + deinit { + hci_remove_event_handler(&callbackRegistration) + } + } +} + +extension CYW43.Bluetooth { + + func setPower(_ state: PowerState) { + hci_power_control(.init(rawValue: state.rawValue)) + } + + func setAdvertisementParameters( + advIntMin: UInt16 = 0x0030, + advIntMax: UInt16 = 0x0030, + advType: UInt8 = 0, + directAddressType: UInt8 = 0, + directAddress: inout BluetoothAddress, + channelMap: UInt8 = 0x07, + filterPolicy: UInt8 = 0x00 + ) { + withUnsafeMutablePointer(to: &directAddress.bytes) { + gap_advertisements_set_params(advIntMin, advIntMax, advType, directAddressType, $0, channelMap, filterPolicy) + } + } +} + +extension CYW43.Bluetooth { + + enum PowerState: UInt8, Sendable { + + case off = 0 + case on = 1 + case sleep = 2 + } + + enum State: UInt8 { + + case off = 0 + case initializing = 1 + case on = 2 + case halting = 3 + case sleeping = 4 + case fallingAsleep = 5 + } +} + +// packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) +@_documentation(visibility: internal) +@_cdecl("bluetooth_packet_handler") +internal func _bluetooth_packet_handler(packetType: UInt8, channel: UInt16, packetPointer: UnsafeMutablePointer?, packetSize: UInt16) { + + switch packetType { + case UInt8(HCI_EVENT_PACKET): + switch hci_event_packet_get_type(packetPointer) { + case UInt8(BTSTACK_EVENT_STATE): + let rawState = btstack_event_state_get_state(packetPointer) + let newValue = CYW43.Bluetooth.State(rawValue: rawState) ?? .off + CYW43.Bluetooth.shared.state = newValue + case UInt8(HCI_EVENT_VENDOR_SPECIFIC): + break + default: + break + } + default: + break + } +} \ No newline at end of file diff --git a/pico-w-blink-sdk/BluetoothAddress.swift b/pico-w-blink-sdk/BluetoothAddress.swift new file mode 100644 index 0000000..5d1bd51 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/BridgingHeader.h b/pico-w-blink-sdk/BridgingHeader.h index 69a2fec..eef8df4 100644 --- a/pico-w-blink-sdk/BridgingHeader.h +++ b/pico-w-blink-sdk/BridgingHeader.h @@ -11,5 +11,7 @@ #pragma once +#include "btstack.h" #include "pico/stdlib.h" #include "pico/cyw43_arch.h" +#include "pico/btstack_cyw43.h" \ No newline at end of file diff --git a/pico-w-blink-sdk/ByteSwap.swift b/pico-w-blink-sdk/ByteSwap.swift new file mode 100644 index 0000000..94c40eb --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/CMakeLists.txt b/pico-w-blink-sdk/CMakeLists.txt index 20c55fe..4c68911 100644 --- a/pico-w-blink-sdk/CMakeLists.txt +++ b/pico-w-blink-sdk/CMakeLists.txt @@ -11,11 +11,72 @@ execute_process(COMMAND which swiftc OUTPUT_VARIABLE SWIFTC OUTPUT_STRIP_TRAILIN endif() add_executable(swift-blinky) +target_link_libraries(swift-blinky + pico_stdlib + hardware_uart + hardware_gpio + pico_lwip_arch + pico_cyw43_arch_none + pico_btstack_ble + pico_btstack_cyw43 +) + +target_include_directories(swift-blinky PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/include # For btstack config +) + +# Gather compile definitions from all dependencies +set_property(GLOBAL PROPERTY visited_targets "") +set_property(GLOBAL PROPERTY compilerdefs_list "") + +function(gather_compile_definitions_recursive target) + # Get the current value of visited_targets + get_property(visited_targets GLOBAL PROPERTY visited_targets) + + # make sure we don't visit the same target twice + # and that we don't visit the special generator expressions + if (${target} MATCHES "\\$<" OR ${target} MATCHES "::@" OR ${target} IN_LIST visited_targets) + return() + endif() + + # Append the target to visited_targets + list(APPEND visited_targets ${target}) + set_property(GLOBAL PROPERTY visited_targets "${visited_targets}") + + # Get the current value of compilerdefs_list + get_property(compilerdefs_list GLOBAL PROPERTY compilerdefs_list) + + get_target_property(target_definitions ${target} INTERFACE_COMPILE_DEFINITIONS) + if (target_definitions) + # Append the target definitions to compilerdefs_list + list(APPEND compilerdefs_list ${target_definitions}) + set_property(GLOBAL PROPERTY compilerdefs_list "${compilerdefs_list}") + endif() + + get_target_property(target_linked_libs ${target} INTERFACE_LINK_LIBRARIES) + if (target_linked_libs) + foreach(linked_target ${target_linked_libs}) + # Recursively gather compile definitions from dependencies + gather_compile_definitions_recursive(${linked_target}) + endforeach() + endif() +endfunction() + +gather_compile_definitions_recursive(swift-blinky) +get_property(COMPILE_DEFINITIONS GLOBAL PROPERTY compilerdefs_list) + +# Parse compiler definitions into a format that swiftc can understand +list(REMOVE_DUPLICATES COMPILE_DEFINITIONS) +list(PREPEND COMPILE_DEFINITIONS "") # adds a semicolon at the beginning +string(REPLACE "$" "$" COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS}") +string(REPLACE ";" ";-Xcc;-D" COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS}") + add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o COMMAND ${SWIFTC} -target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums + ${COMPILE_DEFINITIONS} -Xcc -DCYW43_LWIP -Xcc -DPICO_CYW43_ARCH_THREADSAFE_BACKGROUND -Xcc -I$ENV{PICO_SDK_PATH}/lib/lwip/src/include @@ -25,16 +86,57 @@ add_custom_command( $$\( echo '${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}' | tr ' ' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \) -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h ${CMAKE_CURRENT_LIST_DIR}/Main.swift + ${CMAKE_CURRENT_LIST_DIR}/Error.swift + ${CMAKE_CURRENT_LIST_DIR}/CYW43.swift + ${CMAKE_CURRENT_LIST_DIR}/GPIO.swift + ${CMAKE_CURRENT_LIST_DIR}/Bluetooth.swift + ${CMAKE_CURRENT_LIST_DIR}/BluetoothAddress.swift + ${CMAKE_CURRENT_LIST_DIR}/ByteSwap.swift + ${CMAKE_CURRENT_LIST_DIR}/CompanyIdentifier.swift + ${CMAKE_CURRENT_LIST_DIR}/LowEnergyAdvertisingData.swift + ${CMAKE_CURRENT_LIST_DIR}/Data.swift + ${CMAKE_CURRENT_LIST_DIR}/String.swift + ${CMAKE_CURRENT_LIST_DIR}/Hexadecimal.swift + ${CMAKE_CURRENT_LIST_DIR}/Encoder.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPData.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPDataType.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPFlags.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPShortLocalName.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPManufacturerSpecificData.swift + ${CMAKE_CURRENT_LIST_DIR}/UInt128.swift + ${CMAKE_CURRENT_LIST_DIR}/UUID.swift + ${CMAKE_CURRENT_LIST_DIR}/iBeacon.swift + ${CMAKE_CURRENT_LIST_DIR}/Integer.swift -c -o ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o DEPENDS ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h ${CMAKE_CURRENT_LIST_DIR}/Main.swift + ${CMAKE_CURRENT_LIST_DIR}/Error.swift + ${CMAKE_CURRENT_LIST_DIR}/CYW43.swift + ${CMAKE_CURRENT_LIST_DIR}/GPIO.swift + ${CMAKE_CURRENT_LIST_DIR}/Bluetooth.swift + ${CMAKE_CURRENT_LIST_DIR}/BluetoothAddress.swift + ${CMAKE_CURRENT_LIST_DIR}/ByteSwap.swift + ${CMAKE_CURRENT_LIST_DIR}/CompanyIdentifier.swift + ${CMAKE_CURRENT_LIST_DIR}/LowEnergyAdvertisingData.swift + ${CMAKE_CURRENT_LIST_DIR}/Data.swift + ${CMAKE_CURRENT_LIST_DIR}/String.swift + ${CMAKE_CURRENT_LIST_DIR}/Hexadecimal.swift + ${CMAKE_CURRENT_LIST_DIR}/Encoder.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPData.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPDataType.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPFlags.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPShortLocalName.swift + ${CMAKE_CURRENT_LIST_DIR}/GAPManufacturerSpecificData.swift + ${CMAKE_CURRENT_LIST_DIR}/UInt128.swift + ${CMAKE_CURRENT_LIST_DIR}/UUID.swift + ${CMAKE_CURRENT_LIST_DIR}/iBeacon.swift + ${CMAKE_CURRENT_LIST_DIR}/Integer.swift ) add_custom_target(swift-blinky-swiftcode DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o) target_link_libraries(swift-blinky - pico_stdlib hardware_uart hardware_gpio pico_lwip_arch pico_cyw43_arch_none ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o ) add_dependencies(swift-blinky swift-blinky-swiftcode) -pico_add_extra_outputs(swift-blinky) +pico_add_extra_outputs(swift-blinky) \ No newline at end of file diff --git a/pico-w-blink-sdk/CYW43.swift b/pico-w-blink-sdk/CYW43.swift new file mode 100644 index 0000000..f321422 --- /dev/null +++ b/pico-w-blink-sdk/CYW43.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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 CYW43: ~Copyable { + + /// Initialize the CYW43 architecture. + /// + /// [Documentation](https://www.raspberrypi.com/documentation/pico-sdk/networking.html#group_pico_cyw43_arch_1ga7a05bd21f02a0effadbba1e8266b8771) + init() throws(PicoError) { + let errorCode = cyw43_arch_init() + guard errorCode == 0 else { + throw PicoError(rawValue: errorCode) ?? .unknown + } + } + + deinit { + cyw43_arch_deinit() + } +} \ No newline at end of file diff --git a/pico-w-blink-sdk/CompanyIdentifier.swift b/pico-w-blink-sdk/CompanyIdentifier.swift new file mode 100644 index 0000000..20fa3a7 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/Data.swift b/pico-w-blink-sdk/Data.swift new file mode 100644 index 0000000..76d89c4 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/Encoder.swift b/pico-w-blink-sdk/Encoder.swift new file mode 100644 index 0000000..3a7d067 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/Error.swift b/pico-w-blink-sdk/Error.swift new file mode 100644 index 0000000..7f67f42 --- /dev/null +++ b/pico-w-blink-sdk/Error.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Raspberry Pi Pico Errors +enum PicoError: Int32, Error, Sendable { + + /// An unspecified error occurred. + case unknown = -1 + + /// The function failed due to timeout. + case timeout = -2 + + /// Attempt to read from an empty buffer/FIFO. + case noData = -3 + + /// Permission violation (e.g. write to read-only flash partition). + case notPermitted = -4 + + /// Argument is outside the range of supported values. + case invalidArg = -5 + + /// An I/O error occurred. + case io = -6 + + /// The authorization failed due to bad credentials. + case badAuth = -7 + + /// The connection failed. + case connectFailed = -8 + + /// Dynamic allocation of resources failed. + case insufficientResources = -9 + + /// Address argument was out-of-bounds or inaccessible. + case invalidAddress = -10 + + /// Address was mis-aligned (usually not on a word boundary). + case badAlignment = -11 + + /// Something failed in the past, preventing servicing the current request. + case invalidState = -12 + + /// A user-allocated buffer was too small to hold the result. + case bufferTooSmall = -13 + + /// The call failed because another function must be called first. + case preconditionNotMet = -14 + + /// Cached data was determined to be inconsistent with the actual version. + case modifiedData = -15 + + /// A data structure failed to validate. + case invalidData = -16 + + /// Attempted to access something that does not exist; search failed. + case notFound = -17 + + /// Write is impossible based on previous writes (e.g. attempted to clear an OTP bit). + case unsupportedModification = -18 + + /// A required lock is not owned. + case lockRequired = -19 + + /// A version mismatch occurred (e.g. running incompatible code). + case versionMismatch = -20 + + /// The call could not proceed because required resources were unavailable. + case resourceInUse = -21 +} \ No newline at end of file diff --git a/pico-w-blink-sdk/GAPData.swift b/pico-w-blink-sdk/GAPData.swift new file mode 100644 index 0000000..56dc57a --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/GAPDataType.swift b/pico-w-blink-sdk/GAPDataType.swift new file mode 100644 index 0000000..55ad670 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/GAPFlags.swift b/pico-w-blink-sdk/GAPFlags.swift new file mode 100644 index 0000000..637f5c7 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/GAPManufacturerSpecificData.swift b/pico-w-blink-sdk/GAPManufacturerSpecificData.swift new file mode 100644 index 0000000..5e8f1ff --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/GAPShortLocalName.swift b/pico-w-blink-sdk/GAPShortLocalName.swift new file mode 100644 index 0000000..35808de --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/GPIO.swift b/pico-w-blink-sdk/GPIO.swift new file mode 100644 index 0000000..500f7ae --- /dev/null +++ b/pico-w-blink-sdk/GPIO.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 +// +//===----------------------------------------------------------------------===// + +extension CYW43 { + + enum GPIO: UInt32, CaseIterable, Copyable, Sendable { + + case led = 0 + + case vsys = 1 + + case vbus = 2 + } + + subscript (gpio: GPIO) -> Bool { + get { + cyw43_arch_gpio_get(gpio.rawValue) + } + nonmutating set { + cyw43_arch_gpio_put(gpio.rawValue, newValue) + } + } +} \ No newline at end of file diff --git a/pico-w-blink-sdk/Hexadecimal.swift b/pico-w-blink-sdk/Hexadecimal.swift new file mode 100644 index 0000000..08ada87 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/Integer.swift b/pico-w-blink-sdk/Integer.swift new file mode 100644 index 0000000..c466c4f --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/LowEnergyAdvertisingData.swift b/pico-w-blink-sdk/LowEnergyAdvertisingData.swift new file mode 100644 index 0000000..400694b --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/Main.swift b/pico-w-blink-sdk/Main.swift index 6a20553..0452ac1 100644 --- a/pico-w-blink-sdk/Main.swift +++ b/pico-w-blink-sdk/Main.swift @@ -11,25 +11,56 @@ @main struct Main { + static func main() { - let led = UInt32(CYW43_WL_GPIO_LED_PIN) - if cyw43_arch_init() != 0 { - print("Wi-Fi init failed") - return + + print("Hello World!") + + let cyw43: CYW43 + do { + cyw43 = try CYW43() } - let dot = { - cyw43_arch_gpio_put(led, true) - sleep_ms(250) - cyw43_arch_gpio_put(led, false) - sleep_ms(250) + catch { + print("Wi-Fi init failed.") + return } - let dash = { - cyw43_arch_gpio_put(led, true) + + cyw43[.led] = false + + let bluetooth = CYW43.Bluetooth.shared + bluetooth.setPower(.on) + + // wait for Bluetooth to turn on + while bluetooth.state != .on { sleep_ms(500) - cyw43_arch_gpio_put(led, false) - sleep_ms(250) } + + var address: BluetoothAddress = .zero + bluetooth.setAdvertisementParameters(directAddress: &address) + + // Estimote iBeacon B9407F30-F5F8-466E-AFF9-25556B57FE6D + // Major 0x01 Minor 0x01 + if let uuid = UUID(uuidString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D") { + let beacon = AppleBeacon(uuid: uuid, major: 0x01, minor: 0x01, rssi: -10) + let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] + bluetooth.advertisement = .init(beacon: beacon, flags: flags) + } + + // scan response with name and bluetooth address + let name = GAPShortLocalName(name: "Pico " + address.description) + let scanResponse: LowEnergyAdvertisingData = GAPDataEncoder.encode(name) + bluetooth.scanResponse = scanResponse + bluetooth.isAdvertising = true + while true { + cyw43.blink() + } + } +} + +extension CYW43 { + + func blink() { dot() dot() dot() @@ -42,5 +73,47 @@ struct Main { dot() dot() } + + func dot() { + self[.led] = true + sleep_ms(250) + self[.led] = false + sleep_ms(250) + } + + func dash() { + self[.led] = true + sleep_ms(500) + self[.led] = false + sleep_ms(250) + } +} + +/// Implement `posix_memalign(3)`, which is required by the Embedded Swift runtime but is +/// not provided by the Pi Pico SDK. +@_documentation(visibility: internal) +@_cdecl("posix_memalign") public func posix_memalign( + _ memptr: UnsafeMutablePointer, + _ alignment: Int, + _ size: Int +) -> CInt { + guard let allocation = aligned_alloc(alignment, size) else { + return 0 + } + memptr.pointee = allocation + return 0 +} + +/// Implement `arc4random_buf` which is required by the Embedded Swift runtime for Hashable, Set, Dictionary, +/// and random-number generating APIs but is not provided by the Pi Pico SDK. +@_documentation(visibility: internal) +@_cdecl("arc4random_buf") public func arc4random_buf(buf: UnsafeMutableRawPointer, nbytes: Int) { + for i in stride(from: 0, to: nbytes - 1, by: 2) { + let randomValue = UInt16(rand() & Int32(UInt16.max)) + (buf + i).assumingMemoryBound(to: UInt16.self).pointee = randomValue + } + if nbytes % 2 == 1 { + let randomValue = UInt8(rand() & Int32(UInt8.max)) + (buf + nbytes - 1).assumingMemoryBound(to: UInt8.self).pointee = randomValue } } diff --git a/pico-w-blink-sdk/String.swift b/pico-w-blink-sdk/String.swift new file mode 100644 index 0000000..57374fd --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/UInt128.swift b/pico-w-blink-sdk/UInt128.swift new file mode 100644 index 0000000..d2e0196 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/UUID.swift b/pico-w-blink-sdk/UUID.swift new file mode 100644 index 0000000..0530b1c --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/iBeacon.swift b/pico-w-blink-sdk/iBeacon.swift new file mode 100644 index 0000000..17002c0 --- /dev/null +++ b/pico-w-blink-sdk/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/pico-w-blink-sdk/include/btstack_config.h b/pico-w-blink-sdk/include/btstack_config.h new file mode 100644 index 0000000..c392dff --- /dev/null +++ b/pico-w-blink-sdk/include/btstack_config.h @@ -0,0 +1,58 @@ +#ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H +#define _PICO_BTSTACK_BTSTACK_CONFIG_H + +#ifndef ENABLE_BLE +#error Please link to pico_btstack_ble +#endif + +// BTstack features that can be enabled +#define ENABLE_LE_PERIPHERAL +#define ENABLE_LOG_INFO +#define ENABLE_LOG_ERROR +#define ENABLE_PRINTF_HEXDUMP + +// for the client +#if RUNNING_AS_CLIENT +#define ENABLE_LE_CENTRAL +#define MAX_NR_GATT_CLIENTS 1 +#else +#define MAX_NR_GATT_CLIENTS 0 +#endif + +// BTstack configuration. buffers, sizes, ... +#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 +#define HCI_ACL_PAYLOAD_SIZE (255 + 4) +#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 +#define MAX_NR_HCI_CONNECTIONS 1 +#define MAX_NR_SM_LOOKUP_ENTRIES 3 +#define MAX_NR_WHITELIST_ENTRIES 16 +#define MAX_NR_LE_DEVICE_DB_ENTRIES 16 + +// Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun +#define MAX_NR_CONTROLLER_ACL_BUFFERS 3 +#define MAX_NR_CONTROLLER_SCO_PACKETS 3 + +// Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun +#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL +#define HCI_HOST_ACL_PACKET_LEN (255+4) +#define HCI_HOST_ACL_PACKET_NUM 3 +#define HCI_HOST_SCO_PACKET_LEN 120 +#define HCI_HOST_SCO_PACKET_NUM 3 + +// Link Key DB and LE Device DB using TLV on top of Flash Sector interface +#define NVM_NUM_DEVICE_DB_ENTRIES 16 +#define NVM_NUM_LINK_KEYS 16 + +// We don't give btstack a malloc, so use a fixed-size ATT DB. +#define MAX_ATT_DB_SIZE 512 + +// BTstack HAL configuration +#define HAVE_EMBEDDED_TIME_MS +// map btstack_assert onto Pico SDK assert() +#define HAVE_ASSERT +// Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A). +#define HCI_RESET_RESEND_TIMEOUT_MS 1000 +#define ENABLE_SOFTWARE_AES128 +#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS + +#endif // MICROPY_INCLUDED_EXTMOD_BTSTACK_BTSTACK_CONFIG_H