lib/blue_heron_transport_uart.ex

defmodule BlueHeronTransportUART do
  @moduledoc """
  > The objective of this HCI UART Transport Layer is to make it possible to use the Bluetooth HCI
  > over a serial interface between two UARTs on the same PCB. The HCI UART Transport Layer
  > assumes that the UART communication is free from line errors.

  Reference: Version 5.0, Vol 4, Part A, 1
  """

  use GenServer
  require Logger
  alias Circuits.UART
  alias BlueHeronTransportUART.Framing

  @behaviour BlueHeron.HCI.Transport

  @hci_command_packet 0x01
  @hci_acl_packet 0x02

  defstruct recv: nil,
            uart_pid: nil,
            uart_opts: [],
            device: [],
            init_commands: []

  @impl BlueHeron.HCI.Transport
  def init_commands(%BlueHeronTransportUART{init_commands: init_commands}),
    do: init_commands

  @impl BlueHeron.HCI.Transport
  def start_link(%BlueHeronTransportUART{} = config, recv) when is_function(recv, 1) do
    GenServer.start_link(__MODULE__, %{config | recv: recv})
  end

  @impl BlueHeron.HCI.Transport
  def send_command(pid, command) when is_binary(command) do
    GenServer.call(pid, {:send, [<<@hci_command_packet::8>>, command]})
  end

  @impl BlueHeron.HCI.Transport
  def send_acl(pid, acl) when is_binary(acl) do
    GenServer.call(pid, {:send, [<<@hci_acl_packet::8>>, acl]})
  end

  ## Server Callbacks

  @impl GenServer
  def init(config) do
    Logger.info(%{uart_transport_config: config})
    uart_opts = Keyword.merge(config.uart_opts, active: true, framing: {Framing, []})

    case config.uart_pid do
      nil ->
        {:ok, pid} = UART.start_link()
        :ok = UART.open(pid, config.device, uart_opts)
        {:ok, %{config | uart_pid: pid}}

      pid ->
        UART.controlling_process(pid, self())
        :ok = UART.configure(pid, uart_opts)
        {:ok, %{config | uart_opts: uart_opts}}
    end
  end

  @impl GenServer
  def handle_call({:send, message}, _from, %{uart_pid: uart_pid} = state) do
    {:reply, :ok == UART.write(uart_pid, message), state}
  end

  @impl GenServer
  def handle_info({:circuits_uart, _dev, msg}, state) do
    state.recv.(msg)
    {:noreply, state}
  end
end