Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Beam Bites: Create Your Own Livebook Extensions

livebook/beam-bites.livemd

Beam Bites: Create Your Own Livebook Extensions

Mix.install([
  {:kino_web_bluetooth,
   path: "/Users/erik.m.hansen/Development/private/kino_web_bluetooth"},
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.11"}
])

Livebook

service_uuid = ""

“[…] code notebooks” - the docs

Interactive Notebooks with Kino

“[…] interactive and collaborative code notebooks” - the docs

form =
  Kino.Control.form(
    [
      name: Kino.Input.text("Name"),
      message: Kino.Input.textarea("Message")
    ],
    submit: "Send"
  )
Kino.listen(form, fn event ->
  %{message: message, name: name} = event.data
  IO.puts("Message: #{message} from #{name}")
end)

There is a lot here

There are already a lot of built-in Kinos:

  • Kino.Audio
  • Kino.DataTabel
  • Kino.Download
  • Kino.ETS
  • … *

It’s a Web Application: Leveraging the Browser

“a web application for writing interactive and collaborative code notebooks” - the docs

Which implies that we can leverage all the things a browser can do in our Livebook 🎉

Kino.JS

> Allows for defining custom JavaScript powered kinos.

defmodule KinoDocs.HTML do
  use Kino.JS

  def new(html) do
    Kino.JS.new(__MODULE__, html)
  end

  asset "main.js" do
    """
    export function init(ctx, html) {
      ctx.root.innerHTML = html;
    }
    """
  end
end

It’s kinda like having JS in your Markdown

Kino.JS.Live

> Introduces state and event-driven capabilities to JavaScript powered kinos.

defmodule KinoDocs.LiveHTML do
  use Kino.JS
  use Kino.JS.Live

  def new(html) do
    Kino.JS.Live.new(__MODULE__, html)
  end

  def replace(kino, html) do
    Kino.JS.Live.cast(kino, {:replace, html})
  end

  @impl true
  def init(html, ctx) do
    {:ok, assign(ctx, html: html)}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, ctx.assigns.html, ctx}
  end

  @impl true
  def handle_cast({:replace, html}, ctx) do
    broadcast_event(ctx, "replace", html)
    {:noreply, assign(ctx, html: html)}
  end

  asset "main.js" do
    """
    export function init(ctx, html) {
      ctx.root.innerHTML = html;

      ctx.handleEvent("replace", (html) => {
        ctx.root.innerHTML = html;
      });
    }
    """
  end
end

Kino.SmartCells: The Full JS / Elixir Integration

A Hierachy of Capabilities

Kino.SmartCell builds on top of Kino.JS.Live that builds on top of Kino.JS

Client/Server relationship

Example: A Web Bluetooth Smart Cell

Goal: Interact with a Bluetooth device in Elixir code blocks - via the Web Bluetooth

The overall intent is to have something that works a bit like this:

sequenceDiagram
    actor user as User
    participant robot as LEGO Robot
    participant client as JavaScript UI (Client)
    participant server as Elixir Smart Cell (Server)
    participant elixir as Elixir Code Block

    user ->> client: Enter device info
    client ->> robot: Connect via Web Bluetooth
    loop Incomming Data
      robot ->> client: Send data
      client ->> server: Send data
    end

    elixir ->> server: Get Data...
    elixir ->> elixir: ... and have fun!

Demo first!

service_uuid = "00001623-1212-efde-1623-785feabcd123"
KinoWebBluetooth.BluetoothWriter.send_message([  
                0x10,
                0x00,
                0x41,
                99,
                0x00, # Mode
                0x06, # Delta
                0x00, # Delta 
                0x00, # Delta 
                0x00, # Delta 
                0x01  # Enable notifications
            ])
alias VegaLite, as: Vl

sensor_readings = KinoWebBluetooth.MessageStore.messages()
|> Enum.filter(&(Enum.count(&1) == 10)) # Filter out non-sensor events
|> Enum.map(fn map-> [
  # map["0"], - Ignore lenth byte
  # map["1"], - Ignore hub id
  # map["2"], - Ignore message type (We should be filtering on this)
  map["3"], 
  map["4"], 
  map["5"],
  map["6"], 
  map["7"], 
  map["8"], 
  map["9"]
] end)
|> Enum.with_index()
|> Enum.map(fn({[a, b, c, d, e, f, g], index}) -> 
  %{
    "index" => index,
    "a" => a, 
    "b" => b, 
    "c" => c, 
    "d" => d, 
    "e" => e, 
    "f" => f,
    "g" => g
  } 
  end)

Vl.new(width: 10800, height: 600) #Plot the data!
|> Vl.data_from_values(sensor_readings)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "index", type: :nominal, axis: [])
|> Vl.encode_field(:y, "b", type: :quantitative)

Code Walktrough!

Development Flow - Tips and Tricks

Mix.install([
  {:kino_web_bluetooth,
   path: "/Users/erik.m.hansen/Development/private/kino_web_bluetooth"}, # <--- Local dependency
  {:kino_vega_lite, "~> 0.1.11"}
])
  • Change code - hit refresh (not great - not terrible)
  • Clone the livebook repo and run locally - looking at the logs is very useful
  • Use the JS debugger
  • Make TS/JS build and bundling part of mix compile
    • Just don’t ship it like that! Ship with prebuilt js