feat: add WASM/WebBluetooth support - #436
Conversation
|
What is the status with this PR @acouvreur 😸 |
Are you interested with bringing WASM / WebBluetooth support? The current implementation is a load of duct taping but it works! I can polish the PR a little bit and we can merge it if you think that'd be a good addition. |
I am indeed! |
Cool! Let me clean that up :) |
Still WIP
The listening flag was never set to true, so EnableNotifications(nil) kept the JS listener attached and released no resources. A late event then found a nil callback. A second call added a duplicate listener.
A wrapped Go function blocks the JS event loop until it returns. A callback that calls back into this package waits for a promise and deadlocks the page.
A DataView can be a window into a larger ArrayBuffer. Wrapping only the buffer gave the wrong bytes.
Calling toString on undefined or null throws inside the catch handler and stops the program.
Package level maps grew for the life of the page and were shared by all adapters.
StopScan now returns errNotScanning when no scan runs, and Scan returns errScanning for a second scan. This agrees with the other backends.
Read now has a value receiver, and a UUID that does not parse gives an error instead of a zero UUID.
The server now only serves the html directory. The README gives the build steps for TinyGo and for the standard Go toolchain.
A panic stops the Go runtime, so the page stayed dead after the user closed the device picker.
Without a job that builds for js/wasm, the browser backend can break without a signal.
This matches the README at the root of the repository.
The support file and the module must come from the same toolchain. A mixed pair gives an import error in the browser.
The two build sections already show the pair of commands for each toolchain.
The module now exports globalThis.ble with a function for each operation, and the page does all of the display work. Each export returns a Promise and runs the work on a new goroutine, because a function that JavaScript calls must not block the event loop.
Battery Level sends a new value every second, so the notifications panel shows data immediately. The examples/battery peripheral serves it.
Reset releases the disconnect listeners and drops the known devices, so that Enable can run again. BLEAdapter needs this method.
The screenshot did not show the current page.
512 is the maximum length of an attribute value, not the maximum ATT MTU.
Write can use a long write up to the maximum attribute value length. A write without response must fit the negotiated MTU, which GetMTU does not give.
requestDevice gives only an id and a name, so the RSSI is 0 and the payload has no service UUIDs.
4e04492 to
ef26482
Compare
|
@deadprogram looks good for me now... Clean up the example so that the built wasm exposes functions to be used in a JS program to interact with the tinygo-org/bluetooth APIs. Quite safe merge IMO. Next step for me is to update #468. I would love to add cancellable operations to this library. With JS you can easily map an AbortController Signal to a Go |
deadprogram
left a comment
There was a problem hiding this comment.
Thanks for this!
Here are some comments from an automated revview.
Overall
This is a good addition and it is near to merge. The structure agrees with the other backends. gap_js.go uses the same Address shape as the macOS backend (opaque ID, Set, no-op SetRandom), and the interface assertions (BLEAdapter, GAPDevice, GATTCService, GATTCCharacteristic) are all present. The known WASM traps are correct: user callbacks run on a new goroutine so that they do not stop the JS event loop, DataView values are read at their own offset and length, and js.Func values get released.
Points to think about
1. Adapter.RequestedServices is only on the js backend (adapter_js.go)
No other backend has an exported field on Adapter, so code that sets this field does not compile for Linux, macOS, or Windows. This makes the "same code everywhere" goal weaker. Two options: keep it and accept the limit (it is documented), or add the same field as a no-op to the other backends so that portable code compiles. This is the one API decision in the PR that is difficult to change later.
2. Adapter.Reset leaves a connection open (adapter_js.go)
Reset removes the disconnect listeners and clears the maps, but it does not disconnect the GATT server. The browser keeps the connection to a device that is connected. A gatt.disconnect() on each known device would make the reset complete.
3. EnableNotifications(nil) calls stopNotifications always (gattc_js.go)
If notifications were never started, the code still calls stopNotifications on the characteristic. This is harmless in Chrome, but a return after stopListening when c.listening is false is more clear.
4. Read truncates without a signal (gattc_js.go)
If the value is longer than data, Read gives the first len(data) bytes and no indication. Please make sure that this agrees with the macOS and Windows backends.
5. The example imports syscall/js with no build constraint
go build ./... on Linux now stops at examples/webbluetooth. CI does not use ./..., and examples/circuitplay already breaks it, so this changes nothing today. A //go:build js line in the example is still an inexpensive improvement.
6. requestDevice and user activation
Scan calls navigator.bluetooth.requestDevice from a goroutine, so the call leaves the task of the click. Chrome accepts this while the transient activation is valid (some seconds), which is why the example works. A note in the README that the page must call requestDevice immediately after a user action would prevent a difficult bug report later.
Nits
examples/webbluetooth/html/.gitignorehas no final newline.- The WASM installation steps are given two times (repository README and example README). This is not incorrect, only repetition.
TinyGo gives a smaller module and a faster build than the standard toolchain.
Linux, macOS and Windows give the length of the value, even when it is longer than the buffer. The caller can then see that the value was cut.
Reset removed the listeners but left the browser connected to the device.
The browser refuses requestDevice when the user activation is too old.
A characteristic that comes from a second discovery has listening false while the browser still sends notifications.
|
Thanks for this. I built the branch with both toolchains and Some points from a read of the code. 1. This is the only new public item that portable code must set. A program for Linux and for the browser cannot set it without a build tag, which removes much of the value of the shared API. We can put the field on every 2. The notification listeners leak (
3. A user cancel looks like a failure (
Smaller items
|
This is a work in progress proposal to add support for the Web Bluetooth API through WASM.
I can currently successfully connect to a device,
but I'm still having some trouble/experimentations discovering services, characteristics and interacting with them.AI Disclaimer: This was mostly implemented using Claude Opus 4.6
I had a lot of fun interacting with bluetooth device from a webpage through WASM with the same underlying code that can run in linux, mac, windows or embedded systems.