Skip to content

Commit

Permalink
Cache requests to the ATECC508A
Browse files Browse the repository at this point in the history
This is a trivial cache that speeds up reads and the slow genkey call to
return the public key. It's invalidated by nearly everything. In
practice, though, normal operation is readonly and writes are only at
provisioning time.

Fixes #16
  • Loading branch information
fhunleth committed Feb 7, 2019
1 parent ff677c4 commit 6ae4035
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 22 deletions.
51 changes: 51 additions & 0 deletions lib/atecc508a/transport/cache.ex
Original file line number Diff line number Diff line change
@@ -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
69 changes: 47 additions & 22 deletions lib/atecc508a/transport/i2c_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 """
Expand All @@ -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
<<payload::binary-size(payload_length), crc::binary-size(2), _extra::binary>> =
Expand Down
53 changes: 53 additions & 0 deletions test/atecc508a/transport/cache_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6ae4035

Please sign in to comment.