Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3c4e6f2
feat: add WASM/WebBluetooth support
acouvreur Apr 10, 2026
e03371a
remove generated files
acouvreur Apr 19, 2026
c419ee0
add disconnection event subscription
acouvreur Apr 19, 2026
9e1373a
fix(js): correct the notification listener lifecycle
acouvreur Sep 2, 2026
5b8cc4c
fix(js): run user callbacks on a new goroutine
acouvreur Sep 2, 2026
d98764b
fix(js): read a DataView at its own offset and length
acouvreur Sep 2, 2026
3b3b84c
fix(js): handle a promise that rejects without an object
acouvreur Sep 2, 2026
fd5dc21
fix(js): keep the device state in the adapter
acouvreur Sep 2, 2026
f98b3bd
fix(js): track the scan state
acouvreur Sep 2, 2026
d17fb51
refactor(js): match the receiver and error style of the other backends
acouvreur Sep 2, 2026
0137bc4
examples(webbluetooth): add a README and make the server static
acouvreur Sep 2, 2026
8201568
examples(webbluetooth): report errors instead of a panic
acouvreur Sep 2, 2026
50cf86b
docs: add WASM to the support table and the platform list
acouvreur Sep 2, 2026
3211cf8
ci: build the WASM example in the Linux job
acouvreur Sep 2, 2026
fb9e468
docs(js): shorten the comments to the style of AGENTS.md
acouvreur Sep 2, 2026
64b0284
docs(webbluetooth): keep each paragraph on one line
acouvreur Sep 2, 2026
0036e68
docs(webbluetooth): pair the support file with the toolchain
acouvreur Sep 2, 2026
69c02ac
docs(webbluetooth): remove the mixed toolchain section
acouvreur Sep 2, 2026
87e42de
examples(webbluetooth): give the page a JavaScript API
acouvreur Sep 2, 2026
5072963
examples(webbluetooth): use the Battery Service for notifications
acouvreur Sep 2, 2026
099f71a
feat(js): add Adapter.Reset
acouvreur Sep 2, 2026
7dadaba
docs(webbluetooth): remove the screenshot
acouvreur Sep 2, 2026
ac02fa6
docs(js): give the source for the MTU value in GetMTU
acouvreur Sep 2, 2026
a16684e
docs(js): note the value limits on the write methods
acouvreur Sep 2, 2026
ef26482
docs(js): note what the scan result does not have
acouvreur Sep 2, 2026
2db9712
ci: build the WASM example with TinyGo only
acouvreur Sep 3, 2026
25fd5a2
fix(js): give the full value length from Read
acouvreur Sep 3, 2026
57cb4f3
fix(js): close the connections in Reset
acouvreur Sep 3, 2026
0681e90
docs(js): note that the picker needs a recent user action
acouvreur Sep 3, 2026
c8fcf81
chore(webbluetooth): add the final newline to the gitignore
acouvreur Sep 3, 2026
33590fd
docs(js): say why EnableNotifications always stops the notifications
acouvreur Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ jobs:
run: make smoketest-linux
- name: "Run Windows cross-compiled smoke tests"
run: make smoketest-windows
- name: "Run WASM smoke tests"
run: make smoketest-wasm
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

TINYGO=tinygo

smoketest: smoketest-tinygo smoketest-linux smoketest-windows
smoketest: smoketest-tinygo smoketest-linux smoketest-windows smoketest-wasm

smoketest-tinygo:
# Test all examples (and some boards)
Expand Down Expand Up @@ -85,6 +85,10 @@ smoketest-windows:
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate

smoketest-wasm:
# Test on WASM.
$(TINYGO) build -o /tmp/go-build-discard.wasm -target=wasm ./examples/webbluetooth

smoketest-macos:
# Test on macos.
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner
Expand Down
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.

It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows).
It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows). It also works in the browser with [WASM](#wasm).

By using [TinyGo](https://tinygo.org/), it can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/), on the [Espressif](https://www.espressif.com/) ESP32-C3 and ESP32-S3 with their onboard radio, or on boards that have a Bluetooth co-processor that uses the [Bluetooth Host Controller Interface (HCI)](https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host-controller-interface/host-controller-interface-functional-specification.html).

Expand Down Expand Up @@ -107,17 +107,17 @@ func must(action string, err error) {

## Current support

| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) | ESP32-C3/S3 (espradio) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- | ---------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| | Linux | macOS | Windows | WASM | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) | ESP32-C3/S3 (espradio) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- | ---------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | WebBluetooth | SoftDevice | HCI | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |

## Linux

Expand Down Expand Up @@ -191,6 +191,31 @@ After you have followed the installation, you should be able to compile/run the
cd bluetooth
go run .\examples\scanner

## WASM

Go Bluetooth support for the browser uses the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API). Chrome and Edge support this API. See the [browser compatibility table](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility).

WASM can only act as a BLE Central. The Web Bluetooth API has no peripheral role.

The API has some other limits that come from the browser:

* `Scan` opens the device picker of the browser instead of a continuous scan. The callback runs one time, for the device that the user selects.
* `Address` holds an opaque device ID instead of a MAC address.
* You must put every service that you want to use in `Adapter.RequestedServices` before you call `Scan`.
* The browser handles the security prompt, so the user must select the device.

### Installation

You can compile with [TinyGo](https://tinygo.org/getting-started/install/) or with the standard Go compiler.

You can obtain the Go Bluetooth package using Git:

git clone https://github.com/tinygo-org/bluetooth.git

### Compiling

The "webbluetooth" example shows how to build and serve a page. See [examples/webbluetooth/README.md](./examples/webbluetooth/README.md) for the steps.

## Nordic Semiconductor

Go Bluetooth has bare metal support for several chips from [Nordic Semiconductor](https://www.nordicsemi.com/) that include a built-in Bluetooth Low Energy radio.
Expand Down
88 changes: 88 additions & 0 deletions adapter_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package bluetooth

import (
"errors"
"syscall/js"
)

var _ BLEAdapter = (*Adapter)(nil)

// Adapter represents the WebBluetooth adapter accessed via navigator.bluetooth.
type Adapter struct {
bluetooth js.Value
connectHandler func(device Device, connected bool)

// devices holds the BluetoothDevice objects returned by Scan, keyed by
// device ID, because Connect needs them again.
devices map[string]js.Value

// disconnectListeners holds the gattserverdisconnected listeners, keyed
// by device ID.
disconnectListeners map[string]js.Func

scanning bool

// RequestedServices is the list of service UUIDs to give as
// optionalServices to navigator.bluetooth.requestDevice().
//
// The browser refuses access to a service that is not in this list. Set
// this before you call Scan.
RequestedServices []UUID
}

// DefaultAdapter is the default adapter using the navigator.bluetooth API.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
connectHandler: func(device Device, connected bool) {},
}

// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
navigator := js.Global().Get("navigator")
if navigator.IsUndefined() {
return errors.New("bluetooth: navigator is not available")
}
bt := navigator.Get("bluetooth")
if bt.IsUndefined() {
return errors.New("bluetooth: WebBluetooth is not supported in this browser")
}
a.bluetooth = bt

// Keep the known devices when Enable runs more than once.
if a.devices == nil {
a.devices = map[string]js.Value{}
a.disconnectListeners = map[string]js.Func{}
}
return nil
}

// Reset clears the state of the adapter so that Enable can run again.
//
// The devices from an earlier scan are gone after this, so the user must
// select a device again.
func (a *Adapter) Reset() error {
// Remove the listeners before the disconnect, so that no event arrives
// after the release of the JS function.
for id, listener := range a.disconnectListeners {
if device, ok := a.devices[id]; ok {
device.Call("removeEventListener", "gattserverdisconnected", listener)
}
listener.Release()
}

// The browser keeps the connection until the page closes it.
for _, device := range a.devices {
gatt := device.Get("gatt")
if !gatt.IsUndefined() && gatt.Get("connected").Bool() {
gatt.Call("disconnect")
}
}

a.bluetooth = js.Undefined()
a.devices = nil
a.disconnectListeners = nil
a.scanning = false
return nil
}
101 changes: 101 additions & 0 deletions examples/webbluetooth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# WebBluetooth example

This example gives the WebBluetooth backend to a web page as a small JavaScript API. The page opens the device picker of the browser, reads the Device Information service of the device that you select, and can subscribe to a characteristic that sends notifications.

The WASM module only holds the binding. The page in `html/index.html` does all of the display work.

## Requirements

* [TinyGo](https://tinygo.org/getting-started/install/), or the standard Go toolchain.
* A browser with WebBluetooth support, such as Chrome or Edge. See the [browser compatibility table](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility).

WebBluetooth is only available in a secure context. A page on `localhost` counts as secure, so you do not need HTTPS during development.

## Build and run

The page needs two build products in the `html` directory: the module `wasm.wasm`, and the JavaScript support file `wasm_exec.js`. TinyGo and the standard Go toolchain each have their own support file. Always take both products from the same toolchain.

Run all commands from the root of the repository.

### Build with TinyGo

```shell
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" ./examples/webbluetooth/html/
tinygo build -o ./examples/webbluetooth/html/wasm.wasm -target wasm ./examples/webbluetooth/
```

### Build with the standard Go toolchain

```shell
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" ./examples/webbluetooth/html/
GOOS=js GOARCH=wasm go build -o ./examples/webbluetooth/html/wasm.wasm ./examples/webbluetooth/
```

Go versions before 1.24 keep the support file in `misc/wasm/wasm_exec.js`.

### Windows

The TinyGo commands also work in PowerShell. The standard Go toolchain needs the two variables in the environment:

```powershell
$env:GOOS = "js"; $env:GOARCH = "wasm"
go build -o ./examples/webbluetooth/html/wasm.wasm ./examples/webbluetooth/
```

### Serve the page

```shell
go run ./examples/webbluetooth/server/
```

Open http://localhost:8080 and click **Connect**. The browser shows the device picker. Select a device to see the result in the page.

Both `wasm.wasm` and `wasm_exec.js` are build output, so git ignores them.

## Test with a peripheral

Run one of these examples on a second machine, because a Bluetooth adapter cannot connect to itself. Linux and Windows can be a peripheral. macOS cannot.

* `go run ./examples/device-information` gives the values that the Device Information panel shows.
* `go run ./examples/battery` gives the Battery Level notifications that the Notifications panel uses by default.

## JavaScript API

The module sets `globalThis.ble`, then calls `globalThis.onBleReady` if the page gives that function. Define `onBleReady` before you start the module.

Every function returns a Promise. A function that JavaScript calls must not block, because it holds the event loop until it returns, and each call into the Bluetooth package waits for a promise.

| Function | Result |
| --- | --- |
| `ble.enable()` | Prepares the adapter. |
| `ble.requestDevice(serviceUUIDs)` | Opens the device picker. Gives `{id, name}` for the device that the user selects. |
| `ble.connect(id)` | Connects to the device with that id. |
| `ble.disconnect()` | Closes the connection. |
| `ble.read(service, characteristic)` | Gives the value as a `Uint8Array`. |
| `ble.readString(service, characteristic)` | Gives the value as a string. |
| `ble.subscribe(service, characteristic, callback)` | Starts notifications. Calls the callback with a `Uint8Array` for each new value. |
| `ble.unsubscribe(service, characteristic)` | Stops notifications. |
| `ble.onConnectionChange(callback)` | Calls the callback with a boolean on each connection and disconnection. Call it before `connect`. |

Give every service that the page uses to `requestDevice`. The browser refuses access to a service that the page did not ask for.

```js
globalThis.onBleReady = async () => {
await ble.enable();
const device = await ble.requestDevice(["0000180a-0000-1000-8000-00805f9b34fb"]);
await ble.connect(device.id);
console.log(await ble.readString(
"0000180a-0000-1000-8000-00805f9b34fb",
"00002a29-0000-1000-8000-00805f9b34fb"));
};
```

## Limitations of WebBluetooth

* The browser opens the device picker only after a recent user action, such as a click. Call `ble.requestDevice` soon after that action, and do no slow work first.
* The browser gives an opaque device ID instead of a MAC address. The `Address` type holds this ID.
* You must call `Scan` before `Connect`. The browser object for the device does not survive a page reload.
* You must list every service that you want to use in `Adapter.RequestedServices` before you call `Scan`. The browser refuses access to a service that is not in the list.
* The browser does not report the RSSI of the selected device.
* The browser does not give the negotiated MTU. `GetMTU` returns 512.
* WebBluetooth has no peripheral role, so advertisement and local services are not available.
2 changes: 2 additions & 0 deletions examples/webbluetooth/html/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
wasm_exec.js
wasm.wasm
Loading
Loading