Android NFC开发详解 总结和NFC读卡实例解析
创始人
2025-01-09 04:36:21
0

文章目录

  • 前言
  • 一、什么是NFC?
  • 二、基础知识
    • 1.什么是NDEF?
    • 2.NFC技术的操作模式
    • 3.标签的技术类型
    • 4.实现方式的分类
    • 5.流程
  • 三、获取标签内容
    • 1.检查环境
    • 2.获取NFC标签
      • 2.1 Manifest中注册的方式获取Tag
      • 2.1 前台Activity捕获的方式获取Tag
  • 四、解析标签数据
    • 1. M1卡解析
    • 2. iso15693卡解析
  • 总结


前言

物联网企业Android软件层开发肯定少不了与硬件通信这一步,不论是物流、安防、医疗、教育等行业,其中除去直接调用SDK这种方式,与硬件通信直接相关的技术也不过是最常用的那几种,分别为串口通信、USB通讯、蓝牙、红外、NFC等。

在前面的文章[串口通信开发总结和实例解析、USB通信开发总结和热敏打印机开发实例解析]中已经分别介绍了串口、USB通讯技术。本篇记录的就是NFC开发相关的技术和走过的弯路,希望在帮助自己巩固知识的同时可以帮助到有需要的人。

除了开发总结外,本篇的案例为NFC读取M1卡。
其实读卡这样的需求,我们研发一般分为调用SDK,和用android自带的NFC功能了,
前者没什么好说的,剩下的NFC,进入正题。


一、什么是NFC?

NFC是目前Android手机一个主流的配置硬件项,全称是Near Field Communication,中为近场通信,也叫做近距离无线通信技术。使用了NFC技术的设备(例如移动电话)可以在彼此靠近的情况下进行数据交换,是由非接触式射频识别(RFID)及互连互通技术整合演变而来。

二、基础知识

开始开发之前必须要知道的知识

1.什么是NDEF?

存储在NFC标签中的数据可以采用多种格式编写,但许多 Android 框架 API 都基于名为 NDEF(NFC 数据交换格式)的 NFC Forum 标准。。

简单说就是一种普遍的数据格式标准

2.NFC技术的操作模式

(1) 读取器/写入器模式:支持 NFC 设备读取和/或写入被动 NFC 标签和贴纸。
(2)点对点模式:支持 NFC 设备与其他 NFC 对等设备交换数据;Android Beam 使用的就是此操作模式。
(3)卡模拟模式:支持 NFC 设备本身充当 NFC 卡。然后,可以通过外部 NFC 读取器(例如 NFC 销售终端)访问模拟 NFC 卡。

本篇案例使用的主要是读写卡,就是正常的读写卡需求,后面如果有机会接触到点对点和卡模拟的需求会在此篇做补充

3.标签的技术类型

通常情况下每种分类的标签(卡片)都支持一种或多重技术,
对应关系如下

技术描述卡种
NfcA提供NFC-A(ISO 14443-3A)的性能和I / O操作的访问。M1卡
NfcB提供NFC-B (ISO 14443-3B)的性能和I / O操作的访问。
NfcF提供 NFC-F (JIS 6319-4)的性能和I / O操作的访问。
NfcV提供 NFC-V (ISO 15693)的性能和I / O操作的访问。15693卡
IsoDep提供 ISO-DEP (ISO 14443-4)的性能和I / O操作的访问。CPU卡
Ndef提供NFC标签已被格式化为NDEF的数据和操作的访问。
NdefFormatable提供可能被格式化为NDEF的 formattable的标签。
MifareClassic如果此Android设备支持MIFARE,提供访问的MIFARE Classic性能和I / O操作。m1卡
MifareUltralight如果此Android设备支持MIFARE,提供访问的MIFARE 超轻性能和I / O操作。

如下图,这是Demo 显示得NFC标签的信息。
其中被我圈起来的部分是这个NFC标签支持的技术,这些后面解析数据的时候会用到,得到这些后就可以使用对应的类来解析标签数据。
在这里插入图片描述
开发中我们有对应的方法来获取此标签支持的解析方式,后面我会介绍。

4.实现方式的分类

(1)Manifest注册方式:这种方式主要是在Manifest文件对应的activity下,配置过滤器,以响应不同类型NFC Action。使用这种方式,在刷卡时,如果手机中有多个应用都存在该NFC实现方案,系统会弹出能响应NFC事件的应用列表供用户选择,用户需要点击目标应用来响应本次NFC刷卡事件。

(2)前台响应方式,无需Manifest重配置过滤器,直接使用前台activity来捕获NFC事件进行响应。

区别如下:
响应方式不同:Manifest注册的NFC事件由系统分发,需要选择应用去响应事件
       前台响应方式由前台activity来捕获NFC事件进行响应
优先级不同:前台响应方式的优先级更高于Manifest注册的方式
     (如果安装多个Manifest注册的的App 和一个处于前台捕获方式的App,刷卡后 优先级最高的为前台捕获的,如果前台相应方式的App没有打开,那么将弹出列表让用户选择Manifest中注册了的符合条件的App)

第一种更适合APP需要刷卡调用起来,并且设备没有多个响应NFC标签程序的物联网设备(因为普通安卓手机中自带的卡包APP、微信等优先级都比较高,当弹出列表选择响应的App时,操作会边得繁琐)

第二种更适合前台界面中的读卡,且多个应用的时候
根据自己的项目需求选择适合的实现方式。

5.流程

首先设备要支持NFC权限开启的前提下 不论哪种方式,都是先刷卡,等待系统分发响应的Activity 拿到Tag或者 前台Activity捕获到TAG 。然后根据这个标签支持的技术去解析数据。

三、获取标签内容

1.检查环境

首先 Manifest中添加权限

     

判断是否支持NFC、且打开功能

 NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);         if (null == adapter) {             Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show();         } else if (!adapter.isEnabled()) {             Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);             // 根据包名打开对应的设置界面             startActivity(intent);         }    

2.获取NFC标签

2.1 Manifest中注册的方式获取Tag

这里要介绍三种意图过滤器
前面【实现方式的分类】中对这种方式的特征做了介绍,这种由标签调度系统分发的方式需要在Manifest定义固定的意图过滤器。标签调度系统定义了三种 Intent,按优先级从高到低列出如下:

ACTION_NDEF_DISCOVERED:如果扫描到包含 NDEF 负载的标签,并且可识别其类型,则使用此 Intent 启动 Activity。这是优先级最高的 Intent,标签调度系统会尽可能尝试使用此 Intent 启动 Activity,在行不通时才会尝试使用其他 Intent。

ACTION_TECH_DISCOVERED:如果没有登记要处理 ACTION_NDEF_DISCOVERED Intent 的 Activity,则标签调度系统会尝试使用此 Intent 来启动应用。此外,如果扫描到的标签包含无法映射到 MIME 类型或 URI 的 NDEF 数据,或者该标签不包含 NDEF 数据,但它使用了已知的标签技术,那么也会直接启动此 Intent(无需先启动 ACTION_NDEF_DISCOVERED)。

ACTION_TAG_DISCOVERED:如果没有处理 ACTION_NDEF_DISCOVERED 或者 ACTION_TECH_DISCOVERED Intent 的 Activity,则使用此 Intent 启动 Activity。

添加意图过滤器
这是第一种 最简单和优先级最高的一种,已经满足需求了

                                                             

当然也可以选择第二种

                                                                          

filter_nfc
这个文件就是TECH_DISCOVERED需要配置的,其中,tech-list之间是逻辑或关系,tech之间是逻辑与关系,与方案2中的techLists原理以及用途是类似。

                 android.nfc.tech.Ndef         android.nfc.tech.NfcA                     android.nfc.tech.NfcB                   android.nfc.tech.NfcF       

还剩最后一种

                                                                              

这种一般用不到 感觉意义不大

然后在对应Activity的onCreate方法中就可以拿标签了

class NfcActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_nfc)         val adapter = NfcAdapter.getDefaultAdapter(this)         if (null == adapter) {             Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show()         } else if (!adapter.isEnabled) {             val intent = Intent(Settings.ACTION_NFC_SETTINGS)             // 根据包名打开对应的设置界面             startActivity(intent)         }         val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)     } } 

2.1 前台Activity捕获的方式获取Tag

class MainActivity : AppCompatActivity() {     var mNfcAdapter: NfcAdapter? = null     var pIntent: PendingIntent? = null      override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        initNfc()        }     private fun initNfc() {         mNfcAdapter = M1CardUtils.isNfcAble(this)         pIntent = PendingIntent.getActivity(this, 0,           //在Manifest里或者这里设置当前activity启动模式,否则每次响应NFC事件,activity会重复创建         Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)     }          override fun onResume() {         super.onResume()         mNfcAdapter?.let {             val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)             val tag = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)             val tech = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)             val filters = arrayOf(ndef, tag, tech)             val techList = arrayOf(                 arrayOf(                     "android.nfc.tech.Ndef",                     "android.nfc.tech.NfcA",                     "android.nfc.tech.NfcB",                     "android.nfc.tech.NfcF",                     "android.nfc.tech.NfcV",                     "android.nfc.tech.NdefFormatable",                     "android.nfc.tech.MifareClassic",                     "android.nfc.tech.MifareUltralight",                     "android.nfc.tech.NfcBarcode"                 )             )             it.enableForegroundDispatch(this, pIntent, filters, techList)             XLog.d("开始捕获NFC数据")         }     }     override fun onPause() {         super.onPause()         mNfcAdapter?.disableForegroundDispatch(this)     }     override fun onNewIntent(intent: Intent?) {         super.onNewIntent(intent)         //这里必须setIntent,set  NFC事件响应后的intent才能拿到数据         setIntent(intent)         val tag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG)         //M1CardUtils 我后面会贴出来的         if (M1CardUtils.isMifareClassic(tag)) {             try {                 val reader = M1CardUtils.readCard(tag)                 XLog.d("读卡内容:$reader")                 val data = reader.split("|")             } catch (e: IOException) {                 e.printStackTrace()             }         }     } } 

四、解析标签数据

不论使用哪种方式,当我们获取到TAG标签后,解析方式都是相同的,需要根据不同的卡类型选择对应的解析方式
在这里插入图片描述
如图 我们能拿到卡片的信息,如图,括起来的部分分别对应的是:
支持的技术类型
MifareClassic 类型
扇区存储空间
扇区数
扇区中的块数

1. M1卡解析

这里说一下基础知识,不论是NFC还是读卡模块读,解析流程都是先寻卡,然后验证扇区的密码,取扇区的数据,比如已知要读的数据在2扇区,那么寻卡后验证时把要验证的扇区号、扇区的密码,和扇区的验证密码类型A/B传过去验证通过后,就可以读取数据了。

 import android.app.Activity import android.nfc.NfcAdapter import android.nfc.Tag import com.hjq.toast.ToastUtils import kotlin.Throws import android.nfc.tech.MifareClassic import com.elvishew.xlog.XLog import java.io.IOException import java.lang.StringBuilder import java.nio.charset.Charset   object M1CardUtils {     /**      * 判断是否支持NFC      *      * @return      */     fun isNfcAble(mContext: Activity?): NfcAdapter? {         val mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext)         if (mNfcAdapter == null) {             ToastUtils.show("设备不支持NFC!")         }         if (!mNfcAdapter!!.isEnabled) {             ToastUtils.show("请在系统设置中先启用NFC功能!")         }         return mNfcAdapter     }      /**      * 监测是否支持MifareClassic      *      * @param tag      * @return      */     fun isMifareClassic(tag: Tag): Boolean {         val techList = tag.techList         var haveMifareUltralight = false         for (tech in techList) {             if (tech.contains("MifareClassic")) {                 haveMifareUltralight = true                 break             }         }         if (!haveMifareUltralight) {             ToastUtils.show("不支持MifareClassic")             return false         }         return true     }      /**      * 读取卡片信息      *      * @return      */     @Throws(IOException::class)     fun readCard(tag: Tag?): String {         val mifareClassic = MifareClassic.get(tag)         return try {             mifareClassic.connect()             val metaInfo = StringBuilder()             val gbk = Charset.forName("gbk")              // 获取TAG中包含的扇区数             val sectorCount = mifareClassic.sectorCount             //            for (int j = 0; j < sectorCount; j++) {             val bCount: Int //当前扇区的块数             var bIndex: Int //当前扇区第一块             if (m1Auth(mifareClassic, 2)) {                 bCount = mifareClassic.getBlockCountInSector(2)                 bIndex = mifareClassic.sectorToBlock(2)                 var length = 0                 for (i in 0 until bCount) {                     val data = mifareClassic.readBlock(bIndex)                     for (i1 in data.indices) {                         if (data[i1] == 0.toByte()) {                             length = i1                         }                     }                     val dataString = String(data, 0, length, gbk).trim { it <= ' ' }                     metaInfo.append(dataString)                     bIndex++                 }             } else {                 XLog.e("密码校验失败")             }             //            }             metaInfo.toString()         } catch (e: IOException) {             throw IOException(e)         } finally {             try {                 mifareClassic.close()             } catch (e: IOException) {                 throw IOException(e)             }         }     }      /**      * 改写数据      *      * @param block      * @param blockbyte      */     @Throws(IOException::class)     fun writeBlock(tag: Tag?, block: Int, blockbyte: ByteArray?): Boolean {         val mifareClassic = MifareClassic.get(tag)         try {             mifareClassic.connect()             if (m1Auth(mifareClassic, block / 4)) {                 mifareClassic.writeBlock(block, blockbyte)                 XLog.e("writeBlock", "写入成功")             } else {                 XLog.e("密码是", "没有找到密码")                 return false             }         } catch (e: IOException) {             throw IOException(e)         } finally {             try {                 mifareClassic.close()             } catch (e: IOException) {                 throw IOException(e)             }         }         return true     }      /**      * 密码校验      *      * @param mTag      * @param position      * @return      * @throws IOException      */     @Throws(IOException::class)     fun m1Auth(mTag: MifareClassic, position: Int): Boolean {         if (mTag.authenticateSectorWithKeyA(position, MifareClassic.KEY_DEFAULT)) {             return true         } else if (mTag.authenticateSectorWithKeyB(position, MifareClassic.KEY_DEFAULT)) {             return true         }         return false     }   } 

2. iso15693卡解析

本案例中没有用到这种,只是需要M1所以不需要这个,这是别的大佬封装的类发出来供参考

import android.nfc.tech.NfcV;   import com.haiheng.core.util.ByteUtils;   import java.io.IOException;   /**  * NfcV(ISO 15693)读写操作  *   用法  *  NfcV mNfcV = NfcV.get(tag);  *  mNfcV.connect();  * 

* NfcVUtils mNfcVutil = new NfcVUtils(mNfcV); * 取得UID * mNfcVutil.getUID(); * 读取block在1位置的内容 * mNfcVutil.readOneBlock(1); * 从位置7开始读2个block的内容 * mNfcVutil.readBlocks(7, 2); * 取得block的个数 * mNfcVutil.getBlockNumber(); * 取得1个block的长度 * mNfcVutil.getOneBlockSize(); * 往位置1的block写内容 * mNfcVutil.writeBlock(1, new byte[]{0, 0, 0, 0}) * * @author Kelly * @version 1.0.0 * @filename NfcVUtils.java * @time 2018/10/30 10:29 * @copyright(C) 2018 song */ public class NfcVUtils { private NfcV mNfcV; /** * UID数组行式 */ private byte[] ID; private String UID; private String DSFID; private String AFI; /** * block的个数 */ private int blockNumber; /** * 一个block长度 */ private int oneBlockSize; /** * 信息 */ private byte[] infoRmation; /** * * 初始化 * * @param mNfcV NfcV对象 * * @throws IOException * */ public NfcVUtils(NfcV mNfcV) throws IOException { this.mNfcV = mNfcV; ID = this.mNfcV.getTag().getId(); byte[] uid = new byte[ID.length]; int j = 0; for (int i = ID.length - 1; i >= 0; i--) { uid[j] = ID[i]; j++; } this.UID = ByteUtils.byteArrToHexString(uid); getInfoRmation(); } public String getUID() { return UID; } /** * * 取得标签信息 * */ private byte[] getInfoRmation() throws IOException { byte[] cmd = new byte[10]; cmd[0] = (byte) 0x22; // flag cmd[1] = (byte) 0x2B; // command System.arraycopy(ID, 0, cmd, 2, ID.length); // UID infoRmation = mNfcV.transceive(cmd); blockNumber = infoRmation[12]; oneBlockSize = infoRmation[13]; AFI = ByteUtils.byteArrToHexString(new byte[]{infoRmation[11]}); DSFID = ByteUtils.byteArrToHexString(new byte[]{infoRmation[10]}); return infoRmation; } public String getDSFID() { return DSFID; } public String getAFI() { return AFI; } public int getBlockNumber() { return blockNumber + 1; } public int getOneBlockSize() { return oneBlockSize + 1; } /** * * 读取一个位置在position的block * * @param position 要读取的block位置 * * @return 返回内容字符串 * * @throws IOException * */ public String readOneBlock(int position) throws IOException { byte cmd[] = new byte[11]; cmd[0] = (byte) 0x22; cmd[1] = (byte) 0x20; System.arraycopy(ID, 0, cmd, 2, ID.length); // UID cmd[10] = (byte) position; byte res[] = mNfcV.transceive(cmd); if (res[0] == 0x00) { byte block[] = new byte[res.length - 1]; System.arraycopy(res, 1, block, 0, res.length - 1); return ByteUtils.byteArrToHexString(block); } return null; } /** * * 读取从begin开始end个block * * begin + count 不能超过blockNumber * * @param begin block开始位置 * * @param count 读取block数量 * * @return 返回内容字符串 * * @throws IOException * */ public String readBlocks(int begin, int count) throws IOException { if ((begin + count) > blockNumber) { count = blockNumber - begin; } StringBuffer data = new StringBuffer(); for (int i = begin; i < count + begin; i++) { data.append(readOneBlock(i)); } return data.toString(); } /** * * 将数据写入到block, * * @param position 要写内容的block位置 * * @param data 要写的内容,必须长度为blockOneSize * * @return false为写入失败,true为写入成功 * * @throws IOException * */ public boolean writeBlock(int position, byte[] data) throws IOException { byte cmd[] = new byte[15]; cmd[0] = (byte) 0x22; cmd[1] = (byte) 0x21; System.arraycopy(ID, 0, cmd, 2, ID.length); // UID //block cmd[10] = (byte) position; //value System.arraycopy(data, 0, cmd, 11, data.length); byte[] rsp = mNfcV.transceive(cmd); if (rsp[0] == 0x00) return true; return false; } }

总结

以上就是今天要讲的内容,文章中如有错误或者需要改进的地方欢迎补充指正,本文仅介绍了NFC的使用和M1卡的读取解析场景,关于NFC的历史、卡片类型、Intent filter类型详细描述,其他使用场景等可以参考更多文档,这里贴出来几个我看到的对我很有帮助的文章,也欢迎大家多做参考,

NFC 各种卡类型、区别、历史介绍
https://zhuanlan.zhihu.com/p/344426747
各种官方资料中文说明
https://blog.csdn.net/u013164293/article/details/124474247?spm=1001.2014.3001.5506

相关内容

热门资讯

细节方法!WPk(Wpk)透视... 细节方法!WPk(Wpk)透视辅助!(透视辅助)详细教程(2025已更新)(哔哩哔哩)细节方法!WP...
玩家必看教程微扑克机器人原来确... 您好,微扑克这款游戏可以开挂的,确实是有挂的,需要了解加微【757446909】很多玩家在这款游戏中...
必备教程!微扑克脚本辅助器工具... 必备教程!微扑克脚本辅助器工具(辅助挂)原来是有挂(今日头条)详细教程(哔哩哔哩);微扑克最新软件透...
玩家必看《Wepoke插件》软... 【福星临门,好运相随】;玩家必看《Wepoke插件》软件透明挂!(软件)透明挂内置(2024已更新)...
方法辅助挂微扑克wpk原来真的... 方法辅助挂微扑克wpk原来真的是有挂,太夸张了原来确实是有挂,详细教程(有挂详细)大家肯定在之前微扑...
技术分享!微扑克AI辅助程序(... 技术分享!微扑克AI辅助程序(辅助挂)原来是真的有挂(有挂规律)详细教程(哔哩哔哩);亲,有的,ai...
玩家必看科普微扑克线上原来是有... 玩家必看科普微扑克线上原来是有挂,太过分了原来是有挂,详细教程(有挂解密);亲,有的,ai轻松简单,...
让我来分享经验《微扑克辅助安卓... 让我来分享经验《微扑克辅助安卓版本》微扑克网页版外挂辅助真的假的(哔哩哔哩);微扑克简单的灵活控制,...
发现玩家!wpk检测(WPk)... 发现玩家!wpk检测(WPk)透视辅助!(辅助透视)详细教程(2022已更新)(哔哩哔哩)发现玩家!...
第三方技巧Wepoke最新版软... 自定义新版系统规律,只需要输入自己想要的开挂功能,一键便可以生成出专用辅助器,不管你是想分享给你好友...