lib/blue_heron_transport_usb.ex

defmodule BlueHeronTransportUSB do
  @moduledoc """
  Partially implements Volume 4 Part C of the Bluetooth Spec
  """

  use GenServer
  @behaviour BlueHeron.HCI.Transport

  require Logger

  @hci_command_packet 0x01
  @hci_acl_packet 0x02
  @log_message_packet 0xFC

  defstruct vid: 0,
            pid: 0,
            init_commands: []

  @impl BlueHeron.HCI.Transport
  def init_commands(%__MODULE__{init_commands: init_commands}) do
    init_commands
  end

  @impl BlueHeron.HCI.Transport
  def start_link(%__MODULE__{} = config, recv) when is_function(recv, 1) do
    GenServer.start_link(__MODULE__, {config, 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

  @impl GenServer
  def init({%__MODULE__{} = config, recv}) do
    port =
      Port.open({:spawn_executable, port_executable()}, [
        {:args, open_args(config)},
        :binary,
        :exit_status,
        {:packet, 2}
      ])

    {:ok, %{port: port, recv: recv}}
  end

  defp open_args(%__MODULE__{vid: vid, pid: pid}) when vid > 0 and pid > 0 do
    ["open_by_vid_pid", to_string(vid), to_string(pid)]
  end

  defp open_args(_other), do: ["open_first"]

  @impl true
  def handle_info(
        {port, {:data, <<@log_message_packet, level_num, message::binary>>}},
        %{port: port} = state
      ) do
    Logger.log(level(level_num), ["BlueHeron(USB): ", message])
    {:noreply, state}
  end

  def handle_info({port, {:data, data}}, %{port: port, recv: recv} = state) do
    _ = recv.(data)
    {:noreply, state}
  end

  def handle_info({port, {:exit_status, status}}, %{port: port} = state) do
    {:stop, {:libusb_port_crash, status}, state}
  end

  @impl GenServer
  def handle_call({:send, packet}, _from, state) do
    {:reply, Port.command(state.port, packet), state}
  end

  defp port_executable() do
    Application.app_dir(:blue_heron_transport_usb, ["priv", "hci_transport"])
  end

  # See hci_transport.h for levels
  defp level(0), do: :error
  defp level(1), do: :warn
  defp level(2), do: :debug
end