justinp.io projects about contact


Taptag is a Ruby gem for interacting with the Waveshare PN532 NFC HAT for Raspberry Pi. It provides a ruby wrapper around the provided C code, with easy mechanisms to identify tags, encrypt and encode information, and read and write data to the NFC tags.

Examples code be found in bin/example for basic tasks like uid, reading / writing tags, and handling encrypted and plaintext data written to tags. Also included are setup instructions for getting your Raspberry Pi ready to use the HAT and the Gem. You can find these in RASPI_SETUP.md, and the associated scripts in bin/pi. Finally, there are associated tests in the spec folder which can be run with the aid of a test runner at spec/test_runner.rb


Data preparation is done with the Encoder & Encrypter classes, which can be used to turn strings into organized groups of bytes ready for writing to card memory.

require 'taptag'

e = Taptag::Encrypter.encrypt('This is a top-secret message')
# {
#   :key=> "\xA4\xB2\vQe\xD6\xC3V\x16\xF6m\x12hU#\xA6",
#   :vector=> "\xB4\\\xEB\xFAg:\x94K\x19\xB1]\xD0H\x185>",
#   :data=> "\\_P\x94vc\xB0J\x9F\x90,\x7F\\wZ\xD0\xB8\x11\x94c\xEBe\xCE\x8E\xA0\x84\xAE7\xE3\xAB\xB3\x1C"
# }

# Pass in the key, vector, and data and decode
d = Taptag::Encrypter.decrypt(e) # => 'This is a top-secret message'

u = Taptag::Encoder['Plaintext Data']
# => [[1, [80, 108, 97, 105, 110, 116, 101, 120, 116, 32, 68, 97, 116, 97, 0, 0]]]

You can write out the data to a Mifare or Ntag card once it's been turned into bytes, storing it on the tag for later retrieval.

require 'taptag'
require 'taptag/nfc'

t = Taptag::Encoder["Tag, you're it!"]
# => [[1, [84, 97, 103, 44, 32, 121, 111, 117, 39, 114, 101, 32, 105, 116, 33, 0]]]

Taptag::NFC.write_mifare_card(t)
# or Taptag::NFC.write_ntag_card

# Since this is just one block we could also do
Taptag::NFC.write_mifare_block(1, t[0][1])

Reading from the card is also easy, and allows for reducing IO wait time by requesting only specific or single blocks if need be.

require 'taptag'
require 'taptag/nfc'

entire_card = Taptag::NFC.read_mifare_card
# => [[0, [...]]...,[63, [...]]]
block_range = Taptag::NFC.read_mifare_card(0..10)
# => [[0, [...]], [10, [...]]]
single_block = Taptag::NFC.read_mifare_block(6)
# => [0, 0, 0...]

Decoding and decrypting is also made simple, and supports extensible strategies for doing them

require 'taptag'
require 'taptag/nfc'

c = Taptag::NFC.read_mifare_card
# => [[0, [...]], [1, 84, 97, 105...]]

# Automatically converts 2d byte arrays into strings, removing the non-writing mifare blocks
Taptag::Encoder[c] # => "Tag, you're it!"

# Encrypted data...
str = 'Pemalite Crystal'

enc = Taptag::Encrypter.encrypt(str)

# We join the key, vector, and encrypted data into a single string and encode it.
# Since the key and vector are 16 bytes long, they become blocks 1 and 2
Taptag::NFC.write_mifare_card(Taptag::Encoder[enc.values.join])

# When we decode, we grab all the blocks
blocks = Taptag::NFC.read_mifare_card

dc = {}

# We only write to valid blocks thanks to the encoder
dc[:key] = blocks[1].last.map(&:chr).join
dc[:vector] = blocks[2].last.map(&:chr).join

# We can decode the string and then discard the key and vector bytes
dc[:data] = Taptag::Encoder[blocks][32..-1]

# And then decrypt our data
Taptag::Encrypter.decrypt(dc) # => 'Pemalite Crystal'

Underneath the easy to use ruby frontend, the Waveshare provided C library is accessed through the FFI gem, which is able to load the compiled libraries into the ruby execution stack. Once this is done, the relevant C functions can be attached to the PN532 module by method signature for later use in the internals of the NFC module.

  module PN532
    extend FFI::Library

   # Include system libraries
    ffi_lib 'c'
    ffi_lib_flags :now, :global
    ffi_lib 'wiringPi'

   # Include Waveshare libraries
    ffi_lib 'pn532'

    # Argument for ruby function name, C function name, C function arguments, return type
    # and whether the function should block in C space until completed. 
    attach_function :read_passive_target, :PN532_ReadPassiveTarget,
                    %i[pointer pointer uint8 uint32], :int, blocking: true

    # Pointer arguments are usually to one of the structs defined for device config or data buffering    
    attach_function :sam_configuration, :PN532_SamConfiguration,
                    [:pointer], :int, blocking: true
    attach_function :get_firmware_version, :PN532_GetFirmwareVersion,
                    %i[pointer pointer], :int, blocking: true
    attach_function :mifare_authenticate_block, :PN532_MifareClassicAuthenticateBlock,
                    %i[pointer pointer uint8 uint16 uint16 pointer], :int, blocking: true
    attach_function :mifare_read_block, :PN532_MifareClassicReadBlock,
                    %i[pointer pointer uint16], :int, blocking: true
    attach_function :mifare_write_block, :PN532_MifareClassicWriteBlock,
                    %i[pointer pointer uint16], :int, blocking: true
    attach_function :ntag_read_block, :PN532_Ntag2xxReadBlock,
                    %i[pointer pointer uint16], :int, blocking: true
    attach_function :ntag_write_block, :PN532_Ntag2xxWriteBlock,
                    %i[pointer pointer uint16], :int, blocking: true

    ffi_lib 'pn532_rpi'
    attach_function :spi_init, :PN532_SPI_Init,
                    [:pointer], :void, blocking: true
    attach_function :spi_wait_ready, :PN532_SPI_WaitReady,
                    [:uint32], :bool, blocking: true
  end