diff --git a/lib/atecc508a/transport/cache.ex b/lib/atecc508a/transport/cache.ex new file mode 100644 index 0000000..eb88f37 --- /dev/null +++ b/lib/atecc508a/transport/cache.ex @@ -0,0 +1,51 @@ +defmodule ATECC508A.Transport.Cache do + @moduledoc """ + Simple cache for reducing unnecessary traffic to the ATECC508A + """ + + @type state() :: map() + + @atecc508a_op_read 0x02 + @atecc508a_op_genkey 0x40 + @atecc508a_op_random 0x1B + + @doc """ + Initialize the cache for one ATECC508A + """ + @spec init() :: state() + def init() do + %{} + end + + @doc """ + Check if the specified request is in the cache + """ + @spec get(state(), binary()) :: binary() | nil + def get(cache, request) do + Map.get(cache, request) + end + + @doc """ + Save a response back to the cache + """ + @spec put(state, binary(), any()) :: state() + + # Cache all reads + def put(cache, <<@atecc508a_op_read, _::binary>> = request, response) do + Map.put(cache, request, response) + end + + # Cache the response to getting a public key + def put(cache, <<@atecc508a_op_genkey, 0, _key_id::little-16>> = request, response) do + Map.put(cache, request, response) + end + + # Don't cache random numbers + def put(cache, <<@atecc508a_op_random, _::binary>>, _response), do: cache + + # Flush the cache on everything else: + # writes, locks, etc. + # + # This is overkill, but safe. + def put(_cache, _request, _response), do: %{} +end diff --git a/lib/atecc508a/transport/i2c_server.ex b/lib/atecc508a/transport/i2c_server.ex index e800a9a..6792e86 100644 --- a/lib/atecc508a/transport/i2c_server.ex +++ b/lib/atecc508a/transport/i2c_server.ex @@ -2,6 +2,8 @@ defmodule ATECC508A.Transport.I2CServer do use GenServer require Logger + alias ATECC508A.Transport.Cache + @moduledoc false # 1.5 ms in the datasheet @@ -35,7 +37,7 @@ defmodule ATECC508A.Transport.I2CServer do def init([bus_name, address]) do {:ok, i2c} = Circuits.I2C.open(bus_name) - state = %{i2c: i2c, address: address} + state = %{i2c: i2c, address: address, cache: Cache.init()} {:ok, state, {:continue, :start_asleep}} end @@ -64,28 +66,20 @@ defmodule ATECC508A.Transport.I2CServer do @impl true def handle_call({:request, payload, timeout, response_payload_len}, _from, state) do - to_send = package(payload) - response_len = response_payload_len + 3 - - rc = - with :ok <- wakeup(state.i2c, state.address), - :ok <- Circuits.I2C.write(state.i2c, state.address, to_send), - Process.sleep(timeout), - {:ok, response} <- Circuits.I2C.read(state.i2c, state.address, response_len) do - unpackage(response) - else - error -> - Logger.error( - "ATECC508A: Request failed. #{inspect(to_send, binaries: :as_binaries)}, #{timeout} ms" - ) - - error - end + case Cache.get(state.cache, payload) do + nil -> + case make_request(state, payload, timeout, response_payload_len) do + {:ok, _message} = rc -> + new_cache = Cache.put(state.cache, payload, rc) + {:reply, rc, %{state | cache: new_cache}} + + error -> + {:reply, error, state} + end - # Always send a sleep after a request even if it fails so that the processor is in - # a known state for the next call. - sleep(state.i2c, state.address) - {:reply, rc, state} + response -> + {:reply, response, state} + end end @doc """ @@ -112,6 +106,37 @@ defmodule ATECC508A.Transport.I2CServer do end end + defp make_request(state, payload, timeout, response_payload_len) do + to_send = package(payload) + response_len = response_payload_len + 3 + + min_timeout = round(timeout / 2) + + rc = + with :ok <- wakeup(state.i2c, state.address), + :ok <- Circuits.I2C.write(state.i2c, state.address, to_send), + Process.sleep(min_timeout), + {:ok, response} <- + poll_read(state.i2c, state.address, response_len, min_timeout, timeout) do + unpackage(response) + else + error -> + Logger.error( + "ATECC508A: Request failed: #{inspect(to_send, binaries: :as_binaries)}, #{timeout} ms -> #{ + inspect(error) + }" + ) + + error + end + + # Always send a sleep after a request even if it fails so that the processor is in + # a known state for the next call. + sleep(state.i2c, state.address) + + rc + end + defp extract_payload(payload_length, payload_and_crc) do try do <> = diff --git a/test/atecc508a/transport/cache_test.exs b/test/atecc508a/transport/cache_test.exs new file mode 100644 index 0000000..10ea999 --- /dev/null +++ b/test/atecc508a/transport/cache_test.exs @@ -0,0 +1,53 @@ +defmodule ATECC508A.Transport.CacheTest do + use ExUnit.Case + + alias ATECC508A.Transport.Cache + + test "caches reads" do + req = <<2, 0, 0, 0>> + resp = {:ok, <<1>>} + + cache = Cache.init() + assert Cache.get(cache, req) == nil + + cache = Cache.put(cache, req, resp) + assert Cache.get(cache, req) == resp + end + + test "cache genkey public key calculation " do + req = <<0x40, 0, 1, 0>> + resp = {:ok, <<1, 2, 3, 4, 5>>} + + cache = Cache.init() + assert Cache.get(cache, req) == nil + + cache = Cache.put(cache, req, resp) + assert Cache.get(cache, req) == resp + end + + test "doesn't cache genkey creation" do + req = <<0x40, 1, 1, 0>> + resp = {:ok, <<1, 2, 3, 4, 5>>} + + cache = Cache.init() + assert Cache.get(cache, req) == nil + + cache = Cache.put(cache, req, resp) + assert Cache.get(cache, req) == nil + end + + test "writes clear the cache" do + read_req = <<2, 0, 0, 0>> + write_req = <<0x12, 0, 0, 0>> + resp = {:ok, <<1>>} + + cache = Cache.init() + assert Cache.get(cache, read_req) == nil + + cache = Cache.put(cache, read_req, resp) + assert Cache.get(cache, read_req) == resp + + cache = Cache.put(cache, write_req, resp) + assert cache == %{} + end +end