~ PySerial 库与 NFC 技术尝试 ~

一、概述

  1. PySerial
    • Python 中有一个进行串口通讯的包,叫做 PySerial 就是字面上的意思 “Python 串口”
    • 利用这个库可以非常方便地实现电脑与外部串口设备的通讯,不管你是连接设备还是开发智能硬件,都可以非常方便地使用 Python 写出一个上位机程序
  2. NFC
    • IC 卡 ID 卡等卡片均适用 RFID 技术,现在喜欢称为 NFC,随着智能手机越来越多的开始搭载 NFC 功能,此项技术必然会普及开来,毕竟相比二维码,我是更加喜欢 NFC 技术的,所以了解一下 NFC 也是一件有意义的事情
    • 其实我们的饭卡就是使用此技术的
    • 安卓的智能手机直接有软件提供 NFC 卡片的读取
    • iOS 11 中,苹果也为 iPhone 7 以上设备开放了 Core NFC API
  3. 除了平时使用 IC 卡作为饭卡之外,我们可以只是把它作为一个信息存储工具使用,这样,我们就可以把 IC 卡变成一张明信片
  4. 本程序运行于 Mac OS X 10.13,使用淘宝上二十来块钱的 NFC 模块,PN532 芯片

二、知识准备 *

  1. PySerial 的基本操作
    1. 导入库 import serial
    2. 创建一个串口连接的对象 s s = serial.Serial
    3. 成员函数说明:
    s.write(bytes) # void 写入一个 byte 串
    s.inWaiting() # int 返回存储在寄存器中的字节数
    s.read(int) # bytes 读取寄存器中的接下来 int 个字节
    
    s.read(self.inWaiting()) # 这样就可以读取全部
    
  2. 数据的处理
    1. 因为发送、接受的都是二进制数据,我们需要在发送之前对数据进行处理,为此需要导入包 import binascii
    2. 发送数据:
      • s.write(bytes.fromhex('00 00 FF 03 FD D4 14 01 17 00'))
    3. 接受数据:
      • data = str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
      • 此时 data 的数据为字符串 "0000ff00ff00" 这样的格式
  3. IC 卡的存储结构( M1卡 )

    M1卡分为16个扇区,每个扇区由4块(块0、块1、块2、块3)组成,(我们也将16个扇区的64个块按绝对地址编号为0~63。

  4. PN532 芯片通讯协议
    • 详见芯片手册
    • 程序按照 PN532 模块的通讯指令与其通讯

三、概要设计

  1. 模块规划
    1. NFC 基本操作的模块
    class NFC(serial.Serial):
        def wakeUP(self): # 唤醒模块
        def findCard(self): # 寻卡
        def auth(self): # 卡认证
        def readBlock(self): # 读取某一扇区的某一块
        def readDoc(self): # 读取全部扇区
        def writeBlock(self): # 写某一扇区的某一块
        def writeDoc(self): # 写全部扇区
        def cmd(self): # 发送指令
        def cmd_dcs(self): # 发送带 DCS 的指令
        def addDCS(self): # 在指令尾添加 DCS
        def hexAddDCS(self): 
    
    1. UI 模块
  2. UI 设计
    1. 实现一个简明的交互界面

四、用户手册

  1. 主程序界面如下:

    Alt text

  2. 灰色的按钮不可点击

  3. 输入正确的串口地址后,“连接” 按钮会被点亮

    • 串口地址:
      • Windows:COMx 如:COM1, COM2
      • macOS: /dev/cn.x
      • Linux: /dev/ttyx
  4. 连接成功之后,“读取” 和 “写入” 会被点亮,即可进行操作

  5. 读取卡之后,“保存到文件” 会被点亮,可将文本保存至文件

五、详细设计

  1. UI 模块的设计

    1. 文件名称 UI.ui
    2. 设计工具
      • QTDesigner
    3. 效果图
      • 见用户手册
    4. 把 .ui 文件转换为 .py 代码:
    5. 在终端使用命令 python3 -m PyQt5.uic.pyuic UI.ui -o ui.py -x
  2. NFC 基本操作的模块

class NFC(serial.Serial):
    def __init__(self, *args):
        super(serial.Serial, self).__init__(*args)
    def wakeUP(self):
        c = bytes.fromhex('55 55 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 03 FD D4 14 01 17 00')
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            if data == '0000ff00ff000000ff02fed5151600':
                print('Waked!')
                return True
            else:
                return False

    def findCard(self):
        c = bytes.fromhex('00 00 ff 04 fc d4 4a 02 00 e0 00')
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            print("FIND: " + data )
            if data != '0000ff00ff00':
                self.uid = data[-12:-10] + ' ' + data[-10:-8] + ' ' + data[-8:-6] + ' ' + data[-6:-4]
                print('UID: ' + self.uid)
                return True
            time.sleep(0.1)
        return False

    def auth(self, card = '01', block = '07', pwd = 'FF FF FF FF FF FF', v = True):
        c = bytes.fromhex(self.addDCS('00 00 FF 0F F1 D4 40 %s 60 %s %s %s'%(card, block, pwd, self.uid)))
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            if v:
                print("AUTHED: " + data )
            time.sleep(0.1)
            return True

    def readBlock(self, block, pwd = 'FF FF FF FF FF FF', card = '01'):
        block = str('%.2x'%block)
        while self.wakeUP() == False:
            print('Waking....')
        while(self.findCard() == False):
            time.sleep(1)
        self.auth(card = card, block = block, pwd = pwd)
        c = bytes.fromhex(self.addDCS('00 00 FF 05 Fb D4 40 %s 30 %s'%(card, block)))
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            print(data)
            print('DATA: ' + data[28:-4].upper())
            time.sleep(0.1)
    
    def readDoc(self, pwd = 'FF FF FF FF FF FF', card = '01'):
        while self.wakeUP() == False:
            print('Waking....')
        while(self.findCard() == False):
            time.sleep(1)
        print('Reading....DATA:')
        doc = ''
        for i in range(64):
            if (i+1)%4 == 0 or i == 0:
                continue
            block = str('%.2x'%i)
            self.auth(card = card, block = block, pwd = pwd, v = False)
            c = bytes.fromhex(self.addDCS('00 00 FF 05 Fb D4 40 %s 30 %s'%(card, block)))
            self.write(c)
            while self.inWaiting()==0:
                pass
            flag = False
            while self.inWaiting():
                data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
                if('980604' in data):
                    flag = True
                    break
                doc+=data[28:-4].upper()
            if flag:
                break
        hs = ''
        for i in range(len(doc)):
            if i%2 == 0 and i != 0:
                hs += ' '
            hs += doc[i]
        return bytes.fromhex(hs)

    def writeBlock(self, block, doc, pwd = 'FF FF FF FF FF FF', card = '01'):
        block = str('%.2x'%block)
        while self.wakeUP() == False:
            print('Waking....')

        while(self.findCard() == False):
            time.sleep(1)

        self.auth(card = card, block = block, pwd = pwd)
        
        c = bytes.fromhex(self.addDCS('00 00 FF 15 eb D4 40 %s A0 %s %s'%(card, block, doc)))
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            print(data)
            time.sleep(0.1)

    def writeDoc(self, string, pwd = 'FF FF FF FF FF FF', card = '01', encoding = 'utf8'):
        doc = str(string).encode(encoding)
        flag = 0
        while len(doc) > 752:
            flag = 1
            e = int(len(string) - (len(doc)-752)/(len(doc)/len(string))) - 1
            string = string[0:e]
            doc = str(string).encode(encoding)
        if flag == 1:
            print("Document Cut")
            print(string)

        bu = bytes.fromhex('00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00')

        if(len(doc)%16 != 0):
            doc += bu[len(doc)%16:]

        doclist = []
        for i in range(int(len(doc)/16)+1):
            doclist += [doc[i*16:(i+1)*16]]
        doclist = doclist[:-1]
        if (len(doclist) < 47):
            doclist += [bytes.fromhex('98 06 04 00 00 00 00 00 00 00 00 00 00 00 00 00')]
        while self.wakeUP() == False:
            print('Waking....')

        while(self.findCard() == False):
            time.sleep(1)
        print('Writing....Data....')
        j = 0
        for i in doclist:
            if (j+1)%4 == 0 or j == 0:
                j+=1
            block = str('%.2x'%j)
            self.auth(card = card, block = block, pwd = pwd, v = False)
            docx = str(i)[2:-1].replace('\\x',' ')
            c = self.hexAddDCS(bytes.fromhex('00 00 FF 15 eb d4 40 %s A0 %s'%(card, block)) + i)
            # print(c)
            self.write(c)
            while self.inWaiting()==0:
                pass
            while self.inWaiting():
                data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            j+=1
        print('DONE!')


    def cmd(self, hexcmd):
        c = bytes.fromhex(hexcmd)
        hexcmd = '00 00 ff ' + '%.2x '%len(c) + '%.2x '%(0x100 - len(c)) + hexcmd
        finalcmd = self.addDCS(hexcmd)
        print(finalcmd)
        c = bytes.fromhex(finalcmd)
        self.write(c)
        while self.inWaiting()==0:
            pass
        while self.inWaiting():
            data= str(binascii.b2a_hex(self.read(self.inWaiting())))[2:-1]
            print('OUT:')
            print(data)
            print(data[28:-4])
            time.sleep(0.1)

    def cmd_dcs(self, hexcmd):
        sum = 0
        b = bytes.fromhex(hexcmd)
        for i in b:
            sum += i
        DCS = str(hex(0xff - (0xff & sum)))[2:] + ' 00'
        c = hexcmd + ' ' + DCS
        print("CMD:" + c)
        return self.cmd(c)
    def addDCS(self, hexcmd):
        sum = 0
        b = bytes.fromhex(hexcmd)
        for i in b:
            sum += i
        DCS = str(hex(0xff - (0xff & sum)))[2:] + ' 00'
        c = hexcmd + ' ' + DCS
        return c
    def hexAddDCS(self, hexcmd):
        sum = 0
        b = hexcmd
        for i in b:
            sum += i
        DCS = ('%.2x'%(0xff - (0xff & sum))) + ' 00'
        c = hexcmd + bytes.fromhex(DCS)
        return c

  1. 使用 UI 模块派生的子类,完成程序操作的功能
class MainUI(Ui_MainWindow):
    def __init__(self, *args):
        super(Ui_MainWindow, self).__init__(*args)
        self.MainWindow = QtWidgets.QMainWindow()
        self.setupUi(self.MainWindow)
        self.connModify()
        # logo init
        scene = QtWidgets.QGraphicsScene()
        image = QtGui.QImage()
        image.load('logo.png')
        scene.addPixmap(QtGui.QPixmap().fromImage(image).scaled(self.logoView.width(), self.logoView.height()))
        self.logoView.setScene(scene)
        # self.textEdit.setText('lalalalala')
        self.readButton.setDisabled(True)
        self.connButton.setDisabled(True)
        self.writeButton.setDisabled(True)
        self.saveButton.setDisabled(True)
        self.connButton.setText('连 接')
    def connModify(self):
        self.devButton.clicked.connect(lambda: self.setPort())
        self.connButton.clicked.connect(lambda: self.connDev())
        self.readButton.clicked.connect(lambda: self.readDoc())
        self.writeButton.clicked.connect(lambda: self.writeDoc())
        self.saveButton.clicked.connect(lambda: self.saveDoc())
    def setPort(self):
        self.port = self.devEdit.text()
        print(self.port)
        self.connButton.setDisabled(False)
        self.connButton.setText('连 接')
        self.devButton.setDisabled(True)
        self.devEdit.setDisabled(True)
        self.devEdit.setText('已设定: ' + self.port)
    def connDev(self):
        if self.connButton.text() == '连 接':
            try:
                self.nfc = NFC(self.port,115200)
                self.connButton.setText('断 开')
                self.readButton.setDisabled(False)
                self.writeButton.setDisabled(False)
            except:
                self.textEdit.setText('端口无法连接,请重新设定!')
                self.connButton.setDisabled(True)
                self.devButton.setDisabled(False)
                self.devEdit.setDisabled(False)
                self.devEdit.setText(self.port)
        else:
            self.nfc.close()
            self.connButton.setText('连 接')
            self.readButton.setDisabled(True)
            self.writeButton.setDisabled(True)
    def readDoc(self, encoding = 'utf8'):
        self.textEdit.setText('正 在 读 取... 请 稍 候...')
        try:
            d = self.nfc.readDoc()
            self.textEdit.setText(d.decode(encoding))
            self.saveButton.setDisabled(False)
        except:
            self.textEdit.setText('读 取 错 误 请 重 试 !\n请 确 认 你 读 取 的 是 正 确 的 明 信 片 !')
    def writeDoc(self, encoding = 'utf8'):
        try:
            self.nfc.writeDoc(self.textEdit.toPlainText(), encoding = encoding)
            self.textEdit.setText('写 入 成 功 !')
        except:
            self.textEdit.setText('写 入 时 发 生 错 误,请 重 试 !')
    def saveDoc(self):
        f = open(self.txtEdit.text(),'w', encoding='utf8')
        f.write('%s'%str(self.textEdit.toPlainText()))
        f.close()
        self.saveButton.setDisabled(True)

  1. 主程序调用演示
app = QtWidgets.QApplication(sys.argv)
ui = MainUI()
ui.MainWindow.show()
sys.exit(app.exec_())

六、附录

  • PN532 芯片数据手册下载地址:
    • https://wenku.baidu.com/view/f695b781680203d8ce2f247f.html
  • PN532 模块手册下载地址:
    • https://pan.baidu.com/s/1pJOgAuv