Move the bytewise DDL from Augie because it's probably useful for any chips with a small mailbox.

This commit is contained in:
James Harton 2020-01-12 20:58:09 +13:00
parent cac08bc5f0
commit 8b6211b606
6 changed files with 313 additions and 0 deletions

8
lib/wafer/dll/dll.ex Normal file
View file

@ -0,0 +1,8 @@
defmodule Wafer.DLL do
@moduledoc """
Implements a safe bytewise data-link-layer which allows the transmission and
receiption of arbitrary erlang terms in tiny packets.
See `Rx` and `Tx` for details.
"""
end

71
lib/wafer/dll/rx.ex Normal file
View file

@ -0,0 +1,71 @@
defmodule Wafer.DLL.Rx do
defstruct [:buffer, :state]
alias __MODULE__
@moduledoc """
Bytewise reception buffer for our data link layer.
"""
defguardp is_byte(byte) when is_integer(byte) and byte >= 0 and byte <= 255
@byte_start 0x7D
@byte_end 0x7E
@byte_esc 0x7F
@type t :: %Rx{buffer: binary, state: any}
@doc """
Initialize a new empty buffer reading for receiving.
"""
def init, do: %Rx{buffer: <<>>, state: :idle}
@doc """
Receive a byte into the buffer.
"""
@spec rx(t, byte) :: t
def rx(%Rx{state: state}, @byte_start) when state != :escaping,
do: %Rx{buffer: <<>>, state: :receiving}
def rx(%Rx{state: :idle} = rx, _byte), do: rx
def rx(%Rx{state: :receiving} = rx, @byte_esc), do: %{rx | state: :escaping}
def rx(%Rx{state: :receiving, buffer: <<crc::integer-size(32), buffer::binary>>}, @byte_end) do
if :erlang.crc32(buffer) == crc do
%Rx{buffer: :erlang.binary_to_term(buffer), state: :complete}
else
%Rx{buffer: buffer, state: {:error, :crc_mismatch}}
end
end
def rx(%Rx{state: :receiving} = rx, @byte_end), do: %{rx | state: {:error, :too_short}}
def rx(%Rx{state: :escaping, buffer: buffer}, byte) when is_byte(byte),
do: %Rx{state: :receiving, buffer: <<buffer::binary, byte::integer-size(8)>>}
def rx(%Rx{state: :receiving, buffer: buffer}, byte) when is_byte(byte),
do: %Rx{state: :receiving, buffer: <<buffer::binary, byte::integer-size(8)>>}
def rx(%Rx{state: :complete} = rx, _byte), do: rx
@doc """
Has the reception completed successfully?
"""
@spec complete?(t) :: boolean
def complete?(%Rx{state: :complete}), do: true
def complete?(%Rx{}), do: false
@doc """
Was there an error during reception?
"""
@spec error?(t) :: boolean
def error?(%Rx{state: {:error, _}}), do: true
def error?(%Rx{}), do: false
@doc """
Attempt to retrieve the received value from the buffer.
"""
@spec value(t) :: {:ok, any} | {:error, any}
def value(%Rx{state: :complete, buffer: buffer}), do: {:ok, buffer}
def value(%Rx{state: {:error, reason}}), do: {:error, reason}
def value(%Rx{}), do: {:error, :incomplete}
end

51
lib/wafer/dll/tx.ex Normal file
View file

@ -0,0 +1,51 @@
defmodule Wafer.DLL.Tx do
defstruct [:buffer, :state]
alias __MODULE__
@moduledoc """
A bytewise transmission buffer for our data link layer.
"""
@byte_start 0x7D
@byte_end 0x7E
@byte_esc 0x7F
@type t :: %Tx{buffer: binary, state: any}
@doc """
Initialize a new transmission buffer from a term.
"""
@spec init(any) :: t
def init(term) do
data = :erlang.term_to_binary(term)
crc = :erlang.crc32(data)
%Tx{buffer: <<crc::integer-size(32), data::binary>>, state: :idle}
end
@doc """
Get the next byte to transmit.
"""
@spec tx(t) :: {byte, t} | :done
def tx(%Tx{state: :idle} = tx), do: {@byte_start, %{tx | state: :transmitting}}
def tx(%Tx{state: :transmitting, buffer: <<@byte_start::integer-size(8), _::binary>>} = rx),
do: {@byte_esc, %{rx | state: :escaping}}
def tx(%Tx{state: :transmitting, buffer: <<@byte_end::integer-size(8), _::binary>>} = rx),
do: {@byte_esc, %{rx | state: :escaping}}
def tx(%Tx{state: :transmitting, buffer: <<@byte_esc::integer-size(8), _::binary>>} = rx),
do: {@byte_esc, %{rx | state: :escaping}}
def tx(%Tx{state: :transmitting, buffer: <<byte::integer-size(8), buffer::binary>>} = rx),
do: {byte, %{rx | state: :transmitting, buffer: buffer}}
def tx(%Tx{state: :escaping, buffer: <<byte::integer-size(8), buffer::binary>>} = rx),
do: {byte, %{rx | state: :escaping, buffer: buffer}}
def tx(%Tx{buffer: <<>>, state: :transmitting} = rx), do: {@byte_end, %{rx | state: :complete}}
def tx(%Tx{state: :complete}), do: :done
def complete?(%Tx{state: :complete}), do: true
def complete?(%Tx{}), do: false
end

121
test/dll/rx_test.exs Normal file
View file

@ -0,0 +1,121 @@
defmodule WaferDLLRxTest do
use ExUnit.Case, async: true
alias Wafer.DLL.Rx
test "ignores bytes until it receives the start byte" do
rx = Rx.init()
rx = Rx.rx(rx, 0)
assert rx.state == :idle
rx = Rx.rx(rx, 1)
assert rx.state == :idle
rx = Rx.rx(rx, 2)
assert rx.state == :idle
rx = Rx.rx(rx, 0x7D)
assert rx.state == :receiving
end
test "resynchronises on start byte" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0)
assert rx.buffer == <<0>>
rx = Rx.rx(rx, 0x7D)
assert rx.buffer == <<>>
end
test "handles escaped bytes" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0x7F)
|> Rx.rx(0)
assert rx.buffer == <<0>>
end
test "handles double escapes" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0x7F)
|> Rx.rx(0x7F)
assert rx.buffer == <<0x7F>>
end
test "handles escaped start bytes" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0x7F)
|> Rx.rx(0x7D)
assert rx.buffer == <<0x7D>>
end
test "handles escaped end bytes" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0x7F)
|> Rx.rx(0x7E)
assert rx.buffer == <<0x7E>>
end
test "receives bytes until the end" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(167)
|> Rx.rx(142)
|> Rx.rx(61)
|> Rx.rx(132)
|> Rx.rx(131)
|> Rx.rx(100)
|> Rx.rx(0)
|> Rx.rx(1)
|> Rx.rx(97)
|> Rx.rx(126)
assert rx.state == :complete
assert rx.buffer == :a
end
test "results in an error when the payload is too short" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(0)
|> Rx.rx(0x7E)
assert rx.state == {:error, :too_short}
end
test "ignores non-start bytes when the payload is complete" do
rx =
Rx.init()
|> Rx.rx(0x7D)
|> Rx.rx(167)
|> Rx.rx(142)
|> Rx.rx(61)
|> Rx.rx(132)
|> Rx.rx(131)
|> Rx.rx(100)
|> Rx.rx(0)
|> Rx.rx(1)
|> Rx.rx(97)
|> Rx.rx(126)
|> Rx.rx(0)
assert rx.state == :complete
assert rx.buffer == :a
end
end

34
test/dll/tx_test.exs Normal file
View file

@ -0,0 +1,34 @@
defmodule WaferDllTxTest do
use ExUnit.Case, async: true
alias Wafer.DLL.Tx
test "it sends the start byte first" do
bytes = collect(:test)
byte = List.first(bytes)
assert byte == 0x7D
end
test "it sends the end byte last" do
bytes = collect(:test)
byte = List.last(bytes)
assert byte == 0x7E
end
test "it includes a valid CRC" do
crc0 = :erlang.crc32(:erlang.term_to_binary(:test))
<<_start::integer-size(8), crc1::integer-size(32), _::binary>> =
:binary.list_to_bin(collect(:test))
assert crc0 == crc1
end
defp collect(term), do: collect(Tx.init(term), [])
defp collect(tx, bytes) do
case Tx.tx(tx) do
{byte, tx} -> collect(tx, [byte | bytes])
:done -> Enum.reverse(bytes)
end
end
end

28
test/dll_test.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule WaferDLLTest do
use ExUnit.Case, async: true
alias Wafer.DLL.{Rx, Tx}
test "synchronised transmission" do
value = {:marty, "McFly"}
tx = Tx.init(value)
rx = Rx.init()
{tx, rx} = transmit(tx, rx)
assert Tx.complete?(tx)
assert Rx.complete?(rx)
assert {:ok, value} = Rx.value(rx)
end
def transmit(%Tx{} = tx, %Rx{} = rx) do
case Tx.tx(tx) do
{byte, tx} ->
rx = Rx.rx(rx, byte)
transmit(tx, rx)
:done ->
{tx, rx}
end
end
end