【目的】
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)から利用可能 (← 以下にサンプルコードあり)
【Advanced Card Systems Ltd.の仕様書】
PCSC-sharp
上記のPCSC(winscard.dll)を.Net環境から使えるようにするラッパー。
ライセンス形態はこちら。
PCSC.Iso7816と組み合わせて使う。
【PCSC - Nuget】
【PCSC.Iso7816 - Nuget】
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による処理フロー】
基本的には以下の処理フローを踏襲します。
1. SCardEstablishContext
リソースマネージャに接続してハンドルを取得します。
2. SCardListReaders
PCに接続されているNFCリーダを取得します。(複数可)
取得できなかった場合は接続されるまでループするか、エラーで処理を中止します。
3. SCardConnect
接続されているNFCリーダを指定して、カード(NFCタグ)に接続します。
カードと接続できなかった場合、接続されるまでループするか、エラーで処理を中止します。
なお、NFCリーダ上に読み取れるカードがなかった場合はエラーとなるため、基本的にはループしてカードと接続できるまで待ちます。
4. SCardTransmit
接続したカードにコマンドを送信し、結果を受信します。
ここでIDmを取得したり、カードに保存されている情報を読取り/書込みをお行います。
コマンドや受信データは取得したい情報によって異なるため、該当の仕様書を確認する必要があります。
5. SCardDisconnect
カードから切断します。
これで一連の処理は完了です。
winscard.dllによるサンプル実装 (C# コンソールアプリ)
プログラム本体
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定義
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の定数定義
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を取得するコマンドになっていますが、それ以外のやり取りを行いたい場合、各サービスの仕様書を確認する必要があります。