従業員の入退室管理のため、300枚はあろうかというMifareカードのUID(カード固有の製造番号)を読み取る必要があった。
良さげなフリーソフトが見つからなかったので、自分でコード書いてみたという話。
PCSC-sharpとは
Windows7以降に標準添付されているwinscard.dll
を .NET から簡単に使えるようにするラッパーライブラリ。
【入手元】 https://github.com/danm-de/pcsc-sharp
winscard.dll
はスマートカード(ISO7816規格)をWindowsから操作するPC/SCというAPIの実装である。
PCSC-sharpに頼らず自分でDLLを叩きたいんだ!という人は、
@rhene さんの記事 https://qiita.com/rhene/items/725dfe4a6b6307731cbf
がとても参考になると思う。
NFCとは
RFIDと呼ばれる無線通信方式のひとつで、下表の通り複数の規格がある。
規格 | 日本における用途 |
---|---|
Type-A (Mifare) | Taspoなど |
Type-B | マイナンバーカード、住民基本台帳カード、運転免許証など |
Type-F (FeliCa) | Suica、楽天Edy、nanaco、iD、WAONなどの電子マネー |
ISO/IEC 15693 | 物流の商品タグなど |
Type-AとType-Bは事実上の世界標準で、NFC Pay(クレジットカードのタッチ決済)にも使われている。
日本ではSuicaの自動改札で通信速度が必要だったという背景からFeliCaが採用され、その他電子マネーもこぞって追随したことからFeliCaが普及した。
入退室管理に使うカードではそこまでの性能は必要ないので、安価なMifareが広く使われている。
FeliCaはSONYが開発したものだが、技術力の高さゆえの過剰性能が災いしガラパゴス化した。
4K、8Kテレビは国内メーカーを助けるための開発ではあるが、同じ道を歩まないことを願う。
ソフトウェアの作成手順
開発環境
ソフトウェア
- Visual Studio 2019(C#)
- Windows 10 Pro 1903
ハードウェア
- SONY ICカードリーダ/ライタ PaSoRi RC-S380(非接触型)
- DELL ノートパソコン Latitude 7390 内蔵NFCセンサ(非接触型)
- DELL ノートパソコン Latitude 7390 内蔵スマートカードスロット(接触型)
NuGetパッケージ
次のNuGetパッケージをプロジェクトに追加する。
画面構成
C# コード
冒頭にusing
ディレクティブを追加する。
using PCSC;
using PCSC.Iso7816;
using PCSC.Exceptions;
アプリケーション起動時、接続されているカードリーダの名前をすべてコンボボックスに追加する。
カードリーダがひとつも接続されていなければアプリケーションを終了する。
PCSC.Exceptions.NoServiceException
がスローされたら、ドライバがインストールされていないので、やはり終了する。
private void FormMain_Load(object sender, EventArgs e) {
// リーダーの機器情報を取得
try {
using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
var readerNames = context.GetReaders();
if (readerNames == null || readerNames.Length == 0) {
MessageBox.Show("スマートカードリーダが見つかりません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
Application.Exit(); // スマートカードリーダが無ければ終了する
}
foreach (var readerName in readerNames) {
comboBoxDevice.Items.Add(readerName); // カードリーダの名前をコンボボックスに追加
}
}
} catch (NoServiceException) {
MessageBox.Show("スマートカードリソースマネージャが稼働していません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
Application.Exit();
}
comboBoxDevice.SelectedIndex = 0;
radioButtonNone.Checked = true;
// データグリッドビューの初期設定
dataGridView.AllowUserToResizeColumns = true;
dataGridView.AllowUserToResizeRows = false;
dataGridView.RowHeadersVisible = false;
dataGridView.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText;
dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridView.Columns.Add("seq", "連番");
dataGridView.Columns.Add("datetime", "読み取り日時");
dataGridView.Columns.Add("uid", "UID/IDm");
dataGridView.Columns["seq"].Width = 80;
dataGridView.Columns["datetime"].Width = 200;
dataGridView.Columns["uid"].Width = 250;
}
ボタンがクリックされたら、選択したカードリーダからNFCタグのUIDを読み取り、データグリッドに追加する。
読み取り用のAPDUコマンドを組み立て、送信し、結果を受信するという動きになる。
pcsc-sharp/Examples/ISO7816-4/Transmit/Program.cs を参考にしている。
private void buttonRead_Click(object sender, EventArgs e) {
var readerName = comboBoxDevice.Text; // 選択中のカードリーダ名を得る
using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
try {
using (var rfidReader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any)) {
// APDUコマンドの作成
var apdu = new CommandApdu(IsoCase.Case2Short, rfidReader.Protocol) {
CLA = 0xFF,
Instruction = InstructionCode.GetData,
P1 = 0x00,
P2 = 0x00,
Le = 0 // We don't know the ID tag size
};
// 読み取りコマンド送信
using (rfidReader.Transaction(SCardReaderDisposition.Leave)) {
var sendPci = SCardPCI.GetPci(rfidReader.Protocol);
var receivePci = new SCardPCI(); // IO returned protocol control information.
var receiveBuffer = new byte[256];
var command = apdu.ToArray();
var bytesReceived = rfidReader.Transmit(
sendPci, // Protocol Control Information (T0, T1 or Raw)
command, // command APDU
command.Length,
receivePci, // returning Protocol Control Information
receiveBuffer,
receiveBuffer.Length); // data buffer
var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case2Short, rfidReader.Protocol);
if (responseApdu.HasData) {
// バイナリ文字列の整形
StringBuilder id = new StringBuilder(BitConverter.ToString(responseApdu.GetData()));
if (radioButtonNone.Checked) {
id.Replace("-", string.Empty);
} else if (radioButtonSpace.Checked) {
id.Replace("-", " ");
}
// データグリッドビューに結果を追加し、連番をカウントアップする
int seq = (int)numericUpDown.Value;
DateTime dt = DateTime.Now;
dataGridView.Rows.Add(seq, dt.ToString("yyyy/MM/dd HH:mm:ss"), id.ToString());
numericUpDown.Value = ++seq;
} else {
MessageBox.Show("このカードではIDを取得できません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
} catch (RemovedCardException) {
MessageBox.Show("スマートカードが取り外されたため、通信できません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
} catch (PCSCException ex) {
MessageBox.Show(ex.Message, "スマートカードエラー", MessageBoxButtons.OK, MessageBoxIcon.Hand);
}
}
}