{"id":18226787,"url":"https://github.com/jmerriweather/pn532","last_synced_at":"2025-04-08T05:49:18.346Z","repository":{"id":57535342,"uuid":"227694685","full_name":"jmerriweather/pn532","owner":"jmerriweather","description":"Elixir library to work with the NXP PN532 RFID module","archived":false,"fork":false,"pushed_at":"2021-07-24T03:34:19.000Z","size":7331,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-04T22:51:45.556Z","etag":null,"topics":["elixir","nxp","pn532"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jmerriweather.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-12T20:54:29.000Z","updated_at":"2024-10-25T11:24:23.000Z","dependencies_parsed_at":"2022-08-29T00:30:20.146Z","dependency_job_id":null,"html_url":"https://github.com/jmerriweather/pn532","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmerriweather%2Fpn532","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmerriweather%2Fpn532/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmerriweather%2Fpn532/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmerriweather%2Fpn532/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jmerriweather","download_url":"https://codeload.github.com/jmerriweather/pn532/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247785927,"owners_count":20995644,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","nxp","pn532"],"created_at":"2024-11-04T05:03:29.648Z","updated_at":"2025-04-08T05:49:18.326Z","avatar_url":"https://github.com/jmerriweather.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PN532\r\n\r\n[![Hex pm](http://img.shields.io/hexpm/v/pn532.svg?style=flat)](https://hex.pm/packages/pn532)\r\n\r\n## Hardware\r\n\r\nAny PN532 board should work as long as it supports UART.\r\n\r\n## Installation\r\n\r\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\r\nby adding `pn532` to your list of dependencies in `mix.exs`:\r\n\r\n```elixir\r\ndef deps do\r\n  [\r\n    {:pn532, \"~\u003e 0.1.0\"}\r\n  ]\r\nend\r\n```\r\n\r\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\r\nand published on [HexDocs](https://hexdocs.pm). Once published, the docs can\r\nbe found at [https://hexdocs.pm/pn532](https://hexdocs.pm/pn532).\r\n\r\n## How to use\r\n\r\n### Create Card Handler\r\n\r\n```elixir\r\ndefmodule CardService.CardHandler do\r\n  use PN532.Handler\r\n\r\n  # In the setup handler you can get things ready, I use this to load a\r\n  # custom access key into the database if one doesn't exist\r\n  def setup(_state) do\r\n\r\n    # Check if access key is store in database\r\n    with {:ok, access_key} \u003c- CardService.get_access_key(:key_a) do\r\n      access_key\r\n    else\r\n      {:error, :empty} -\u003e\r\n        # If no access key in database, get the one from the applicaiton config\r\n        card_service_config = Application.get_env(:card_service, :config)\r\n        with {:ok, secret} \u003c- Keyword.get(card_service_config, :secret) |\u003e Base.decode64() do\r\n\r\n          # Save access key into the database\r\n          CardService.set_access_key(:key_a, secret)\r\n        end\r\n      {:error, error} -\u003e\r\n        throw(error)\r\n    end\r\n  end\r\n\r\n  # The connected handler function runs when your application is connected to the PN532\r\n  def connected(connect_info) do\r\n\r\n    # Get information about the PN532\r\n    with %{port: port, firmware_version: %{version: version, revision: revision, ic_version: ic_version}} \u003c- connect_info do\r\n      Logger.info(\"Connected on port #{inspect port} with firmware version #{version}.#{revision}, IC version #{ic_version}\")\r\n    end\r\n\r\n    # Begin target detection, this will poll the PN532 for cards\r\n    :ok = PN532.Client.start_target_detection()\r\n  end\r\n\r\n  # Handle card detected event\r\n  def handle_event(:cards_detected, cards, client, data) do\r\n    # Make sure PN532 is awake\r\n    new_power_mode = client.wakeup(data)\r\n\r\n    # Get access key from database\r\n    {:ok, key_a} = CardService.get_access_key(:key_a)\r\n\r\n    Logger.info(\"About to use key_a #{inspect key_a}\")\r\n    # This will call out to a function to attempt to authenticate the card using the access key,\r\n    # otherwise will try the default keys. I'll document this module next.\r\n    detected_cards = CardService.CardDetector.detect_cards(client, data, cards, key_a)\r\n\r\n    Logger.info(\"decoded: #{inspect detected_cards}\")\r\n    # Log each card detected\r\n    ids = for %{ nfcid: identifier, type: type } \u003c- detected_cards do\r\n      \"#{inspect type} card with ID: #{inspect Base.encode16(identifier)}\"\r\n    end\r\n\r\n    # Check if any of the cards are authenticated\r\n    authenticated = Enum.any?(detected_cards, fn card -\u003e card.authenticated == :success end)\r\n\r\n    # If any of the cards are authenticated\r\n    if authenticated do\r\n      # Do something, in my case i'm unlocking a door\r\n      DoorService.unlock()\r\n    end\r\n\r\n    {:noreply, %{data | detected_cards: detected_cards, connection_options: %{data.connection_options | power_mode: new_power_mode}}}\r\n  end\r\n\r\n  # Handle card lost event\r\n  def handle_event(:cards_lost, lost_cards, _client, data) do\r\n\r\n    # log cards no longer detected by the PN532\r\n    ids = for %{nfcid: identifier, type: type} \u003c- lost_cards do\r\n      \"#{inspect type} card with ID: #{inspect Base.encode16(identifier)}\"\r\n    end\r\n\r\n    Logger.info(\"Lost connection with #{Enum.join(ids, \" and \")}\")\r\n\r\n    {:noreply, data}\r\n  end\r\nend\r\n```\r\n\r\n## Create card detector\r\n\r\n```elixir\r\ndefmodule CardService.CardDetector do\r\n  require Logger\r\n\r\n  # this is the default access key for mifare\r\n  @default_keys [\u003c\u003c0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF\u003e\u003e]\r\n\r\n  # iterate over the detected cards\r\n  def detect_cards(client, data, cards, key) do\r\n    detect_cards(client, data, cards, key, [])\r\n  end\r\n\r\n  # We only care about mifare cards which have a tg and nfcid field\r\n  def detect_cards(client, data, [%{tg: _target_number, nfcid: _identifier} = card | rest], key, acc) do\r\n    # attempt to authenticate the card\r\n    with {:ok, authenticated_card, new_client, new_data} \u003c- authenticate_card(client, data, card, key) do\r\n      # Accumulate our detected cards with the authenticated result\r\n      detect_cards(new_client, new_data, rest, key, [authenticated_card | acc])\r\n    else\r\n      {:error, message, result, new_client, new_data} -\u003e\r\n        Logger.error(\"Error occurred authenticating card: #{inspect message}\")\r\n        detect_cards(new_client, new_data, rest, key, [result | acc])\r\n    end\r\n  end\r\n\r\n  def detect_cards(client, data, [unsupported | rest], key, acc) when is_map(unsupported) do\r\n    detect_cards(client, data, rest, key, acc)\r\n  end\r\n\r\n  def detect_cards(_, _, _, _, acc) do\r\n    acc\r\n  end\r\n\r\n  # Attempt to authenticate card\r\n  def authenticate_card(client, data, %{tg: target_number, nfcid: identifier} = card, key) do\r\n    Logger.info(\"About to try authenticate key: #{inspect key}\")\r\n    # To re-authenticate a card we need to first deselect then select\r\n    with :ok \u003c- client.deselect(data, target_number),\r\n         :ok \u003c- client.select(data, target_number),\r\n         :ok \u003c- client.authenticate(data, target_number, 1, :key_a, key, identifier) do\r\n      result =\r\n        # Once we have successfully authenticated the card, get the card token stored locally\r\n        with {:load_secure_code, {:ok, user_token}} \u003c- {:load_secure_code, CardService.get_card_token(identifier)},\r\n              # Get the token stored on the card\r\n             {:read_secure_code, {:ok, secure_code}} \u003c- {:read_secure_code, client.read(data, target_number, 1)},\r\n              # Compare the token stored locally and the token stored on the card\r\n             {:verify_secure_code, {true, ^user_token, ^secure_code}} \u003c- {:verify_secure_code, {user_token === secure_code, user_token, secure_code}} do\r\n\r\n        # Update the card with the fact that it is authenticated, store the authenticated key used and\r\n        # the token that was stored on the card\r\n        card = card\r\n        |\u003e Map.put(:authenticated, :success)\r\n        |\u003e Map.put(:key, key)\r\n        |\u003e Map.put(:access_code, secure_code)\r\n\r\n        # Get user associated with the card and add the user to the card\r\n        with {:ok, user} \u003c- CardService.get_card_user(identifier) do\r\n          Map.put(card, :user, user)\r\n        else\r\n          _ -\u003e\r\n            card\r\n        end\r\n      else\r\n        {:load_secure_code, error} -\u003e\r\n          Logger.error(\"Error occurred loading secure code: #{inspect error}\")\r\n          card\r\n          |\u003e Map.put(:authenticated, :failure)\r\n          |\u003e Map.put(:key, key)\r\n          |\u003e Map.put(:error, error)\r\n        {:read_secure_code, error} -\u003e\r\n          Logger.error(\"Error occurred reading secure code on card: #{inspect error}\")\r\n          card\r\n          |\u003e Map.put(:authenticated, :failure)\r\n          |\u003e Map.put(:key, key)\r\n          |\u003e Map.put(:error, error)\r\n        {:verify_secure_code, {result, user_token, secure_code}} -\u003e\r\n          Logger.error(\"Secure Codes do not match #{inspect user_token} != #{inspect secure_code}\")\r\n          card\r\n          |\u003e Map.put(:authenticated, :failure)\r\n          |\u003e Map.put(:key, key)\r\n          |\u003e Map.put(:error, :secure_code_invalid)\r\n        {:error, error} -\u003e\r\n          Logger.error(\"Error occurred reading access code: #{inspect error}\")\r\n          card\r\n          |\u003e Map.put(:authenticated, :failure)\r\n          |\u003e Map.put(:error, error)\r\n      end\r\n\r\n      authenticate_card(result, client, data)\r\n    else\r\n      # If mifare authentication failed using the stored key, use the default mifare key\r\n      {:error, {:mifare_authentication_error, _}} -\u003e\r\n        authenticate_card_defaults(client, data, card, @default_keys)\r\n      {:error, error} -\u003e\r\n        Logger.error(\"Error occurred authenticating card: #{inspect error}\")\r\n        card\r\n        |\u003e Map.put(:authenticated, :failure)\r\n        |\u003e Map.put(:error, error)\r\n        |\u003e authenticate_card(client, data)\r\n    end\r\n  end\r\n\r\n  defp authenticate_card(%{error: message} = result, client, data) do\r\n    {:error, message, result, client, data}\r\n  end\r\n\r\n  defp authenticate_card(result, client, data) do\r\n    {:ok, result, client, data}\r\n  end\r\n\r\n  # Authenticate using default key\r\n  defp authenticate_card_defaults(client, data, %{tg: target_number, nfcid: identifier} = card, [first_key | rest]) do\r\n    Logger.info(\"About to try default authenticate key: #{inspect first_key}\")\r\n    with  :ok \u003c- client.deselect(data, target_number),\r\n          :ok \u003c- client.select(data, target_number),\r\n          :ok \u003c- client.authenticate(data, target_number, 1, :key_a, first_key, identifier) do\r\n      card\r\n      |\u003e Map.put(:authenticated, :default)\r\n      |\u003e Map.put(:key, first_key)\r\n      |\u003e authenticate_card(client, data)\r\n    else\r\n      {:error, {:mifare_authentication_error, _}} -\u003e\r\n        authenticate_card_defaults(client, data, card, rest)\r\n      {:error, error} -\u003e\r\n        Logger.error(\"Error occurred authenticating card: #{inspect error}\")\r\n        card\r\n        |\u003e Map.put(:authenticated, :failure)\r\n        |\u003e Map.put(:error, error)\r\n        |\u003e authenticate_card(client, data)\r\n    end\r\n  end\r\n\r\n  # ignore cards we are not interested in\r\n  defp authenticate_card_defaults(client, data, card, []) do\r\n    card\r\n    |\u003e Map.put(:authenticated, :failure)\r\n    |\u003e Map.put(:error, :unknown_key_a)\r\n    |\u003e authenticate_card(client, data)\r\n  end\r\nend\r\n```\r\n\r\n### Add configuration to your config.exs, if using Nerves you'll want to put different config for host or target\r\n\r\nIn target.exs you could have the following\r\n\r\n```elixir\r\nconfig :card_service, :config,\r\n  uart_port: \"/dev/ttyAMA0\"\r\n```\r\n\r\nIn a Linux host you might have the following in your host.exs:\r\n\r\n```elixir\r\nconfig :card_service, :config,\r\n  uart_port: \"/dev/ttyS7\"\r\n```\r\n\r\nIn a Windows host you might have the following in your host.exs:\r\n\r\n```elixir\r\nconfig :card_service, :config,\r\n  uart_port: \"COM2\"\r\n```\r\n\r\n### Add PN532 Supervisor to you application Supervisor\r\n\r\n```elixir\r\ndefmodule CardService.Supervisor do\r\n  use Supervisor\r\n  require Logger\r\n\r\n  def start_link(args) do\r\n    Logger.info(\"about to start #{inspect __MODULE__}\")\r\n    Supervisor.start_link(__MODULE__, [args], name: __MODULE__)\r\n  end\r\n\r\n  def init([args]) do\r\n    card_service_config = Application.get_env(:card_service, :config)\r\n    uart_port = Keyword.get(card_service_config, :uart_port)\r\n    uart_speed = Keyword.get(card_service_config, :uart_speed)\r\n\r\n    children = [\r\n      {PN532.Supervisor, [%{target_type: :iso_14443_type_a, handler: CardService.CardHandler, uart_port: uart_port, uart_speed: uart_speed}]}\r\n    ]\r\n\r\n    Supervisor.init(children, strategy: :one_for_one)\r\n  end\r\nend\r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmerriweather%2Fpn532","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjmerriweather%2Fpn532","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmerriweather%2Fpn532/lists"}