Skip to content

Update BluetoothGATT trait and support Embedded Swift in GATTPeripheral#43

Merged
colemancda merged 5 commits into
masterfrom
feature/swiftpm-traits
Jul 18, 2026
Merged

Update BluetoothGATT trait and support Embedded Swift in GATTPeripheral#43
colemancda merged 5 commits into
masterfrom
feature/swiftpm-traits

Conversation

@colemancda

Copy link
Copy Markdown
Member

Overview

Reworks the SwiftPM trait introduced in #42 and makes the pure Swift GATT server build for Embedded Swift targets so it can be driven by platform stacks like BTStack, NimBLE, and Zephyr.

Trait changes

  • Renamed the GATTServer trait to BluetoothGATT: the trait now gates the import of the BluetoothGATT module (and BluetoothHCI) rather than individual source files.
  • With the trait enabled (default), the pure Swift GATT server (GATTPeripheral, PeripheralManager) is available and the central uses the BluetoothGATT types (GATTCharacteristicProperties, ATTAttributePermissions, ATTMaximumTransmissionUnit) via typealiases.
  • With the trait disabled, the lightweight shim types are used instead, for central-only clients built on a platform Bluetooth stack such as CoreBluetooth on iOS or the Android Bluetooth API. DarwinGATT's BluetoothGATT dependency is gated on the trait as well, so DarwinPeripheral is only built when the GATT module provides PeripheralManager.

Embedded Swift support

GATTPeripheral and GATTServerConnection now compile under Embedded Swift:

  • Added a non-blocking run() method that accepts pending connections and services I/O for connected centrals, so the peripheral can be driven by polling from the platform's run loop (e.g. btstack_run_loop on the Pi Pico W).
  • Dropped the BluetoothHostControllerInterface constraint from the class, since the all-async HCI protocol cannot be implemented by synchronous embedded stacks. The HCI-driven advertising APIs (async start() and start(options:)) moved to a constrained extension, so desktop and Linux behavior is unchanged. Re-advertising after a disconnect is a stored callback installed by start(options:).
  • Replaced NSLock with an internal Lock wrapper that is a no-op on platforms without threading, and the server callbacks capture the peripheral strongly on Embedded, where weak references are unavailable.
  • The background thread driver is kept on platforms with Foundation threads, sharing the non-blocking accept implementation.

GATTCentral remains excluded from Embedded builds because GATTClient in BluetoothGATT is an actor and requires the concurrency runtime.

CI

The WebAssembly and Embedded WebAssembly jobs now build both trait configurations.

Testing

  • macOS: full test suite passes in both trait configurations; symbol inspection confirms the peripheral, pure central, and DarwinPeripheral compile to nothing with the trait disabled while the shim types are used.
  • WebAssembly and Embedded WebAssembly: build verified in both trait configurations, debug and release.

The GATTServer trait previously gated the BluetoothGATT and
BluetoothHCI dependencies, which removed the GATT client (GATTCentral)
along with the server when disabled. Make those dependencies
unconditional so the central is always available, and gate only
GATTPeripheral and GATTServerConnection behind the trait's
compilation condition.

Guard the pure Swift central and peripheral against Embedded Swift
and WASI explicitly, since the modules are now always importable
there. Keep the code generation plugin enabled in the WASM CI job
because BluetoothGATT now always builds.
The trait now controls whether the BluetoothGATT module (and
BluetoothHCI) is imported. When enabled, the pure Swift GATT server
(GATTPeripheral) is available and the central uses the BluetoothGATT
types (GATTCharacteristicProperties, ATTAttributePermissions,
ATTMaximumTransmissionUnit) via typealiases. When disabled, the
lightweight shim types are used instead, for central-only clients
built on a platform Bluetooth stack such as CoreBluetooth on iOS or
the Android Bluetooth API.

DarwinGATT's BluetoothGATT dependency is also gated on the trait so
DarwinPeripheral is only built when the GATT module provides
PeripheralManager. The WASM CI job disables plugins again since
BluetoothGATT is no longer built with default traits disabled.
Gate the pure Swift GATT peripheral and central purely on
canImport(BluetoothGATT)/canImport(BluetoothHCI), controlled by the
BluetoothGATT trait.
Make the pure Swift GATT server compile for Embedded Swift targets
(Pi Pico W, ESP32, nRF52840) running stacks like BTStack and NimBLE:

- Add a non-blocking run() method that accepts pending connections
  and services I/O for connected centrals, so the peripheral can be
  driven by polling from the platform's run loop.
- Drop the BluetoothHostControllerInterface constraint from the
  class; the async protocol cannot be implemented by synchronous
  embedded stacks like BTStack. The HCI-driven advertising APIs
  (async start() and start(options:)) move to a constrained
  extension, and re-advertising after a disconnect is now a stored
  callback installed by start(options:).
- Replace NSLock with an internal Lock wrapper that is a no-op on
  platforms without threading.
- Keep the background thread driver on platforms with Foundation
  threads; the blocking accept loop moved to acceptThread(_:) and
  shares the non-blocking accept(_:) implementation.
- Capture the peripheral strongly in server callbacks on Embedded,
  where weak references are unavailable.
- GATTCentral remains excluded from Embedded builds because
  GATTClient in BluetoothGATT is an actor and requires concurrency.
- Build both trait configurations in the WebAssembly CI jobs.
@colemancda
colemancda force-pushed the feature/swiftpm-traits branch from 95175ee to 7d0fda8 Compare July 18, 2026 15:07
@github-code-quality

github-code-quality Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Coverage Overview

Languages: Swift

Swift / code-coverage/llvm-cov

The overall coverage in the branch is 78%. The coverage in the branch is 81%.

Show a code coverage summary of the most impacted files.
File 3c4a9e9 44d674c +/-
Sources/GATT/GA...eripheral.swift 77% 71% -6%
Sources/GATT/GATTCentral.swift 90% 89% -1%
Sources/GATT/Lock.swift 0% 100% +100%

Updated July 18, 2026 15:45 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@colemancda colemancda changed the title Scope BluetoothGATT trait and support Embedded Swift in GATTPeripheral Update BluetoothGATT trait and support Embedded Swift in GATTPeripheral Jul 18, 2026
Remove BluetoothGATT from the default trait set so GATT builds against
the lightweight shim types unless the consumer opts in. This suits
central-only clients on a platform Bluetooth stack (CoreBluetooth on
iOS, the Android Bluetooth API) and keeps the default dependency
footprint small.

Gate the trait-conditional code on the BluetoothGATT compilation
condition (which SwiftPM defines when the trait is enabled) instead of
canImport(BluetoothGATT)/canImport(BluetoothHCI). canImport was a false
positive: the module is discoverable on the dependency package's search
path even when the trait leaves it unlinked, so the test executable
failed to link once the trait was no longer enabled by default. The
trait condition tracks the dependency exactly.

CI now exercises both trait configurations: the macOS, Linux, Windows
and WebAssembly jobs build (and where applicable test) with and without
the trait, the coverage job enables the trait via SWIFT_BUILD_FLAGS to
measure the pure Swift stack, and the ARM job keeps the trait on so the
published libGATT.so still ships the full stack for BlueZ/BluetoothLinux.

On Darwin the CoreBluetooth peripheral (DarwinPeripheral) now requires
the trait; DarwinCentral and the shim types remain available by default.
@colemancda
colemancda merged commit ba4b36a into master Jul 18, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant