こんちには。
今回、PaSoRi RC-S380 を使って、
Arduino等を使わずに、直接UnityからFeliCa/NFC(TypeA/TypeB)タグの情報を取得しようとしていました。
そこで PC/SC APIを用いてSuicaカードの利用履歴情報の読み取りという記事を見つけ、
こちらの記事を引用させていただき、Unityで使えるように書き換えてみました。
FeliCa / NFC(TypeA/TypeB) について
非接触型ICには主に FeliCa / NFC の2つのタイプがあります。
FeliCaは海外の国のほとんどが利用していません。※徐々にアジア展開を広げています。 (2018/07/05 現在)
海外ではNFCの「TypeA」または「TypeB」を利用しています。
> 参考 : Sony NFCとFeliCaの関係
Felica
Felicaは、Sonyの非接触型ICカードの通信技術NFCの通信システムのひとつです。
Suica, PASMOなどの交通系ICカードや、楽天Edy、nanaco、WAONなどのプリペイド型電子マネーなど
日本国内で電子マネー決済に利用される主流の規格となっています。
また、iPhone7以降はFelicaを搭載しています。
TypeA
オランダのNXPセミコンダクターズが開発した規格です。
日本では、タバコの購入時に必要なTaspoカードに利用されています。
TypeB
アメリカのモトローラが開発した規格です。
日本では個人番号カード(マイナンバーカード)や住民基本台帳カード、
運転免許証、在留カード、パスポートに利用されています。
PC/SC (Personal Computer/Smart Card)
Windowsの標準APIにPC/SCというものがあります。
FeliCa/NFC(TypeA/TypeB)を読み取るものです。
Windows標準のWinSCard.dll
を使用します。
開発環境
– Windows PC (Surface Pro 4 / Windows 10)
※WinSCard.dllを使用するため、Windowsのみの対応です。
– PaSoRi RC-S380
– Unity 2018.1
– PASMO , Suica
実装
Windows PCにPaSoRiを繋いで、
ICカードをPasoriにタッチしたら情報を閲覧できるシステムをUnityで作ります。
そして、PASMOやSuicaのIDm(FeliCaチップの製造番号)と、残高を出力します。
1. PaSoRiのセットアップ
> 参考:かんたんセットアップガイド
2. コードを書く
文頭にも書いた通り、PC/SC APIを用いてSuicaカードの利用履歴情報の読み取りという素晴らしい記事を参考にさせていただきました。
主に、NfcMain.cs をUnityのUpdate()で動作するように書き換えます。
NfcApi.csとNfcConstant.csに関しては、ほぼそのまま引用させていただきました。
NfcApi.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
using System; using System.Runtime.InteropServices; namespace NfcPcSc { class NfcApi { [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 { /// <summary> /// Reader /// </summary> internal string szReader; /// <summary> /// User Data /// </summary> internal IntPtr pvUserData; /// <summary> /// Current State /// </summary> internal UInt32 dwCurrentState; /// <summary> /// Event State/ New State /// </summary> internal UInt32 dwEventState; /// <summary> /// ATR Length /// </summary> internal UInt32 cbAtr; /// <summary> /// Card ATR /// </summary> [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("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); } } |
NfcConstant.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; namespace NfcPcSc { class NfcConstant { 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 int SCARD_STATE_UNAWARE = 0x0000; public const int SCARD_STATE_PRESENT = 0x00000020;// This implies that there is a card public const int SCARD_SHARE_SHARED = 0x00000002; // - This application will allow others to share the reader public const int SCARD_PROTOCOL_T1 = 2;// - Use the T=1 protocol (value = 0x00000002) public const int SCARD_LEAVE_CARD = 0; // Don't do anything special on close public const int NFC_STATE_DISCONNECT = 0; public const int NFC_STATE_CONNECT = 1; } } |
NfcMain.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
using System; using System.Collections.Generic; using System.Text; using UnityEngine; namespace NfcPcSc { public class NfcMain : MonoBehaviour { private IntPtr context; private List<string> readersList; private NfcApi.SCARD_READERSTATE[] readerStateArray; private int nfc_state = NfcConstant.NFC_STATE_DISCONNECT; private bool isBeginTouch = true; private bool isTouchingCard = false; void Start() { initNfc(); } void Update() { context = establishContext(); readersList = getReaders(context, nfc_state == NfcConstant.NFC_STATE_DISCONNECT); readerStateArray = readerStateChange(context, readersList); setState(); if (readerStateArray.Length != 0) { if ((readerStateArray[0].dwEventState & NfcConstant.SCARD_STATE_PRESENT) == NfcConstant.SCARD_STATE_PRESENT) { if (isBeginTouch) { Debug.Log("タッチし始め"); readCard(context, readerStateArray[0].szReader); SendCommand(context, readerStateArray[0].szReader); isTouchingCard = true; isBeginTouch = false; } Debug.Log("カードタッチ中"); NfcApi.SCardReleaseContext(context); } else { if (isTouchingCard) { Debug.Log("カードが離れた"); isBeginTouch = true; isTouchingCard = false; } Debug.Log("カードがタッチされていない"); } } else { isBeginTouch = true; } } private void initNfc() { context = establishContext(); readersList = getReaders(context, true); readerStateArray = readerStateChange(context, readersList); setState(); } private void setState() { if (readerStateArray.Length != 0) { nfc_state = NfcConstant.NFC_STATE_CONNECT; } else { nfc_state = NfcConstant.NFC_STATE_DISCONNECT; } } private IntPtr establishContext() { IntPtr eContext = IntPtr.Zero; uint ret = NfcApi.SCardEstablishContext(NfcConstant.SCARD_SCOPE_USER, IntPtr.Zero, IntPtr.Zero, out eContext); if (ret != NfcConstant.SCARD_S_SUCCESS) { switch (ret) { case NfcConstant.SCARD_E_NO_SERVICE: Debug.LogWarning("サービスが起動されていません。"); break; default: Debug.LogWarning("Smart Cardサービスに接続できません。code = " + ret); break; } return IntPtr.Zero; } return eContext; } List<string> getReaders(IntPtr hContext, bool isInit) { uint pcchReaders = 0; uint ret = NfcApi.SCardListReaders(hContext, null, null, ref pcchReaders); if (ret != NfcConstant.SCARD_S_SUCCESS) { return new List<string>();//リーダーの情報が取得できません。 } byte[] mszReaders = new byte[pcchReaders * 2]; // 1文字2byte // Fill readers buffer with second call. ret = NfcApi.SCardListReaders(hContext, null, mszReaders, ref pcchReaders); if (ret != NfcConstant.SCARD_S_SUCCESS) { return new List<string>();//リーダーの情報が取得できません。 } UnicodeEncoding unicodeEncoding = new UnicodeEncoding(); string readerNameMultiString = unicodeEncoding.GetString(mszReaders); if (isInit) { Debug.Log("【接続】リーダー名: " + readerNameMultiString); } List<string> readersList = new List<string>(); int nullindex = readerNameMultiString.IndexOf((char)0); // 装置は1台のみ readersList.Add(readerNameMultiString.Substring(0, nullindex)); return readersList; } NfcApi.SCARD_READERSTATE[] readerStateChange(IntPtr hContext, List<string> readerNameList) { NfcApi.SCARD_READERSTATE[] readerStateArray = new NfcApi.SCARD_READERSTATE[readerNameList.Count]; int i = 0; foreach (string readerName in readerNameList) { readerStateArray[i].dwCurrentState = NfcConstant.SCARD_STATE_UNAWARE; readerStateArray[i].szReader = readerName; i++; } uint ret = NfcApi.SCardGetStatusChange(hContext, 100/*msec*/, readerStateArray, readerStateArray.Length); if (ret != NfcConstant.SCARD_S_SUCCESS) { //Debug.Log("error..."); } if (nfc_state == NfcConstant.NFC_STATE_DISCONNECT) { Debug.LogWarning("リーダーの状態の取得に失敗。code = " + ret); } return readerStateArray; } ReadResult readCard(IntPtr eContext, string readerName) { IntPtr hCard = connect(eContext, readerName); string readerSerialNumber = readReaderSerialNumber(hCard); string cardId = readCardId(hCard); Debug.Log(readerName + " (S/N " + readerSerialNumber + ") から、カードを読み取りました。" + cardId); disconnect(hCard); ReadResult.readerSerialNumber = readerSerialNumber; ReadResult.cardId = cardId; return null; } string readReaderSerialNumber(IntPtr hCard) { int controlCode = 0x003136b0; // SCARD_CTL_CODE(3500) の値 // IOCTL_PCSC_CCID_ESCAPE // SONY SDK for NFC M579_PC_SC_2.1j.pdf 3.1.1 IOCTRL_PCSC_CCID_ESCAPE byte[] sendBuffer = new byte[] { 0xc0, 0x08 }; // ESC_CMD_GET_INFO / Product Serial Number byte[] recvBuffer = new byte[64]; int recvLength = control(hCard, controlCode, sendBuffer, recvBuffer); ASCIIEncoding asciiEncoding = new ASCIIEncoding(); string serialNumber = asciiEncoding.GetString(recvBuffer, 0, recvLength - 1); // recvBufferには\0で終わる文字列が取得されるので、長さを-1する。 return serialNumber; } string readCardId(IntPtr hCard) { byte maxRecvDataLen = 64; byte[] recvBuffer = new byte[maxRecvDataLen + 2]; byte[] sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, maxRecvDataLen }; int recvLength = transmit(hCard, sendBuffer, recvBuffer); string cardId = BitConverter.ToString(recvBuffer, 0, recvLength - 2).Replace("-", ""); return cardId; } void SendCommand(IntPtr hContext, string readerName) { int dwResponseSize; long lResult; byte[] response = new byte[2048]; byte[] commnadSelectFile = { 0xff, 0xA4, 0x00, 0x01, 0x02, 0x0f, 0x09 }; byte[] commnadReadBinary = { 0xff, 0xb0, 0x00, 0x00, 0x00 }; IntPtr SCARD_PCI_T1 = getPciT1(); NfcApi.SCARD_IO_REQUEST ioRecv = new NfcApi.SCARD_IO_REQUEST(); ioRecv.cbPciLength = 2048; IntPtr hCard = connect(hContext, readerName); dwResponseSize = response.Length; lResult = NfcApi.SCardTransmit(hCard, SCARD_PCI_T1, commnadSelectFile, commnadSelectFile.Length, ioRecv, response, ref dwResponseSize); if (lResult != NfcConstant.SCARD_S_SUCCESS) { //Debug.Log("SelectFile error"); return; } dwResponseSize = response.Length; lResult = NfcApi.SCardTransmit(hCard, SCARD_PCI_T1, commnadReadBinary, commnadReadBinary.Length, ioRecv, response, ref dwResponseSize); if (lResult != NfcConstant.SCARD_S_SUCCESS) { Debug.Log("ReadBinary error"); return; } parse_tag(response); } private void parse_tag(byte[] data) { byte[] balance = new byte[] { data[10], data[11] }; Debug.Log("残高:" + BitConverter.ToInt16(balance, 0) + "円"); ReadResult.cardBalance = BitConverter.ToInt16(balance, 0); } private IntPtr getPciT1() { IntPtr handle = NfcApi.LoadLibrary("Winscard.dll"); IntPtr pci = NfcApi.GetProcAddress(handle, "g_rgSCardT1Pci"); NfcApi.FreeLibrary(handle); return pci; } IntPtr connect(IntPtr hContext, string readerName) { IntPtr hCard = IntPtr.Zero; IntPtr activeProtocol = IntPtr.Zero; uint ret = NfcApi.SCardConnect(hContext, readerName, NfcConstant.SCARD_SHARE_SHARED, NfcConstant.SCARD_PROTOCOL_T1, ref hCard, ref activeProtocol); if (ret != NfcConstant.SCARD_S_SUCCESS) { Debug.LogWarning("カードに接続できません。code = " + ret); } return hCard; } void disconnect(IntPtr hCard) { uint ret = NfcApi.SCardDisconnect(hCard, NfcConstant.SCARD_LEAVE_CARD); if (ret != NfcConstant.SCARD_S_SUCCESS) { Debug.LogWarning("カードとの接続を切断できません。code = " + ret); } } int control(IntPtr hCard, int controlCode, byte[] sendBuffer, byte[] recvBuffer) { int bytesReturned = 0; uint ret = NfcApi.SCardControl(hCard, controlCode, sendBuffer, sendBuffer.Length, recvBuffer, recvBuffer.Length, ref bytesReturned); if (ret != NfcConstant.SCARD_S_SUCCESS) { Debug.LogWarning("カードへの制御命令送信に失敗しました。code = " + ret); } return bytesReturned; } int transmit(IntPtr hCard, byte[] sendBuffer, byte[] recvBuffer) { NfcApi.SCARD_IO_REQUEST ioRecv = new NfcApi.SCARD_IO_REQUEST(); ioRecv.cbPciLength = 255; int pcbRecvLength = recvBuffer.Length; int cbSendLength = sendBuffer.Length; IntPtr SCARD_PCI_T1 = getPciT1(); uint ret = NfcApi.SCardTransmit(hCard, SCARD_PCI_T1, sendBuffer, cbSendLength, ioRecv, recvBuffer, ref pcbRecvLength); if (ret != NfcConstant.SCARD_S_SUCCESS) { Debug.LogWarning("カードへの送信に失敗しました。code = " + ret); } return pcbRecvLength; // 受信したバイト数(recvBufferに受け取ったバイト数) } } /// <summary> /// 読み取った情報の結果を保持する /// </summary> class ReadResult { /// <summary> /// リーダーの製造番号 /// </summary> public static string readerSerialNumber; /// <summary> /// カードのID /// </summary> public static string cardId; /// <summary> /// カードの残高 /// </summary> public static int cardBalance; } } |
説明
NfcApi.cs
– winscard.dllのラッパー
NfcConstant.cs
– 定数の定義
以上2つのスクリプトに関しては、上記に記載している引用ページの説明をご覧ください。
NfcMain.cs
– Update()の中でPC/SC APIを呼び出しています。
PC/SCでの操作の流れは以下のようになります.
SCardEstablishContext スマートカードリソースマネージャへの接続
↓
SCardListReaders カードリーダーの列挙
↓
SCardGetStatusChange カードリーダーの状態の取得
↓
SCardConnect カードへの接続
↓
SCardTransmit カードとの通信
↓
SCardDisconnect カードへから切断
↓
SCardReleaseContext スマートカードリソースマネージャからの切断
また今回は例として、PASMOおよびSuicaの残高を出力しています。
SendCommand関数のSCardTransmit によって、カードの利用履歴情報を受け取ります。
こちらによると、残高を示すのは10-11バイト目なので、以下のように残高を取得しています。
(今回は、電子マネー以外をタッチした際、エラー出力などはせず、残高0円 と出力するようにしています。)
1 2 3 4 5 6 |
private void parse_tag(byte[] data) { byte[] balance = new byte[] { data[10], data[11] }; Debug.Log("残高:" + BitConverter.ToInt16(balance, 0) + "円"); ReadResult.cardBalance = BitConverter.ToInt16(balance, 0); } |
また、以下例のように記述すると、外部からリーダーやカード情報にアクセスすることができます。
1 2 3 4 5 6 7 8 9 10 11 |
using UnityEngine; using NfcPcSc;//追加 public class GameController : MonoBehaviour { void CheckReaderResult() { Debug.Log(ReadResult.readerSerialNumber);//リーダーの製造番号 Debug.Log(ReadResult.cardId);//カードのID Debug.Log(ReadResult.cardBalance);//カードの残高 } } |
3. 実行
3.1 NfcMain.csを適当に作成したGameObjectにアタッチしてリーダーを接続した状態で実行
3.2 Suicaをリーダーにタッチし、一定時間後に離してみた結果です。IDと残高が表示されています。
3.3 続いてPASMOをタッチして、一定時間後に離してみた結果です。続けて処理ができます。
まとめ
ICカードをPaSoRiにタッチしたらカードIDと電子マネーの残高を閲覧できるシステムをUnityで作りました。
PC/SC とUnityの連携の記事はあまり見かけないため、今回記事にしました。
Unityで作ることによって、単にNFCの情報を閲覧するだけでなく、サイネージやゲームとの連携などの応用に繋げることができます。