Skip to content

Elixir library to work with the NXP PN532 RFID module

License

Notifications You must be signed in to change notification settings

jmerriweather/pn532

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PN532

Hex pm

Hardware

Any PN532 board should work as long as it supports UART.

Installation

If available in Hex, the package can be installed by adding pn532 to your list of dependencies in mix.exs:

def deps do
  [
    {:pn532, "~> 0.1.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/pn532.

How to use

Create Card Handler

defmodule CardService.CardHandler do
  use PN532.Handler

  # In the setup handler you can get things ready, I use this to load a
  # custom access key into the database if one doesn't exist
  def setup(_state) do

    # Check if access key is store in database
    with {:ok, access_key} <- CardService.get_access_key(:key_a) do
      access_key
    else
      {:error, :empty} ->
        # If no access key in database, get the one from the applicaiton config
        card_service_config = Application.get_env(:card_service, :config)
        with {:ok, secret} <- Keyword.get(card_service_config, :secret) |> Base.decode64() do

          # Save access key into the database
          CardService.set_access_key(:key_a, secret)
        end
      {:error, error} ->
        throw(error)
    end
  end

  # The connected handler function runs when your application is connected to the PN532
  def connected(connect_info) do

    # Get information about the PN532
    with %{port: port, firmware_version: %{version: version, revision: revision, ic_version: ic_version}} <- connect_info do
      Logger.info("Connected on port #{inspect port} with firmware version #{version}.#{revision}, IC version #{ic_version}")
    end

    # Begin target detection, this will poll the PN532 for cards
    :ok = PN532.Client.start_target_detection()
  end

  # Handle card detected event
  def handle_event(:cards_detected, cards, client, data) do
    # Make sure PN532 is awake
    new_power_mode = client.wakeup(data)

    # Get access key from database
    {:ok, key_a} = CardService.get_access_key(:key_a)

    Logger.info("About to use key_a #{inspect key_a}")
    # This will call out to a function to attempt to authenticate the card using the access key,
    # otherwise will try the default keys. I'll document this module next.
    detected_cards = CardService.CardDetector.detect_cards(client, data, cards, key_a)

    Logger.info("decoded: #{inspect detected_cards}")
    # Log each card detected
    ids = for %{ nfcid: identifier, type: type } <- detected_cards do
      "#{inspect type} card with ID: #{inspect Base.encode16(identifier)}"
    end

    # Check if any of the cards are authenticated
    authenticated = Enum.any?(detected_cards, fn card -> card.authenticated == :success end)

    # If any of the cards are authenticated
    if authenticated do
      # Do something, in my case i'm unlocking a door
      DoorService.unlock()
    end

    {:noreply, %{data | detected_cards: detected_cards, connection_options: %{data.connection_options | power_mode: new_power_mode}}}
  end

  # Handle card lost event
  def handle_event(:cards_lost, lost_cards, _client, data) do

    # log cards no longer detected by the PN532
    ids = for %{nfcid: identifier, type: type} <- lost_cards do
      "#{inspect type} card with ID: #{inspect Base.encode16(identifier)}"
    end

    Logger.info("Lost connection with #{Enum.join(ids, " and ")}")

    {:noreply, data}
  end
end

Create card detector

defmodule CardService.CardDetector do
  require Logger

  # this is the default access key for mifare
  @default_keys [<<0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF>>]

  # iterate over the detected cards
  def detect_cards(client, data, cards, key) do
    detect_cards(client, data, cards, key, [])
  end

  # We only care about mifare cards which have a tg and nfcid field
  def detect_cards(client, data, [%{tg: _target_number, nfcid: _identifier} = card | rest], key, acc) do
    # attempt to authenticate the card
    with {:ok, authenticated_card, new_client, new_data} <- authenticate_card(client, data, card, key) do
      # Accumulate our detected cards with the authenticated result
      detect_cards(new_client, new_data, rest, key, [authenticated_card | acc])
    else
      {:error, message, result, new_client, new_data} ->
        Logger.error("Error occurred authenticating card: #{inspect message}")
        detect_cards(new_client, new_data, rest, key, [result | acc])
    end
  end

  def detect_cards(client, data, [unsupported | rest], key, acc) when is_map(unsupported) do
    detect_cards(client, data, rest, key, acc)
  end

  def detect_cards(_, _, _, _, acc) do
    acc
  end

  # Attempt to authenticate card
  def authenticate_card(client, data, %{tg: target_number, nfcid: identifier} = card, key) do
    Logger.info("About to try authenticate key: #{inspect key}")
    # To re-authenticate a card we need to first deselect then select
    with :ok <- client.deselect(data, target_number),
         :ok <- client.select(data, target_number),
         :ok <- client.authenticate(data, target_number, 1, :key_a, key, identifier) do
      result =
        # Once we have successfully authenticated the card, get the card token stored locally
        with {:load_secure_code, {:ok, user_token}} <- {:load_secure_code, CardService.get_card_token(identifier)},
              # Get the token stored on the card
             {:read_secure_code, {:ok, secure_code}} <- {:read_secure_code, client.read(data, target_number, 1)},
              # Compare the token stored locally and the token stored on the card
             {:verify_secure_code, {true, ^user_token, ^secure_code}} <- {:verify_secure_code, {user_token === secure_code, user_token, secure_code}} do

        # Update the card with the fact that it is authenticated, store the authenticated key used and
        # the token that was stored on the card
        card = card
        |> Map.put(:authenticated, :success)
        |> Map.put(:key, key)
        |> Map.put(:access_code, secure_code)

        # Get user associated with the card and add the user to the card
        with {:ok, user} <- CardService.get_card_user(identifier) do
          Map.put(card, :user, user)
        else
          _ ->
            card
        end
      else
        {:load_secure_code, error} ->
          Logger.error("Error occurred loading secure code: #{inspect error}")
          card
          |> Map.put(:authenticated, :failure)
          |> Map.put(:key, key)
          |> Map.put(:error, error)
        {:read_secure_code, error} ->
          Logger.error("Error occurred reading secure code on card: #{inspect error}")
          card
          |> Map.put(:authenticated, :failure)
          |> Map.put(:key, key)
          |> Map.put(:error, error)
        {:verify_secure_code, {result, user_token, secure_code}} ->
          Logger.error("Secure Codes do not match #{inspect user_token} != #{inspect secure_code}")
          card
          |> Map.put(:authenticated, :failure)
          |> Map.put(:key, key)
          |> Map.put(:error, :secure_code_invalid)
        {:error, error} ->
          Logger.error("Error occurred reading access code: #{inspect error}")
          card
          |> Map.put(:authenticated, :failure)
          |> Map.put(:error, error)
      end

      authenticate_card(result, client, data)
    else
      # If mifare authentication failed using the stored key, use the default mifare key
      {:error, {:mifare_authentication_error, _}} ->
        authenticate_card_defaults(client, data, card, @default_keys)
      {:error, error} ->
        Logger.error("Error occurred authenticating card: #{inspect error}")
        card
        |> Map.put(:authenticated, :failure)
        |> Map.put(:error, error)
        |> authenticate_card(client, data)
    end
  end

  defp authenticate_card(%{error: message} = result, client, data) do
    {:error, message, result, client, data}
  end

  defp authenticate_card(result, client, data) do
    {:ok, result, client, data}
  end

  # Authenticate using default key
  defp authenticate_card_defaults(client, data, %{tg: target_number, nfcid: identifier} = card, [first_key | rest]) do
    Logger.info("About to try default authenticate key: #{inspect first_key}")
    with  :ok <- client.deselect(data, target_number),
          :ok <- client.select(data, target_number),
          :ok <- client.authenticate(data, target_number, 1, :key_a, first_key, identifier) do
      card
      |> Map.put(:authenticated, :default)
      |> Map.put(:key, first_key)
      |> authenticate_card(client, data)
    else
      {:error, {:mifare_authentication_error, _}} ->
        authenticate_card_defaults(client, data, card, rest)
      {:error, error} ->
        Logger.error("Error occurred authenticating card: #{inspect error}")
        card
        |> Map.put(:authenticated, :failure)
        |> Map.put(:error, error)
        |> authenticate_card(client, data)
    end
  end

  # ignore cards we are not interested in
  defp authenticate_card_defaults(client, data, card, []) do
    card
    |> Map.put(:authenticated, :failure)
    |> Map.put(:error, :unknown_key_a)
    |> authenticate_card(client, data)
  end
end

Add configuration to your config.exs, if using Nerves you'll want to put different config for host or target

In target.exs you could have the following

config :card_service, :config,
  uart_port: "/dev/ttyAMA0"

In a Linux host you might have the following in your host.exs:

config :card_service, :config,
  uart_port: "/dev/ttyS7"

In a Windows host you might have the following in your host.exs:

config :card_service, :config,
  uart_port: "COM2"

Add PN532 Supervisor to you application Supervisor

defmodule CardService.Supervisor do
  use Supervisor
  require Logger

  def start_link(args) do
    Logger.info("about to start #{inspect __MODULE__}")
    Supervisor.start_link(__MODULE__, [args], name: __MODULE__)
  end

  def init([args]) do
    card_service_config = Application.get_env(:card_service, :config)
    uart_port = Keyword.get(card_service_config, :uart_port)
    uart_speed = Keyword.get(card_service_config, :uart_speed)

    children = [
      {PN532.Supervisor, [%{target_type: :iso_14443_type_a, handler: CardService.CardHandler, uart_port: uart_port, uart_speed: uart_speed}]}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

About

Elixir library to work with the NXP PN532 RFID module

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages