-
Notifications
You must be signed in to change notification settings - Fork 197
feat: add WASM/WebBluetooth support #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
acouvreur
wants to merge
31
commits into
tinygo-org:dev
Choose a base branch
from
acouvreur:add-webbluetooth
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
3c4e6f2
feat: add WASM/WebBluetooth support
acouvreur e03371a
remove generated files
acouvreur c419ee0
add disconnection event subscription
acouvreur 9e1373a
fix(js): correct the notification listener lifecycle
acouvreur 5b8cc4c
fix(js): run user callbacks on a new goroutine
acouvreur d98764b
fix(js): read a DataView at its own offset and length
acouvreur 3b3b84c
fix(js): handle a promise that rejects without an object
acouvreur fd5dc21
fix(js): keep the device state in the adapter
acouvreur f98b3bd
fix(js): track the scan state
acouvreur d17fb51
refactor(js): match the receiver and error style of the other backends
acouvreur 0137bc4
examples(webbluetooth): add a README and make the server static
acouvreur 8201568
examples(webbluetooth): report errors instead of a panic
acouvreur 50cf86b
docs: add WASM to the support table and the platform list
acouvreur 3211cf8
ci: build the WASM example in the Linux job
acouvreur fb9e468
docs(js): shorten the comments to the style of AGENTS.md
acouvreur 64b0284
docs(webbluetooth): keep each paragraph on one line
acouvreur 0036e68
docs(webbluetooth): pair the support file with the toolchain
acouvreur 69c02ac
docs(webbluetooth): remove the mixed toolchain section
acouvreur 87e42de
examples(webbluetooth): give the page a JavaScript API
acouvreur 5072963
examples(webbluetooth): use the Battery Service for notifications
acouvreur 099f71a
feat(js): add Adapter.Reset
acouvreur 7dadaba
docs(webbluetooth): remove the screenshot
acouvreur ac02fa6
docs(js): give the source for the MTU value in GetMTU
acouvreur a16684e
docs(js): note the value limits on the write methods
acouvreur ef26482
docs(js): note what the scan result does not have
acouvreur 2db9712
ci: build the WASM example with TinyGo only
acouvreur 25fd5a2
fix(js): give the full value length from Read
acouvreur 57cb4f3
fix(js): close the connections in Reset
acouvreur 0681e90
docs(js): note that the picker needs a recent user action
acouvreur c8fcf81
chore(webbluetooth): add the final newline to the gitignore
acouvreur 33590fd
docs(js): say why EnableNotifications always stops the notifications
acouvreur File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| 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 { | ||
| for id, listener := range a.disconnectListeners { | ||
| if device, ok := a.devices[id]; ok { | ||
| device.Call("removeEventListener", "gattserverdisconnected", listener) | ||
| } | ||
| listener.Release() | ||
| } | ||
|
|
||
| a.bluetooth = js.Undefined() | ||
| a.devices = nil | ||
| a.disconnectListeners = nil | ||
| a.scanning = false | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # 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 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| wasm_exec.js | ||
| wasm.wasm |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.