# SPDX-License-Identifier: Apache-2.0 defmodule LibNFC.Mock do @moduledoc """ NFC device emulation for testing purposes without physical interaction with a NFC device. This module allows to emulate the presence of NFC targets in range of a "mock" device. `LibNFC.Mock` is a GenServer (to be spawned within a supervision tree) that holds the status of NFC targets in range. Functions on `LibNFC` accepting a `:mock` parameter instead of the NFC device reference will fetch data from this server instead of actually talking to libnfc. """ @moduledoc since: "0.1.0" use GenServer @nif_delay_ms 400 @nif_delay_jitter 0.5 @type modulation :: atom @type target_info :: %{required(String.t()) => binary} @type target :: %{ required(:modulation) => modulation, required(:target_info) => target_info } @typedoc """ Server options * `nif_delay` emulates delay of NIF calls (default: #{@nif_delay_ms}) * `nif_delay_jitter` jitter factor for delay (default: #{@nif_delay_jitter}) """ @type server_options :: [ {:nif_delay, non_neg_integer | false} | {:nif_delay_jitter, non_neg_integer} ] @doc false @spec child_spec(server_options) :: Supervisor.child_spec() def child_spec(opts) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [opts]} } end @doc """ Starts the mock process. """ @doc since: "0.1.0" @spec start_link() :: GenServer.on_start() @spec start_link(server_options) :: GenServer.on_start() def start_link(opts \\ []) when is_list(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Puts a NFC target in range. """ @doc since: "0.1.0" @spec put_target() :: :ok @spec put_target(target_info) :: :ok @spec put_target(target_info, modulation) :: :ok def put_target(target_info \\ %{"uid" => :rand.bytes(4)}, modulation \\ :iso14443a) def put_target(target_info, modulation) when is_binary(target_info) do put_target(%{"uid" => target_info}, modulation) end def put_target(target_info, modulation) when is_map(target_info) do GenServer.call(__MODULE__, {:put_target, {target_info, modulation}}) end @doc """ Removes the NFC target. """ @doc since: "0.1.0" @spec clear_target() :: :ok def clear_target do GenServer.call(__MODULE__, :clear_target) end @doc false @spec initiator_select_passive_target(modulation) :: {:ok, target_info} | nil def initiator_select_passive_target(modulation) do GenServer.call(__MODULE__, {:select_target, modulation}) end @doc false @spec initiator_target_is_present() :: integer() def initiator_target_is_present do GenServer.call(__MODULE__, :target_is_present) end @impl GenServer def init(opts) do {:ok, %{target: nil, selected: false, opts: opts}} end @impl GenServer def handle_call(msg, _from, %{opts: opts} = state) do {emulated_nif_call?, reply, new_state} = case {msg, state} do {{:put_target, {target_info, modulation}}, state} -> {false, :ok, %{state | target: %{target_info: target_info, modulation: modulation}}} {:clear_target, state} -> {false, :ok, %{state | target: nil}} {{:select_target, nm}, %{target: %{modulation: nm, target_info: tai}}} -> {true, tai, %{state | selected: true}} {{:select_target, _nm}, _nil_or_different_nm} -> {true, nil, state} {:target_is_present, %{target: target, selected: true}} when not is_nil(target) -> {true, 0, state} {:target_is_present, %{target: nil, selected: true}} -> {true, -2, %{state | selected: false}} {:target_is_present, _not_selected} -> {true, -10, state} end if emulated_nif_call? do emulate_nif_delay(opts) end {:reply, reply, new_state} end defp emulate_nif_delay(opts) do if nif_delay_ms = Keyword.get(opts, :nif_delay, @nif_delay_ms) do nif_delay_jitter = Keyword.get(opts, :nif_delay_jitter, @nif_delay_jitter) nif_delay_jitter = trunc(nif_delay_ms * nif_delay_jitter) Process.sleep(nif_delay_ms + :rand.uniform(nif_delay_jitter) - div(nif_delay_jitter, 2)) end end end