以下是一个完整、最简化的 FeliCa 读取整合示例(无需 SDK,基于 PCSC
NuGet 包),你可以直接运行这个控制台程序,验证能否识别 RC-S300 并读取卡片 UID:
🧪 示例说明
-
📦 使用 NuGet 包
PCSC
-
🎯 功能:初始化读卡器,读取插卡的 UID(部分 FeliCa 卡支持)
-
✅ 仅依赖 Windows 驱动(无需 SDK)
🛠 使用方式
1. 创建控制台项目
dotnet new console -n FelicaReaderExample
cd FelicaReaderExample
2. 安装依赖包
dotnet add package PCSC
直接选第一个宝贝!!!
3. 替换 Program.cs
将下载的 FelicaReaderExample.cs
文件内容,覆盖 Program.cs
或直接复制粘贴内容。
4. 运行程序
插入 FeliCa 卡到 RC-S300,然后运行:
dotnet run
✅ 成功输出示例
找到读卡器:Sony RC-S300
读取成功,卡片 UID: 01-23-45-67-89-AB-CD
FelicaReaderService.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using PCSC;
using PCSC.Utils;
namespace StarMauiPrinter.Services
{
/// <summary>
/// 提供对 FeliCa 卡(如 ICOCA)的读取功能,仅获取 UID(IDm)
/// </summary>
public class FelicaReaderService
{
/// <summary>
/// 读取 FeliCa 卡的唯一识别码(IDm / UID)
/// </summary>
/// <returns>UID 字符串 或 错误信息</returns>
public async Task<string?> ReadCardUidAsync()
{
try
{
// 1. 建立与 PC/SC 子系统的连接上下文
using var context = ContextFactory.Instance.Establish(SCardScope.System);
// 2. 获取所有已连接的智能卡读卡器列表
var readerNames = context.GetReaders();
if (readerNames == null || readerNames.Length == 0)
return "未检测到任何读卡器。";
var readerName = readerNames[0];
// 3. 使用第一个读卡器进行连接
using var reader = new SCardReader(context);
var result = reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
if (result != SCardError.Success)
return $"连接失败: {SCardHelper.StringifyError(result)}";
// 4. 构造 APDU 指令读取 UID(IDm)
// 标准 PC/SC 指令: FF CA 00 00 00
var command = new byte[] { 0xFF, 0xCA, 0x00, 0x00, 0x00 };
var receivePci = new SCardPCI();
var sendPci = SCardPCI.GetPci(reader.ActiveProtocol);
var receiveBuffer = new byte[256];
// 5. 发送指令并接收响应
var transmitResult = reader.Transmit(
sendPci, // 协议控制结构
command, // 发送的指令
receivePci, // 接收的控制结构
ref receiveBuffer // 输出缓冲区
);
if (transmitResult != SCardError.Success)
return $"发送失败: {SCardHelper.StringifyError(transmitResult)}";
// 6. 解析返回的 UID(通常为 8 字节的 IDm)
var uid = receiveBuffer.TakeWhile(b => b != 0x00).ToArray();
if (uid.Length == 0)
return "未读取到卡片 UID";
return $"UID: {BitConverter.ToString(uid)}";
}
catch (Exception ex)
{
return $"异常: {ex.Message}";
}
}
}
}
FelicaReaderTest.razor
@page "/felicareadertest"
@inject StarMauiPrinter.Services.FelicaReaderService FelicaReaderService
<h3>ICOCA UID を取得する</h3>
<button class="btn btn-primary" @onclick="ReadUid">取得</button>
<p>@uid</p>
@code {
private string? uid;
private async Task ReadUid()
{
try
{
uid = await FelicaReaderService.ReadCardUidAsync();
}
catch (Exception ex)
{
uid = $"読み取りに失敗しました: {ex.Message}";
}
}
}
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using PCSC;
using PCSC.Utils;
namespace StarMauiPrinter.Services
{
/// <summary>
/// Felica读卡器服务类,用于读取ICOCA等Felica格式的IC卡信息
/// </summary>
public class FelicaReaderService
{
#region 常量定义
/// <summary>
/// 获取UID的APDU命令
/// </summary>
private static readonly byte[] GET_UID_COMMAND = { 0xFF, 0xCA, 0x00, 0x00, 0x00 };
/// <summary>
/// 接收缓冲区大小
/// </summary>
private const int RECEIVE_BUFFER_SIZE = 256;
/// <summary>
/// 最大重试次数
/// </summary>
private const int MAX_RETRY_COUNT = 3;
/// <summary>
/// 重试延迟基数(毫秒)
/// </summary>
private const int RETRY_DELAY_BASE = 500;
#endregion
#region 公共方法
/// <summary>
/// 异步读取卡片UID(带重试机制)
/// </summary>
/// <returns>返回UID字符串,如果失败则返回错误信息</returns>
public async Task<string> ReadCardUidAsync()
{
return await ExecuteWithRetryAsync(async () =>
{
// 方案1:直接创建SCardContext实例
using var context = new SCardContext();
context.Establish(SCardScope.System);
// 获取可用读卡器
var readerName = GetFirstAvailableReader(context);
// 连接读卡器并读取UID
using var reader = new SCardReader(context);
ConnectToCard(reader, readerName);
var uid = ReadCardUid(reader);
return FormatUid(uid);
});
}
/// <summary>
/// 简化版:异步读取ICOCA卡的详细信息
/// </summary>
/// <returns>返回包含UID、余额和基本信息的字符串</returns>
public async Task<string> ReadIcocaDetailsAsync()
{
return await ExecuteWithRetryAsync(async () =>
{
using var context = new SCardContext();
context.Establish(SCardScope.System);
var readerName = GetFirstAvailableReader(context);
using var reader = new SCardReader(context);
ConnectToCard(reader, readerName);
// 读取基本UID信息
var uid = ReadCardUid(reader);
var uidString = FormatUid(uid);
// 尝试读取余额
var balance = TryReadBalance(reader);
// 尝试读取最近一次交易
var lastTransaction = TryReadLastTransaction(reader);
return $"ICOCA卡信息:\n" +
$"UID: {uidString}\n" +
$"余额: {balance}\n" +
$"最近交易: {lastTransaction}";
});
}
/// <summary>
/// 尝试读取余额(修复版)
/// </summary>
private string TryReadBalance(SCardReader reader)
{
try
{
// ICOCA余额读取命令
var command = new byte[] { 0xFF, 0xCA, 0x00, 0x01, 0x04, 0x8B, 0x00, 0x83, 0x00 };
var response = new byte[64];
// 修复:正确使用ref参数
var result = reader.Transmit(command, ref response);
// 检查命令是否成功执行
if (result == SCardError.Success)
{
// 检查响应长度
var actualLength = response.Length;
if (actualLength >= 4)
{
// 正确解析余额数据
var balance = (response[1] << 8) | response[0]; // Little-endian
if (balance > 0 && balance < 50000)
{
return $"¥{balance}";
}
// 尝试其他可能的位置
for (int i = 0; i < actualLength - 1; i++)
{
var testBalance = (response[i + 1] << 8) | response[i];
if (testBalance > 0 && testBalance < 50000)
{
return $"¥{testBalance}";
}
}
}
}
else
{
return $"命令失败: {result}";
}
}
catch (Exception ex)
{
return $"读取失败: {ex.Message}";
}
return "无法读取";
}
/// <summary>
/// 尝试读取最近一次交易(修复版)
/// </summary>
private string TryReadLastTransaction(SCardReader reader)
{
try
{
// 交易记录读取命令
var command = new byte[] { 0xFF, 0xCA, 0x00, 0x02, 0x04, 0x8C, 0x00, 0x80, 0x00 };
var response = new byte[64];
// 修复:正确使用ref参数
var result = reader.Transmit(command, ref response);
if (result == SCardError.Success && response.Length >= 8)
{
var amount = (response[5] << 8) | response[4];
var type = response[0] == 0x05 ? "乘车" : "其他";
if (amount > 0 && amount < 10000) // 合理的交易金额范围
{
return $"{type} ¥{amount}";
}
}
}
catch (Exception)
{
// 忽略错误
}
return "无记录";
}
/// <summary>
/// 检查读卡器状态
/// </summary>
/// <returns>返回读卡器状态信息</returns>
public async Task<string> CheckReaderStatusAsync()
{
return await Task.Run(() =>
{
try
{
// 修复:直接创建SCardContext实例
using var context = new SCardContext();
context.Establish(SCardScope.System);
var readerNames = context.GetReaders();
if (readerNames.Length == 0)
return "状态: 未检测到任何读卡器";
var statusInfo = new List<string>();
foreach (var readerName in readerNames)
{
try
{
// 检查读卡器中是否有卡
var cardStatus = CheckCardPresence(context, readerName);
statusInfo.Add($"读卡器: {readerName} - 状态: {cardStatus}");
}
catch (Exception ex)
{
statusInfo.Add($"读卡器: {readerName} - 状态: 检查失败 ({ex.Message})");
}
}
return string.Join("\n", statusInfo);
}
catch (Exception ex)
{
return $"检查状态时发生异常: {ex.Message}";
}
});
}
/// <summary>
/// 测试读卡器连接
/// </summary>
/// <returns>连接测试结果</returns>
public async Task<string> TestReaderConnectionAsync()
{
return await Task.Run(() =>
{
try
{
using var context = ContextFactory.Instance.Establish(SCardScope.System);
var readerNames = context.GetReaders();
if (readerNames.Length == 0)
return "测试失败: 未检测到任何读卡器";
var readerName = readerNames[0];
using var reader = new SCardReader(context);
var result = reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
if (result == SCardError.Success)
{
var protocol = reader.ActiveProtocol;
return $"连接测试成功!\n读卡器: {readerName}\n协议: {protocol}";
}
else
{
return $"连接测试失败: {SCardHelper.StringifyError(result)}";
}
}
catch (Exception ex)
{
return $"连接测试异常: {ex.Message}";
}
});
}
#endregion
#region 私有方法
/// <summary>
/// 获取第一个可用的读卡器
/// </summary>
/// <param name="context">PC/SC上下文</param>
/// <returns>读卡器名称</returns>
/// <exception cref="InvalidOperationException">当没有可用读卡器时抛出</exception>
private string GetFirstAvailableReader(SCardContext context)
{
var readerNames = context.GetReaders();
if (readerNames.Length == 0)
throw new InvalidOperationException("未检测到任何读卡器,请确认读卡器已正确连接");
return readerNames[0];
}
/// <summary>
/// 连接到卡片
/// </summary>
/// <param name="reader">读卡器实例</param>
/// <param name="readerName">读卡器名称</param>
/// <exception cref="InvalidOperationException">当连接失败时抛出</exception>
private void ConnectToCard(SCardReader reader, string readerName)
{
var result = reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
if (result != SCardError.Success)
{
throw new InvalidOperationException(
$"连接到读卡器失败: {SCardHelper.StringifyError(result)}。" +
"请确认卡片已正确放置在读卡器上");
}
}
/// <summary>
/// 读取卡片UID
/// </summary>
/// <param name="reader">读卡器实例</param>
/// <returns>UID字节数组</returns>
/// <exception cref="InvalidOperationException">当读取失败时抛出</exception>
private byte[] ReadCardUid(SCardReader reader)
{
var receivePci = new SCardPCI();
var sendPci = SCardPCI.GetPci(reader.ActiveProtocol);
var receiveBuffer = new byte[RECEIVE_BUFFER_SIZE];
var transmitResult = reader.Transmit(
sendPci,
GET_UID_COMMAND,
receivePci,
ref receiveBuffer
);
if (transmitResult != SCardError.Success)
{
throw new InvalidOperationException(
$"发送UID读取命令失败: {SCardHelper.StringifyError(transmitResult)}");
}
// 提取有效的UID数据(去除填充的0x00)
var uid = receiveBuffer.TakeWhile(b => b != 0x00).ToArray();
if (uid.Length == 0 || uid.Length < 4)
{
throw new InvalidOperationException("读取到的UID无效或长度不足");
}
return uid;
}
/// <summary>
/// 尝试读取额外的卡片信息
/// </summary>
/// <param name="reader">读卡器实例</param>
/// <returns>额外信息字符串</returns>
private string TryReadAdditionalCardInfo(SCardReader reader)
{
try
{
// 尝试读取更多信息的示例命令
var infoCommand = new byte[] { 0xFF, 0xB0, 0x00, 0x00, 0x10 };
var receivePci = new SCardPCI();
var sendPci = SCardPCI.GetPci(reader.ActiveProtocol);
var receiveBuffer = new byte[RECEIVE_BUFFER_SIZE];
var result = reader.Transmit(
sendPci,
infoCommand,
receivePci,
ref receiveBuffer
);
if (result == SCardError.Success)
{
var responseData = receiveBuffer.TakeWhile(b => b != 0x00).ToArray();
if (responseData.Length > 0)
{
return $"附加信息: {BitConverter.ToString(responseData)}";
}
}
return "附加信息: 无法读取或不支持";
}
catch (Exception ex)
{
return $"附加信息: 读取异常 ({ex.Message})";
}
}
/// <summary>
/// 检查指定读卡器中是否有卡片
/// </summary>
/// <param name="context">PC/SC上下文</param>
/// <param name="readerName">读卡器名称</param>
/// <returns>卡片状态描述</returns>
private string CheckCardPresence(SCardContext context, string readerName)
{
try
{
using var reader = new SCardReader(context);
var connectResult = reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
if (connectResult == SCardError.Success)
{
return "有卡 (已连接)";
}
else if (connectResult == SCardError.NoSmartcard)
{
return "无卡";
}
else
{
return $"未知状态 ({SCardHelper.StringifyError(connectResult)})";
}
}
catch (Exception)
{
return "检查失败";
}
}
/// <summary>
/// 格式化UID为可读字符串
/// </summary>
/// <param name="uid">UID字节数组</param>
/// <returns>格式化的UID字符串</returns>
private string FormatUid(byte[] uid)
{
if (uid == null || uid.Length == 0)
return "UID: 无效";
var uidString = BitConverter.ToString(uid).Replace("-", ":");
return $"UID: {uidString} (长度: {uid.Length} 字节)";
}
/// <summary>
/// 执行带重试机制的异步操作
/// </summary>
/// <param name="operation">要执行的操作</param>
/// <returns>操作结果</returns>
private async Task<string> ExecuteWithRetryAsync(Func<Task<string>> operation)
{
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++)
{
try
{
return await operation();
}
catch (Exception ex)
{
lastException = ex;
if (attempt == MAX_RETRY_COUNT)
break;
// 在重试之间添加递增延迟
await Task.Delay(RETRY_DELAY_BASE * attempt);
}
}
// 如果所有重试都失败,返回错误信息
return $"操作失败 (重试 {MAX_RETRY_COUNT} 次): {lastException?.Message ?? "未知错误"}";
}
#endregion
#region 资源清理
/// <summary>
/// 清理资源
/// </summary>
public void Dispose()
{
// 强制垃圾回收,释放未管理资源
GC.Collect();
}
#endregion
}
#region 扩展类和数据结构
/// <summary>
/// ICOCA卡信息结构
/// </summary>
public class IcocaCardInfo
{
public string Uid { get; set; } = string.Empty;
public int Balance { get; set; }
public DateTime LastUsed { get; set; }
public List<TransactionRecord> History { get; set; } = new List<TransactionRecord>();
}
/// <summary>
/// 交易记录结构
/// </summary>
public class TransactionRecord
{
public DateTime Date { get; set; }
public string Type { get; set; } = string.Empty;
public int Amount { get; set; }
public string Station { get; set; } = string.Empty;
}
#endregion
}
using System;
using System.Linq;
using System.Threading.Tasks;
using PCSC;
using PCSC.Utils;
namespace StarMauiPrinter.Services
{
public class FelicaReaderService
{
/// <summary>
/// 获取卡的 IDm(UID)、System Code,并根据 IDm 前缀推断卡类型
/// </summary>
public async Task<string?> GetCardInfoAsync()
{
try
{
using var context = ContextFactory.Instance.Establish(SCardScope.System);
var readers = context.GetReaders();
if (readers == null || readers.Length == 0)
return "未检测到读卡器";
var readerName = readers[0];
using var reader = new SCardReader(context);
var conn = reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
if (conn != SCardError.Success)
return $"连接失败: {SCardHelper.StringifyError(conn)}";
var sendPci = SCardPCI.GetPci(reader.ActiveProtocol);
var recvPci = new SCardPCI();
var buffer = new byte[256];
// 发送 Polling 命令以获取卡信息
var polling = new byte[] {
0xFF, 0x00, 0x00, 0x00, 0x04,
0xD4, 0x04, 0x00, 0x01
};
var result = reader.Transmit(sendPci, polling, recvPci, ref buffer);
if (result != SCardError.Success)
return $"轮询失败: {SCardHelper.StringifyError(result)}";
var response = buffer.TakeWhile(b => b != 0x00).ToArray();
if (response.Length < 18)
return "响应数据不足,无法解析卡片信息";
var idm = response.Skip(9).Take(8).ToArray();
var idmStr = BitConverter.ToString(idm);
var idPrefix = $"{idm[0]:X2}-{idm[1]:X2}";
var systemCode = response.Length >= 27 ? response.Skip(25).Take(2).ToArray() : new byte[] { 0x00, 0x00 };
var sysCodeStr = $"{systemCode[0]:X2}{systemCode[1]:X2}";
// 根据 IDm 前缀 和 System Code 判断卡种
string cardType = sysCodeStr switch
{
"0003" => idPrefix switch
{
"01-01" or "02-01" => "Suica",
"03-01" => "PASMO",
"07-01" or "07-02" => "ICOCA",
_ => "未知交通卡"
},
"FE00" or "FE01" => "Edy",
"8B01" => "WAON",
_ => $"未知卡片(SystemCode: {sysCodeStr}, IDm前缀: {idPrefix})"
};
return $"卡类型: {cardType}\nIDm: {idmStr}\nSystem Code: {sysCodeStr}";
}
catch (Exception ex)
{
return $"异常: {ex.Message}";
}
}
}
}