defmodule Circuits.UART.Framing.MIDI.Buffer do alias __MODULE__ defstruct buffer: <<>> @type t :: %Buffer{buffer: binary} @moduledoc """ Implements a buffer for incoming serial data. """ @doc """ Initialise a new empty buffer. """ @spec init :: Buffer.t() def init, do: %Buffer{} @doc """ Consume as much of the buffer as possible, splitting it into messages. """ def get_packets(%Buffer{buffer: buffer}) do with {:ok, buffer} <- drop_leading_data_bytes(buffer), {:ok, messages, buffer} <- consume_messages([], buffer) do {:ok, messages, %Buffer{buffer: buffer}} else :error -> {:ok, [], %Buffer{buffer: buffer}} end end @doc """ Apend new data onto the end of the buffer. """ @spec append(Buffer.t(), binary) :: Buffer.t() def append(%Buffer{buffer: buffer}, data) when is_binary(data), do: %Buffer{buffer: buffer <> data} @doc """ Indicates whether the buffer is empty or not. """ @spec empty?(Buffer.t()) :: boolean def empty?(%Buffer{buffer: <<>>}), do: true def empty?(%Buffer{}), do: false defp consume_status_byte(<<1::integer-size(1), byte::bitstring-size(7), rest::binary>>), do: {:ok, <<1::integer-size(1), byte::bitstring-size(7)>>, rest} defp consume_status_byte(data), do: {:error, data} defp consume_data_byte(<<0::integer-size(1), byte::bitstring-size(7), rest::binary>>), do: {:ok, <<0::integer-size(1), byte::bitstring-size(7)>>, rest} defp consume_data_byte(data), do: {:error, data} defp drop_leading_data_bytes(data) when is_binary(data) do data |> consume_data_byte() |> drop_leading_data_bytes() end defp drop_leading_data_bytes({:ok, _byte, <<>>}), do: :error defp drop_leading_data_bytes({:ok, _byte, data}) do data |> consume_data_byte() |> drop_leading_data_bytes() end defp drop_leading_data_bytes({:error, data}), do: {:ok, data} defp consume_data_bytes(bytes, <<>>), do: {:ok, bytes, <<>>} defp consume_data_bytes(bytes, data) do case consume_data_byte(data) do {:ok, byte, rest} -> consume_data_bytes(bytes <> byte, rest) {:error, rest} -> {:ok, bytes, rest} end end defp consume_messages(messages, <<>>), do: {:ok, Enum.reverse(messages), <<>>} defp consume_messages(messages, data) when is_binary(data) do case consume_message(data) do {:ok, message, rest} -> consume_messages([message | messages], rest) {:error, rest} -> {:ok, messages, rest} end end defp consume_message(data) when is_binary(data) do case consume_status_byte(data) do {:ok, <<0xF0>>, rest} -> consume_sysex_payload(<<0xF0>>, rest) {:ok, status_byte, rest} -> consume_data_bytes(status_byte, rest) {:error, rest} -> {:error, rest} end end defp consume_sysex_payload(payload, <<0xF7, rest::binary>>), do: {:ok, payload, rest} defp consume_sysex_payload(payload, <<>>), do: {:ok, <<>>, payload} defp consume_sysex_payload(payload, <>), do: consume_sysex_payload(payload <> byte, rest) end