NFC系列之基础概述

1,775 阅读14分钟

前言

本文基本是结合Android Developer官网进行梳理,NFC系列将由基本概念到具体操作,到NDEF和非NDEF的数据读写。 

NFC系列之基础概述

NFC系列之协议概述

参考资料

NFC概览

官方NFC读写实例包括基于NDEF的读写和非NDEF读写以及AAR读写唤醒APPLICATION

  • NDEF数据的读写需要通过 MifareClassic 去扇区验证,验证通过才能读取

目录

一、NFC概述

近距离无线通信 (NFC :``Near Field Communication``) 是一组近距离无线技术,通常只有在距离不超过 4 厘米时才能启动连接。借助 NFC,您可以在 NFC 标签与 Android 设备之间或者两台 Android 设备之间共享小型负载。

标签的复杂度可能各有不同

  • 简单标签仅提供读取和写入语义,有时可使用一次性可编程区域将卡片设置为只读

  • 较复杂的标签可提供数学运算,还可使用加密硬件来验证对扇区的访问权限

  • 最为复杂的标签可包含操作环境,允许与针对标签执行的代码进行复杂的互动

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

支持 NFCAndroid 设备同时支持以下三种主要操作模式:

  • 读取器/写入器模式:支持 NFC 设备读取和/或写入被动 NFC 标签和贴纸。

  • 点对点模式:支持 NFC 设备与其他 NFC 对等设备交换数据;Android Beam 使用的就是此操作模式。

  • 卡模拟模式:支持 NFC 设备本身充当 NFC 卡。然后,可以通过外部 NFC 读取器(例如 NFC 销售终端)访问模拟 NFC 卡。

二、NFC设备类型及工作原理

1、NFC设备

主要有两种 ---- 无源NFC设备与有源NFC设备

  • 无源NFC设备包括NFC标签和其他小型发射器,它们可以向其他NFC设备发送信息,而不需要电源。但是,它们不能处理来自其他源的信息,也不能连接到其他无源设备。

  • 有源NFC设备能够发送和接收数据,并且可以彼此通信,也可以与无源设备通信。目前来说,智能手机是最常见的有源NFC设备,其它常见的例子还包括公交读卡器和支付终端

如果从工作模式上来介绍的话,可以这样概括

NFC工作模式分为被动模式主动模式

被动模式NFC发起设备(也称为主设备)需要供电设备,主设备利用供电设备的能量来提供射频场,并将数据发送到NFC目标设备(也称作从设备),传输速率需在106kbps212kbps424kbps中选择其中一种。从设备不产生射频场,所以可以不需要供电设备,而是利用主设备产生的射频场转换为电能,为从设备的电路供电,接收主设备发送的数据,并且利用负载调制(load modulation)技术,以相同的速度将从设备数据传回主设备。因为此工作模式下从设备不产生射频场,而是被动接收主设备产生的射频场,所以被称作被动模式,在此模式下,NFC主设备可以检测非接触式卡或NFC目标设备,与之建立连接。

主动模式中,发起设备和目标设备在向对方发送数据时,都必须主动产生射频场,所以称为主动模式,它们都需要供电设备来提供产生射频场的能量。这种通信模式是对等网络通信的标准模式,可以获得非常快速的连接速率。

2、工作原理

它的工作原理是什么呢?就像蓝牙、Wi-Fi以及其他各种无线通信技术一样,NFC的工作原理也是通过无线电波发送信息,也是无线数据传输的一种标准。这意味着设备必须遵守特定的规范,才能正确地相互通信。在NFC中使用的技术是基于旧的免接触式射频识别RFID)演变而来,即使用电磁感应来传输信息,并向下兼容RFID

这也是NFC蓝牙/WiFi的一个主要区别,NFC使用电磁感应原理,有源NFC元件可以在无源元件中感应出电流和发送数据。这意味着无源设备不需要自己的电源,当NFC组件进入通信范围时,它们可以由有源NFC组件产生的电磁场提供动力。不过,NFC技术没有足够的感应力来给我们的智能手机充电,QI无线充电也是基于同样的原理。

NFC的工作频率为13.56MHz,距离在10厘米内,其传输速度有106Kbit/秒、212Kbit/秒或者424Kbit/秒三种,这种传输速度对于传输图片和音乐等文件已经足够了。

三、NFC对比蓝牙的优势

NFC的优势

  • NFC相比蓝牙的一大优势是功耗更少,这使得NFC非常适合于作为无源设备,比如前面提到的广告标签,因为它们可以在没有电源的情况下运行。

  • 还有一个主要优势:连接速度更快。NFC由于使用了电感耦合技术,无需手动配对,在两个设备之间建立连接仅需不到十分之一秒的时间。虽然现代蓝牙连接速度已经非常快了,但依然远不及NFC的连接速度,而快速的连接对于某些场景是至关重要的,例如移动支付。

当然,缺点也有

  • 最明显的是,NFC的传输距离比蓝牙短得多。上面说过,NFC的最大传输范围约为10厘米,而蓝牙连接的传输距离可以高达10米甚至更远。
  • NFC的另一个缺点是其传输速度比蓝牙慢很多。NFC传输数据的最高速度仅为424 kbit/s,而蓝牙2.1的传输速度为2.1 Mbit/s,**蓝牙LE(低功耗)**的传输速度也达到了为1 Mbit/s。

四、NFC基础知识

1、基础知识

NDEF 数据与 Android 结合使用时,会有两个主要用例

  • NFC 标签读取 NDEF 数据

    • NFC 标签读取 NDEF 数据的操作由标签调度系统进行处理,该系统会分析已发现的 NFC 标签,对相应数据进行适当分类,然后启动对分类后的数据感兴趣的应用。如果某个应用想要处理扫描到的 NFC 标签,则可以声明 Intent 过滤器,并请求对数据进行处理
  • 使用 Android Beam™NDEF 消息从一台设备传输到另一台设备

    • 借助 Android Beam™ 功能,设备可以将 NDEF 消息推送到另一台设备,方法是将两台设备靠在一起。与蓝牙等其他无线技术相比,这种互动可提供更简便的数据发送方式,因为使用 NFC 时无需手动发现设备并将其配对。当两台设备之间的距离近到一定范围内时,系统会自动开始连接。Android Beam 功能通过一组 NFC API 提供,因此任何应用都可以在设备间传输信息。例如,通讯录、浏览器和 YouTube 应用可使用 Android Beam 在多台设备之间共享联系人信息、网页和视频。

2、NFC标签调度系统

Android 设备通常会在屏幕解锁后查找 NFC 标签,除非设备的“设置”菜单中停用了 NFC 功能。

Android 设备发现 NFC 标签后,期望的行为就是让最合适的 Activity 来处理该 Intent,而不是询问用户应使用哪个应用。

由于设备需要在非常近的范围内扫描 NFC 标签,因此,让用户手动选择 Activity 可能会迫使他们将设备从标签处移开并导致连接中断。应以适当方式开发 Activity,使其仅处理所关注的 NFC 标签,以避免 Activity 选择器出现。为了解决这个问题,Android 提供了一个特殊的标签调度系统,用于分析扫描到的 NFC 标签、解析它们并尝试找到对扫描到的数据感兴趣的应用。这个标签调度系统通过以下操作来实现这些目的:

  1. 解析 NFC 标签并确定 MIME 类型或 URI(后者用于标识标签中的数据负载)。

  2. MIME 类型或 URI 与负载一起封装到 Intent 中。如何将 NFC 标签映射到 MIME 类型和 URI 中介绍了前两个步骤。

  3. 根据 Intent 启动 Activity如何将 NFC 标签分发到应用中介绍了此步骤。

3、如何将 NFC 标签映射到 MIME 类型和 URI

AndroidNFC Forum 定义的 NDEF 标准的支持最完备,NDEF数据封装在包含一条或多条记录(NdefRecord)的消息(NdefMessage)中,也就是NdefMessage消息中会有不少NdefRecord记录,我们都是先创建NdefRecord记录后再放到NdefMessage中。

注意:要下载完整的 NDEF 规范,请转到 NFC Forum 规范和应用文档网站,并参阅创建常见类型的 NDEF 记录,查看有关如何构造 NDEF 记录的示例。

Android 设备扫描包含 NDEF 格式数据的 NFC 标签时,它会解析该消息并尝试确定数据的 MIME 类型或起标识作用的 URI。为此,系统需要读取 NdefMessage 中的第一条 NdefRecord,以确定如何解读整个 NDEF 消息(一个 NDEF 消息可能具有多条 NDEF 记录)。在格式正确的 NDEF 消息中,第一条 NdefRecord 包含以下字段

3 位 TNF(类型名称格式)

表示如何解读可变长度类型字段。下表 中介绍了有效的值。

详细可查看 官网表1

可变长度类型

介绍了记录的类型。如果使用 TNF_WELL_KNOWN,那么请使用此字段来指定记录类型定义 (RTD)。下表 中介绍了有效的 RTD

详细可查看 官网表2

可变长度 ID

记录的唯一标识符。此字段并不经常使用,但如果您需要对标签进行唯一标识,则可为其创建 ID

可变长度负载

您要读取或写入的实际数据负载。一个 NDEF 消息可以包含多条 NDEF 记录,因此不要假定 NDEF 消息的第一条 NDEF 记录中就有完整的负载

具体操作,后续会进行代码分析,这里可以对照看下小米碰碰贴的NFC的NDEF记录

4、支持的标签技术

具体支持可查看 官网

Android设备还可以选择支持以下标签技术

5、如何将NFC标签分发到应用

当标签调度系统创建完用于封装 NFC 标签及其标识信息的 Intent 后,

它会将该 Intent 发送给感兴趣的应用,由这些应用对其进行过滤。

如果有多个应用可处理该 Intent,系统会显示 Activity 选择器,供用户选择要使用的 Activity

标签调度系统定义了三种 Intent,按优先级从高到低列出如下

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

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

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

标签的调度系统基本工作方式如下

  • 在解析 NFC 标签(ACTION_NDEF_DISCOVEREDACTION_TECH_DISCOVERED)时,尝试使用由标签调度系统创建的 Intent 启动 Activity

  • 如果不存在过滤该 IntentActivity,则尝试使用下一优先级的 IntentACTION_TECH_DISCOVEREDACTION_TAG_DISCOVERED)启动 Activity,直到应用过滤该 Intent 或者直到标签调度系统试完所有可能的 Intent

  • 如果没有应用过滤任何 Intent,则不执行任何操作

从上我们也能看出,尽可能使用 NDEF 消息和 ACTION_NDEF_DISCOVERED Intent,因为它是三种 Intent 中最具体的一种。与其他两种 Intent 相比,此 Intent 可使您在更恰当的时间启动应用,从而为用户带来更好的体验

五、Android中清单文件相关配置

1、权限

用于访问 NFC 硬件的 NFC <uses-permission> 元素

    <uses-permission android:name="android.permission.NFC" />    

2、应用支持的最低SDK版本

API 级别 9 仅通过 ACTION_TAG_DISCOVERED 支持有限的标签调度,但是现在target API都到30了,所以这块基本不需要修改

3、uses-feature 元素,以便您的应用仅在那些具备 NFC 硬件的设备的 Google Play 中显示

   <uses-feature android:name="android.hardware.nfc" android:required="true" />

4、过滤NFC Intent配置

ACTION_NDEF_DISCOVERED

以下示例展示了如何过滤 MIME 类型为 text/plainACTION_NDEF_DISCOVERED Intent

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain" />
    </intent-filter>

以下示例展示了如何过滤采用 https://developer.android.com/index.html 形式的 URI

       <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
       <data android:scheme="http"
                  android:host="developer.android.com"
                  android:pathPrefix="/index.html" />
    </intent-filter>

ACTION_TECH_DISCOVERED

必须创建一个 XML 资源文件,用它在 tech-list 集内指定您的 Activity 所支持的技术。如果 tech-list 集是标签所支持的技术(可通过调用 getTechList() 来获取)的子集

以下示例定义了所有技术。您可以移除自己不需要的技术。将此文件(你可以随便命名)保存到 <project-root>/res/xml 文件夹中

        <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
        <tech-list>
            <tech>android.nfc.tech.IsoDep</tech>
            <tech>android.nfc.tech.NfcA</tech>
            <tech>android.nfc.tech.NfcB</tech>
            <tech>android.nfc.tech.NfcF</tech>
            <tech>android.nfc.tech.NfcV</tech>
            <tech>android.nfc.tech.Ndef</tech>
            <tech>android.nfc.tech.NdefFormatable</tech>
            <tech>android.nfc.tech.MifareClassic</tech>
            <tech>android.nfc.tech.MifareUltralight</tech>
        </tech-list>
    </resources>

以下示例展示了如何与支持 NfcANdef 技术的标签或者支持 NfcBNdef 技术的标签相匹配

       <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
        <tech-list>
            <tech>android.nfc.tech.NfcA</tech>
            <tech>android.nfc.tech.Ndef</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcB</tech>
            <tech>android.nfc.tech.Ndef</tech>
        </tech-list>
    </resources>

最后清单文件中添加

    <activity>
    ...
    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED"/>
    </intent-filter>

    <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
        android:resource="@xml/nfc_tech_filter" />
    ...
    </activity>

ACTION_TAG_DISCOVERED

<intent-filter>
        <action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>

5、从Intent中获取消息

如果某个 Activity 由于 NFC Intent 而启动,您可以从该 Intent 中获取有关扫描到的 NFC 标签的信息。Intent 可以包含以下 extra,具体取决于扫描到的标签

  • EXTRA_TAG(必需):一个 Tag 对象,表示扫描到的标签

  • EXTRA_NDEF_MESSAGES(可选):从标签中解析出的一组 NDEF 消息。此 extra 对于 ACTION_NDEF_DISCOVERED Intent 而言是必需的

  • EXTRA_ID(可选):标签的低级别 ID

六、NFC读写

官方NFC读写实例包括基于NDEF的读写和非NDEF读写以及AAR读写唤醒APPLICATION

这里就简单通过官方给的例子写部分示例代码

非NDEF数据的格式

  • 将NFC标签的存储区域分为16个页,每一个页可以存储4个字节,一个可存储64个字节(512位)。页码从0开始(0至15)。前4页(0至3)存储了NFC标签相关的信息(如NFC标签的序列号、控制位等)。从第5页开始存储实际的数据(4至15页)

1、非NDEF读操作

private void mifareClassicResolve(Intent intent) {

        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        boolean isAuth = false;
        if (isSupportTechs(tag.getTechList())) {
            MifareClassic mfc = MifareClassic.get(tag);
            if (mfc != null) {
                try {
                    mfc.connect();
                    int nSecount = mfc.getSectorCount();
                    Log.i(TAG, "secount = " + nSecount);
                    for (int i = 0; i < nSecount; i++) {
                        //每个扇区都需要密码验证
                        if (mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_DEFAULT)) {
                            isAuth = true;
                        } else if (mfc.authenticateSectorWithKeyA(i, KEY_A)) {
                            //厂家会自定义密码
                            isAuth = true;
                        }
                        if (isAuth) {
                            int nBlock = mfc.getBlockCountInSector(i);
                            Log.i(TAG, "nBlock = " + nBlock);
                            for (int j = 0; j < nBlock; j++) {
                                byte[] data = mfc.readBlock(j);
                                Log.i(TAG, "readBlock -> " + new String(data));
                            }
                        }
                    }

                } catch (Exception e) {
                    Log.i(TAG, "read Exception " + e.getMessage());
                }
            }
        }
    }

 private boolean isSupportTechs(String[] techList) {
        for (String s : techList) {
            Log.i(TAG, "supportedTechs : " + s);
        }
        boolean isSupport = false;
        for (String s : techList) {
            if ("android.nfc.tech.MifareClassic".equals(s)) {
                isSupport = true;
            } else if ("android.nfc.tech.MifareUltralight".equals(s)) {
                isSupport = true;
            } else if ("android.nfc.tech.NfcA".equals(s)) {
                isSupport = true;
            } else if ("android.nfc.tech.Ndef".equals(s)) {
                isSupport = true;
            } else {
                isSupport = false;
            }
        }
        return isSupport;
    }

2、非NDEF写操作

private void writeMifare(Intent intent) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        boolean isAuth = false;
        if (isSupportTechs(tag.getTechList())) {
            MifareClassic mfc = MifareClassic.get(tag);
            //写
            try {
                mfc.connect();
                //验证 扇区 0-15 假设我们使用默认密码往第0个扇区写数据
                //keyA 或者keyB 验证通过
                if (mfc.authenticateSectorWithKeyA(0, MifareClassic.KEY_DEFAULT)
                        || mfc.authenticateSectorWithKeyB(0, MifareClassic.KEY_DEFAULT)) {
                    //数据是16个字节
                    mfc.writeBlock(0,"1234567890123456".getBytes());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

3、NDEF读操作

读取 uritext

 private void ndefRead(Intent intent) {
        String action = intent.getAction();
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
            NdefMessage[] messages = null;
            Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
            if (rawMsgs != null) {
                messages = new NdefMessage[rawMsgs.length];
                for (int i = 0; i < rawMsgs.length; i++) {
                    messages[i] = (NdefMessage) rawMsgs[i];
                }
            } else {
                Log.i(TAG,"no rawMsgs");
                //没有数据
                byte[] empty = new byte[]{};
                NdefRecord record = new NdefRecord(NdefRecord.TNF_UNKNOWN, empty, empty, empty);
                NdefMessage msg = new NdefMessage(new NdefRecord[]{record});
                messages = new NdefMessage[]{msg};
            }
            //处理数据
             processMsg(messages) ;
        } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {

        } else if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {

        } else {

        }
    }

   private String processMsg(NdefMessage[] messages) {
        if (messages == null || messages.length == 0) {
            return "null";
        }
        for (int i = 0; i < messages.length; i++) {
            int length = messages[i].getRecords().length;
            Log.i(TAG,"record LENGHT -> "+ length);
            NdefRecord[] records = messages[i].getRecords();
            for (int j = 0; j < length; j++) {
                for (NdefRecord record : records) {
                    if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) {
                        Log.i(TAG,"RTD_URI match");
                        Uri uri = null;
                        try {
                            uri = parseUriRecord(record);
                        } catch (UnsupportedEncodingException e) {

                        }
                        if (uri != null) {
                            return uri.toString();
                        }
                    } else if (Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)) {
                        return parseTextRecord(record);
                    }else{
                        Log.i(TAG,""+ new String(record.getType()));
                    }
                }
            }
        }
        return "null null";
    }
  //uri数据匹配
   private Uri parseUriRecord(NdefRecord record) throws UnsupportedEncodingException {
        //判断是否是URI类型
       //  Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI));

        byte[] payload = record.getPayload();
        //前缀 payload第一个内容
        String prefix = URI_PREFIX_MAP.get(payload[0]);
        byte[] fullUri = Bytes.concat(prefix.getBytes(Charset.forName("UTF-8")),
                Arrays.copyOfRange(payload, 1, payload.length));
        Uri uri = Uri.parse(new String(fullUri, "UTF-8"));
        return uri;
    }
  //注 URI_PREFIX_MAP 具体可以看NdefRecord.java类的URI_PREFIX_MAP,是一样的

  // text数据匹配
   private String parseTextRecord(NdefRecord record) {
        //Check Text
        Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_TEXT));

        //获取payload内容
        byte[] payload = record.getPayload();
        //解析
        Byte statusByte = record.getPayload()[0];
        String textEncoding = (statusByte & 0x80) == 0 ? "UTF-8" : "UTF-16";
        int langLength = statusByte & 0x3F;
        String payLoadText = "null";
        try {
            payLoadText = new String(payload, langLength + 1, payload.length - langLength - 1, textEncoding);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Log.i(TAG, "payLoad Text Content -> " + payLoadText);
        return payLoadText;
    }


4、NEDF写操作

  uritext 的写入

//uri    
public NdefMessage createNdefMsgRtdURI(String data,byte identifierCode){
        try {
        byte[] urlField   = data.getBytes("US-ASCII");

        byte[] payload = new byte[urlField.length + 1];
        payload[0] = identifierCode;//前缀 例如 https
        System.arraycopy(urlField,0,payload,1,urlField.length);
        NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,NdefRecord.RTD_URI, new byte[0],payload);
        return new NdefMessage(new NdefRecord[]{record});
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return  null;
    }
   //text
    public NdefMessage getNdefMsgRtdText(String data, boolean encodeUTF8){
        Locale local = new Locale("en","US");
        byte[] langBytes = new byte[0];
        try {
            langBytes = local.getLanguage().getBytes("US-ASCII");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Charset utfEncoding = encodeUTF8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16");
        //第 7 位 0就是utf8
        int utfBit = encodeUTF8 ? 0 : (1<<7);
        char status = (char) (utfBit + langBytes.length);
        byte[] textBytes = data.getBytes(utfEncoding);
        byte[] payload = new byte[langBytes.length + textBytes.length + 1];
        payload[0] = (byte) status;
        //复制语言码
        System.arraycopy(langBytes,0,payload,1,langBytes.length);
        //复制实际文本数据
        System.arraycopy(textBytes,0,payload,1+langBytes.length,textBytes.length);
        NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,NdefRecord.RTD_TEXT, new byte[0],payload);

        return new NdefMessage(new NdefRecord[]{record,NdefRecord.createApplicationRecord("com.rjx.nfcdemo")});
    }