Files
PN532-Aime-Reader/src/NfcAime.Dll/AimeReader.cs
2026-05-17 01:52:51 +08:00

185 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using NfcAime.Dll.PN532;
using static NfcAime.Dll.MiFareHandle;
namespace NfcAime.Dll;
public class AimeReader
{
private string Port = "COM15";
private int Baud = 115200;
private Pn532Session session = null;
public enum CardKind { Felica, MifareClassic, Null}
private sealed record FlowResult(CardKind CardKind, byte[] CardId, string AccessCode);
private sealed record CardTarget(CardKind Kind, byte Tg, byte[] CardId);
// 兼容低版本 .NET实现 ToHexString 和 FromHexString
public static string ToHexString(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", "");
}
public static byte[] FromHexString(string hex)
{
if (hex.Length % 2 != 0)
throw new ArgumentException("Invalid hex string length.");
var bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
return bytes;
}
public AimeReader(string port, int baud)
{
Port = port;
Baud = baud;
TimeSpan timeout = TimeSpan.FromMilliseconds(100);
using var transport = new SerialFrameTransport(Port, Baud, timeout);
session = new Pn532Session(transport, timeout, 0);
}
public (CardKind CardKind, byte[] IDm, string AccessCode) ReadCard()
{
session.Open();
try
{
var result = RunPn532Flow(session);
return (result.CardKind , result.CardId, result.AccessCode);
}
finally { session.Close(); }
}
public void CloseReader()
{
session.Close();
}
private FlowResult RunPn532Flow(Pn532Session session)
{
ExpectPn532ResponseCode(session.SendCommand(new byte[] { 0x02 }), expectedResponseCode: 0x03);
ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x14, 0x01, 0x14, 0x01 }), expectedResponseCode: 0x15);
ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x32, 0x01, 0x03 }), expectedResponseCode: 0x33);
var target = WaitForCard(session);
Thread.Sleep(100);
if (target.Kind == CardKind.Null)
{
return new FlowResult(target.Kind, target.CardId, "");
}
if (target.Kind == CardKind.Felica)
{
Console.WriteLine($"Card detected! IDm: {ToHexString(target.CardId)}");
var readCmd = FelicaCommandBuilder.BuildReadWithoutEncryptionCommand(target.CardId);
var readResponse = SendInDataExchange(session, target.Tg, readCmd, TimeSpan.FromSeconds(5));
var spad0 = FelicaResponseParser.ParseSpad0(readResponse);
var decryptor = new FeliCaDecryptor();
var decrypted = decryptor.Decrypt(spad0);
var accessCode = AccessCodeFormatter.ToAccessCodeString(decrypted);
return new FlowResult(target.Kind, target.CardId, accessCode);
}
Console.WriteLine($"Card detected! TypeA UID: {ToHexString(target.CardId)}");
var m1AccessCode = TryReadMifareClassicAccessCode(session, target.Tg, target.CardId);
if (m1AccessCode is null)
{
throw new InvalidOperationException("Failed to read Mifare Classic AccessCode: no key matched sector 0.");
}
return new FlowResult(target.Kind, target.CardId, m1AccessCode);
}
private byte[] SendInDataExchange(Pn532Session session, byte tg, ReadOnlySpan<byte> payloadToTarget, TimeSpan? timeout = null)
{
var pn532Payload = new byte[2 + payloadToTarget.Length];
pn532Payload[0] = 0x40; pn532Payload[1] = tg;
payloadToTarget.CopyTo(pn532Payload.AsSpan(2));
var response = ExpectPn532ResponseCode(session.SendCommand(pn532Payload, responseTimeout: timeout ?? TimeSpan.FromSeconds(4)), expectedResponseCode: 0x41);
if (response.Payload.Length < 2 || response.Payload[1] != 0x00)
throw new InvalidOperationException($"InDataExchange failed: 0x{(response.Payload.Length > 1 ? response.Payload[1] : 0):X2}");
// 替换 System.Range 语法
return response.Payload.Length == 2 ? Array.Empty<byte>() : response.Payload.Skip(2).ToArray();
}
private string? TryReadMifareClassicAccessCode(Pn532Session session, byte tg, ReadOnlySpan<byte> uid)
{
const byte blockNumber = 2; // sector 0 block 2
var uid4 = new byte[4];
Array.Copy(uid.ToArray(), 0, uid4, 0, 4);
foreach (var keyHex in MifareClassicKeys)
{
var key = FromHexString(keyHex);
if (TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: true, key, uid4) ||
TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: false, key, uid4))
{
var block = ReadMifareBlock(session, tg, blockNumber);
var hex = ToHexString(block);
return hex.Length <= 20 ? hex : hex.Substring(hex.Length - 20, 20);
}
}
return null;
}
public static Pn532FrameParseResult ExpectPn532ResponseCode(Pn532FrameParseResult response, byte expectedResponseCode)
{
if (response.Kind != Pn532FrameKind.Data) throw new InvalidOperationException($"PN532 error: {response.Kind} ({response.Error}).");
if (response.Payload.Length < 1 || response.Payload[0] != expectedResponseCode)
throw new InvalidOperationException($"Expected 0x{expectedResponseCode:X2}, got 0x{(response.Payload.Length > 0 ? response.Payload[0] : 0):X2}");
return response;
}
private void ExpectPn532StatusOk(Pn532FrameParseResult response, byte expectedResponseCode)
{
response = ExpectPn532ResponseCode(response, expectedResponseCode);
if (response.Payload.Length >= 2 && response.Payload[1] != 0x00) throw new InvalidOperationException($"Status fail: 0x{response.Payload[1]:X2}");
}
private CardTarget WaitForCard(Pn532Session session)
{
var f212 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x01, 0x00, 0xFF, 0xFF, 0x01, 0x00 }, TimeSpan.FromMilliseconds(500));
if (ExtractFeliCaTarget(f212, out var target)) return target;
var a106 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x00 }, TimeSpan.FromMilliseconds(600));
if (ExtractTypeATarget(a106, out target)) return target;
return new CardTarget(CardKind.Null, 0, [0]);
}
private bool ExtractFeliCaTarget(Pn532FrameParseResult result, out CardTarget target)
{
target = null;
if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 15 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
{
var tg = result.Payload[2];
var idm = result.Payload.AsSpan(5, 8).ToArray();
target = new CardTarget(CardKind.Felica, tg, idm);
return true;
}
return false;
}
private bool ExtractTypeATarget(Pn532FrameParseResult result, out CardTarget target)
{
target = null;
if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 8 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
{
var tg = result.Payload[2];
var uidLen = result.Payload[6];
var uidOffset = 7;
if (uidLen > 0 && result.Payload.Length >= uidOffset + uidLen)
{
var uid = result.Payload.AsSpan(uidOffset, uidLen).ToArray();
target = new CardTarget(CardKind.MifareClassic, tg, uid);
return true;
}
}
return false;
}
}