2014/10/07

libllcp in nfc-tools

1. nfc-tools

nfc-tools 開發 nfc 相關的 tool, 最有名的應該是libnfc
底下是 nfc-tools 的網頁
https://code.google.com/p/nfc-tools/

但是libnfc還是太底層, 為了實作 NDEF 或 NPP, 可能還要裝 libfreefare 或 libllcp

libfreefare 是專門用在 Mifare
https://code.google.com/p/libfreefare/

libllcp 則是一般性的NDEF communication
https://code.google.com/p/libllcp/
關於怎麼install:
http://nfc-tools.org/index.php?title=Libllcp

LLCP (Logical Link Control Protocol), 有點像是 IP (Internet Protocol)
它可以經由建立連線之後, 由兩端傳data
而 LLCP 又實作了兩種 protocol
一個是 NPP (NDEF Push Protocol), 就像是 UDP 一樣, 丟封包出去就不用等
另個是 SNEP (Simple NDEF Exchange Protocol), 就像是 TCP, 丟封包出去要等ACK, (但沒有三方交握)

2. NPP sample code

在 libllcp/examples/npp-server/ 有個npp server的範例
它的用途是建立連線之後, 接收從tag來的資料

但是裡面似乎有些bug, 所以我將它重寫並測試過, sample code如下:
(錯的部份是, header 檔像 llcp.h 應該在 nfc/llcp.h底下, 然後stdout的部份會強制導到file)

#include <err.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#include <nfc/llcp.h>
#include <nfc/llc_service.h>
#include <nfc/llc_link.h>
#include <nfc/llc_connection.h>
#include <nfc/mac.h>

#define errx(code, ...) do { \
    fprintf (stderr, __VA_ARGS__); \
    fprintf (stderr, "\n"); \
    exit (code); \
  } while (0)

struct mac_link *mac_link;
nfc_device *device;

static void stop_mac_link(int sig) {
 if (mac_link && mac_link->device) {
  nfc_abort_command(mac_link->device);
 }
}

static void bye(VOID) {
 if (device) {
  nfc_close(device);
 }
}

static void print_hex(const uint8_t *pbtData, const size_t szBytes) {
 size_t szPos;
 for (szPos = 0; szPos < szBytes; szPos++) {
  printf("%02x ", pbtData[szPos]);
 }
 printf("\n");
}

static void* com_android_npp_thread(void *arg) {
 struct llc_connection *connection = (struct llc_connection *)arg;
 uint8_t buffer[1024];

 int len;
 size_t n = 0;

 len = llc_connection_recv(connection, buffer, sizeof(buffer), NULL);
 if (len < 0) return NULL;
 if (len < 10) return NULL; // NPP's header (5 bytes) + NDEF entry header (5 bytes)

 // byte 0 : protocol version
 printf("NDEF Push Protocol version: %02x\n", buffer[n]);
 if (buffer[n] != 0x01) return NULL; // version not supported
 n++;

 // byte 1~4 : number of NDEF entries
 uint32_t ndef_entries_count = be32toh(*((uint32_t *)(buffer+n)));
 printf("NDEF entries count: %u\n", ndef_entries_count);
 if (ndef_entries_count != 1) return NULL; // Version 0x01 has value 0x00 0x00 0x00 0x01
 n += 4;

 // NDEF Entry
 printf("Action code: %02x\n", buffer[n]);
 if (buffer[n] != 0x01) return NULL; // Action code not supported
 n++;

 uint32_t ndef_length = be32toh(*((uint32_t *)(buffer+n)));
 n += 4;

 if ((len-n) < ndef_length) return NULL; // less received bytes than expected?

 printf("NDEF entry received (%u bytes): ", ndef_length);
 print_hex(buffer + n, ndef_length);

 llc_connection_stop(connection);

 return NULL;
}

int main(int argc, char *argv[]) {

 int ret;

 if (llcp_init() < 0) errx(EXIT_FAILURE, "llcp_init()");

 signal(SIGINT, stop_mac_link);
 atexit(bye);

 nfc_context *context;
 nfc_init(&context);
 if (context == NULL) errx(EXIT_FAILURE, "nfc_init()");

 device = nfc_open(context, NULL);
 if (device == NULL) errx(EXIT_FAILURE, "Cannot connect to NFC device");

 struct llc_link *llc_link = llc_link_new();
 if (llc_link == NULL) errx(EXIT_FAILURE, "Cannot allocate LLC link data structures");

 struct llc_service *com_android_npp 
  = llc_service_new_with_uri(NULL, com_android_npp_thread, "com.android.npp", NULL);
 if (com_android_npp == NULL) errx(EXIT_FAILURE, "cannot create com.android.npp service");

 llc_service_set_miu(com_android_npp, 512);
 llc_service_set_rw(com_android_npp, 2);

 ret = llc_link_service_bind(llc_link, com_android_npp, -1);
 if (ret < 0) errx(EXIT_FAILURE, "Cannot bind service");

 mac_link = mac_link_new(device, llc_link);
 if (mac_link == NULL) errx(EXIT_FAILURE, "Cannot create MAC link");

 ret = mac_link_activate_as_target(mac_link);
 if (ret < 0) errx(EXIT_FAILURE, "Cannot create MAC link");

 void *err;
 mac_link_wait(mac_link, &err);

 mac_link_free(mac_link);
 llc_link_free(llc_link);

 nfc_close(device);
 device = NULL;

 llcp_fini();
 nfc_exit(context);
 exit(EXIT_SUCCESS);
}

然後將它compile, 其中 llcp 的部份官網寫 -lnfc-llcp是錯的
 $ gcc -o npp_server_example npp_server_example.c -lnfc -lllcp

有些手機仍舊可以接受NPP
我手上有台 HTC ONE, 將它的瀏覽器打開, 把手機放在 reader 上面
然後執行剛剛的程式, 手機會感應到, 手機螢幕顯示點選以傳送資料, 點它, 程式就會收到data, 最後將手機移開reader, 程式就結束

$ ./npp_server_example
NDEF Push Protocol version: 01
NDEF entries count: 1
Action code: 01
NDEF entry received (23 bytes): d1 01 13 55 04 74 77 2e 6d 6f 62 69 2e 79 61 68 6f 6f 2e 63 6f 6d 2f

其中
74 77 2e 6d 6f 62 69 2e 79 61 68 6f 6f 2e 63 6f 6d 2f = tw.mobi.yahoo.com/
(TODO: 前面header應該可以parse)

3. npp server code analysis


llcp_init() 是使用llcp要用的初始化, 裡面設定signal handler以及log function,
llcp_fini() 則是結束時釋放資源
int llcp_init(void)
int llcp_fini(void)

nfc context 以及 nfc device 的用法和 libnfc 相同

llc_link 用於紀錄 llc link 相關資訊
struct llc_link {
  uint8_t role;
  enum {
    LL_ACTIVATED,
    LL_DEACTIVATED,
  } status;
  struct llcp_version version;
  uint16_t local_miu;
  uint16_t remote_miu;
  uint16_t remote_wks;
  struct timeval local_lto;
  struct timeval remote_lto;
  uint8_t local_lsc;
  uint8_t remote_lsc;
  uint8_t opt;

  pthread_t thread;
  char *mq_up_name;
  char *mq_down_name;
  mqd_t llc_up;
  mqd_t llc_down;

  struct llc_service *available_services[MAX_LLC_LINK_SERVICE + 1];
  struct llc_connection *datagram_handlers[MAX_LOGICAL_DATA_LINK];
  struct llc_connection *transmission_handlers[MAX_LLC_LINK_SERVICE + 1];

  /* Unit tests metadata */
  void *cut_test_context;
  struct mac_link *mac_link;
};

用 llc_link_new() 來設定 llc link 的 default 值
struct llc_link* llc_link_new(void)


llc_service 用來建 LLC(logical link control) 會用到的相關設定
struct llc_service {
  char *uri;
  void *(*accept_routine)(void *);
  void *(*thread_routine)(void *);
  int8_t sap;
  uint8_t rw;
  uint16_t miu;
  void *user_data;
};

llc_service_new_with_uri() 則是設定 llc_service 成default
struct llc_service* llc_service_new_with_uri(void * (*accept_routine)(void *),
                                             void * (*thread_routine)(void *),
                                             const char *uri,
                                             void *user_data)
accept_routine 是 thread function, 在建立連線時(收到PDU_CONNECT) 呼叫
thread_routine 也是 thread function, 在收到 PDU_UI 時呼叫
uri 應該只是 llc service identify
user_data 可以送 data, 這個例子裡沒用到

有了相關設定之後, 可以bind service
int llc_link_service_bind(struct llc_link *link, struct llc_service *service, int8_t sap)


接著是 mac layer 相關設定
struct mac_link {
  enum { MAC_LINK_UNSET, MAC_LINK_INITIATOR, MAC_LINK_TARGET } mode;
  nfc_device *device;
  struct llc_link *llc_link;
  uint8_t nfcid[10];
  uint8_t buffer[BUFSIZ];
  size_t buffer_size;
  pthread_t *__restrict__ exchange_pdus_thread;
};

一樣有設定default值的function
struct mac_link* mac_link_new(nfc_device *device, struct llc_link *llc_link)

然後enable mac layer setting
int mac_link_activate_as_target(struct mac_link *mac_link)
裡面做的事是用 libnfc 選 target tag
其中 mac_link_run() 開了一條thread 用於傳送接收apdu
預設是
void* mac_link_exchange_pdus(void *arg)

最後用這個 function 等 mac_link_exchange_pdus 這條 thread 結束
int mac_link_wait(struct mac_link *link, void **value_ptr)

4. snep sample code

我一樣改寫了 libllcp 的 libllcp/examples/snep-server/

#include <err.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#include <nfc/llcp.h>
#include <nfc/llc_service.h>
#include <nfc/llc_link.h>
#include <nfc/llc_connection.h>
#include <nfc/mac.h>

#define LOG_DGB 1

#define errx(code, ...) do { \
    fprintf (stderr, __VA_ARGS__); \
    fprintf (stderr, "\n"); \
    exit (code); \
  } while (0)

struct mac_link *mac_link;
nfc_device *device;

static void stop_mac_link(int sig) {
 if (mac_link && mac_link->device) {
  nfc_abort_command(mac_link->device);
 }
}

static void bye(VOID) {
 if (device) {
  nfc_close(device);
 }
}

static void print_hex(const uint8_t *pbtData, const size_t szBytes) {
 size_t szPos;
 for (szPos = 0; szPos < szBytes; szPos++) {
  printf("%02x ", pbtData[szPos]);
 }
 printf("\n");
}

static void* com_android_snep_thread(void *arg) {
 struct llc_connection *connection = (struct llc_connection *) arg;
 uint8_t buffer[1024];
 uint8_t frame_snep_response_success[] = {
  0x10, 0x81, 0x00, 0x00, 0x00, 0x00
 };

 int len;
 len = llc_connection_recv(connection, buffer, sizeof(buffer), NULL);
 if (len < 0 || len < 7) {
  // SNEP's header (2 bytes) + NDEF entry header (5 bytes)
printf("ERR: invalid length\n"); return NULL; } size_t n = 0; printf("SNEP version: %d.%d\n", buffer[n]>>4, buffer[n] & 0x0f ); if (buffer[n] != 0x10) { printf("WRN: snep version other than version 1.0 may not support.\n"); } n++; uint8_t action_code = buffer[n]; n++; switch(action_code) { case 0x00: // Continue break; case 0x01: // GET break; case 0x02: // PUT { uint32_t ndef_length = be32toh(*((uint32_t *)(buffer + n))); if ((len - 6) < ndef_length) { printf("ERR: less received bytes than expected?\n"); return NULL; } n+=4; /** return snep success response package */ llc_connection_send(connection, frame_snep_response_success, sizeof(frame_snep_response_success)); print_hex(buffer + n, ndef_length); break; } default: printf("unknown action code\n"); break; } llc_connection_stop(connection); return NULL; } int main(int argc, char *argv[]) { int ret; if (llcp_init() < 0) errx(EXIT_FAILURE, "llcp_init()"); signal(SIGINT, stop_mac_link); atexit(bye); nfc_context *context; nfc_init(&context); if (context == NULL) errx(EXIT_FAILURE, "nfc_init()"); device = nfc_open(context, NULL); if (device == NULL) errx(EXIT_FAILURE, "Cannot connect to NFC device"); struct llc_link *llc_link = llc_link_new(); if (llc_link == NULL) errx(EXIT_FAILURE, "Cannot allocate LLC link data structures"); struct llc_service *com_android_snep = llc_service_new_with_uri(NULL, com_android_snep_thread, "urn:nfc:sn:snep", NULL); if (com_android_snep == NULL) errx(EXIT_FAILURE, "cannot create com.android.npp service"); llc_service_set_miu(com_android_snep, 512); llc_service_set_rw(com_android_snep, 2); ret = llc_link_service_bind(llc_link, com_android_snep, LLCP_SNEP_SAP); if (ret < 0) errx(EXIT_FAILURE, "Cannot bind service"); mac_link = mac_link_new(device, llc_link); if (mac_link == NULL) errx(EXIT_FAILURE, "Cannot create MAC link"); ret = mac_link_activate_as_target(mac_link); if (ret < 0) errx(EXIT_FAILURE, "Cannot create MAC link"); void *err; mac_link_wait(mac_link, &err); mac_link_free(mac_link); llc_link_free(llc_link); nfc_close(device); device = NULL; llcp_fini(); nfc_exit(context); exit(EXIT_SUCCESS); }


compile 之後, 實驗方式跟 npp server 一樣

5. code analysis


整份 code 跟 npp server 很像, 但有些小差異
在 llc_link_service_bind(), 後面帶的 sap 是 LLCP_SNEP_SAP

另個差異是 header 不一樣

然後在收到data之後, 要回傳OK packet

其它的就沒有差異了




8 則留言:

  1. 請問一下可以用snep-server或snep-client把data (比如文字或網址甚至圖片) 從PN532傳送到android嗎?
    如果可以請問要怎樣做到?

    回覆刪除
  2. 我沒有實際試過, 但我猜 PN532 應該要扮演 snep-client 的角色, 也就是NFC裡的TAG, 讓Android 扮演 NFC的reader, 然後把要傳的東西包成NDEF, NDEF的格式在網路上可以找到, 文字或圖片的格式都不太一樣

    回覆刪除
  3. 我試過用snep-client把http://google.com傳到手機, 我把com_android_snep_service下的uint8_t frame[] 改成了
    { 0xD1, 0x01, 0x0B, 0x55, 0x01,
    0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d } // 這串是google.com
    可是手機只震了一下, 沒有把http://google.com傳過去
    請問要像snep-server一樣改他的source code還是有其他問題?

    回覆刪除
    回覆
    1. 如果要像snep-server一樣改source code的話 請問應該怎樣改? 一直google也找不到解決方法..

      刪除
    2. 很抱歉, 最近有點忙, 沒辦法把玩PN532這張卡, 所以在code上面沒辦法給有用的建議。
      底下是一些debug的方向供你參考:
      我之前用過不同手機測試, 有的手機會正確收到data, 但有的只會震, 可能要看手機的log才會知道它實際上把它認成什麼。
      Android 偵測到卡的時候會嘗試解讀卡的內容 (同時會震或者會嗶一聲), 卡分成三種, ACTION_TAG_DISCOVERED, ACTION_TECH_DISCOVERED, ACTION_NDEF_DISCOVERED, 只有ACTION_NDEF_DISCOVERED 會真的把NDEF的內容讀出來並做出對應的事情 (Ex. 開網頁).
      你可以抓一些讀TAG的APP, (Ex. NXP的NFC Reader), 用這些APP讀看看實際上它把卡認成什麼內容。

      刪除
  4. 您好,

    拙學有一個NFC的疑惑請前輩指引,NFC手機和NFC晶片卡之間,
    從晶片卡讀取資料,處理加入其他資料(如時間),再寫入晶片卡,
    這一連串動作,是否可以在一次感應下就完成?
    因為目前拙學作出來是感應時,只有讀取,不會寫入,再感應才會寫入,
    也就是讀取和寫入要分兩次感應,能不能一次感應就完成讀取,處理,寫入的動作呢?
    查過許多參考書和網路資料,有的說可以,但都沒有實例,全部都是讀寫分開,
    到底可不可以一次感應就完成呢?

    拙學 Stephen tzushow@gmail.com

    回覆刪除
  5. 您好,

    拙學有一個NFC的疑惑請前輩指引,NFC手機和NFC晶片卡之間,
    從晶片卡讀取資料,處理加入其他資料(如時間),再寫入晶片卡,
    這一連串動作,是否可以在一次感應下就完成?
    因為目前拙學作出來是感應時,只有讀取,不會寫入,再感應才會寫入,
    也就是讀取和寫入要分兩次感應,能不能一次感應就完成讀取,處理,寫入的動作呢?
    查過許多參考書和網路資料,有的說可以,但都沒有實例,全部都是讀寫分開,
    到底可不可以一次感應就完成呢?

    拙學 Stephen tzushow@gmail.com

    回覆刪除
  6. Dear Andrew,
    我在 Android手機 + NXP 203F Tag 上實作過, 當Android detect 到 NFC TAG 的時候, 只要藉由Tag物件就可以寫入Tag

    回覆刪除