KinoWebSerial

A Livebook Smart Cell and Elixir API for serial ports, using the browser's Web Serial API.

You pick a port in the Smart Cell, for example an Arduino or another USB serial device. Then you can send and receive data, either in the cell or from Elixir code.

Requirements

Installation

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

The Smart Cell

Add a Web Serial Smart Cell. Then:

  1. Choose the baud rate and, if needed, the data bits, parity, stop bits and flow control. Enter a USB vendor and product ID (in hex, such as 2341) to only offer matching ports.
  2. Click Connect and pick a port in the browser dialog.
  3. The monitor shows the data sent and received, as text or hex. Send data as text or hex (01 ff), with an optional line ending.

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

The API

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

# Bound by the Smart Cell
serial = KinoWebSerial.port("serial-...")

# Wait until the port is connected in the Smart Cell
:ok = KinoWebSerial.await_connected(serial)

# Subscribe before writing, to not miss the response
:ok = KinoWebSerial.subscribe(serial)
:ok = KinoWebSerial.write(serial, "status\n")

receive do
  {:kino_web_serial, :data, ^serial, data} -> data
end

# Write without waiting
:ok = KinoWebSerial.write_async(serial, <<0x01, 0xFF>>)

# Consume received data as lines...
serial
|> KinoWebSerial.lines()
|> Kino.listen(fn line -> IO.puts(line) end)

# ...or as raw chunks
serial
|> KinoWebSerial.stream()
|> Kino.listen(&IO.inspect/1)

# Control signals, such as toggling DTR to reset an Arduino
:ok = KinoWebSerial.set_signals(serial, data_terminal_ready: false)
:ok = KinoWebSerial.set_signals(serial, data_terminal_ready: true)
{:ok, %{clear_to_send: cts}} = KinoWebSerial.signals(serial)

See the KinoWebSerial module docs for all functions.

Example: LEGO® SPIKE™ Prime

Run in Livebook

notebooks/spike_prime_usb.livemd connects to a SPIKE Prime hub over USB, using ExSPIKE for the protocol. It asks the hub for its info, shows its console output and sensor readings, and uploads and runs a program.

How it works

Each Smart Cell starts a KinoWebSerial.Port GenServer. It holds the connection state and the subscribers, and runs browser operations (writes, signals) one at a time, in order.

The state lives in Elixir. The browser keeps only what it has to: the open SerialPort and its reader and writer. While the port is open, the browser reads from it continuously and sends the data to Elixir. The Smart Cell sends operations to the browser tab that opened the port, and sends the results back.

Development

mix deps.get
mix test

Spec

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

@tag spec: "UI-5"
test "shows when the port is lost" 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.