[NFC] 读羊城通卡片信息
學習開源項目NFCard,最新版源碼以及幾年前代碼相比較,發現之前的版本可以支持讀羊城通,而現在不再支持讀羊城通卡信息,那定一個小目標。通過NFC讀取羊城通卡片信息之余額和交易記錄。
實現的效果如圖:
目錄
1.建立工程,編寫NFC相關代碼;
2.根據開源項目中的指令,讀取余額;
3.根據開源項目中的指令,讀取交易記錄;
4.根據卡片原始信息解析數據;
一、編寫NFC相關代碼
import android.app.PendingIntent;import android.content.Intent;import android.nfc.NfcAdapter;import android.nfc.Tag;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity {private NfcAdapter mNfcAdapter;private PendingIntent mPendingIntent;private Intent mIntent;private final int READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK;private NfcAdapter.ReaderCallback mReaderCallback = new NfcAdapter.ReaderCallback() {@Overridepublic void onTagDiscovered(Tag tag) {System.out.println(tag.toString());}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}@Overrideprotected void onStart() {super.onStart();initNfc();}@Overrideprotected void onResume() {super.onResume();registerNfc();}@Overrideprotected void onPause() {super.onPause();unRegisterNfc();}private void initNfc() {mNfcAdapter = NfcAdapter.getDefaultAdapter(this);mIntent = new Intent(NfcAdapter.ACTION_TECH_DISCOVERED);mIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);mPendingIntent = PendingIntent.getActivity(this, 0, mIntent, 0);}private void registerNfc() {Bundle bundle = new Bundle();mNfcAdapter.enableReaderMode(this, mReaderCallback, READER_FLAGS, bundle);}private void unRegisterNfc() {mNfcAdapter.disableReaderMode(this);}}運行上述代碼的效果:(羊城通緊貼在手機NFC感應處)
I/System.out: TAG: Tech [android.nfc.tech.IsoDep, android.nfc.tech.NfcA]看源碼解釋一下:
注冊NFC調用了NfcAdapter的enableReaderMode方法,先看看源碼:
三行代碼三大段注釋,nice。enableReaderMode()摳腳翻譯如下:
限制NFC的模式為讀卡器模式
在這種模式中,就僅僅是可以用NFC讀寫帶有NFC芯片的標簽(卡片、貼紙等),不允許點對點模式和卡模擬模式。
可以通過FLAG_READER_SKIP_NDEF_CHECK這個標志過濾NDEF標簽,這個就是標志就是第三個參數啦,NDEF(NFC Data Exchange Format,NFC數據交換格式)是Android SDK API主要支持NFC論壇標準。
如果是準備與另一臺Android卡模擬設備交互,那么建議設置的標志就是FLAG_READER_NFC_A和FLAG_READER_SKIP_NDEF_CHECK
參數callback:發現符合的標簽就回調到callback
參數extras : 對讀卡器模式進行一些配置(先晾它一會,目前只是傳了一個空bundle進去)
參數flags:標志
上述參數中有一個flags,我們順便也看看有哪些flag以及flag的作用是什么,看源碼然后摳腳解釋下:
/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag enables polling for Nfc-A technology.*/public static final int FLAG_READER_NFC_A = 0x1;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag enables polling for Nfc-B technology.*/public static final int FLAG_READER_NFC_B = 0x2;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag enables polling for Nfc-F technology.*/public static final int FLAG_READER_NFC_F = 0x4;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag enables polling for Nfc-V (ISO15693) technology.*/public static final int FLAG_READER_NFC_V = 0x8;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag enables polling for NfcBarcode technology.*/public static final int FLAG_READER_NFC_BARCODE = 0x10;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag allows the caller to prevent the* platform from performing an NDEF check on the tags it* finds.*/public static final int FLAG_READER_SKIP_NDEF_CHECK = 0x80;/*** Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.* <p>* Setting this flag allows the caller to prevent the* platform from playing sounds when it discovers a tag.*/public static final int FLAG_READER_NO_PLATFORM_SOUNDS = 0x100;| FLAG_READER_NFC_A | 0x1 | 支持NFCA數據格式 |
| FLAG_READER_NFC_B | 0x2 | 支持NFCB數據格式 |
| FLAG_READER_NFC_F | 0x4 | 支持NFCF數據格式 |
| FLAG_READER_NFC_V | 0x8 | 支持NFCV數據格式 |
| FLAG_READER_NFC_BARCODE | 0x10 | 支持NFCBARCODE數據格式 |
| FLAG_READER_SKIP_NDEF_CHECK | 0x80 | 過濾NDEF數據格式 |
| FLAG_READER_NO_PLATFORM_SOUNDS | 0x100 | 關閉發現TAG時的聲音 |
看完上述,估計有點蒙,好像也有點跑偏,趕緊回到注冊NFC的這個方法中,我們在onResume中調用了enableReaderMode,此方法在卡片(此處指羊城通)貼到手機NFC感應處時會回調到ReaderCallback中,所以我們在onTagDiscovered這個回調中即可與卡片進行交互。
二、根據指令讀取羊城通余額
首先我們可以去交通信息中心下載一份《城市公共交通IC卡技術規范》卡片的部分,認真去閱讀(一頭扎進去估計難看懂),我們知道選擇目錄的指令為:00A40400+lc+文件名+00;讀取余額的指令為:805C000204(指令為7816報文格式)
其次我們可以去閱讀NFCard這個開源項目,從源碼中知道,選擇的文件名為:”PAY.TICL”,lc為:08
到此,我們整理下所需指令:
| 00A40400085041592E5449434C00 | 選擇PAY.TICL目錄(P的十六進制ASCII碼為50) |
| 805C000204 | 讀取余額 |
上述拼接指令過程中,需要把字符換成對應的十六進制ASCII碼,好在Google的Sample給我們提供了這些轉換方法,恩,又可以抄一波(具體Sample路徑:”sdk根目錄”\samples\android-“version”\connectivity\CardReader..\LoyaltyCardReader.java):
/*** Utility class to convert a byte array to a hexadecimal string.** @param bytes Bytes to convert* @return String, containing hexadecimal representation.*/public static String ByteArrayToHexString(byte[] bytes) {final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};char[] hexChars = new char[bytes.length * 2];int v;for ( int j = 0; j < bytes.length; j++ ) {v = bytes[j] & 0xFF;hexChars[j * 2] = hexArray[v >>> 4];hexChars[j * 2 + 1] = hexArray[v & 0x0F];}return new String(hexChars);}/*** Utility class to convert a hexadecimal string to a byte string.** <p>Behavior with input strings containing non-hexadecimal characters is undefined.** @param s String containing hexadecimal characters to convert* @return Byte array generated from input*/public static byte[] HexStringToByteArray(String s) {int len = s.length();byte[] data = new byte[len / 2];for (int i = 0; i < len; i += 2) {data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)+ Character.digit(s.charAt(i+1), 16));}return data;}有了卡片,有了卡片指令,我們就開始通過NFC進行交互
回到我們NFC的回調方法中,我們可以從回調方法onTagDiscovered(Tag tag)拿到一個TAG,這個TAG從輸出的日志看,有IsoDep,通過IsoDep類的transceive方法即可發送指令數據到卡片并且返回響應數據:
具體數據為:
I/System.out: 指令報文:00A40400085041592E5449434C00 響應報文:6F3484085041592E5449434CA5289F0801029F0C21FFFFFFFFFFFFFFFF000000000000000000000000000000002016122400000186A09000 I/System.out: 指令報文:805C000204 響應報文:00000E479000根據7816報文格式,響應報文格式為DATA+SW1+SW2,SW1和SW2為狀態字,分別占一個字節,由此DATA=00000E47,SW1=90,SW2=00。00000E47轉十進制則是3655,這個時候我們用QQ來讀一下羊城通對比一下余額是否正確,QQ讀出余額如下圖:
我們使用的transceive方法,將原始數據發送至卡片標簽,并且得到響應,如果中途移開卡片,則會拋出TagLostException(也是繼承IOException),如果中途讀寫失敗或者取消,則拋出IOException。
/*** Send raw ISO-DEP data to the tag and receive the response.** <p>Applications must only send the INF payload, and not the start of frame and* end of frame indicators. Applications do not need to fragment the payload, it* will be automatically fragmented and defragmented by {@link #transceive} if* it exceeds FSD/FSC limits.** <p>Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes* that can be sent with {@link #transceive}.** <p>This is an I/O operation and will block until complete. It must* not be called from the main application thread. A blocked call will be canceled with* {@link IOException} if {@link #close} is called from another thread.** <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.** @param data command bytes to send, must not be null* @return response bytes received, will not be null* @throws TagLostException if the tag leaves the field* @throws IOException if there is an I/O failure, or this operation is canceled*/public byte[] transceive(byte[] data) throws IOException { return transceive(data, true);}小結:
我們通過開源項目NFCard、Google的Sample之CardReader、交通信息中心的《城市公共交通IC卡技術規范》文檔,成功讀出了羊城通余額。
這里需要提醒的是,最新版的NFCard源碼讀不出我手中的羊城通,顯示為未知卡片,反而找到2013年的版本才讀出來,本人手中的卡有效期也是2013年-2018年。這樣估計是各版本的卡有所不同。
接下來就是讀取卡片的卡號、有效期、交易記錄這些信息,并且解析數據,顯示在界面上。
總結
以上是生活随笔為你收集整理的[NFC] 读羊城通卡片信息的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 初识beego
- 下一篇: 用C语言实现简单的一字棋游戏