12/08/2018, 14:59

Lấy block data của Felica (NFC) ở Android

Bài viết dịch từ http://qiita.com/pear510/items/38f94d61c020a17314b6 Thẻ sinh viên của tôi ở đại học ở thẻ Felica, tôi muốn lấy dữ liệu ở thẻ đó nên đã quyết định sẽ học coding ở Android. Tuy nhiên, dù đã tìm hiểu rất nhiều nhưng các bài viết hay sách thường chỉ đề cập đến việc lấy IDm nên tôi ...

Bài viết dịch từ http://qiita.com/pear510/items/38f94d61c020a17314b6

Thẻ sinh viên của tôi ở đại học ở thẻ Felica, tôi muốn lấy dữ liệu ở thẻ đó nên đã quyết định sẽ học coding ở Android. Tuy nhiên, dù đã tìm hiểu rất nhiều nhưng các bài viết hay sách thường chỉ đề cập đến việc lấy IDm nên tôi quyết định sẽ viết về việc lấy block data.

Felica được cấu tạo từ System / Area /Service / Block data. Lần này tôi muốn lấy block data trong phần service. Ngoài ra, Felica có thể chia hệ thống ra nhỏ hơn, cũng có thể tạo ra nhiều hệ thống. Từng hệ thống đó tách rời nhau về mặt function, security, không giao thoa với nhau. Felica lần này tôi muốn lấy sẽ có cấu trúc như dưới đây:

- System 0: (System Code -> 0x8CDA)
    - IDm
    - Area
    - Area
        - Service
        - Service
        - ...
- System 1: (System Code -> 0xFE00)
    - IDm
    - Area
    - Area
    - ...
    - Area
        - Service
        - Service: (Service Code -> 0x1A8B) ← Muốn access vào đây
        - ...

Dù có thay đổi thẻ sinh viên thì vẫn dùng chung 1 System Code, Service Code.

Dưới đây là phương pháp để lấy Felica. (Felica thuộc NFC-TypeF.) Lần này tôi chỉ muốn lấy Felica khi App khởi động sử dụng enableForegroundDispatch.

NFCReaderActivity.java
private IntentFilter[] intentFiltersArray;
private String[][] techListsArray;
private NfcAdapter mAdapter;
private PendingIntent pendingIntent;
private NfcReader nfcReader = new NfcReader();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_nfcreader);

    pendingIntent = PendingIntent.getActivity(
            this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

    IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

    try {
        ndef.addDataType("text/plain");
    }
    catch (IntentFilter.MalformedMimeTypeException e) {
        throw new RuntimeException("fail", e);
    }
    intentFiltersArray = new IntentFilter[] {ndef};

    // Felica là NFC-TypeF nên chỉ cần chỉ định NfcF là OK
    techListsArray = new String[][] {
        new String[] { NfcF.class.getName() }
    };

    // Lấy NfcAdapter
    mAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
}

@Override
protected void onResume() {
    super.onResume();
    // Kích hoạtt lấy NFC
    mAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
}

@Override
protected void onNewIntent(Intent intent) {
// Lấy vì data cơ bản của Tag sẽ đi vào trong Intent.
    Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag == null) {
        return;
    }

    // Sử dụng Tag đã lấy ở đây để tiến hành load data
}

@Override
protected void onPause() {
    super.onPause();
    mAdapter.disableForegroundDispatch(this);
}
Ngoài ra, đừng quên viết sẵn phần dưới đây vào Manifest nhé!
AndroidManifest.xml
...

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

...

Ở bước này, khi khởi động App và giữ NFC thì data sẽ đi vào onNewIntent. Khi lấy Tag từ Intent như trên thì sẽ có thể access vào Felica đang giữ từ Tag đã lấy.

Đầu tiên, cần sử dụng command Felica để access vào Felica.

Để access vào Felica ở Android, chỉ cần connect bằng NfcF.connect() của package android.nfc.NfcF có trong SDK, gửi command bằng NfcF.tranceive(byte[] data) rồi close lại bằng NfcF.close() là được. Command gửi đi sẽ được reform thành packet data.

Để command, cần có IDm của system region chứa service mình muốn lấy. Trường hợp muốn access vào service thuộc region System 0, có thể lấy IDm của region System 0 từ tag đã lấy ở trên bằng cách sử dụng tag.getId(). Tuy nhiên, lần này tôi muốn access vào Service thuộc region System 1 nên trước tiên, cần tiến hành xử lý polling rồi lấy IDm của System 1. Sau đó, sử dụng IDm đã lấy để lấy block data mong muốn. Ngoài ra, command cần thiết là command Poliing và command Read Without Rncryption. ※ Read Without Rncryption dùng khi acces data không cần xác thực, trường hợp access đến data cần xác thực thì sẽ dùng command khác.

NfcReader.java
import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class NfcReader {

    public byte[][] readTag(Tag tag) {
        NfcF nfc = NfcF.get(tag);
        try {
            nfc.connect();
            //System code của  System 1-> 0xFE00
            byte[] targetSystemCode = new byte[]{(byte) 0xfe,(byte) 0x00};

            // Tạo command polling 
            byte[] polling = polling(targetSystemCode);

            // Gửi command rồi lấy kết quả            byte[] pollingRes = nfc.transceive(polling);

            // Lấy IDm của System 0 (Byte thứ 1 là data size, byte thứ 2 là response code, size của IDm là 8 byte)
            byte[] targetIDm = Arrays.copyOfRange(pollingRes, 2, 10);

            // Size của data chứa trong service (Lần này là 4)
            int size = 4;

            // Service code của đối tượng là  -> 0x1A8B
            byte[] targetServiceCode = new byte[]{(byte) 0x1A, (byte) 0x8B};

            // Tạo command Read Without Encryption
            byte[] req = readWithoutEncryption(targetIDm, size, targetServiceCode);

            // Gửi command rồi lấy kết quả
            byte[] res = nfc.transceive(req);

            nfc.close();

            // Parse kết quả, chỉ lấy data
            return parse(res);
        } catch (Exception e) {
            Log.e(TAG, e.getMessage() , e);
        }

        return null;
    }

    /**
     * Lấy command Pollin
     * System code chỉ định @param systemCode byte[] 
     * Command @return Polling
     * @throws IOException
     */
    private byte[] polling(byte[] systemCode) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0x00);           // Dummy của byte độ dài data
        bout.write(0x00);           // Command code
        bout.write(systemCode[0]);  // systemCode
        bout.write(systemCode[1]);  // systemCode
        bout.write(0x01);           // Request code
        bout.write(0x0f);           // Timeslot

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 1 byte ở đầu là chiều dài data
        return msg;
    }

    /**
     * Lấy command Read Without Encryption 
     * ID của system chỉ định @param IDm 
     * Số data lấy @param size 
     * Command @return Read Without Encryption
     * @throws IOException
     */
    private byte[] readWithoutEncryption(byte[] idm, int size, byte[] serviceCode) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0);              // データ長バイトのダミー
        bout.write(0x06);           // コマンドコード
        bout.write(idm);            // IDm 8byte
        bout.write(1);              // サービス数の長さ(以下2バイトがこの数分繰り返す)

        // Việc chỉ định service code là little endian nên sẽ chỉ định từ low byte.
        bout.write(serviceCode[1]); // Service code low byte
        bout.write(serviceCode[0]); // Service code high byte
        bout.write(size);           // Số block

        // Chỉ định Block number
        for (int i = 0; i < size; i++) {
            bout.write(0x80);       // Block element high byte 
            bout.write(i);          // Block number
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 1 byte ở đầu là chiều dài của data
        return msg;
    }

    /**
     * Phân tích kết quả Read Without Encryption
     * @param res byte[]
     * Show string @return 
     * @throws Exception
     */
    private byte[][] parse(byte[] res) throws Exception {
        // res[10] error code. Trường hợp 0x00 thì không có vấn đề gì
        if (res[10] != 0x00) 
            throw new RuntimeException("Read Without Encryption Command Error");

        // res[12] Số block trả lời
        // res[13 + n * 16]  Lặp lại 16 data thực (byte/block)
        int size = res[12];
        byte[][] data = new byte[size][16];
        String str = "";
        for (int i = 0; i < size; i++) {
            byte[] tmp = new byte[16];
            int offset = 13 + i * 16;
            for (int j = 0; j < 16; j++) {
                tmp[j] = res[offset + j];
            }

            data[i] = tmp;
        }
        return data;
    }
}

Chú ý

• PacketData gửi bằng NfcF.tranceive(byte[] data) cần gắn size data vào byte thứ 1. Ngoài ra, size data này sẽ là size chứa size data gắn vào byte thứ 1. • Khi nhận command không phù hợp, Felica không trả về error mà sẽ không có phản ứng. Đại khái, khi NfcF.tranceive(byte[] data) thì sẽ không phản ứng, android.nfc.TagLostException sẽ bị chậm lại.

Command Polling

Là command lấy ID(IDm) cấu trúc và parameter cấu trúc (PMm) của hệ thống chỉ định bằng system command. Bằng việc chỉ định request code hoặc timeslot mà có thể chỉ định được system code của hệ thống hay chỉ định được số timeslot tối đa có thể response.

Ký hiệu của size tất cả là 16 số thập phân.

Command packet data

Tên Parameter Size Data Ghi chú Ex
Command code 1 0x00 0x00
System code 2 Chỉ đinh system code 0xFE00
Request code 1 Text Chỉ định data request 0x00: Không yêu cầu 0x01: Yêu cầu request code 0x02: Yêu cầu tính năng gửi Khác: Đặt trước 0x01
Timeslot 1 Chỉ định số slot tối đa có thể response 0x0F

Data packet response

Tên Parameter Size Data Ghi chú Ex
Response code 1 0x00 0x00
System code 2 Chỉ định system code 0xFE00
Request code 1 Chỉ định request data 0x00: Không yêu cầu 0x01: Yêu cầu system code 0x02: Yêu cầu tính năng gửi Khác: Đặt trước 0x01
Timeslot 1 Chỉ định số timeslot tối đa có thể response 0x0F

Read Without Encryption

Là command dùng để load block data từ service không cần xác thực.

Command packet data

Tên Parameter Size Data Ghi chú Ex
Command code 1 0x06 0x06
IDm 8 targetIDm
Số Service 1 m 1 <= m <= 16 1
List service code 2m Service code chi định bằng little endian 0x01
Số block 1 n 4
List block N 2n <= N <= 3n

Response packet data

Tên parameter Size Data Ghi chú Ex
Response code 1 0x07 0x07
IDm 8 targetIDm
Status flag 1 1 0xFE00
Status flag 2 1 0x01
Số block 1 n Chỉ đưa ra trong trường hợp status flag 1 là 0x00
Block data 16n Chỉ đưa ra trong trường hợp status flag 1 là 0x00
0