新开普 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 控制器,再重新枚举、打开并初始化设备。

当前资料已经足够实现以下流程:

  1. 按 VID/PID 查找并打开读卡器;
  2. 初始化读卡器控制通道;
  3. 激活射频卡片并读取 4 字节 UID;
  4. 选择一卡通应用和钱包文件;
  5. 读取账号、卡号、状态、余额、消费计数等信息;
  6. 识别无卡、移卡、换卡、CRC 错误、USB 超时等异常。
  7. 通过 PSAM 对主钱包执行本地消费,并返回交易序号、TAC 和扣款后计数;补助钱包使用同一流程但仍待有余额卡验证。

以下内容仍不能视为厂商正式协议:

2. 证据等级

标记 含义 本文使用方式
抓包确认 Windows Bus Hound 样本中可重复观察 可直接作为兼容实现依据
真机确认 Android 设备枚举或 HID 描述符直接读取 可作为 Android USB 参数依据
代码实现 OnecardDemo 当前 Java 代码的行为 表示当前实现,不等于厂商正式定义
语义推定 根据命令前后行为推断 使用时保留兼容和日志能力
待确认 样本不足或涉及卡片/终端动态数据 不应固化为跨项目通用常量

主要证据来源:

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 权限、openDeviceclaimInterface 均成功,但任何 OUT 写入都失败。setConfigurationsetInterfaceUSBDEVFS_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 关闭通道/会话 0102 行为确认,名称为语义推定 响应 9000
0x6B 读卡器或卡片私有控制 0102 抓包确认,子命令语义推定 初始化、寻卡、基础控制
0x6F 透传卡片 APDU 01 抓包确认,名称为语义推定 SELECT、READ BINARY、钱包命令

通道:

7. 连接与初始化

Android 侧连接步骤:

  1. 枚举 USB 设备并匹配 0610:2010
  2. 请求并确认 Android USB 权限;
  3. 找到 HID 接口和 Interrupt IN 端点;
  4. openDeviceclaimInterface(force=true)
  5. 执行以下控制通道初始化;
  6. 两条命令均返回 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=0x6Fchannel=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

处理顺序:

  1. 至少应有 4 字节;
  2. 对除末尾 2 字节外的数据重新计算 CRC-A;
  3. 校验 CRC-A;
  4. 从 CRC 前两个字节读取 SW1/SW2
  5. 9000 表示成功,成功后向上层返回状态字之前的数据。

响应样本:

00 AA 03 00 00 02 6D 60 99 2A B9 90 00 35 65
└──────────── data ─────────────┘ └SW─┘ └CRC┘

9.3 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 钱包读取

EC11EC12 分别执行:

  1. 选择 EC01
  2. 选择钱包 FID;
  3. 发送 80 50 01 02 0B 01 00 00 00 00 00 6A 28 6A AC FD 0F,取返回数据作为钱包余额和消费计数;
  4. 发送 00 20 00 00 06 00 00 00 FF FF FF
  5. 发送 80 50 00 02 0B 01 00 00 00 00 00 6A 28 6A AC FD 10
  6. 关闭卡片通道。

重要限制: 上述 80 5000 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=M18=CPU。当前底层流程只在 CPU 卡样本上确认,不能据此认定 M1 卡可复用同一套 APDU。

11.5 钱包数据

字段 偏移 长度 字节序 主钱包样本
Ye/Balance(余额) 0 4 大端无符号 605
OpCount(消费计数) 4 2 大端无符号 42

补助钱包使用相同结构,对应 SubYeSubCount;当前样本两者均为 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_ERROR6A82 在当前代码中映射 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 事务要求

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;不缓存旧的 UsbDeviceUsbDeviceConnection
isConnected() 检查 connection/interface/input endpoint 是否存在

13.1 Android 全链路诊断日志

Android 实现通过 Logcat 标签 OnecardReader 和应用私有目录 files/reader_logs/ 双写结构化日志。每条记录以本机绝对时间 yyyy-MM-dd HH:mm:ss.SSS 开头,并在可计时步骤中附加单调时钟计算的 elapsedMs

覆盖层级:

日志示例:

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=5005openDevice/claimInterface 也成功,但第一条 0x6B 初始化 SET_REPORT 返回 -1,物理拔插后恢复。第二次自然故障在 deviceId=5006 上进一步确认 SET_REPORT 与 Interrupt OUT 都返回 -1,所以故障边界应描述为该读卡器/主控组合的 USB OUT 状态卡死,而不是单独控制写路径失效。应用会记录 CONNECT_CONTROLLER_RECOVERY_REQUESTEDUSB_CONTROLLER_RECOVERY_START/SUCCESS/FAILURECONNECT_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 轮的连续测试,并设置首次失败立即停止:

该结果证明单次和短期连续读取可用,但不能证明 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. 下一轮最小验证步骤

  1. 在现有 APDU 命令和逻辑响应日志基础上,继续记录失败前后每一条 HID 请求/响应的 opcode、channel、offset、chunk 长度和原始字节,确认 0301 来自设备原始报告还是分片重组结果;
  2. 将“每轮重新连接”与“一次连接内连续读卡”做独立对照,并按需测试更长间隔,区分 USB 重新枚举、读卡器固件状态和射频会话恢复因素;
  3. 无卡执行寻卡和读卡,确认分别得到 NO_CARD
  4. 读卡过程中移卡,确认上层得到 CARD_REMOVED,且重新放卡后可以再次连接/读取;
  5. 读卡过程中拔出读卡器,确认连接被释放,重新插入后可以恢复;
  6. 让读卡器保持连接并长时间空闲,随后关闭再打开;若首写失败,确认日志依次出现 CONNECT_CONTROLLER_RECOVERY_REQUESTEDUSB_CONTROLLER_RECOVERY_STARTUSB_CONTROLLER_RECOVERY_SUCCESSCONNECT_SUCCESS recovered=true,且无需物理拔插;
  7. 后续如需扩大支持范围,至少补充第二张 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 7080 72 全部复用该通道。0101 直接检查下一个槽位;其他异常响应只在任何扣款 APDU 之前关闭当前槽并重试一次。两槽均不可用才返回 PSAM_ERROR,错误列出各槽原始响应和长度。该逻辑不覆盖任何写卡后异常,不能用于 80 54 结果不确定场景。

17.2 HID 请求分包

普通请求仍使用单个报告。逻辑载荷超过 24 字节时:

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

  1. 用户卡初始化消费:

    80 50 01 02 0B 01 00000001 000621000206 0F
    

    入口动态字段:金额 4 字节大端、PSAM ID 6 字节。返回 15 字节数据按当前样本解析为:余额 4、消费计数 2、透支限额 3、密钥版本 1、算法标识 1、随机数 4。

  2. 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 字节。

  3. 用户卡正式扣款:

    80 54 01 00 0F 0000003E 20260823142600 30D31CE3 08
    

    返回 A4441B33 B83F9C91:TAC 4 字节、MAC2 4 字节。

  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 异常与重试边界