mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-27 04:36:31 +00:00
Berry driver for PN532 NFC/Mifare reader (#22899)
This commit is contained in:
parent
60570dec76
commit
9bb7b7913a
@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- GPS driver select baudrate using GPIO GPS_RX1 (9600bps), GPS_RX2 (19200bps) or GPS_RX3 (38400bps) (#22869)
|
- GPS driver select baudrate using GPIO GPS_RX1 (9600bps), GPS_RX2 (19200bps) or GPS_RX3 (38400bps) (#22869)
|
||||||
- LVLG/HASPmota add color names from OpenHASP (#22879)
|
- LVLG/HASPmota add color names from OpenHASP (#22879)
|
||||||
- HASPmota support for `buttonmatrix` events
|
- HASPmota support for `buttonmatrix` events
|
||||||
|
- Berry driver for PN532 NFC/Mifare reader
|
||||||
|
|
||||||
### Breaking Changed
|
### Breaking Changed
|
||||||
|
|
||||||
|
430
tasmota/berry/drivers/PN532.be
Normal file
430
tasmota/berry/drivers/PN532.be
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
#-------------------------------------------------------------
|
||||||
|
- Driver for PN532 RFID chip connected in Serial mode
|
||||||
|
-------------------------------------------------------------#
|
||||||
|
|
||||||
|
#@ solidify:PN532
|
||||||
|
class PN532
|
||||||
|
var ser # serial object
|
||||||
|
var cmd # last cmd sent or received
|
||||||
|
var timer_clear_uid # time to clear UID
|
||||||
|
|
||||||
|
var uid # last UID read
|
||||||
|
var atqa #
|
||||||
|
|
||||||
|
static var _TIMER_CLEAR_UID = 4000 # time to clear UID, default 5 seconds
|
||||||
|
static var _TIMER_ID = "pn532_timer" # unique id for the timer
|
||||||
|
static var _NOUID = bytes("00000000")
|
||||||
|
static var _WAKEUP = bytes("5555000000")
|
||||||
|
static var _GETFIRMWAREVERSION = bytes("02") # PN532_COMMAND_GETFIRMWAREVERSION
|
||||||
|
static var _ACK = bytes("0000FF00FF00") # Ack message
|
||||||
|
static var _PRE = bytes("0000FF") # PN532_PREAMBLE + PN532_STARTCODE1 + PN532_STARTCODE2
|
||||||
|
static var _EMPTY = bytes("")
|
||||||
|
static var _CONFIG_RETRIESF = bytes("3205020100") # minimum retries
|
||||||
|
static var _SAMCONFIG = bytes("14011400")
|
||||||
|
static var _KEY_MIFARE = bytes("FFFFFFFFFFFF") # default encryption key for MIFARE
|
||||||
|
static var _SUCCESS = bytes("00")
|
||||||
|
static var _INRELEASE = bytes("5201") #
|
||||||
|
static var _AUTH = bytes("400160") # auth prefix
|
||||||
|
static var _READ = bytes("400130")
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# init(tx, rx)
|
||||||
|
#
|
||||||
|
# Initialize PN532 driver, with tx/rx GPIOs
|
||||||
|
def init(tx, rx)
|
||||||
|
if (tx == nil || rx == nil)
|
||||||
|
raise "value_error", "tx/rx cannot be nil"
|
||||||
|
end
|
||||||
|
self.uid = self._NOUID
|
||||||
|
self.timer_clear_uid = self._TIMER_CLEAR_UID
|
||||||
|
|
||||||
|
# import gpio
|
||||||
|
# gpio.pin_mode(tx, gpio.OUTPUT)
|
||||||
|
# gpio.pin_mode(rx, gpio.INPUT)
|
||||||
|
|
||||||
|
self.ser = serial(rx, tx, 115200)
|
||||||
|
log(f"NFC: PN532 Rx {rx} Tx {tx}", 3)
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
if self.ser
|
||||||
|
tasmota.add_driver(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def start()
|
||||||
|
try
|
||||||
|
# read firmware version
|
||||||
|
var b = self.command(self._GETFIRMWAREVERSION, 100)
|
||||||
|
log(f"NFC: GetFirmwareVersion '{b.tohex()}'", 3)
|
||||||
|
# bytes('32010607')
|
||||||
|
log(f"NFC: PN532 NFC Reader detected v{b[1]}.{b[2]}", 2)
|
||||||
|
|
||||||
|
# PN532_setPassiveActivationRetries(0xFF)
|
||||||
|
b = self.command(self._CONFIG_RETRIESF, 100)
|
||||||
|
# log(f"NFC: setPassiveActivationRetries '{b.tohex()}'", 3)
|
||||||
|
|
||||||
|
# PN532_SAMConfig()
|
||||||
|
self.command(self._SAMCONFIG, 100)
|
||||||
|
# log(f"NFC: SAMConfig '{b.tohex()}'", 3)
|
||||||
|
|
||||||
|
except "pn532" as _, m
|
||||||
|
log(f"NFC: error '{m}' more='{self.ser.read().tohex()}'", 2)
|
||||||
|
self.stop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop()
|
||||||
|
tasmota.remove_driver(self)
|
||||||
|
self.ser.close()
|
||||||
|
self.ser = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# command(payload : bytes [, no_response : bool, timeout_ms = 50])
|
||||||
|
#
|
||||||
|
# Send a command as a bytes buffer (first byte is command)
|
||||||
|
# Timeout in millisecond, defaut is 50 ms
|
||||||
|
#
|
||||||
|
# Returns a bytes() object as the response
|
||||||
|
# to the command (checks internally for Ack) or
|
||||||
|
# raise an exception of class 'pn532' if something went wrong
|
||||||
|
def command(payload, timeout_ms)
|
||||||
|
if !self.ser return nil end
|
||||||
|
if (payload == nil) payload = self._EMPTY end
|
||||||
|
if (timeout_ms == nil) timeout_ms = 50 end
|
||||||
|
if tasmota.loglevel(4)
|
||||||
|
log(f"NFC: sending command '{payload.tohex()}'", 4)
|
||||||
|
end
|
||||||
|
var ser = self.ser
|
||||||
|
var b
|
||||||
|
self.cmd = nil # reset cmd
|
||||||
|
|
||||||
|
b = bytes()
|
||||||
|
b.append(self._PRE)
|
||||||
|
var length = size(payload) + 1 # length of data field: TFI + DATA
|
||||||
|
b.add(length)
|
||||||
|
b.add(~length + 1) # checksum of length
|
||||||
|
b.add(0xD4) # PN532_HOSTTOPN532
|
||||||
|
var sum = 0xD4
|
||||||
|
if (size(payload) > 0) # write payload
|
||||||
|
self.cmd = payload[0]
|
||||||
|
b.append(payload)
|
||||||
|
var idx = 0
|
||||||
|
while idx < size(payload)
|
||||||
|
sum += payload[idx]
|
||||||
|
idx += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
b.add(~sum + 1) # checksum of TFI + DATA
|
||||||
|
b.add(0x00) # PN532_POSTAMBLE
|
||||||
|
if tasmota.loglevel(4)
|
||||||
|
log(f"NFC: sending raw '{b.tohex()}'", 4)
|
||||||
|
end
|
||||||
|
self._wakeup() # wakeup
|
||||||
|
ser.flush() # clear the serial buffer just in case
|
||||||
|
ser.write(b)
|
||||||
|
|
||||||
|
########
|
||||||
|
# check for Ack
|
||||||
|
var until_ms = tasmota.millis(10) # Ack max timeout is 10ms for 115200
|
||||||
|
b = self._read(size(self._ACK), until_ms)
|
||||||
|
if (b != self._ACK)
|
||||||
|
log(f"NFC: Invalid ACK '{b.tohex()}'", 3)
|
||||||
|
raise "pn532", "invalid_ack"
|
||||||
|
end
|
||||||
|
if tasmota.loglevel(4)
|
||||||
|
log(f"NFC: ACK received", 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
########
|
||||||
|
# read response
|
||||||
|
var ret = self._read_response(self.cmd, timeout_ms)
|
||||||
|
if tasmota.loglevel(4)
|
||||||
|
log(f"NFC: received response '{ret.tohex()}' cmd 0x{self.cmd:02X}", 4)
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
def _read_response(cmd_excpected, timeout_ms)
|
||||||
|
var until_ms = tasmota.millis(timeout_ms != nil ? timeout_ms : 50)
|
||||||
|
var b
|
||||||
|
|
||||||
|
b = self._read(3, until_ms)
|
||||||
|
# log(f"NFC: recv pre '{b.tohex()}'", 4)
|
||||||
|
if (b != self._PRE)
|
||||||
|
log(f"NFC: invalid prefix '{b.tohex()}' expected '{self._PRE.tohex()}'")
|
||||||
|
raise 'pn532', 'invalid_pre'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get length of data to be received
|
||||||
|
b = self._read(2, until_ms)
|
||||||
|
# log(f"NFC: recv len '{b.tohex()}'", 4)
|
||||||
|
if ((b[0] + b[1]) & 0xFF) != 0
|
||||||
|
log(f"NFC: invalid length '{b.tohex()}'")
|
||||||
|
raise 'pn532', 'invalid_len'
|
||||||
|
end
|
||||||
|
|
||||||
|
var len = b[0] - 2
|
||||||
|
|
||||||
|
# Get cmd
|
||||||
|
b = self._read(2, until_ms)
|
||||||
|
# log(f"NFC: recv cmd '{b.tohex()}'", 4)
|
||||||
|
var cmd = b[1]
|
||||||
|
if (b[0] != 0xD5 #-PN532_PN532TOHOST-#) || (cmd_excpected != nil && self.cmd + 1 != cmd)
|
||||||
|
log(f"NFC: invalid command '{b.tohex()}'")
|
||||||
|
raise 'pn532', 'invalid_cmd'
|
||||||
|
else
|
||||||
|
self.cmd = cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
var ret
|
||||||
|
if (len > 0)
|
||||||
|
ret = self._read(len, until_ms)
|
||||||
|
# log(f"NFC: recv ret '{ret.tohex()}'", 4)
|
||||||
|
else
|
||||||
|
ret = bytes()
|
||||||
|
end
|
||||||
|
|
||||||
|
# read checksum and end
|
||||||
|
b = self._read(2, until_ms)
|
||||||
|
# log(f"NFC: recv post '{b.tohex()}'", 4)
|
||||||
|
if (b[1] != 0)
|
||||||
|
log(f"NFC: invalid frame '{b.tohex()}'")
|
||||||
|
raise 'pn532', 'invalid_frame'
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# _read(len : int [, until_ms = now + 15])
|
||||||
|
#
|
||||||
|
# Internal function: read exactly `len` bytes or raise an exception if timeout
|
||||||
|
# Retuns `bytes()` buffer
|
||||||
|
#
|
||||||
|
# if `timeout_ms` is `0`, do a non-blocking read and return `nil` if response not available
|
||||||
|
# if `len` is `0`, get whatever is in the buffer (no limit) and return `nil` if empty
|
||||||
|
#
|
||||||
|
# Note: if `len > 0 && timeout_ms > 0` then it returns either `bytes()` or raises an exception
|
||||||
|
# does not return `nil` in such case
|
||||||
|
def _read(len, until_ms)
|
||||||
|
if (until_ms == nil) until_ms = tasmota.millis(15) end
|
||||||
|
|
||||||
|
if (len <= 0)
|
||||||
|
return self.ser.read()
|
||||||
|
else
|
||||||
|
while !tasmota.time_reached(until_ms)
|
||||||
|
if self.ser.available() >= len
|
||||||
|
return self.ser.read(len)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
raise "pn532", "timeout"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# wakeup
|
||||||
|
#
|
||||||
|
# Send a serial sequence to wake up the PN532
|
||||||
|
def _wakeup()
|
||||||
|
self.ser.write(self._WAKEUP) # send the wake up seuence
|
||||||
|
self.ser.flush() # remove any left-overs in buffers
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# set_timer_clear_uid(ms)
|
||||||
|
#
|
||||||
|
# Change the default value of the timer to clear UID, minimum is 1s
|
||||||
|
def set_timer_clear_uid(ms)
|
||||||
|
ms = tasmota.int(ms, 1000, nil) # ensure the value is int and within boundaries
|
||||||
|
self.timer_clear_uid = ms
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# read_passive_target_id(card_baud_rate)
|
||||||
|
#
|
||||||
|
# card_baud_rate:
|
||||||
|
# 0 = MIFARE_ISO14443A
|
||||||
|
def read_passive_target_id(card_baud_rate)
|
||||||
|
if !self.ser return nil end
|
||||||
|
if (card_baud_rate == nil) card_baud_rate = 0 #-MIFARE-# end
|
||||||
|
|
||||||
|
# first release whatever is on-going
|
||||||
|
self.release_card() # we don't care if there's an error
|
||||||
|
|
||||||
|
var cmd = bytes("4A01")
|
||||||
|
cmd.add(card_baud_rate)
|
||||||
|
try
|
||||||
|
var b = self.command(cmd, 25) # aggressive 25ms timeout since we don't want to stop Tasmota for too long - response is normally recevied in 15ms
|
||||||
|
log(f"NFC: ReadPassiveTargetID '{b.tohex()}'", 4)
|
||||||
|
|
||||||
|
if (b[0] > 0) && (b[1] == 1)
|
||||||
|
# b0 Tags Found
|
||||||
|
# b1 Tag Number (only one used in this example)
|
||||||
|
# b2..3 SENS_RES
|
||||||
|
# b4 SEL_RES
|
||||||
|
# b5 NFCID Length
|
||||||
|
# b6..NFCIDLen NFCID
|
||||||
|
|
||||||
|
# we got a card
|
||||||
|
self.atqa = b.get(2, -2) # read 16 bits, at position 2, Big Endian
|
||||||
|
var uid_len = b[5]
|
||||||
|
if (uid_len > 0)
|
||||||
|
self.uid = b[6 .. uid_len + 5]
|
||||||
|
if (tasmota.loglevel(3))
|
||||||
|
log(f"NFC: detected MIFARE UID '{self.uid.tohex()}'", 4)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.uid = self._NOUID
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
except 'pn532'
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# release_card()
|
||||||
|
#
|
||||||
|
# close connection to a card. Can be called if no connection is available
|
||||||
|
def release_card()
|
||||||
|
if !self.ser return nil end
|
||||||
|
self.command(self._INRELEASE)
|
||||||
|
# we don't really care about the result
|
||||||
|
# result is bytes('00') if successful or bytes('27') if no connection is ongoing
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# authenticate_card(card_baud_rate)
|
||||||
|
#
|
||||||
|
# authenticate to card:
|
||||||
|
# sector: sector number (0..63 for 1K, 0..255 for 4K)
|
||||||
|
# auth_B: 'false' = auth_A, 'true' = auth_B
|
||||||
|
# key: encryption key or bytes("FFFFFFFFFFFF") if 'nil'
|
||||||
|
#
|
||||||
|
# return 'true' if succesful
|
||||||
|
def authenticate_card(sector, auth_B, key)
|
||||||
|
if !self.ser return nil end
|
||||||
|
if (key == nil) key = self._KEY_MIFARE end
|
||||||
|
auth_B = bool(auth_B) # force boolean
|
||||||
|
var payload = self._AUTH.copy()
|
||||||
|
if (auth_B) payload[-1] += 1 end # increment last byte if auth_B
|
||||||
|
payload.add(sector)
|
||||||
|
payload.append(key)
|
||||||
|
payload.append(self.uid)
|
||||||
|
|
||||||
|
var ret = self.command(payload)
|
||||||
|
return ret == self._SUCCESS
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# reset_uid()
|
||||||
|
#
|
||||||
|
# Reset the card UID, so we generate a NEW_CARD event
|
||||||
|
def reset_uid()
|
||||||
|
self.uid = self._NOUID
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# read_card(sector)
|
||||||
|
#
|
||||||
|
# read a sector of 16 bytes (0..63 for 1K, 0..255 for 4K)
|
||||||
|
#
|
||||||
|
# return bytes(16) or nil
|
||||||
|
def read_card(sector)
|
||||||
|
if !self.ser return nil end
|
||||||
|
var payload = self._READ.copy()
|
||||||
|
payload.add(sector)
|
||||||
|
|
||||||
|
return self.command(payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# card_detected()
|
||||||
|
#
|
||||||
|
# Called whenever a card is detected on recurrent polling
|
||||||
|
def card_detected()
|
||||||
|
# override in your subclass
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# new_card_detected()
|
||||||
|
#
|
||||||
|
# Called whenever a new card is detected on recurrent polling (i.e. different UID)
|
||||||
|
def new_card_detected()
|
||||||
|
# override in your subclass
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# publish_card_detected(new : bool)
|
||||||
|
#
|
||||||
|
# Handle the event of a card detected
|
||||||
|
def publish_card_detected(new_card)
|
||||||
|
# we detected a card
|
||||||
|
if new_card
|
||||||
|
log(f"NFC: New card detected with UID {self.uid.tohex()}", 3)
|
||||||
|
self.new_card_detected()
|
||||||
|
var event_card = format('{"PN532":{"NEW_CARD":{"UID":"%s"}}}', self.uid.tohex())
|
||||||
|
tasmota.publish_rule(event_card)
|
||||||
|
else
|
||||||
|
self.card_detected()
|
||||||
|
end
|
||||||
|
# reset timer
|
||||||
|
# remove any previous timer
|
||||||
|
tasmota.remove_timer(self._TIMER_ID)
|
||||||
|
tasmota.set_timer(self.timer_clear_uid, def () self.reset_uid() end, self._TIMER_ID)
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# every_250ms()
|
||||||
|
#
|
||||||
|
# Probe card 4 times per second
|
||||||
|
def every_250ms()
|
||||||
|
if !self.ser return nil end
|
||||||
|
var old_uid = self.uid
|
||||||
|
if self.read_passive_target_id(0)
|
||||||
|
self.publish_card_detected(old_uid != self.uid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# web_sensor()
|
||||||
|
#
|
||||||
|
# Display UID on main page
|
||||||
|
def web_sensor()
|
||||||
|
if !self.ser return nil end
|
||||||
|
var msg = format("{s}PN532 UID{m}%s{e}", self.uid.tohex())
|
||||||
|
tasmota.web_send(msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
return PN532
|
||||||
|
|
||||||
|
#-
|
||||||
|
# Example
|
||||||
|
|
||||||
|
import PN532
|
||||||
|
|
||||||
|
class CardReader : PN532
|
||||||
|
def init(tx, rx)
|
||||||
|
super(self).init(tx, rx)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_card_detected()
|
||||||
|
# authenticate to sector 4, auth_A, default key
|
||||||
|
if self.authenticate_card(4, false, nil)
|
||||||
|
var ret = self.read_card(4)
|
||||||
|
if (ret != nil)
|
||||||
|
# do your stuff
|
||||||
|
print(f"NEW CARD with content {ret[12..15].tohex()}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pn532 = CardReader(22, 23)
|
||||||
|
-#
|
Loading…
x
Reference in New Issue
Block a user