Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BleTether

Real Bluetooth in the iOS Simulator. No changes to your app.

Flutter React Native Swift macOS 26+

Real Bluetooth devices appearing in the iOS Simulator

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.


Why it is different

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.


Install

Requires macOS 26+, Xcode 26+, Apple Silicon.

  1. Download BleTether.app from Releases, drag it to Applications, and open it. Grant Bluetooth access when asked.

  2. On the Overview tab, click Install command line tool. That puts bletether on 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
  3. 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 on before 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

The app

  • 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, not msg 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.


CLI

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.


Recorded devices

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.

Replaying a recorded device into the simulator from a fixture
bletether record                       # every device this session connected to
bletether record "DEVICE" device.json    # just one

Replaying a fixture needs no Bluetooth, no permission, no daemon, and no device in the room:

bletether ci device.json

Everything 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.


CI

- 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.gz asset 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.

Worked examples

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.


What works, and what does not

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. CBPeripheralManager is 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.


Troubleshooting

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.


Licence & support

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.


Reporting a problem

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.

About

BleTether — Real Bluetooth in the iOS Simulator without changing a single line of code. Bridge Mac's physical BLE hardware or record GATT fixtures for fully automated CI/CD runs. Native Swift, Flutter & React Native ready.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages