新开普 HID RF Picc Reader 通讯协议(逆向整理)
| 项目 | 内容 |
|---|---|
| 文档版本 | 1.4 |
| 整理日期 | 2026-08-24 |
| 适配项目 | OnecardDemo(Java / Android) |
| 适配设备 | New-CAP 0005 / HID RF Picc Reader |
| USB 标识 | VID 0x0610,PID 0x2010 |
| 当前范围 | 连接读卡器、寻卡、读卡、CPU 卡本地消费;服务器记录上传另见单点功能文档 |
| 文档性质 | 根据 Windows Bus Hound 抓包、Android 真机描述符、现有 Java 实现和厂商上层接口文档逆向整理,不是厂商正式底层协议 |
1. 结论与使用边界
该设备是一个厂商自定义 USB HID 读卡器。Android 发送命令固定使用 HID SET_REPORT,响应通过 Interrupt IN 0x81 读取;HID 有效载荷内部再封装新开普私有命令、卡片 APDU 和 ISO/IEC 14443 Type A CRC-A。F700-HW 上若长时间在线后首次连接初始化写入返回 -1,当前实现会在任何卡片操作开始前、且仅在第一次失败时重绑该读卡器所属的 xHCI 控制器,再重新枚举、打开并初始化设备。
当前资料已经足够实现以下流程:
- 按 VID/PID 查找并打开读卡器;
- 初始化读卡器控制通道;
- 激活射频卡片并读取 4 字节 UID;
- 选择一卡通应用和钱包文件;
- 读取账号、卡号、状态、余额、消费计数等信息;
- 识别无卡、移卡、换卡、CRC 错误、USB 超时等异常。
- 通过 PSAM 对主钱包执行本地消费,并返回交易序号、TAC 和扣款后计数;补助钱包使用同一流程但仍待有余额卡验证。
以下内容仍不能视为厂商正式协议:
- 私有操作码
0x62、0x63、0x6B、0x6F的厂商正式名称; 0x6B私有控制载荷中每个字段的完整语义;- 读钱包时使用的固定
80 50 ...命令中各字段的正式定义;消费命令的字段位置已由成功样本确认,但厂商正式名称仍以厂商资料为准; - M1 卡、NFC 复合卡、挂失卡、过期卡和坏卡的完整设备错误码;
- 不同 Android 主控、不同固件版本下的 HID Report 兼容范围。
2. 证据等级
| 标记 | 含义 | 本文使用方式 |
|---|---|---|
| 抓包确认 | Windows Bus Hound 样本中可重复观察 | 可直接作为兼容实现依据 |
| 真机确认 | Android 设备枚举或 HID 描述符直接读取 | 可作为 Android USB 参数依据 |
| 代码实现 | OnecardDemo 当前 Java 代码的行为 | 表示当前实现,不等于厂商正式定义 |
| 语义推定 | 根据命令前后行为推断 | 使用时保留兼容和日志能力 |
| 待确认 | 样本不足或涉及卡片/终端动态数据 | 不应固化为跨项目通用常量 |
主要证据来源:
- Bus Hound:
test.zip、寻卡.zip、重复读卡.zip、重复读取.zip、无卡直接读卡.zip、移卡.zip、本地消费1分-20260823-142600.zip; - Android 真机:USB 设备枚举、端点信息和 HID Report Descriptor;
- Java 实现:
AndroidUsbHidTransport、HidFrameCodec、NewcapecProtocol、CrcA、CardInfoParser; - 厂商上层资料:《一卡通 Change 接口操作方法》《T4.5-Change 测试工具使用说明》。
3. 协议分层
业务接口
connect / findCard / readCard / consume / disconnect
│
一卡通读卡流程
激活 → 寻卡 → 选择文件 → 读记录 → 读钱包 → 关闭通道
│
卡片命令层
ISO 7816 风格 APDU + CRC-A + 尾随 00
│
厂商私有层
opcode + channel + 分片头 + payload
│
USB HID 层
SET_REPORT(Output)
Interrupt IN 0x81(Input)
4. USB HID 传输层
4.1 设备与接口
| 参数 | 当前值 | 证据 |
|---|---|---|
| Vendor ID | 0x0610 |
抓包确认 / 真机确认 |
| Product ID | 0x2010 |
抓包确认 / 真机确认 |
| Product | HID RF Picc Reader |
抓包确认 |
| Interface Class | 0x03(HID) |
真机确认 |
| Interrupt IN | 0x81,最大包长 64 |
真机确认 |
| Interrupt OUT | 0x02,最大包长 64 |
描述符/真机确认存在;自然故障时同样写入 -1,当前实现不使用 |
| Android I/O 超时 | 3000 ms | 代码实现 |
真机读取到的 HID Report Descriptor 为:
06 00 FF 09 01 A1 01 15 00 26 FF 00 75 08 95 40
09 01 81 02 95 40 09 01 91 02 95 01 09 01 B1 02 C0
其中 Report Size = 8 bit、Input/Output Report Count = 0x40,所以 Android 原始 USB 收发缓冲区按 64 字节处理。
4.2 主机发送方式
Windows 抓包和 Android 正常路径使用 HID Class Control Transfer:
| 字段 | 值 | 含义 |
|---|---|---|
bmRequestType |
0x21 |
Host-to-device、Class、Interface |
bRequest |
0x09 |
SET_REPORT |
wValue |
0x0200 |
Output Report,Report ID 0 |
wIndex |
HID interface id,当前为 0 | 目标接口 |
wLength |
Android 为 64 | 设备描述符声明的 Output Report 长度 |
响应通过 Interrupt IN 端点读取,当前 Android 实现要求一次返回完整 64 字节 Report。健康状态下强制使用 Interrupt OUT 0x02 曾能完成初始化,但 2026-08-24 的长时间在线自然故障中,HID SET_REPORT 和 Interrupt OUT 0x02 都返回 -1。因此“仅控制写路径失效、切换 Interrupt OUT 即可恢复”的假设已被现场证据推翻,当前代码已移除该备用写路径。
自然故障时,设备仍可枚举,USB 权限、openDevice 和 claimInterface 均成功,但任何 OUT 写入都失败。setConfiguration、setInterface、USBDEVFS_RESET、sysfs authorized 0→1 以及仅 unbind/rebind USB 设备节点均未恢复通讯;重绑 /sys/bus/platform/drivers/xhci-hcd/xhci-hcd.1.auto 后,读卡器重新枚举并立即完成协议初始化。当前自动恢复仅在以下条件同时满足时执行一次:设备型号严格为 F700-HW、失败发生于 connect() 初始化阶段、错误为 IO_WRITE_FAILED、尚未进行寻卡/读卡/消费。权限、open、claim、读超时、协议错误和第二次写失败均不触发控制器重绑。
恢复命令执行前还会核验 /sys/bus/usb/devices/5-1 的 VID/PID 必须是 0610:2010,并核验目标控制器节点存在。该能力依赖本机 /system/xbin/su 和 root/sysfs 写权限,属于 F700-HW 专用适配,不可直接视为标准 Android 通用能力;恢复失败映射为 DEVICE_RECOVERY_FAILED,要求物理插拔读卡器。
4.3 Windows 33 字节与 Android 64 字节的差异
Bus Hound 中应用层数据常显示为 33 字节,例如:
00 62 00 00 00 01 00 00 01 ... 共 33 字节
第一个 00 是 Windows HID API 使用的 Report ID 占位字节,后面的 32 字节才是当时 Windows 程序提交的私有报告前缀。Android 直接面向 USB 接口,不能照搬“33 字节”长度;当前真机描述符明确要求 64 字节,因此 Android 的帧从操作码开始,不额外前置 Report ID,并将未使用部分补 00。
该差异是此前 Android 连接初始化超时的主要排查结论。2026-08-23 已在 F700-HW 真机确认:64 字节 Report 可以完成连接、寻卡和完整读卡。Interrupt OUT 在健康状态下可写,但不能恢复长时间在线后的自然故障,因此当前发送路径仍固定为抓包一致的 SET_REPORT。
完整读卡还暴露了第二个长度边界:64 字节输入报告的响应头占 7 字节,因此单片数据最大为 64 - 7 = 57 字节。连接和寻卡响应较短,没有触发旧限制;读卡的首个长响应实际返回 57 字节,旧代码仍按 Windows 32 字节视图限制为 25,因而报 Invalid HID response chunk length: 57。当前实现已改为根据 Report 长度计算响应容量。
5. 厂商私有 HID 帧
以下偏移均以“去掉 Windows Report ID 后的私有 Report 第 0 字节”为起点。
5.1 请求帧
| 偏移 | 长度 | 字段 | 字节序 | 说明 |
|---|---|---|---|---|
0 |
1 | opcode |
- | 私有操作码 |
1 |
1 | reserved1 |
- | 抓包为 00 |
2..3 |
2 | requestOffset/reserved |
小端 | 当前普通请求固定 0000,正式语义待确认 |
4..5 |
2 | payloadLength |
小端 | 有效载荷长度 |
6 |
1 | reserved2 |
- | 当前为 00 |
7 |
1 | channel |
- | 01 卡片通道,02 控制通道 |
8.. |
N | payload |
- | 当前实现最多 24 字节 |
| 其余 | - | padding | - | 补 00 到 Report 长度 |
伪代码:
report[0] = opcode
report[4..5] = payload.length, little-endian
report[7] = channel
report[8..] = payload
remaining bytes = 00
5.2 响应帧
| 偏移 | 长度 | 字段 | 字节序 | 说明 |
|---|---|---|---|---|
0 |
1 | channel |
- | 应与请求通道一致 |
1 |
1 | reserved |
- | 抓包为 00 |
2..3 |
2 | offset |
小端 | 当前分片在完整响应中的起始位置 |
4..5 |
2 | chunkLength |
小端 | 本片有效数据长度;Windows 32 字节视图最大 25,Android 64 字节 Report 最大 57 |
6 |
1 | remaining |
- | 当前片之后仍剩余的字节数 |
7.. |
N | data |
- | 本片响应数据 |
| 其余 | - | padding | - | 补 00 |
接收端必须校验:通道相同、偏移连续、分片长度不超过 REPORT_SIZE - 7、分片数不异常。当前 Android 代码上限为 57 字节,并限制最多 16 片。
5.3 长响应续包
当响应头的 remaining != 0 时,主机发送 opcode=0x50 获取下一片:
opcode = 50
channel = 原通道
payload = FF <channel> 00 <offsetLow> <offsetHigh> 00 00
抓包示例,第一片返回 25 字节后请求偏移 25:
请求:50 00 00 00 07 00 00 01 FF 01 00 19 00 00 00 ...
响应:01 00 19 00 19 00 0B <25 bytes data> ...
Windows 32 字节抓包样本中的偏移按 0、25、50、75... 递增;Android 64 字节真机的首个长响应片为 57 字节。实现必须按“本片实际数据长度”累加下一次 offset,不能把步长固定成 25 或 57;最后一片 remaining=0。
6. 通道和操作码
| 值 | 当前名称 | 通道 | 证据等级 | 已观察用途 |
|---|---|---|---|---|
0x50 |
获取续包 | 原通道 | 抓包确认 | 根据 offset 读取下一片响应 |
0x62 |
激活卡片/开启射频会话 | 01 |
行为确认,名称为语义推定 | 判断有卡并返回 UID |
0x63 |
关闭通道/会话 | 01 或 02 |
行为确认,名称为语义推定 | 响应 9000 |
0x6B |
读卡器或卡片私有控制 | 01 或 02 |
抓包确认,子命令语义推定 | 初始化、寻卡、基础控制 |
0x6F |
透传卡片 APDU | 01 |
抓包确认,名称为语义推定 | SELECT、READ BINARY、钱包命令 |
通道:
01:卡片操作通道;02:读卡器初始化/控制通道。
7. 连接与初始化
Android 侧连接步骤:
- 枚举 USB 设备并匹配
0610:2010; - 请求并确认 Android USB 权限;
- 找到 HID 接口和 Interrupt IN 端点;
openDevice并claimInterface(force=true);- 执行以下控制通道初始化;
- 两条命令均返回
0101,关闭控制通道返回9000后,才算连接成功。
7.1 初始化 ON
opcode = 6B
channel = 02
payload = FF 8B 00 00 07 80 05 90 B0 01 00 00 00
expect = 01 01
完整有效前缀:
6B 00 00 00 0D 00 00 02 FF 8B 00 00 07 80 05 90
B0 01 00 00 00
7.2 初始化 OFF
opcode = 6B
channel = 02
payload = FF 8B 00 00 07 80 05 90 B0 00 00 00 00
expect = 01 01
7.3 关闭控制通道
opcode = 63
channel = 02
payload = 00 00 00 00 00 00 00 00 00 00
expect = 90 00
8. 寻卡协议
8.1 激活卡片
opcode = 62
channel = 01
payload = 00
有卡响应样本:
10 78 80 90 02 20 90 00 00 00 00 00 92 07 1F C9 90 00
解析规则:响应末尾必须是 90 00;末尾状态字之前的 4 字节是当前样本中的卡片 UID,即 92 07 1F C9。
无卡响应:
01 03
8.2 寻卡
opcode = 6B
channel = 01
payload = FF 04 01 00 00 00
有卡响应样本:
08 00 20 92 07 1F C9 90 00
末尾 90 00 之前的 4 字节同样为 UID。激活和寻卡返回的 UID 必须一致,否则按换卡处理。
无卡响应:
01 02
8.3 关闭卡片通道
opcode = 63
channel = 01
payload = 00 00 00 00 00 00 00 00 00 00
expect = 90 00
完整寻卡流程:
激活(62) → 寻卡(6B/FF0401...) → 比较 UID → 关闭通道(63)
9. APDU 封装与 CRC-A
9.1 发送格式
卡片 APDU 通过 opcode=0x6F、channel=0x01 发送。私有 payload 为:
APDU command + CRC-A low + CRC-A high + 00
示例:选择文件 EC01。
APDU core : 00 A4 00 00 02 EC 01
CRC-A : 41 F3
payload : 00 A4 00 00 02 EC 01 41 F3 00
示例:读取 ASN 记录。
APDU core : 00 B0 95 09 0B
CRC-A : C5 B8
payload : 00 B0 95 09 0B C5 B8 00
9.2 响应格式
response data + SW1 + SW2 + CRC-A low + CRC-A high
处理顺序:
- 至少应有 4 字节;
- 对除末尾 2 字节外的数据重新计算 CRC-A;
- 校验 CRC-A;
- 从 CRC 前两个字节读取
SW1/SW2; 9000表示成功,成功后向上层返回状态字之前的数据。
响应样本:
00 AA 03 00 00 02 6D 60 99 2A B9 90 00 35 65
└──────────── data ─────────────┘ └SW─┘ └CRC┘
9.3 CRC-A 算法
- 初值:
0x6363; - 输出:低字节在前;
- 算法:ISO/IEC 14443 Type A CRC-A 变体。
等价伪代码:
crc = 0x6363
for each byte:
value = byte XOR (crc AND 0xFF)
value = value XOR ((value << 4) AND 0xFF)
crc = (crc >> 8) XOR (value << 8) XOR (value << 3) XOR (value >> 4)
return [crc low, crc high]
10. 读卡指令序列
10.1 总体事务
当前 Java 实现提供两条入口:单独调用 readCard() 时会先完整寻卡得到基准 UID;已经显式调用 findCard() 的组合流程则调用 readCard(expectedUid),直接复用这次寻卡得到的 UID,不再重复发送第二组 0x62 → 0x6B → 0x63 完整寻卡命令。随后仍分别读取基础信息、主钱包和补助钱包,每个阶段重新执行 0x62 激活并校验 UID,避免用户中途移卡或换卡。
寻卡并记录 UID
├─ 激活 → EC01 → 读取基础记录 → 私有基础控制 → 关闭
├─ 激活 → EC01 → EC11 → 读取主钱包 → 关闭
└─ 激活 → EC01 → EC12 → 读取补助钱包 → 关闭
10.2 文件标识
| FID | 当前用途 | 证据等级 |
|---|---|---|
EC01 |
一卡通根应用/目录 | 抓包确认,名称为语义推定 |
EC11 |
主钱包 | 代码实现 + 读值匹配 |
EC12 |
补助钱包 | 代码实现 + 读值匹配 |
选择文件命令:
00 A4 00 00 02 <FID high> <FID low>
10.3 基础信息读取
| 顺序 | APDU core / 私有命令 | 读取用途 | 当前是否解析 |
|---|---|---|---|
| 1 | 00 A4 00 00 02 EC 01 |
选择一卡通应用 | 校验状态 |
| 2 | 00 B0 95 09 0B |
应用序列号 ASN | 是 |
| 3 | 00 B0 82 00 0E |
账号、卡号、持卡序号 | 是 |
| 4 | 00 B0 96 00 80 |
扩展记录第 1 段 | 暂不解析 |
| 5 | 00 B0 96 80 35 |
扩展记录第 2 段 | 暂不解析 |
| 6 | 00 B0 83 00 15 |
卡状态记录 | 是 |
| 7 | 00 B0 84 00 14 |
卡类型、卡类别、总额 | 是 |
| 8 | 0x6B / FF 02 01 02 02 1E 01 00 |
基础读取后的私有控制 | 只校验 9000 |
10.4 钱包读取
对 EC11 和 EC12 分别执行:
- 选择
EC01; - 选择钱包 FID;
- 发送
80 50 01 02 0B 01 00 00 00 00 00 6A 28 6A AC FD 0F,取返回数据作为钱包余额和消费计数; - 发送
00 20 00 00 06 00 00 00 FF FF FF; - 发送
80 50 00 02 0B 01 00 00 00 00 00 6A 28 6A AC FD 10; - 关闭卡片通道。
重要限制: 上述 80 50 和 00 20 序列来自当前单卡抓包,命令尾部字段可能包含密钥索引、终端号、交易序号、随机数或会话参数。其字段定义尚未由厂商资料确认。当前代码把样本字节固化仅用于复现读卡流程,不能据此宣称已获得通用消费或安全认证协议;换卡种、换项目或换 PSAM 环境前必须重新验证。
11. 卡信息字段解析
所有金额单位均为“分”。整数解析除 HID 分片头外,当前卡片业务记录按大端解析。
11.1 ASN 记录
| 字段 | 偏移 | 长度 | 规则 | 样本 |
|---|---|---|---|---|
| CardASN | 1 | 10 | 转大写十六进制字符串 | AA030000026D60992AB9 |
11.2 基础记录 00 B0 82 00 0E
样本:
29 00 72 BD 00 1A 07 63 00 02 00 00 00 14
| 字段 | 偏移 | 长度 | 字节序 | 样本解析值 |
|---|---|---|---|---|
| CustomerID(账号) | 2 | 2 | 大端无符号 | 29373 |
| CardNO(卡号) | 4 | 4 | 大端无符号 | 1705827 |
| CardSN(个人持卡序号) | 8 | 2 | 大端无符号 | 2 |
未列出的字节含义待确认。
11.3 状态记录 00 B0 83 00 15
| 字段 | 偏移 | 长度 | 规则 | 已知值 |
|---|---|---|---|---|
| Status | 0 | 1 | 无符号字节 | 0xF1 正常;厂商上层文档还列出 0xF3 挂失 |
上层测试工具可能以十进制或附加前缀显示状态,协议实现应保留原始字节值。
11.4 金额/类别记录 00 B0 84 00 14
样本:
00 00 01 F4 00 00 01 F4 08 14 02 00 99 99 12 31 00 01 05 94
| 字段 | 偏移 | 长度 | 字节序 | 样本解析值 |
|---|---|---|---|---|
| CardClass(卡类型) | 8 | 1 | - | 8,CPU 卡 |
| SubType/CardType(卡类别) | 9 | 1 | - | 20 |
| Ze/TotalAmount(总额) | 16 | 4 | 大端无符号 | 66964 分 |
厂商上层文档给出的 CardClass:4=M1、8=CPU。当前底层流程只在 CPU 卡样本上确认,不能据此认定 M1 卡可复用同一套 APDU。
11.5 钱包数据
| 字段 | 偏移 | 长度 | 字节序 | 主钱包样本 |
|---|---|---|---|---|
| Ye/Balance(余额) | 0 | 4 | 大端无符号 | 605 分 |
| OpCount(消费计数) | 4 | 2 | 大端无符号 | 42 |
补助钱包使用相同结构,对应 SubYe 和 SubCount;当前样本两者均为 0。
11.6 Android 返回模型
当前 CardInfo 返回:
| Java 字段 | 厂商上层字段 | 说明 |
|---|---|---|
uid |
SCardsnr(近似) |
4 字节射频 UID;不是业务卡号 |
cardClass |
CardClass/Cardkind |
4=M1,8=CPU |
applicationSerialNumber |
CardASN/ASN |
卡应用序列号 |
customerId |
CustomerID |
账号 |
cardNumber |
CardNO |
业务卡号 |
cardSerialNumber |
CardSN |
个人持卡序号 |
status |
Status |
原始卡状态 |
cardType |
SubType/CardType |
卡类别 |
totalAmount |
Ze/SumFare |
总额,分 |
balance |
Ye/OddFare |
主钱包余额,分 |
operationCount |
OpCount |
主钱包消费计数 |
subsidyBalance |
SubYe/SubOddFare |
补助余额,分 |
subsidyOperationCount |
SubCount/SubOpCount |
补助消费计数 |
12. 状态、错误与异常
12.1 已观察到的设备响应
| 响应 | 场景 | 当前上层错误 |
|---|---|---|
01 03 |
激活时无卡 | 寻卡时最终转为 NO_CARD;读卡阶段转为 CARD_REMOVED |
01 02 |
私有寻卡命令无卡 | NO_CARD |
90 00 |
关闭通道或 APDU 成功状态 | 成功 |
非 9000 |
卡片 APDU 状态失败 | PROTOCOL_ERROR;6A82 在当前代码中映射 UNSUPPORTED_CARD |
6A82 的映射属于 ISO 7816 常见语义和当前代码策略,尚未在本项目样本中确认。
12.2 Android 本地错误
| 错误 | 含义 |
|---|---|
DEVICE_NOT_FOUND |
未找到 0610:2010 |
PERMISSION_REQUIRED |
Android USB 权限尚未授予 |
OPEN_FAILED |
openDevice 失败 |
INTERFACE_NOT_FOUND |
未找到 HID 接口 |
ENDPOINT_NOT_FOUND |
未找到 Interrupt IN |
CLAIM_FAILED |
接口占用或 claim 失败 |
NOT_CONNECTED |
未连接就寻卡/读卡 |
IO_WRITE_FAILED |
HID SET_REPORT 写入长度异常;仅 F700-HW 的首次连接初始化失败可触发一次控制器恢复 |
DEVICE_RECOVERY_FAILED |
F700-HW 专用 xHCI 重绑命令失败、超时,或设备未带权限重新出现;需物理插拔读卡器 |
IO_TIMEOUT |
等待 Interrupt IN 超时 |
SHORT_REPORT |
返回长度不是当前期望的 64 字节 |
PROTOCOL_ERROR |
通道、偏移、长度、状态或固定响应不符合预期 |
CRC_ERROR |
APDU 响应 CRC-A 失败 |
NO_CARD |
寻卡时无卡 |
CARD_REMOVED |
读卡事务中卡片被拿走 |
CARD_CHANGED |
读卡事务中 UID 发生变化 |
UNSUPPORTED_CARD |
当前只对 6A82 做的兼容映射 |
12.3 事务要求
- 同一读卡器上的命令必须串行执行;
- 每次发送后等待对应响应,不并发寻卡和读卡;
- 分片必须按 offset 连续拼接;
- 读卡各阶段都要核对 UID;
- 成功或失败后尽量发送
0x63关闭卡片通道; - USB 拔出后立即释放连接,重新插入后重新申请权限和初始化;
- 日志应记录方向、opcode、channel、offset、长度、状态字和错误,不记录现场注册码、密钥或完整安全参数。
13. Android Java 接口对应关系
| 对外方法 | 底层行为 |
|---|---|
connect() |
重新枚举设备、权限检查、打开/claim HID、控制通道初始化;F700-HW 首次初始化 IO_WRITE_FAILED 时重绑固定 xHCI 控制器一次,再完整重连 |
findCard() |
0x62 激活、0x6B 寻卡、比较 UID、0x63 关闭 |
readCard() |
先寻卡,再分三段读取基础信息/主钱包/补助钱包并解析 |
readCard(expectedUid) |
复用已经寻到的 UID,跳过重复完整寻卡,直接分三段读取;每段仍以 0x62 激活校验同卡 |
disconnect() |
release interface、close connection;不缓存旧的 UsbDevice 或 UsbDeviceConnection |
isConnected() |
检查 connection/interface/input endpoint 是否存在 |
13.1 Android 全链路诊断日志
Android 实现通过 Logcat 标签 OnecardReader 和应用私有目录 files/reader_logs/ 双写结构化日志。每条记录以本机绝对时间 yyyy-MM-dd HH:mm:ss.SSS 开头,并在可计时步骤中附加单调时钟计算的 elapsedMs。
覆盖层级:
- 业务层:连接请求、手动寻卡/读卡、一键寻卡读卡、自动会话、USB 插拔和关闭;
- 设备层:设备、权限、HID 接口、Interrupt IN 端点、open、claim、SET_REPORT、Interrupt IN 读取、release 和 close;
- 协议层:控制通道初始化、激活、完整寻卡、基础信息、主/补助钱包、每条 APDU、分片重组和通道关闭;
- 异常层:
ReaderError、Java 异常类型、原始错误信息、失败阶段和已执行耗时。短 APDU 响应继续通过错误信息保存命令及原始逻辑响应。
日志示例:
2026-08-23 13:17:05.713 | session=1 | level=INFO | event=HID_EXCHANGE_START | opcode=6B channel=2 payloadBytes=13
2026-08-23 13:17:05.750 | session=1 | level=ERROR | event=USB_WRITE_FAILURE | opcode=6B channel=2 written=-1 elapsedMs=24 exception=ReaderException error=IO_WRITE_FAILED message=USB SET_REPORT wrote -1 bytes
日志按天命名为 reader-YYYY-MM-DD.log,单文件达到 5MB 后使用序号分卷,只保留最近7个自然日。正常记录请求/响应长度和协议摘要,不保存每个64字节补零报告,不增加密钥、注册码或完整安全参数。
2026-08-24 长时间连接后重开失败的第一份现场证据为:releaseInterface/close 成功;重新连接时同一枚举实例 deviceId=5005 的 openDevice/claimInterface 也成功,但第一条 0x6B 初始化 SET_REPORT 返回 -1,物理拔插后恢复。第二次自然故障在 deviceId=5006 上进一步确认 SET_REPORT 与 Interrupt OUT 都返回 -1,所以故障边界应描述为该读卡器/主控组合的 USB OUT 状态卡死,而不是单独控制写路径失效。应用会记录 CONNECT_CONTROLLER_RECOVERY_REQUESTED、USB_CONTROLLER_RECOVERY_START/SUCCESS/FAILURE 和 CONNECT_SUCCESS recovered=true/false,并记录恢复前后设备 ID 与毫秒耗时。
实现文件位置:
app/src/main/java/com/cpt/onecarddemo/reader/
├── AndroidUsbHidTransport.java USB HID 连接与收发
├── F700UsbControllerRecovery.java F700-HW xHCI 控制器限次恢复
├── HidFrameCodec.java 私有帧编解码与续包
├── NewcapecProtocol.java 初始化、寻卡、APDU、读卡流程
├── CrcA.java CRC-A
├── CardInfoParser.java 业务字段偏移解析
├── CardInfo.java 返回数据模型
└── ReaderError.java 错误分类
14. 已覆盖与待验证矩阵
| 项目 | Windows 抓包 | Android 代码 | Android 真机结果 |
|---|---|---|---|
| 设备识别与权限 | 不适用 | 已实现 | 设备识别/权限已观察 |
| 控制通道初始化 | 成功样本 | 已实现 | F700_HW 已通过 |
| 有卡寻卡 | 已抓取 | 已实现 | F700_HW 已通过,UID 92071FC9 |
| 无卡寻卡 | 已抓取 0103/0102 |
已实现 | 待验证 |
| 单卡完整读取 | 已抓取 | 已实现 | F700_HW 已通过,57 字节长响应已验证 |
| HID 长请求分包 | 36 字节 PSAM 样本 | 已实现并单测 | 待随 Android 现金消费验证 |
| 主钱包本地消费 | 1 分成功样本 | 已实现并按样本单测 | 待 Android 1 分实卡验证 |
| 补助钱包本地消费 | 无补助余额扣款样本 | 与主钱包共用流程 | 待有补助余额卡验证 |
| 交易结果不确定保护 | 上层文档要求避免非法记录 | 禁止自动重试,要求重新读卡 | 待异常注入/现场验证 |
| 同卡连续读取 | 已抓取 | 已实现 | 逐轮重连测试曾出现第 33、30 轮短响应和第 17 轮 NO_CARD;单次连接测试第 37 轮在读卡中返回 CARD_REMOVED |
| 读卡中移卡 | 已抓取 | 已做 UID/无卡保护 | 待验证 |
| 换卡 | 无第二张卡完整样本 | 已做 UID 保护 | 待验证 |
| USB 热拔插 | 无底层样本 | Android UI 已处理 | 待验证 |
| M1 卡 | 无 | 未确认支持 | 待卡样本/厂商资料 |
| 挂失/过期/坏卡 | 无 | 只有通用协议异常 | 待卡样本/厂商错误码 |
| 长时间在线后重开恢复 | 无 | F700-HW 首次初始化写失败时重绑 xHCI 一次 | 人工执行重绑已从自然故障恢复;应用内恢复器真机测试已完成重绑、重枚举与协议重连;仍待下一次长时间空闲自然触发端到端验收 |
14.1 Android 连续循环测试结果
2026-08-23 在 F700_HW 上执行“连接 → 寻卡 → 完整读卡 → 关闭”最多 100 轮的连续测试,并设置首次失败立即停止:
- 第 1–32 轮全部通过,UID、卡号、余额和消费计数保持一致;
- 成功轮次平均耗时 732.81 ms,前 10 轮平均 698.5 ms,后 10 轮平均 780.0 ms;
- 第 33 轮在补助钱包读取阶段重新激活卡片后,首次选择
EC01时返回不足 4 字节的 APDU 响应; - 上层错误为
PROTOCOL_ERROR / APDU response is too short; - 测试按约定停止,没有执行第 34–100 轮;
- 测试停止后 USB 读卡器仍可枚举,未观察到物理拔出。
该结果证明单次和短期连续读取可用,但不能证明 100 轮高频稳定。失败响应的原始十六进制内容尚未记录,具体原因仍待确认。
后续诊断版本在短响应异常中增加了“APDU 命令 + 原始响应十六进制 + 长度”,不改变通讯行为。使用同一设备、同一卡片和零轮间延迟重新执行 100 轮,100 轮全部通过,总耗时 71.702 秒,原短响应未复现。
卡片长时间放置会触发读卡器报警,但报警后保持卡片不动立即执行连接、寻卡、完整读卡和关闭仍成功。因此报警不是读卡失败的充分条件;报警触发瞬间是否存在偶发时序碰撞仍待带原始响应日志的失败样本确认。
随后按正式口径增加 500ms 轮间间隔再次执行最多 100 轮。间隔严格位于“上一轮关闭成功”与“下一轮连接开始”之间,首次失败仍立即停止且不重试。结果为第 1–29 轮通过,第 30 轮在基础信息读取阶段执行 00 B0 96 80 35 时收到原始逻辑响应 03 01(2 字节),因此以 PROTOCOL_ERROR 停止,第 31–100 轮未执行。
正常 APDU 逻辑响应最短也需要 SW1 + SW2 + CRC-A(2 bytes) 共 4 字节,所以 03 01 不能按当前正常响应格式解析。其厂商正式含义尚不明确,不能直接等同于“无卡”“报警”或标准 ISO 7816 状态字。该新样本同时说明:异常不固定在第 33 轮、不固定在补助钱包阶段,且 500ms 轮间等待不足以保证连续稳定。
同样保持 500ms 轮间间隔再次独立复测时,第 1–16 轮通过,第 17 轮连接成功后在 findCard 阶段返回 NO_CARD 并停止。用户确认失败发生前卡片一直放在读卡器上,随后读卡器持续报警,用户在报警后才拿走卡片;现场检查 Android 设备在线且 USB 读卡器 0610:2010 仍可枚举。因此该轮不是用户提前移卡或 USB 物理断开,更接近“物理有卡但射频/寻卡链路未识别”。
该现场增强了持续放卡、报警和射频失联存在关联的证据,但仍不能证明持续放卡是根因:相同持续放置条件曾有 100/100 轮通过,且此前报警后即时完整读卡成功。报警可能是失联的结果,也可能与失联共享某个设备内部状态,仍需“持续放置”与“每轮移开再放回”的独立对照确认。
为隔离逐轮 USB 打开/关闭的影响,后续执行了“测试开始只连接一次 → 循环 100 次寻卡和完整读卡,每轮成功后等待 200ms → 完成或失败后只关闭一次”的独立测试。第 1–36 轮通过,第 37 轮外层寻卡成功后,在 readCard → readBaseData → activateExpected 重新激活卡片时返回无卡,上层映射为 CARD_REMOVED,随后 finally 关闭连接并停止;第 38–100 轮未执行。全场原始日志只有一次 CONNECTED、一次 UsbDeviceConnectionJNI: close 和一次 DISCONNECTED,测试后 USB 读卡器仍可枚举。
该结果证明频繁打开/关闭不是故障发生的必要条件,也不是完整根因;它是否会提高失败概率仍需多组对照。当前主要嫌疑转向持续读卡期间的射频重新激活、卡片持续放置/报警相关内部状态,以及激活响应的原始 HID 字节。下一步应分别记录激活无卡的原始响应,并比较“卡片持续放置”与“每轮检测移开后重新放入”。
15. 下一轮最小验证步骤
- 在现有 APDU 命令和逻辑响应日志基础上,继续记录失败前后每一条 HID 请求/响应的 opcode、channel、offset、chunk 长度和原始字节,确认
0301来自设备原始报告还是分片重组结果; - 将“每轮重新连接”与“一次连接内连续读卡”做独立对照,并按需测试更长间隔,区分 USB 重新枚举、读卡器固件状态和射频会话恢复因素;
- 无卡执行寻卡和读卡,确认分别得到
NO_CARD; - 读卡过程中移卡,确认上层得到
CARD_REMOVED,且重新放卡后可以再次连接/读取; - 读卡过程中拔出读卡器,确认连接被释放,重新插入后可以恢复;
- 让读卡器保持连接并长时间空闲,随后关闭再打开;若首写失败,确认日志依次出现
CONNECT_CONTROLLER_RECOVERY_REQUESTED、USB_CONTROLLER_RECOVERY_START、USB_CONTROLLER_RECOVERY_SUCCESS和CONNECT_SUCCESS recovered=true,且无需物理拔插; - 后续如需扩大支持范围,至少补充第二张 CPU 卡、M1 卡以及一张挂失或过期卡的样本。
16. 版本记录
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.4 | 2026-08-24 | 第二次自然故障确认 SET_REPORT 与 Interrupt OUT 均返回 -1,撤销备用写路径;F700-HW 上增加首次连接写失败时的一次性 xHCI 控制器重绑,完成恢复器真机重绑与协议重连测试,并保留长时间自然触发验收项 |
| 1.3 | 2026-08-24 | 根据长时间连接后重开日志,确认失败点为同一 USB 枚举实例 open/claim 成功后的 SET_REPORT=-1;连接初始化阶段增加 Interrupt OUT 0x02 单次备用写路径,并完成 F700_HW 强制路径真机验证 |
| 1.2 | 2026-08-23 | 根据第二次 Android 现场失败确认消费阶段通道 2、3 是两个 PSAM 候选槽位;改为动态探测并选择返回 ATR/9000 的槽位,纠正固定使用通道 3 的假设 |
| 1.1 | 2026-08-23 | 根据 Android 本地消费现场失败补充 PSAM 激活异常短响应:写卡前关闭通道 3 并限次重试一次;仍失败时停止并记录原始响应和长度 |
| 1.0 | 2026-08-23 | 增加 HID 长请求分包、控制/卡片/PSAM 三通道、本地消费四阶段 APDU、动态字段偏移、1 分成功证据与防重复扣款边界 |
| 0.9 | 2026-08-23 | 增加 Android 全链路本地日志规范:毫秒时间、步骤耗时、业务/USB/协议/APDU/异常覆盖、5MB分卷、7天保留和应用内查看边界 |
| 0.8 | 2026-08-23 | 新增复用已寻卡 UID 的 readCard(expectedUid) 流程;一键和自动读卡只执行一次完整寻卡,同时保留三段读取前的激活与同卡校验 |
| 0.7 | 2026-08-23 | 记录单次 USB 连接、200ms 间隔循环测试:36 轮通过,第 37 轮读卡基础阶段重新激活返回 CARD_REMOVED;证明频繁打开/关闭不是故障必要条件 |
| 0.6 | 2026-08-23 | 记录第二次 500ms 复测:16 轮通过,第 17 轮物理有卡时寻卡返回 NO_CARD,随后读卡器报警且 USB 仍在线;增强持续放卡/报警与射频失联的关联证据,但不判定因果 |
| 0.5 | 2026-08-23 | 记录 500ms 轮间间隔正式测试:29 轮通过,第 30 轮 00B0968035 收到 0301 两字节短响应并停止;确认 500ms 间隔不足以规避问题,且异常不固定轮次和 APDU 阶段 |
| 0.4 | 2026-08-23 | 记录短响应诊断复现:同条件 100 轮全部通过,报警后即时读卡成功;确认报警不是失败的充分条件,原第 33 轮异常属于未复现的偶发短响应 |
| 0.3 | 2026-08-23 | 记录 Android 高频循环稳定性测试:32 轮通过,第 33 轮补助钱包阶段 APDU 短响应并停止;补充耗时趋势和下一轮取证要求 |
| 0.2 | 2026-08-23 | 真机确认 Android 64 字节 Report 可完成连接、寻卡和读卡;修正响应单片容量为 57,并记录长响应报错根因与验证结果 |
| 0.1 | 2026-08-23 | 首次独立汇总 USB HID、私有帧、续包、寻卡、APDU、CRC-A、读卡序列、字段偏移、错误和待验证项 |
17. CPU 卡本地消费协议(1.2 补充)
17.1 三通道与初始化
| 通道 | 用途 | 关键操作 |
|---|---|---|
1 |
用户卡 | 激活、选择 EC01/钱包、初始化消费、正式扣款 |
2 |
读卡器初始化控制、PSAM 候选槽 1 | 连接初始化使用 0x6B;消费前以 0x62 / 00 探测,返回 ATR/9000 时作为 PSAM 通道 |
3 |
PSAM 候选槽 2 | 通道 2 未发现 PSAM 时关闭并探测;返回 ATR/9000 时作为 PSAM 通道 |
PSAM 激活响应样本为 3B7D94000057443751908693855B630602339000。选择 3F00 后,通过 00 B0 96 00 06 读取 6 字节 PSAM ID;样本为 00 06 21 00 02 06,按无符号十六进制数显示为十进制 26323452422。
Windows 成功抓包中,通道 2 的 0x62 / 00 返回 0101,通道 3 返回上述 20 字节 ATR,因此旧实现固定使用通道 3。第二次 Android 现场失败中,通道 2直接返回 20 字节,证明当前设备的 PSAM 位于另一个槽位,0101 应视为该候选槽未发现可用 PSAM,而不是消费必须满足的“控制通道成功”条件。
当前实现依次探测通道 2、3:响应长度大于 2 且以 9000 结尾时选为本次交易的 PSAM 通道,后续选择文件、读取 PSAM ID、80 70 与 80 72 全部复用该通道。0101 直接检查下一个槽位;其他异常响应只在任何扣款 APDU 之前关闭当前槽并重试一次。两槽均不可用才返回 PSAM_ERROR,错误列出各槽原始响应和长度。该逻辑不覆盖任何写卡后异常,不能用于 80 54 结果不确定场景。
17.2 HID 请求分包
普通请求仍使用单个报告。逻辑载荷超过 24 字节时:
- 每个报告最多携带 24 字节逻辑载荷;
- offset 位于报告字节
2..3,小端; - 字节
4..5表示从当前 offset 起尚需发送的字节数; - 字节
6表示当前片之后的剩余字节数; - 中间片只执行
SET_REPORT,最后一片发送后才读取 Interrupt IN 响应。
36 字节 PSAM 80 70 请求的两片头为:
6F 00 00 00 24 00 0C 03 <24 bytes>
6F 00 18 00 0C 00 00 03 <12 bytes>
17.3 现金钱包 1 分成功链路
交易前主钱包余额 604 分、消费计数 43;交易后余额 603 分、计数 44。消费时间为 2026-08-23 14:26:00。
用户卡初始化消费:
80 50 01 02 0B 01 00000001 000621000206 0F入口动态字段:金额 4 字节大端、PSAM ID 6 字节。返回 15 字节数据按当前样本解析为:余额 4、消费计数 2、透支限额 3、密钥版本 1、算法标识 1、随机数 4。
PSAM 计算:
80 70 00 00 1C 00C53F7F 002B 00000001 06 20260823142600 01 000000026D60992AB9动态字段依次为卡片随机数 4、原消费计数 2、金额 4、交易类型
06、7 字节 BCD 时间、密钥版本 1、PSAM 使用的 9 字节卡应用序列。返回0000003E 30D31CE3:PSAM 脱机交易序号 4 字节、MAC1 4 字节。用户卡正式扣款:
80 54 01 00 0F 0000003E 20260823142600 30D31CE3 08返回
A4441B33 B83F9C91:TAC 4 字节、MAC2 4 字节。PSAM 确认:
80 72 00 00 04 B83F9C91
所有 APDU core 后追加 CRC-A;读卡器私有传输再带 1 字节尾随值。PSAM 80 70 成功样本的 CRC 后尾随字节为 30,其他样本为 00;尾随字节不参与 CRC,当前实现按成功样本复现。
17.4 钱包与分账
主钱包选择 EC11,补助钱包选择 EC12。单钱包 API 接收账号、金额、钱包和时间毫秒值。补贴优先协调层先依据读卡结果生成最多两笔计划:先补助、后主钱包。两笔交易分别产生自己的消费计数、PSAM 序号和 TAC,不能合并成一条卡上交易。
补助钱包当前只有读取证据,没有非零余额扣款样本,因此代码状态为“已实现、待补助卡验证”,不能按现金钱包的证据等级对外承诺。
17.5 异常与重试边界
- 账号不一致、卡状态不是
0xF1、金额不合法或余额不足:写卡前拒绝。 80 54已发送但没有获得可信响应:返回TRANSACTION_OUTCOME_UNKNOWN,禁止自动重试;必须重新读卡核对余额和计数。- 用户卡已返回 TAC,但 PSAM
80 72失败:卡片扣款仍视为发生;结果标记psamConfirmed=false,禁止再次扣卡。 - 补贴优先第一笔成功、第二笔失败:返回部分成功及第一笔完整结果,不自动回滚或重试。