Initial revision.

This commit is contained in:
James Harton 2020-05-04 21:26:45 +12:00
parent abc7822072
commit b1fa051cfd
10 changed files with 345 additions and 19 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
circuits_uart_midi_framing-*.tar
.elixir_ls

56
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,56 @@
image: elixir:latest
cache:
key: "$CI_JOB_NAME"
paths:
- deps
- _build
- /root/.mix
variables:
MIX_ENV: "test"
before_script:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get --only test
test:
stage: test
script:
- mix test
credo:
stage: test
script:
- mix credo
audit:
stage: test
script:
- mix hex.audit
format:
stage: test
script:
- mix format --check-formatted
pages:
stage: deploy
script:
- mix docs -o public
artifacts:
paths:
- public
only:
- master
package:
stage: deploy
only:
- /^v[0-9]+\.[0-9]+\.[0-9]+$/
script:
- mix hex.build
artifacts:
paths:
- "circuits_uart_midi_framing-*.tar"

16
LICENSE Normal file
View file

@ -0,0 +1,16 @@
Copyright 2020 James Harton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
* No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/).
* Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above.
* Enforceability: If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software.

View file

@ -1,11 +1,13 @@
# Circuits.UART.Framing.MIDI
**TODO: Add description**
Implements a simple framing that splits incoming serial data into individual
messages.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `circuits_uart_midi_framing` to your list of dependencies in `mix.exs`:
by adding `circuits_uart_midi_framing` to your list of dependencies in
`mix.exs`:
```elixir
def deps do

View file

@ -1,18 +1,46 @@
defmodule Circuits.UART.Framing.MIDI do
@behaviour Circuits.UART.Framing
alias __MODULE__.Buffer
@moduledoc """
Documentation for `Circuits.UART.Framing.MIDI`.
Implements framing for the MIDI protocol.
It doesn't decode any messages, it simply splits incoming serial frames apart
into individual MIDI messages as per the specification.
"""
@impl true
def init(_args), do: {:ok, Buffer.init()}
@doc """
Hello world.
## Examples
iex> Circuits.UART.Framing.MIDI.hello()
:world
Basically a no-op from a framing point of view. There's no intra-message
delimeter required, so we simply return the data unchanged.
"""
def hello do
:world
@impl true
def add_framing(data, state) do
{:ok, data, state}
end
@doc """
Process incoming data and break it apart into separate MIDI packets.
"""
@impl true
def remove_framing(data, %Buffer{} = buffer) do
buffer = Buffer.append(buffer, data)
case Buffer.get_packets(buffer) do
{:ok, messages, %Buffer{buffer: <<>>} = buffer} -> {:ok, messages, buffer}
{:ok, messages, %Buffer{} = buffer} -> {:in_frame, messages, buffer}
{:error, reason} -> {:error, reason}
end
end
@impl true
def frame_timeout(buffer), do: buffer
@impl true
def flush(direction, _buffer) when direction == :receive or direction == :both,
do: Buffer.init()
def flush(:transmit, buffer), do: buffer
end

View file

@ -0,0 +1,100 @@
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, <<byte::binary-size(1), rest::binary>>),
do: consume_sysex_payload(payload <> byte, rest)
end

26
mix.exs
View file

@ -1,16 +1,34 @@
defmodule Circuits.UART.Framing.MIDI.MixProject do
use Mix.Project
@version "0.1.0"
@description """
Implements MIDI framing for Serial ports connected via Circuits.UART.
"""
def project do
[
app: :circuits_uart_midi_framing,
version: "0.1.0",
version: @version,
elixir: "~> 1.10",
package: package(),
description: @description,
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def package do
[
maintainers: ["James Harton <james@automat.nz>"],
licenses: ["Hippocratic"],
links: %{
"Source" => "https://gitlab.com/jimsy/circuits_uart_midi_framing"
}
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
@ -21,8 +39,10 @@ defmodule Circuits.UART.Framing.MIDI.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
{:circuits_uart, "~> 1.4"},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
{:earmark, ">= 0.0.0", only: [:dev, :test]},
{:ex_doc, ">= 0.0.0", only: [:dev, :test]}
]
end
end

12
mix.lock Normal file
View file

@ -0,0 +1,12 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"circuits_uart": {:hex, :circuits_uart, "1.4.1", "f8151bfb5ac29fe2e791eec00bc6ec298fdb8fa81ddd80bdb0f9f2828f20d7b8", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8cc3065de6d9b859f76d10fa438e243103eea1ba53b9749e4c63005c6d89780b"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
"makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
}

View file

@ -0,0 +1,95 @@
defmodule Circuits.UART.Framing.MIDI.BufferTest do
use ExUnit.Case, async: true
alias Circuits.UART.Framing.MIDI.Buffer
@note_off <<0x80, 0x3C, 0x40>>
@note_on <<0x90, 0x3C, 0x40>>
describe "init/0" do
test "it creates an empty buffer" do
assert %Buffer{buffer: <<>>} = Buffer.init()
end
end
describe "append/2" do
test "it appends data to the buffer" do
buffer = %Buffer{buffer: <<0, 1>>}
buffer = Buffer.append(buffer, <<2, 3>>)
assert %Buffer{buffer: <<0, 1, 2, 3>>} = buffer
end
end
describe "get_packets/1" do
test "it can decode a single note-on" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(@note_on)
|> Buffer.get_packets()
assert [@note_on] = messages
assert Buffer.empty?(buffer)
end
test "it can decode a single note-off" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(@note_off)
|> Buffer.get_packets()
assert [@note_off] = messages
assert Buffer.empty?(buffer)
end
test "it drops leading data bytes" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(<<0, 0, 0>> <> @note_off)
|> Buffer.get_packets()
assert [@note_off] = messages
assert Buffer.empty?(buffer)
end
test "it splits multiple messages" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(@note_on <> @note_off)
|> Buffer.get_packets()
assert [@note_on, @note_off] = messages
assert Buffer.empty?(buffer)
end
test "it handles running status" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(<<0x90, 0x3C, 0x40, 0x3D, 0x40, 0x3E, 0x40>>)
|> Buffer.get_packets()
assert [<<0x90, 0x3C, 0x40, 0x3D, 0x40, 0x3E, 0x40>>] = messages
assert Buffer.empty?(buffer)
end
test "it handles multiple messages without data bytes" do
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(<<0xFF, 0xFE, 0xF8>>)
|> Buffer.get_packets()
assert [<<0xFF>>, <<0xFE>>, <<0xF8>>] = messages
assert Buffer.empty?(buffer)
end
test "it handles arbitrary-length sysex messages" do
message = <<0xF0, 0x7D>> <> Base.encode64("Marty McFly") <> <<0xF7>>
{:ok, messages, buffer} =
Buffer.init()
|> Buffer.append(message)
|> Buffer.get_packets()
assert [message] = messages
assert Buffer.empty?(buffer)
end
end
end

View file

@ -1,8 +1,4 @@
defmodule Circuits.UART.Framing.MIDITest do
use ExUnit.Case
doctest Circuits.UART.Framing.MIDI
test "greets the world" do
assert Circuits.UART.Framing.MIDI.hello() == :world
end
end