Your simulator sees the actual BLE devices around your Mac — scanning, connecting, reading, writing, notifications. The app under test calls CoreBluetooth exactly as it always has and never knows the difference.
Record a real device once, and replay it forever after — on any machine, in CI, with no hardware in the room.
Verified end to end across Flutter (flutter_blue_plus / flutter_blue_ultra),
React Native (react-native-ble-plx) and native Swift — scanning, connecting
and reading, with no code changes to any app.
Your app keeps calling Apple's own CoreBluetooth. CBCentralManager, CBPeripheral,
CBService and CBCharacteristic are the real classes, running Apple's real state
machine and dispatching your real delegates. Nothing is mocked and nothing is
subclassed.
That matters most if you did not write the Bluetooth code yourself.
The established alternative for native apps is Nordic's CoreBluetoothMock, which
works by having your Swift code call CBMCentralManager instead of
CBCentralManager. That is fine when you own every call site. A Flutter app's
Bluetooth calls happen inside flutter_blue_plus_darwin's Objective-C, and a React
Native app's inside react-native-ble-plx — code the app author does not own and
cannot rewrite.
BleTether sits underneath all of them, so it does not care whose code is calling. For a Flutter or React Native team it is currently the only way to test Bluetooth without hardware.
Requires macOS 26+, Xcode 26+, Apple Silicon.
-
Download BleTether.app from Releases, drag it to Applications, and open it. Grant Bluetooth access when asked.
-
On the Overview tab, click Install command line tool. That puts
bletetheron your PATH (it asks for your password once). Prefer to do it by hand:sudo ln -sf /Applications/BleTether.app/Contents/MacOS/bletether /usr/local/bin/bletether
-
Enable bridging for your simulators:
bletether install
bletether install is a one-off. Bridging stays enabled across simulator reboots.
Now just run your app — ⌘R in Xcode, flutter run, npx react-native run-ios.
Nothing to configure, no dependency to add, no code to change.
Wait for
host bluetooth: powered onbefore launching your app. macOS CoreBluetooth can take up to a minute to come up, and an app started before that is told Bluetooth is off — it will not retry on its own.until bletether doctor >/dev/null; do sleep 2; done
- Overview — one plain-language line for whether the bridge is working, plus a switch that makes the simulator believe Bluetooth is off. Useful for testing that path without turning off your Mac's own radio and losing your keyboard with it.
- Devices — everything the bridge has seen, with signal strength and state.
- Activity — every event in both directions, named rather than numbered
(
didDiscoverPeripheral, notmsg 91). Filter by category, click any row for a full hex dump. Scanning alone can produce tens of thousands of events, which is why the filters exist. - Fixtures — devices recorded from real hardware. Record one from the current session, then replay it into any simulator with no Bluetooth involved at all.
- Simulators — boot, shut down, and toggle bridging per simulator.
The menu bar icon shows state at a glance: crossed-out antenna means no Bluetooth, a dot means idle, a full antenna means traffic is flowing.
bletether doctor check every link in the chain
bletether devices list simulators
bletether apps list installed apps
bletether install [udid] enable bridging (survives simulator reboots)
bletether uninstall [udid] disable it
bletether run <bundle-id> launch one app through the bridge
bletether power [on|off] simulate Bluetooth being off
bletether record build fixtures from the recording
bletether record --list devices present in the recording
bletether ci <fixture> [udid] serve a fixture to a simulator
bletether daemon daemon status
install bridges everything launched on that simulator, including Xcode's ⌘R.
run leaves the device alone and bridges a single launch instead.
Both the app and the CLI go through the same layer, so they cannot drift apart.
Once an app has talked to a real device through the bridge, that session can be turned into a fixture — a file describing the peripheral, its GATT tree and the values it returned.
bletether record # every device this session connected to
bletether record "DEVICE" device.json # just oneReplaying a fixture needs no Bluetooth, no permission, no daemon, and no device in the room:
bletether ci device.jsonEverything launched on that simulator afterwards — including xcodebuild test — sees
the recorded device as though it were really there.
Verified against the hardware it came from: identical characteristics and properties, byte-identical values on every read. Completion order differs between runs, as it does on the device itself.
Fixtures are JSON with sorted keys, so re-recording the same device produces a byte-identical file and a genuine change reads as a clean diff in review.
- uses: yuriipopow/bletether@v1
with:
fixture: fixtures/device.json
- run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17'The action installs BleTether, validates the fixture, prepares the simulator, and then reads the setting back. If anything did not take, the step fails — rather than letting the pipeline go green while testing an app that never saw a device.
Verified on macos-14, macos-15 and macos-26 hosted runners.
The action fetches the
bletether-ci.tar.gzasset for you — it is the fixture engine alone, with no hardware bridge, and is not something you install by hand. For working at your desk, use BleTether.app above.
Three real BLE scanner apps, one per stack, each with a CI run that scans, connects, and reads a characteristic — passing on a hosted runner with no hardware. Nothing in any of them knows BleTether is there.
- bletether-flutter-example —
flutter_blue_ultra - bletether-react-native-example —
react-native-ble-plx - bletether-swift-example — native SwiftUI + CoreBluetooth
Works
- Central role: scan, connect, discover services / characteristics / descriptors, read, write (with and without response), notifications, MTU, RSSI
- Swift, Objective-C, Flutter, React Native
- Xcode
⌘R,flutter run,simctl launch,xcodebuild test - Survives daemon restarts, simulator reboots, and apps dying mid-session
Does not
- One simulator at a time. A second one waits; the app says so rather than failing silently.
- Peripheral role.
CBPeripheralManageris not bridged. - L2CAP channels.
- Pairing and bonding are untested.
- Peripheral identifiers are macOS
CBUUIDs, not device MAC addresses — the same physical device gets a different identifier than it would on a real iPhone.
Caveat worth knowing: after updating BleTether, reboot the simulator. Processes already running keep the old copy in memory.
Start with bletether doctor — it checks each link and says what to do next.
| Symptom | Cause | Fix |
|---|---|---|
daemon: not reachable |
BleTether.app is not running | Open it |
App stuck on unsupported |
Bridging not enabled | bletether install, then relaunch the app |
| App says Bluetooth is off | Started before the host adapter was ready | Wait for a green doctor, relaunch |
| Bluetooth cycles off and on | BleTether restarted | Expected — the app recovers by itself |
| Second simulator sees nothing | One session at a time | Quit the first app |
Live log: ~/Library/Logs/BleTether/daemon.log.
Full event history: ~/Library/Logs/BleTether/packets.jsonl.
Free to use — personally or at a company, interactively and in CI. Redistribution, resale, and offering it as a service need a separate licence; see LICENSE.
BleTether relies on private, undocumented Apple interfaces, so it can break with a major iOS release — and keeping it working across iOS versions is ongoing work. If your team depends on it in CI and wants priority fixes and guaranteed updates for new iOS versions, a support & continuity subscription is available — email yurapopov522@gmail.com. Everyone else: it stays free.
It runs entirely on your own machine and redistributes no Apple code.
Open an issue with the output of bletether doctor and, where the failure involves a
specific device, the fixture or the relevant part of
~/Library/Logs/BleTether/packets.jsonl.
BleTether is distributed as a binary; this repository holds the documentation, the GitHub Action and the releases.

