Android NFC三种工作模式实践

发布于:2024-05-01 ⋅ 阅读:(36) ⋅ 点赞:(0)

Android NFC三种工作模式实践

前言

最近在完善Android硬件部分的实践,NFC很久之前我也写过,只不过当时能力有限,只写了个读卡的,这篇文章我想把NFC的几种工作模式都做一下。

NFC有三种工作模式:

  • 读卡器模式
  • 点对点模式
  • 模拟卡模式

其中,点对点模式被标记Deprecated推荐使用蓝牙,模拟卡模式我这没读成功,不知道是不是手机问题,下面详细说下吧。

官方文档

官方文档镇楼,比国内那些抄来抄去的文章好多了:

虽然我之前的文章也是抄,不过这次亲自实践了

读卡器模式

读卡器模式就是我们常见的读标签,这个也是最简单的,总的来说分成两种使用方法:

  • 前台使用
  • Intent匹配

前台使用就是我们APP在前台时,调用NFC的API去读卡,优先级高,不会被其他应用拦截NFC读取事件。Intent匹配就是,我们设置好intent-filter,当系统读取到NFC事件时,就会发intent到我们的activity。

下面分别来看下两种使用.

前台使用

前台使用需要用到一个activity,在activity可见的时候,调用NFC的API,并用它去接收intent,获取数据。

添加权限

NFC权限不用动态申请,manifest里面加上就行:

<!--NFC权限-->
<uses-permission android:name="android.permission.NFC" />
<!-- 如果要求设备必须要有NFC功能,可以加上下面这条 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />

检查NFC可用性

在使用NFC前还是要检查下NFC是否可用,要具备NFC且处于开启状态时,才能够使用:

/**
 * 检查NFC状态
 *
 * @param context 上下文
 */
fun checkNfcState(context: Context): Int {
    val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
    return if (nfcAdapter == null) {
        // 设备不支持 NFC
        NFC_STATE_UNVALUABLE
    } else if (!nfcAdapter.isEnabled) {
        // NFC 未启用
        NFC_STATE_UNOPEN
    }else {
        NFC_STATE_OPENED
    }
}

一般来说,一个设备只有一个NFC模块,直接通过getDefaultAdapter获取就行了。

开启前台监听

当NFC可用,并且我们的activity处于前台时(onResume - onPause之间),就可以调用enableForegroundDispatch开启读卡模式监听了:

/**
 * 前台使用,开启NFC监听意图,请在onResume内或之后调用
 *
 * 通过启用前台调度,应用程序将成为当前设备上的 NFC 优先处理应用,可以在应用在前台时直接接收 NFC 事件
 *
 * @param activity 当前activity,提供上下文
 * @param clz nfc要跳转处理的activity类
 */
fun enableNfcIntent(activity: Activity, clz: Class<out Activity>) {
    if (checkNfcState(activity) != NFC_STATE_OPENED) {
        throw IllegalStateException("NFC UNVALUABLE or UNOPEN!")
    }

    val intent = Intent(activity, clz).apply {
        addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
    }

    // 存在一样的PendingIntent则用新的替换旧的
    intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING)
    val pendingIntent = PendingIntent.getActivity(
        activity,
        0,
        intent,
        PendingIntent.FLAG_MUTABLE
    )

    // 过滤要接收的action类型,实际上接收所有action直接设置为null就行
    val intentFiltersArray = arrayOf(
        IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED),
        IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
        IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
    )

    // 过滤使用ACTION_TECH_DISCOVERED时的技术列表,设置所有时,也是直接设置为null就行
    val techListsArray = arrayOf(
        arrayOf(
            "android.nfc.tech.NfcA"
        )
    )

    val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
    nfcAdapter?.enableForegroundDispatch(
        activity,
        pendingIntent,
        intentFiltersArray,
        techListsArray
    )
}

/**
 * 前台使用,关闭NFC监听意图,请在onPause内或之前调用
 *
 * @param activity 当前activity,提供上下文
 */
fun disableNfcIntent(activity: Activity) {
    if (checkNfcState(activity) != NFC_STATE_OPENED) {
        throw IllegalStateException("NFC UNVALUABLE or UNOPEN!")
    }

    val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
    nfcAdapter?.disableForegroundDispatch(activity)
}

我这写成了工具类,直接调用就行,记得开启监听后要关闭监听。

还有个坑就是,我发现启用监听后,只能触发一次读取,第二次读取会跳到intent匹配里面,所以读取成功后,想要继续读取要再开一次。

关于intentFiltersArray和techListsArray,这里不打算详细讲,大致就是按优先级,会有三种intent:

  • NDEF
  • TECH
  • TAG

NDEF是Google设定的一种数据格式,TECH是一些常见的NFC格式,TAG类型是兜底的intent。也就是说按顺序来,是NDEF数据格式,就发NDEF的intent,不熟就看是不是支持的TECH,如果不是就发TAG兜底。

对于TECH类型,要设置过滤的技术列表,比如下面就只过滤NfcA类型(我就想过滤深圳通的),要想支持的类型多,就尽量多加类型:

val techListsArray = arrayOf(
    arrayOf(
        "android.nfc.tech.NfcA"
    )
)

数据读取

上面开启前台监听的时候,我们给intent增加了FLAG_ACTIVITY_SINGLE_TOP,所以到activity的onNewIntent里面处理数据就行:

/**
 * 处理NFC Intent
 *
 * @param intent NFC传递过来的intent
 * @param callback 回调
 */
fun handleNfcIntent(intent: Intent, callback: Consumer<String>) {
    val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
    Log.d("TAG", "handleNfcIntent ${intent.action}: $tag, $intent")

    when(intent.action) {
        // 当读取到包含 NDEF(NFC Data Exchange Format)格式的数据时触发,优先级高
        NfcAdapter.ACTION_NDEF_DISCOVERED -> {
            val ndefMessage = getNdefMessageFromTag(tag)

            if (ndefMessage != null) {
                val records = ndefMessage.records
                if (records != null && records.isNotEmpty()) {
                    val ndefRecord = records[0]
                    val payload = String(ndefRecord.payload)
                    // 在此处处理读取到的 NFC 数据
                    callback.accept(payload)
                }
            }
        }
        // 当读取到特定技术(Tech)标签时触发,优先级第二
        NfcAdapter.ACTION_TECH_DISCOVERED -> {

            // 根据techList判断拿到标签的技术类型
            tag?.techList?.let {

                // 解析一个NfcA
                if (it.contains("android.nfc.tech.NfcA")) {
                    callback.accept(readNfcATag(tag))
                }
            }
        }
        // 当读取到任何类型的标签时触发,不限于特定的数据格式,兜底的intent
        NfcAdapter.ACTION_TAG_DISCOVERED -> {
            Log.d("TAG", "handleNfcIntent tag: $tag")
        }
    }
}

// 解析Ndef格式
private fun getNdefMessageFromTag(tag: Tag?): NdefMessage? {
    val ndef = Ndef.get(tag)
    ndef?.let {
        try {
            it.connect()
            return it.ndefMessage
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                it.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
    return null
}

// 读取NfcA类型的NFC卡片: 适用于深圳通
private fun readNfcATag(tag: Tag): String {
    val result = StringBuilder()
    val nfcA = NfcA.get(tag)

    try {
        // 注意读取的时候不要拿开手机!!!
        nfcA.connect()

        // 读取信息
        result.appendLine("nfcA.sak: " + nfcA.sak)
        result.appendLine("nfcA.atqa: " + nfcA.atqa)
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        nfcA.close()
    }

    return result.toString()
}

上面写了一个读取NDEF数据的,一个读取NFCA格式的,我是用的深圳通测试的,结果如下:

nfcA.sak: 32
nfcA.atqa: [B@9565e69

nfcA就这两属性,我也不知道干嘛用的,算是成功了吧。关于数据的格式,根据需要查资料吧,不过也可以叫GPT直接帮忙写解析的方法。

Intent匹配

前台使用有个限制,就是我们要让activity到前台,才能读取数据,比较麻烦,可以设置好activity的intent-filter,让系统打开我们的activity。

添加权限

同样是添加权限,如果前面加了,就不用改了:

<!--NFC权限-->
<uses-permission android:name="android.permission.NFC" />
<!-- 如果要求设备必须要有NFC功能,可以加上下面这条 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />

配置Activity

这里我们要向外提供一个activity来护理NFC的事件,记得要exported,设置好要接收的类型(NDEF、TECH、TAG):

<activity android:name=".nfc.NfcActivity"
    android:configChanges="orientation|keyboardHidden|screenSize"
    android:exported="true"
    android:screenOrientation="sensor">

    <!--  NFC配置: NDEF  -->
    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED" />

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

    <!--  NFC配置: TECH  -->
    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED"/>

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

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

    <!--  NFC配置: TAG  -->
    <intent-filter>
        <action android:name="android.nfc.action.TAG_DISCOVERED"/>

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

</activity>

和上面TECH设置过滤的技术列表一样,这里我们要写一个xml来描述我们的过滤类型:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!--  支持的类型列表,以下示例定义了所有技术(实际是用来过滤的,并不是越多越好)  -->
    <tech-list>
        <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.IsoDep</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>
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
</resources>

看情况过滤呗,比如我这就想过滤深圳通的NfcA,加它一个就行。

数据读取

既然是用activity来接收数据,那么数据还是intent,只不过和前台不同的是,intent也可能从onCreate来,处理还是和上面一样,就不重复写了。

看下实际使用的图:

1.jpg 2.jpg

这里有个坑就是,在选择应用的时候,手机和NFC标签不能拿开来,不然读取数据的时候,connect就失败崩了。所以,感觉不如前台使用。

点对点模式

在查找了一些资料后,我发现这个点对点模式还是比较鸡肋的,Google都不推荐使用了,直接标记Deprecated了。说白了,它就是两个手机贴近的时候,发送一些短消息过去,感觉像自动交换名片。

下面就来看下怎么操作吧,我这代码写好了,当时没实操成功,我这只有一台苹果手机和一台安卓手机,凑合看下吧,这里也有三种使用方法。

创建Ndef消息

不管那种使用方法,都得创建一条Ndef消息,并将它发出去,所以先来看下如何创建Ndef消息:

/**
 * 创建一条https链接的的Ndef消息,其他格式可以参考官方文档:
 * https://developer.android.google.cn/develop/connectivity/nfc/nfc?hl=zh-cn
 *
 * @param httpsUrl 链接
 */
@Suppress("MemberVisibilityCanBePrivate")
fun createNdefMessage(
    httpsUrl: String = "https://developer.android.com/index.html"
): NdefMessage {
    // 创建一条 TNF_ABSOLUTE_URI NDEF 记录:
    val uriRecord = ByteArray(0).let { emptyByteArray ->
        NdefRecord(
            NdefRecord.TNF_ABSOLUTE_URI,
            httpsUrl.toByteArray(Charset.forName("US-ASCII")),
            emptyByteArray,
            emptyByteArray
        )
    }

    // 可以通过下面intent-filter去匹配
//        <intent-filter>
//            <action android:name="android.nfc.action.NDEF_DISCOVERED" />
//            <category android:name="android.intent.category.DEFAULT" />
//            <data android:scheme="https"
//            android:host="developer.android.com"
//            android:pathPrefix="/index.html" />
//        </intent-filter>

    // 可以指定一个AAR(Android 应用记录),来让NFC打开特定的应用,没得话去Google Play下载
    // aarRecord不能放NdefMessage第一条,Android系统会查询第一条record来标记 MIME 类型或 URI
//        val aarRecord =
//            NdefRecord.createApplicationRecord("com.silencefly96.module_hardware")

    // 创建 NDEF 消息
    return NdefMessage(arrayOf(
        uriRecord,
//            aarRecord
    ))
}

这里就举个例子吧,其实很多类型在官方文档都有例子可以copy:

enableForegroundNdefPush

首先是远古的API,在activity可见状态的时候enableForegroundNdefPush,传出一条ndef消息就OK了:

override fun onResume() {
    super.onResume()

    // 创建要推送的 NDEF 消息
    val ndefMessage = createNdefMessage()

    // 启用前台 NDEF 推送
    nfcAdapter?.enableForegroundNdefPush(this, ndefMessage)
}

override fun onPause() {
    super.onPause()

    // 禁用前台 NDEF 推送
    nfcAdapter?.disableForegroundNdefPush(this)
}

这个功能是Android API 10引入的,但是后面不推荐了,因为它要手动去关闭,官方更推荐setNdefPushMessage,能根据activity的生命周期控制。

setNdefPushMessage

setNdefPushMessage接受传输的NdefMessage,在两台设备距离足够近时,会自动传输消息。使用起来也很简单:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main)

    // 获取 NFC 适配器
    nfcAdapter = NfcAdapter.getDefaultAdapter(this);

    // 创建要推送的 NDEF 消息
    NdefMessage ndefMessage = createNdefMessage();

    // 设置要推送的 NDEF 消息
    nfcAdapter.setNdefPushMessage(ndefMessage, this);
}

调用setNdefPushMessage后,在activity被destroy之前,都会在设备触碰时把Ndef消息发送出去,不过看代码也能看出,这条消息是固定的,不能修改的。

setNdefPushMessageCallback

和上面setNdefPushMessage不一样,setNdefPushMessageCallback可以动态地返回Ndef消息,用法也差不多:

/**
 * 开启NFC写入回调,监测到其他NFC设备时会把这条Ndef Message发过去
 *
 * @param activity 发送Ndef Message的activity,并提供上下文
 */
fun setNdefPushMessageCallback(activity: Activity) {
    val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
    nfcAdapter?.setNdefPushMessageCallback({
        createNdefMessage()
    }, activity)
}

在activity地onCreate方法里面调用,每次设备接触就会通过这个callback创建新的Ndef消息并发送出去。

如果要监听是否发送完这个消息,可以用下面方法:

nfcAdapter?.setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback {
    // Beam 消息发送完成的回调方法
    // 可以在此处执行相应的操作
}, activity)

接收数据

接收数据其实和前面读卡器模式地前台使用一样,去onNewIntent里面解析intent就行。

连续通信问题

感觉既然可以通过setNdefPushMessageCallback动态返回消息,又能接收新消息,那NFC支持双向连续通吗?

问了GPT,又看了下官方文档,感觉这好像不太行,下面是官方文档的一些描述:

Android Beam 可在两台 Android 设备之间进行简单的点对点数据交换。

activity 一次只能推送一条 NDEF 消息,因此 setNdefPushMessageCallback() 优先于 setNdefPushMessage()(如果同时设置了两者)。

一般来说,如果当两台设备处于通信的范围内时,如果您的 activity 只需要始终推送同一 NDEF 消息,您通常可以使用 setNdefPushMessage()。如果您的应用关注应用的当前上下文并希望根据用户在应用中执行的操作来推送 NDEF 消息,可以使用 setNdefPushMessageCallback。

所以啊,在两个设备触碰的时候,这一个过程实际是只能发一条数据的,虽然可以根据上下文生成每个过程不同的Ndef消息,但是没法连续通信。

不过,我也想到了一个使用场景: 每触发一次碰撞,生成的Ndef消息可以记录序号,这样其他手机就能知道它的位次,类似签到?

模拟卡模式

其实我们手机用的最多的就是模拟卡模式了,比如公交卡、房卡之类的,只不过这里复杂的很。

设备上有很多元件可以模拟卡,他们被叫做安全元件:

  • SD卡(银联主推)
  • 手机内置(Embeded,终端厂商主推)
  • SIM卡内置(运营商主推)

这些安全元件比较复杂,也不好改,好处就是好像没电了也能用,但是这不是下面我要讲的,下面要讲的是HCE模式:

HCE(Host Card Emulation)就是Google提供的,利用主机CPU来模拟NFC卡的一种功能,下面就来简单看看吧。

HostApduService

要实现HCE功能,我们首先要继承HCE,创建一个向外暴露的Service:

class NfcHostApduService : HostApduService() {
    override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
        // 具体逻辑
    }
    
    override fun onDeactivated(reason: Int) {
        // 模拟卡被停用时的回调
        Log.d("TAG", "onDeactivated: $reason")
    }
}

逻辑我们后面讲,既然是Service,就需要到manifest里面注册:

<service
    android:name=".nfc.NfcHostApduService"
    android:exported="true"
    android:permission="android.permission.BIND_NFC_SERVICE">

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

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice"/>

</service>

注意要向外暴露,权限是加在service里面的,不是application里面的,intent-filter用来匹配命令事件,下面还要写一个xml来描述这个HostApduService:

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/nfc_main_fragment"
    android:requireDeviceUnlock="false"
    android:apduServiceBanner="@mipmap/ic_launcher_round">

    <aid-group
        android:description="@string/nfc_main_fragment"
        android:category="payment">

        <aid-filter android:name="F0010203040506"/>

    </aid-group>

    <aid-group
        android:description="@string/nfc_main_fragment"
        android:category="other">

        <aid-filter android:name="F0010203040506"/>

    </aid-group>

</host-apdu-service>

AID内容还是看官方文档吧:

这里随便写了一个AID,后面我们要在NfcHostApduService的processCommandApdu去匹配它。

处理逻辑

上面说到了NfcHostApduService的逻辑都是在processCommandApdu处理的,这里我们就来看一下:

override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
    Log.d("TAG", "processCommandApdu: $commandApdu")
    return when {
        commandApdu.contentEquals(SELECT_APDU) -> selectAid()
        commandApdu.contentEquals(READ_DATA_APDU) -> readCardData()
        else -> byteArrayOf(0x6A.toByte(), 0x81.toByte()) // 返回错误状态码
    }
}

实际上,这里是一个命令模式,commandApdu是命令,我们要根据不同的commandApdu去处理不同的逻辑:

companion object {
    // 用于识别特定的应用程序
    const val AID = "F0010203040506"

    // 模拟卡片数据
    private val CARD_DATA = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06)

    // 用于选择应用程序的APDU命令
    private val SELECT_APDU = byteArrayOf(
        0x00.toByte(), // CLA (Class)
        0xA4.toByte(), // INS (Instruction)
        0x04.toByte(), // P1 (Parameter 1)
        0x00.toByte(), // P2 (Parameter 2)
        0x06.toByte(), // Lc (Length of data)
        0xF0.toByte(), 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte(), 0x05.toByte(), 0x06.toByte() // AID
    )

    // 用于读取数据的 APDU 命令
    private val READ_DATA_APDU = byteArrayOf(
        0x00.toByte(), // CLA (Class)
        0xB0.toByte(), // INS (Instruction)
        0x00.toByte(), // P1 (Parameter 1)
        0x00.toByte(), // P2 (Parameter 2)
        0x00.toByte() // Le (Length of expected data)
    )
}

这里我们定义了SELECT_APDU和READ_DATA_APDU两种命令,其他commandApdu都返回错误码。

注意下SELECT_APDU命令,它是由00 A4 04 00 06 F0的固定格式,加上你的AID的字节数据,在对于的命令下返回对于的数据就完成逻辑了:

private fun selectAid(): ByteArray {
    return byteArrayOf(0x90.toByte(), 0x00.toByte()) // 返回成功状态码
}

private fun readCardData(): ByteArray {
    return CARD_DATA
}

关于这些数据格式,专业性太强了,还是按需要去找资料吧,这里就是简单的用法。

设置为默认模拟卡应用

在手机里面是可以设置默认的付款应用的,以我的荣耀10为例,默认就是华为钱包,每次使用NFC都会跳到它那里去,我们也可以去设置里面改了它:

c9ac9b339bc1e48420f52be614aeb94.jpg

也可以代码实现,封装的方法如下(不过好像other类型并没有默认一说):

/**
 * 设置默认的NFC模拟卡的HostApduService,支持payment和other两种
 *
 * @param activity 设置的activity,并提供上下文
 * @param service HCE模拟卡的HostApduService
 * @param categroty 类别,payment或other
 * @return 是否是默认的NFC处理APP
 */
fun setDefaultHostApduService(
    activity: Activity,
    service: Class<out HostApduService>,
    categroty: String = CardEmulation.CATEGORY_PAYMENT
): Boolean {
    val manager = CardEmulation.getInstance(NfcAdapter.getDefaultAdapter(activity))
    val componentName = ComponentName(activity, service.canonicalName!!)
    if (!manager.isDefaultServiceForCategory(componentName, categroty)) {
        val intent = Intent(CardEmulation.ACTION_CHANGE_DEFAULT)
        intent.putExtra(CardEmulation.EXTRA_CATEGORY, CardEmulation.CATEGORY_PAYMENT)
        intent.putExtra(CardEmulation.EXTRA_SERVICE_COMPONENT, componentName)
        activity.startActivityForResult(intent, 0)
        return false
    }
    return true
}

具体使用

按官方文档来说,上面代码已经完成了HCE功能: ,说是Android系统会自动启动咱们饿NfcHostApduService,去处理NFC事件。

不过我这好像没成功,也用下面方法试了手机是否支持HCE:

/**
 * 判断是否支持NFC HCE
 *
 * @param packageManager 包管理器
 * @return 是否支持HCE服务
 */
fun hasNfcHostCardEmulationFeature(packageManager: PackageManager): Boolean {
    return packageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
}

我这手机是支持的,上面方法返回了true,不知道是不是我用读取的方式不对,我这用苹果手机读取的,连processCommandApdu的日志打印都没显示,不知道是代码问题,还是需要两个Android手机去试,后面有机会试下,再更新文章。

源码及Demo

源码和Demo放在我练手的仓库里面,有兴趣可以看下:

参考文章

实践NFC三种工作模式,问GPT好多好多,也看了挺多很好的文章,我这写的比较简单,有兴趣可以看下其他文章:

官方文档:

简单文档:

点对点模式:

模拟卡模式:

小结

花了些时间,把NFC的三种工作模式: 读卡器模式、点对点模式、模拟卡模式实践了下,还是学到了一些东西!


网站公告

今日签到

点亮在社区的每一天
去签到