wendylabsinc/bluetooth
Cross-platform Bluetooth Low Energy (BLE) Swift package.
Installation
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/wendylabsinc/bluetooth.git", from: "0.0.2")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "Bluetooth", package: "bluetooth")
]
)
]Linux Requirements
The Linux backend uses BlueZ over D-Bus.
- BlueZ (includes
bluetoothd) - D-Bus (system bus) with the Bluetooth service running
- A user with permissions to access Bluetooth (often the
bluetoothgroup)
Ubuntu/Debian:
sudo apt install bluez
sudo systemctl enable --now bluetoothOptional for debugging (requires root): btmon for sniffing HCI traffic.
btmon listens at the HCI layer and can confirm that advertising commands were issued and that packets are going out. It needs root or the appropriate capabilities (CAP_NET_ADMIN), for example:
sudo btmonCommon filters while advertising:
sudo btmon | rg -i "LE Set Advertising|LE Advertising Report|Advertising"Note: most controllers do not loop back their own advertisements, so a local scan on the same adapter may not show your own packets even when advertising is active.
Usage
Advertising a local name:
import Bluetooth
@main
struct Demo {
static func main() async throws {
let manager = PeripheralManager()
let data = AdvertisementData(localName: "wendyble")
let params = AdvertisingParameters(isConnectable: false, isScannable: false)
try await manager.startAdvertising(advertisingData: data, parameters: params)
try await Task.sleep(nanoseconds: 5_000_000_000)
await manager.stopAdvertising()
}
}GATT server (Linux BlueZ backend supported; other backends pending):
import Bluetooth
let manager = PeripheralManager()
let service = GATTServiceDefinition(
uuid: .bit16(0x180A),
characteristics: [
GATTCharacteristicDefinition(
uuid: .bit16(0x2A29),
properties: [.read, .notify],
permissions: [.readable],
initialValue: Data("wendylabsinc".utf8)
)
]
)
_ = try await manager.addService(service)
let requests = try await manager.gattRequests()
for try await request in requests {
switch request {
case .read(let read):
await read.respond(.success(Data("wendylabsinc".utf8)))
case .write(let write):
await write.respond(.failure(.att(.writeNotPermitted)))
default:
break
}
}Use removeService(_:) to unregister a service when you no longer need it.
L2CAP server (Linux BlueZ backend supported; Windows pending):
import Bluetooth
let manager = PeripheralManager()
let psm = try await manager.publishL2CAPChannel()
let incoming = try await manager.incomingL2CAPChannels(psm: psm)
for try await channel in incoming {
for try await data in channel.incoming() {
try await channel.send(data) // echo
}
}L2CAP client (Linux BlueZ backend supported):
import Bluetooth
let manager = CentralManager()
let peripheral = Peripheral(id: .address(BluetoothAddress("AA:BB:CC:DD:EE:FF")))
let connection = try await manager.connect(to: peripheral)
let channel = try await connection.openL2CAPChannel(psm: L2CAPPSM(rawValue: 0x0080))
try await channel.send(Data("hello".utf8))
for try await data in channel.incoming() {
print("Received: \(data)")
}Examples
Run the advertising example:
swift run BluetoothAdvertisingExample --name wendyble --verboseRun the discovery example:
swift run BluetoothDiscoveryExample --time 10000 --verboseRun the GATT example (Linux BlueZ backend supported):
swift run BluetoothGATTExample --verboseRun the L2CAP example (Linux BlueZ backend supported):
swift run BluetoothL2CAPExample --verboseRun the L2CAP client example (requires a known address + PSM):
swift run BluetoothL2CAPClientExample --address AA:BB:CC:DD:EE:FF --psm 0x0080 --verboseRun the central pairing example (Linux BlueZ backend supported):
swift run BluetoothCentralPairingExample --address AA:BB:CC:DD:EE:FF --verboseOptional flags:
--connectableto advertise as connectable (may trigger pairing prompts)--time <ms>to exit after a duration (advertising/discovery)--uuid <uuid>to filter discovery by a service UUID (repeatable)--name-prefix <prefix>to filter discovery by local name prefix--duplicatesto allow duplicate discovery results--adapter <name>to select a BlueZ adapter (for examplehci1)--verboseto show BlueZ output
Companion Apps
Use the companion apps to test BLE functionality from a mobile device or desktop:
- Apple (
CompanionApps/Apple/): iOS, macOS, tvOS, watchOS, visionOS
- Open BluetoothCompanionApp.xcodeproj in Xcode and run on your target device
- Android (
CompanionApps/Android/): Android 12+ (API 31+)
- Open in Android Studio and run on your device or emulator
Both apps provide BLE scanning and device discovery for testing against the library examples.
Adapter Selection (Linux BlueZ)
Select a specific adapter by name:
let options = BluetoothOptions(adapter: BluetoothAdapter("hci1"))
let central = CentralManager(options: options)
let peripheral = PeripheralManager(options: options)Or use an environment variable:
export BLUETOOTH_BLUEZ_ADAPTER=hci1Pairing (Linux BlueZ)
The Linux backend uses a BlueZ Agent to handle pairing and authorization prompts. You can configure the agent using environment variables:
BLUETOOTH_BLUEZ_AGENT_CAPABILITY(defaultNoInputNoOutput)
- Supported values: DisplayOnly, DisplayYesNo, KeyboardOnly, NoInputNoOutput, KeyboardDisplay, External
BLUETOOTH_BLUEZ_AGENT_PIN(string PIN to return forRequestPinCode)BLUETOOTH_BLUEZ_AGENT_PASSKEY(numeric passkey forRequestPasskey)BLUETOOTH_BLUEZ_AGENT_AUTO_ACCEPT(defaulttrue; set tofalseto reject confirmations/authorizations)
Example:
export BLUETOOTH_BLUEZ_AGENT_CAPABILITY=DisplayYesNo
export BLUETOOTH_BLUEZ_AGENT_AUTO_ACCEPT=false
swift run BluetoothGATTExample --verboseProgrammatic pairing handling (peripheral + central roles):
- Peripheral role:
PeripheralManager().pairingRequests() - Central role:
CentralManager().pairingRequests() - Requests include
centralorperipheraldepending on the local role.
let manager = PeripheralManager()
let requests = try await manager.pairingRequests()
Task {
for try await request in requests {
switch request {
case .confirmation(let confirmation):
await confirmation.respond(true)
case .authorization(let authorization):
await authorization.respond(true)
case .serviceAuthorization(let service):
await service.respond(true)
case .pinCode(let pin):
await pin.respond("0000")
case .passkey(let passkey):
await passkey.respond(123456)
case .displayPinCode(let display):
print("PIN: \(display.pinCode)")
case .displayPasskey(let display):
print("Passkey: \(display.passkey)")
}
}
}Remove bonding (Linux BlueZ):
let centralManager = CentralManager()
try await centralManager.removeBond(for: peripheral)
let peripheralManager = PeripheralManager()
try await peripheralManager.removeBond(for: central)Connection Tuning (API Surface)
The API includes connection parameter + PHY update calls, but Linux BlueZ support is not yet implemented:
try await connection.updateConnectionParameters(
ConnectionParameters(minIntervalMs: 15, maxIntervalMs: 30, latency: 0, supervisionTimeoutMs: 2000)
)
try await connection.updatePHY(PHYPreference(tx: .le2M, rx: .le2M))Package Metadata
Repository: wendylabsinc/bluetooth
Default branch: main
README: README.md