KinoWebBluetooth

A Livebook Smart Cell and Elixir API for Bluetooth Low Energy devices, using the browser's Web Bluetooth API.

You pick a device in the Smart Cell. Then you can read, write and subscribe to its characteristics, either in the cell or from Elixir code.

Requirements

Installation

Mix.install([
{:kino_web_bluetooth, "~> 0.1"}
])

The Smart Cell

Add a Web Bluetooth Smart Cell. Then:

  1. Enter a GATT service UUID. This can be a full UUID, a 16-bit alias such as 0x180D, or a standard name such as heart_rate.
  2. Click Connect and pick a device in the browser dialog.
  3. The cell lists the services and characteristics of the device. Use Read, Subscribe and Write to work with them. Write values are entered as hex (01 ff) or as text.

The cell saves your input in the notebook. When you evaluate it, the device is bound to a variable (device by default).

The API

Browsers only let a user pick a device, so you always connect in the Smart Cell. Everything else also works from code:

# Bound by the Smart Cell
device = KinoWebBluetooth.device("ble-...")
# Wait until the device is connected in the Smart Cell
:ok = KinoWebBluetooth.await_connected(device)
# Read
location = KinoWebBluetooth.characteristic!(device, "body_sensor_location")
{:ok, <<sensor_location>>} = KinoWebBluetooth.read(location)
# Write, synchronously or asynchronously
control_point = KinoWebBluetooth.characteristic!(device, "heart_rate_control_point")
:ok = KinoWebBluetooth.write(control_point, <<1>>)
:ok = KinoWebBluetooth.write_async(control_point, <<1>>)
# Subscribe to notifications
measurement = KinoWebBluetooth.characteristic!(device, "heart_rate_measurement")
:ok = KinoWebBluetooth.subscribe(measurement)
receive do
{:kino_web_bluetooth, :notification, ^measurement, <<_flags, bpm, _rest::binary>>} -> bpm
end
# ...or consume them as a stream
measurement
|> KinoWebBluetooth.stream()
|> Kino.listen(fn <<_flags, bpm, _rest::binary>> -> IO.puts("#{bpm} bpm") end)

See the KinoWebBluetooth module docs for all functions.

How it works

Each Smart Cell starts a KinoWebBluetooth.Device process. When the device connects, the Device process starts one KinoWebBluetooth.Characteristic GenServer for each characteristic. That GenServer holds the last value and the subscribers. It also runs GATT operations one at a time, because browsers reject concurrent operations on the same characteristic.

The state lives in Elixir. The browser keeps only what it has to: the BluetoothDevice and its characteristic objects. The Smart Cell sends operations to the browser tab that connected the device, and sends the results back.

Development

mix deps.get
mix test

Spec

The requirements are in spec/kino_web_bluetooth.spec.md, each with an ID such as UI-4. Tests declare the requirements they verify with a tag:

@tag spec: "UI-4"
test "shows when the device disconnects" do

Requirements that tests can't fully cover are reviewed by hand and recorded in spec/reviews.exs.

mix spec runs the tests and writes spec/STATUS.md, which lists each requirement as tested, reviewed, failing or open. Commit it together with spec and code changes. CI fails when it is out of date.

Releasing

  1. Bump @version in mix.exs and merge to main.
  2. In GitHub, run Actions → Publish to Hex on main with that version. Tick Dry run first to check the package without publishing.

The workflow runs all checks, publishes the package and docs to Hex, and tags the release vX.Y.Z. It needs a HEX_API_KEY secret.

License

MIT, see LICENSE.