UnityでPaSoRiからFeliCaのデータを取得する（winscard.dll使用）

# UnityでFeliCaからIDmを取得する
UnityでPaSoRiから取得したIDmをもとにログインなどの処理を行う。
winscard.dll(PC/SC)を用いるため、Windowsのみ対応。

# 環境
- Windows 10 Pro
- Unity 2019.2.0f1
- Sony PaSoRi RC-S380

# ソースコード
ソースコードは 「[WindowsでNFCタグを読み取る](https://qiita.com/rhene/items/725dfe4a6b6307731cbf)」を参考にした（API部分はほとんどそのまま）。

```C#:NfcApi.cs
using System;
using System.Runtime.InteropServices;

namespace SmardCard
{
    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
        {
            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 IntPtr protocol, 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);

    }
}
```

```C#:NfcConstant.cs
using System;

namespace SmardCard
{
    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 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;
    }
}
```
```C#:NfcReader.cs
using System;
using UnityEngine;
using SmardCard;
using System.Text;

public class NfcReader : MonoBehaviour
{
    private IntPtr hContext = IntPtr.Zero;
    private IntPtr hCard;
    private IntPtr activeProtocol;

    public string readerName;
    public string cardId;
    public bool DetectOnlyFeliCa;
    public bool ThrowExceptionLog;

    public class CardData
    {
        public string ReaderName = "";
        public string CardID = "";
    }

    public CardData ReadCardData()
    {
        try
        {
            SCardEstablishContext();
            SCardListReaders();
            SCardConnect();
            SCardStatus();
            SCardTransmit();
            SCardDisconnect();
        }
        catch(Exception e)
        {
            if (ThrowExceptionLog)
            {
                Debug.LogWarning(e);
            }
            return new CardData();
        }
        return new CardData { ReaderName = readerName, CardID = cardId };
    }

    public string GetCardReaderName()
    {
        try
        {
            SCardEstablishContext();
            SCardListReaders();
        }
        catch (Exception e)
        {
            if (ThrowExceptionLog)
            {
                Debug.LogWarning(e);
            }
            return "";
        }
        return readerName;
    }

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

        if (hContext == IntPtr.Zero)
        {
            throw new ApplicationException("コンテキストの取得に失敗しました。");
            
        }
        return ret;
    }

    private void SCardListReaders()
    {
        uint pcchReaders = 0;

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

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


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

        // 認識したNDCリーダの最初の1台を使用
        int nullindex = readerNameMultiString.IndexOf((char)0);
        readerName = readerNameMultiString.Substring(0, nullindex);
    }

    private void SCardConnect()
    {
        activeProtocol = IntPtr.Zero;
        hCard = 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)
        {
            throw new ApplicationException("カードに接続できません。code = " + ret);
        }
    }

    private void SCardStatus()
    {
        int dwReaderLen = readerName.Length;
        int dwState = 0;
        byte[] atr = new byte[64]; //ATR
        int dwAtrLen = atr.Length;
        long lResult = NfcApi.SCardStatus(hCard, null, ref dwReaderLen, ref dwState, ref activeProtocol, atr, ref dwAtrLen);
        if (lResult != NfcConstant.SCARD_S_SUCCESS)
        {
            throw new ApplicationException("ATR取得に失敗しました。");
        }

        lResult = NfcApi.SCardStatus(hCard, readerName, ref dwReaderLen, ref dwState, ref activeProtocol, atr, ref dwAtrLen);
        if (lResult != NfcConstant.SCARD_S_SUCCESS)
        {
            throw new ApplicationException("ATR取得に失敗しました。");
        }

        // FeliCaかどうか判別
        if ((atr[13] != 0x00 || atr[14] != 0x3b) && DetectOnlyFeliCa)
        {
            throw new ApplicationException("FeliCaではありません。");
        }
    }

    private void SCardTransmit()
    {
        uint maxRecvDataLen = 256;
        var recvBuffer = new byte[maxRecvDataLen + 2];
        var sendBuffer = new byte[] { 0xff, 0xca, 0x00, 0x00, 0x00 };  // IDmを取得するコマンド

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

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

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

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

        // 受信データからIDmを抽出
        cardId = BitConverter.ToString(recvBuffer, 0, pcbRecvLength - 2);
    }

    private void SCardDisconnect()
    {
        uint ret = NfcApi.SCardDisconnect(hCard, NfcConstant.SCARD_LEAVE_CARD);
        if (ret != NfcConstant.SCARD_S_SUCCESS)
        {
            throw new ApplicationException("NFCカードとの切断に失敗しました。code = " + ret);
        }
    }
}
```

# ゲーム上での使用
任意のスクリプト（ここでは`LoginController`とする）から`NfcReader`を用いて、IDmを一定間隔で常時取得する例を示す。
インスペクターから`NfcReader`を含むゲームオブジェクトを、`LoginController`の`NfcReader`にドラッグアンドドロップしておく。

```C#:LoginController.cs
using UnityEngine;

public class LoginController : MonoBehaviour
{
    NFCReader R;
    public GameObject NfcReader;

    public float TimeOutLength = 1f;
    private float LastCardReadTime;
    public string IDm;

    void Awake()
    {
        R = NfcReader.GetComponent<NfcReader>();
        DontDestroyOnLoad(this.gameObject);
    }

    void Update()
    {
        if(Time.time - LastCardReadTime >= TimeOutLength && CardReadingEnabled)
        {
            LastCardReadTime = Time.time;
            IDm = R.ReadCardData().CardID;
            
            if(IDm != "")
            {
                //ここで処理を行う
            }
        }
    }
}
```

## 参考
- [WindowsでNFCタグを読み取る](https://qiita.com/rhene/items/725dfe4a6b6307731cbf)
- [ソフテックだより 技術レポート「非接触ICカード技術"FeliCa(フェリカ)"のIDm読み取り方法」](https://www.softech.co.jp/mm_140205_pc.htm)
- [丸の内テック「PC/SCを用いてUnityから非接触型ICカードの情報を読み取る」](https://marunouchi-tech.i-studio.co.jp/5133/)
- [ロボットと共に「C#とFelica(NFC)でSuicaを読み込んでみる」](http://robot-egg.hatenablog.com/entry/2017/06/23/163808)
- [TomoSoft「PC/SC APIを用いてSuicaカードの利用履歴情報の読み取り」](https://tomosoft.jp/design/?p=5543)