32
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WindowsでNFCタグを読み取る

Posted at

【目的】

Windows端末で社員証などのNFCタグからタグの固有ID(IDm)を取得して、アプリケーションで利用します。
先に公開した「Visual Studio 2019 によるExcelアドインの作成」「Visual Studio 2019 によるExcelアドインの作成 - VBAからアドイン内のメソッドを呼び出す」と併用することで、ExcelからAddIn経由でNFC情報を扱うことも可能になります。

【使用機器】

機器 備考
パソコン Windows 10 Pro
NFCリーダ Sony PaSoRi RC-S380/S
開発環境 VisualStudio 2019/2017

【各種ライブラリなど】

Windows環境で利用できるNFC開発環境を以下にリストアップします。
比較的容易に実装できて、無償で利用できるPSCS(またはPCSC-sharp)の利用をお勧めします。

Personal Computer/Smart Card (winscard.dll)

Windows7以降ではOSに標準で添付 (無償)
DLLImportすれば.Net開発環境(C#、VB)から利用可能 (← 以下にサンプルコードあり)

【winscard.h header - MSDN】

【Advanced Card Systems Ltd.の仕様書】

PCSC-sharp

上記のPCSC(winscard.dll)を.Net環境から使えるようにするラッパー。
ライセンス形態はこちら
PCSC.Iso7816と組み合わせて使う。

【PCSC - Nuget】
【PCSC.Iso7816 - Nuget】

【PCSC-sharp - Github】

Windows.Devices.SmartCards

UWPアプリ用の標準ライブラリ (無償)

【Windows.Devices.SmartCards Namespace - MSDN】

SDK for NFC Starter Kit

SONYが提供しているSDK。
商用利用は有償。

【SDK for NFC Starter Kit - SONY】

nfcpy

Python用のNFCライブラリ。
以前はPython2にしか対応していなかったが、最近Python3に対応したらしい。
Windowsでは別途ドライバの導入作業が必要。(要管理者権限)
ライセンス形態はEUPL1.1

【nfcpy - PyPi】
【nfcpy - Github】

【PCSCによる処理フロー】

基本的には以下の処理フローを踏襲します。

image.png

1. SCardEstablishContext

リソースマネージャに接続してハンドルを取得します。

2. SCardListReaders

PCに接続されているNFCリーダを取得します。(複数可)
取得できなかった場合は接続されるまでループするか、エラーで処理を中止します。

3. SCardConnect

接続されているNFCリーダを指定して、カード(NFCタグ)に接続します。
カードと接続できなかった場合、接続されるまでループするか、エラーで処理を中止します。
なお、NFCリーダ上に読み取れるカードがなかった場合はエラーとなるため、基本的にはループしてカードと接続できるまで待ちます。

4. SCardTransmit

接続したカードにコマンドを送信し、結果を受信します。
ここでIDmを取得したり、カードに保存されている情報を読取り/書込みをお行います。
コマンドや受信データは取得したい情報によって異なるため、該当の仕様書を確認する必要があります。

5. SCardDisconnect

カードから切断します。
これで一連の処理は完了です。

winscard.dllによるサンプル実装 (C# コンソールアプリ)

プログラム本体

Program.cs
using PCSC;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PCSC_Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            IntPtr hContext = IntPtr.Zero;

            // ##################################################
            // 1. SCardEstablishContext
            // ##################################################
            Console.WriteLine("***** 1. SCardEstablishContext *****");
            uint ret = Api.SCardEstablishContext(Constant.SCARD_SCOPE_USER, IntPtr.Zero, IntPtr.Zero, out hContext);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                string message;
                switch (ret)
                {
                    case Constant.SCARD_E_NO_SERVICE:
                        message = "サービスが起動されていません。";
                        break;
                    default:
                        message = "サービスに接続できません。code = " + ret;
                        break;
                }
                throw new ApplicationException(message);
            }

            if (hContext == IntPtr.Zero)
            {
                throw new ApplicationException("コンテキストの取得に失敗しました。");
            }
            Console.WriteLine(" サービスに接続しました。");


            // ##################################################
            // 2. SCardListReaders
            // ##################################################
            Console.WriteLine("***** 2. SCardListReaders *****");
            uint pcchReaders = 0;

            // NFCリーダの文字列バッファのサイズを取得
            ret = Api.SCardListReaders(hContext, null, null, ref pcchReaders);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                // 検出失敗
                throw new ApplicationException("NFCリーダを確認できません。");
            }

            // NFCリーダの文字列を取得
            byte[] mszReaders = new byte[pcchReaders * 2]; // 1文字2byte
            ret = Api.SCardListReaders(hContext, null, mszReaders, ref pcchReaders);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                // 検出失敗
                throw new ApplicationException("NFCリーダの取得に失敗しました。");
            }


            UnicodeEncoding unicodeEncoding = new UnicodeEncoding();
            string readerNameMultiString = unicodeEncoding.GetString(mszReaders);

            // 認識したNDCリーダの最初の1台を使用
            int nullindex = readerNameMultiString.IndexOf((char)0);
            var readerName = readerNameMultiString.Substring(0, nullindex);
            Console.WriteLine(" NFCリーダを検出しました。 " + readerName);



            // ##################################################
            // 3. SCardConnect
            // ##################################################
            Console.WriteLine("***** 3. SCardConnect *****");
            IntPtr hCard = IntPtr.Zero;
            IntPtr activeProtocol = IntPtr.Zero;
            ret = Api.SCardConnect(hContext, readerName, Constant.SCARD_SHARE_SHARED, Constant.SCARD_PROTOCOL_T1, ref hCard, ref activeProtocol);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("カードに接続できません。code = " + ret);
            }
            Console.WriteLine(" カードに接続しました。");



            // ##################################################
            // 4. SCardTransmit
            // ##################################################
            Console.WriteLine("***** 4. SCardTransmit *****");
            uint maxRecvDataLen = 256;
            var recvBuffer = new byte[maxRecvDataLen + 2];
            var sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, 0x00 };  // ← IDmを取得するコマンド

            Api.SCARD_IO_REQUEST ioRecv = new Api.SCARD_IO_REQUEST();
            ioRecv.cbPciLength = 255;

            int pcbRecvLength = recvBuffer.Length;
            int cbSendLength = sendBuffer.Length;

            IntPtr handle = Api.LoadLibrary("Winscard.dll");
            IntPtr pci = Api.GetProcAddress(handle, "g_rgSCardT1Pci");
            Api.FreeLibrary(handle);

            ret = Api.SCardTransmit(hCard, pci, sendBuffer, cbSendLength, ioRecv, recvBuffer, ref pcbRecvLength);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("NFCカードへの送信に失敗しました。code = " + ret);
            }

            // 受信データからIDmを抽出する
            // recvBuffer = IDm + SW1 + SW2 (SW = StatusWord)
            // SW1 = 0x90 (144) SW1 = 0x00 (0) で正常だが、ここでは見ていない
            string cardId = BitConverter.ToString(recvBuffer, 0, pcbRecvLength - 2);
            Console.WriteLine(" カードからデータを取得しました。");
            Console.WriteLine(" 【IDm】:" + cardId);


            // ##################################################
            // 5. SCardDisconnect
            // ##################################################
            Console.WriteLine("***** 5. SCardDisconnect *****");
            ret = Api.SCardDisconnect(hCard, Constant.SCARD_LEAVE_CARD);
            if (ret != Constant.SCARD_S_SUCCESS)
            {
                throw new ApplicationException("NFCカードとの切断に失敗しました。code = " + ret);
            }
            Console.WriteLine(" カードを切断しました。");
        }
    }
}

API定義

nfcapi.cs
using System;
using System.Runtime.InteropServices;

namespace PCSC
{
    class Api
    {
        [DllImport("winscard.dll")]
        public static extern uint SCardEstablishContext(uint dwScope, IntPtr pvReserved1, IntPtr pvReserved2, out IntPtr phContext);

        [DllImport("winscard.dll", EntryPoint = "SCardListReadersW", CharSet = CharSet.Unicode)]
        public static extern uint SCardListReaders(
          IntPtr hContext, byte[] mszGroups, byte[] mszReaders, ref UInt32 pcchReaders);

        [DllImport("winscard.dll")]
        public static extern uint SCardReleaseContext(IntPtr phContext);

        [DllImport("winscard.dll", EntryPoint = "SCardConnectW", CharSet = CharSet.Unicode)]
        public static extern uint SCardConnect(IntPtr hContext, string szReader,
             uint dwShareMode, uint dwPreferredProtocols, ref IntPtr phCard,
             ref IntPtr pdwActiveProtocol);

        [DllImport("winscard.dll")]
        public static extern uint SCardDisconnect(IntPtr hCard, int Disposition);

        [StructLayout(LayoutKind.Sequential)]
        internal class SCARD_IO_REQUEST
        {
            internal uint dwProtocol;
            internal int cbPciLength;
            public SCARD_IO_REQUEST()
            {
                dwProtocol = 0;
            }
        }

        [DllImport("winscard.dll")]
        public static extern uint SCardTransmit(IntPtr hCard, IntPtr pioSendRequest, byte[] SendBuff, int SendBuffLen, SCARD_IO_REQUEST pioRecvRequest,
                byte[] RecvBuff, ref int RecvBuffLen);

        [DllImport("winscard.dll")]
        public static extern uint SCardControl(IntPtr hCard, int controlCode, byte[] inBuffer, int inBufferLen, byte[] outBuffer, int outBufferLen, ref int bytesReturned);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct SCARD_READERSTATE
        {
            internal string szReader;
            internal IntPtr pvUserData;
            internal UInt32 dwCurrentState;
            internal UInt32 dwEventState;
            internal UInt32 cbAtr;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
            internal byte[] rgbAtr;
        }

        [DllImport("winscard.dll", EntryPoint = "SCardGetStatusChangeW", CharSet = CharSet.Unicode)]
        public static extern uint SCardGetStatusChange(IntPtr hContext, int dwTimeout, [In, Out] SCARD_READERSTATE[] rgReaderStates, int cReaders);

        [DllImport("winscard.dll")]
        public static extern int SCardStatus(IntPtr hCard, string szReader, ref int cch, ref int state, ref int protocol, ref byte[] bAttr, ref int cByte);


        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr LoadLibrary(string lpFileName);

        [DllImport("kernel32.dll")]
        public static extern void FreeLibrary(IntPtr handle);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr handle, string procName);

    }
}

APIの定数定義

pcsc_const.cs
using System;

namespace PCSC
{
    class Constant
    {
        public const uint SCARD_S_SUCCESS = 0;
        public const uint SCARD_E_NO_SERVICE = 0x8010001D;
        public const uint SCARD_E_TIMEOUT = 0x8010000A;

        public const uint SCARD_SCOPE_USER = 0;
        public const uint SCARD_SCOPE_TERMINAL = 1;
        public const uint SCARD_SCOPE_SYSTEM = 2;

        public const int SCARD_STATE_UNAWARE = 0x0000;
        public const int SCARD_STATE_CHANGED = 0x00000002;
        public const int SCARD_STATE_PRESENT = 0x00000020;
        public const UInt32 SCARD_STATE_EMPTY = 0x00000010;
        public const int SCARD_SHARE_SHARED = 0x00000002;
        public const int SCARD_SHARE_EXCLUSIVE = 0x00000001;
        public const int SCARD_SHARE_DIRECT = 0x00000003;

        public const int SCARD_PROTOCOL_T0 = 1;
        public const int SCARD_PROTOCOL_T1 = 2;
        public const int SCARD_PROTOCOL_RAW = 4;

        public const int SCARD_LEAVE_CARD = 0;
        public const int SCARD_RESET_CARD = 1;
        public const int SCARD_UNPOWER_CARD = 2;
        public const int SCARD_EJECT_CARD = 3;

        // SCardStatus status values
        public const int SCARD_UNKNOWN = 0x00000000;
        public const int SCARD_ABSENT = 0x00000001;
        public const int SCARD_PRESENT = 0x00000002;
        public const int SCARD_SWALLOWED = 0x00000003;
        public const int SCARD_POWERED = 0x00000004;
        public const int SCARD_NEGOTIABLE = 0x00000005;
        public const int SCARD_SPECIFICMODE = 0x00000006;
    }
}

実行結果

本プログラムでは非常に基本的なことしか実装していません。
パソコンにNFCリーダを接続し、事前にNFCリーダ上にNFCタグ(カード、タグ、スマホなど)を置いた上で実行すると、コンソール上に以下のように表示されます。

***** 1. SCardEstablishContext *****
 サービスに接続しました。
***** 2. SCardListReaders *****
 NFCリーダを検出しました。 Sony FeliCa Port/PaSoRi 3.0 0
***** 3. SCardConnect *****
 カードに接続しました。
***** 4. SCardTransmit *****
 カードからデータを取得しました。
 【IDm】:04-D6-0E-42-**-**-**
***** 5. SCardDisconnect *****
 カードを切断しました。

4. SCardTransmit で表示している 【IDm】: 以降がNFCタグから取得した固有IDの値になります。
NFCリーダが認識できなかったり、NFCタグが存在しない/繋がらない場合など、異常時には例外を吐きます。

なお、NFCタグがリーダにかざされるまで待つには 3. SCardConnect でループさせるか、 SCardGetStatusChange メソッドを使ってNFCリーダの状態変化を検出するまで待つ必要があります。

また、ソースコード中の var sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, 0x00 }; の部分がNFCタグに送信するコマンドになります。
サンプルではIDmを取得するコマンドになっていますが、それ以外のやり取りを行いたい場合、各サービスの仕様書を確認する必要があります。

32
52
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?