项目背景:某医院HIS离线系统,要把一堆数据(啥数据反正你们也不关心,此处省略)写到NFC卡内,给患者拿走流窜使用(领药啊,验血啊...)

卡片选取:FM1208卡。此卡具体资料,需要的同学可自行搜索。我这边选取理由就是:安全性高(防嗅探,防复制),读写空间足够(8K),成本能接受(3-4元)

FM1208卡内置了一套COS(Card Operation System)系统,功能很多,我这边用到的主要是:

  • 卡片检测,
  • 卡复位检测,
  • 密钥系统(我这里只使用了【外部密钥】和【用户口令】),
  • 目录及文件(含读写权限设置)

因为是第一次处理NFC卡项目,我总结了几个要注意的坑:

  1. 卡片内数据,都要存为二进制,也就是字节数组。
  2. 卡内单次读写,最大255字节,所以要处理分段读写。
  3. 卡内空间有限,合理的规划数据结构是重点。

好了,本篇主要内容,其实就是处理以上三点的过程和思路。那么,正文开始。

android nfc 读CPU卡 nfc读写cpu卡_学习方法


 

1、数据序列化问题:

程序内数据流动,一般都是基于model类,现在主流语言也都是使用ORM处理数据库,这个基本避不开。举例: 我这边患者信息和药方信息,其实都是一个类对象(明细是对象集合),在我的程序各功能里来回流窜。

所以,NFC卡内数据,也得保持好结构,方便读写解析。

然后就是选择序列化方案了。最常用的json和xml首先被排除了,因为冗余字段太多了,对NFC卡这种小空间实在不友好。

经搜索,最后目标方案有两个:TLV和Protobuf。(相关资料请自查)

在TLV上,我花了大量时间研究,(大概80%的时间)git上的方案也试了好几个,并且写了一版解析工具类,基本可以开搞了。

但研究了protobuf,并写完测试程序后(大概20%的时间),我果断把TLV无情抛弃  ヾ( ̄▽ ̄)

android nfc 读CPU卡 nfc读写cpu卡_学习方法_02

 

原因有2:

(1)两者底层结构基本一致,都是基于TLV思想(Tag,Length,Value),而且都可以灵活变通,如根据需要,会变种为TV(Tag + Value),LV(Length + Value),甚至,只有一个Value。最终生成的二进制数据,几乎无差别。

(2)但是!啊,主要是因为这个但是哈。TLV反正我没找到现成的轮子,只有转码工具代码(也就是单值 -->TLV格式),这就要求,你自己封装mode转List<TLV>的工具类。

不过这也没办法,因为TLV本身就是给通讯底层用的,一般web或者移动项目,用API接口获取数据的话,json是最优解,所以基本没人费事去做这玩意,主要是做了也基本没人用。除了像我这种一开始没想明白的选手。这块我基本做出来了,用的是特性标注(序列化) + 反射 (反序列化)。

但是protobuf可不一样啊,它的出生就是为了解决Google内部开发不同开发语言组之间,数据通讯和共享的标准数据格式。性能和易用性都没的说,大厂出品,那肯定比我自己做的靠谱。

解析库选择:

我用的库是 protobuf-net,是git上一个开源库,其作者封装了google开源的c#版的底层库,非常简单易用。附上链接:GIT链接:Protobuf

不过该作者是根据一个file物理文件写的示例代码,但究其原理就是一个内存流处理。为避免有看官迷糊,我放上一段自写的示例代码,以作补充说明:

//测试类对象
Test2 test = new Test2() { 
state1=1, state2=2 ,
state3 = "ABCDE"
};
           
byte[] bytes = null;
//序列化为二进制数组
using (MemoryStream memoryStream = new MemoryStream())
{
    ProtoBuf.Serializer.Serialize(memoryStream, test);
    bytes = memoryStream.ToArray();
    ShowByte(bytes, "test");//打印测试
}

//二进制数组 反序列化为 类对象
using (MemoryStream ms = new MemoryStream(bytes))
{
    var test2 = ProtoBuf.Serializer.Deserialize<Test2>(ms);
    //utf-8里,汉字3字节,GBK里汉字2字节,汉字多的字段,最好用GBK转换,毕竟NFC卡空间小
    // string state3_str = Encoding.GetEncoding("GBK").GetString(test.state3);
    string json2 = System.Text.Json.JsonSerializer.Serialize(test2);
    Console.WriteLine($"json2={json2}");
}

Test2类:

[ProtoContract]
    public class Test2
    {
       
        [ProtoMember(1)]
        public byte state1 { get; set; }

        [ProtoMember(2)]
        public byte state2 { get; set; }

        [ProtoMember(3)]
        public string state3 { get; set; }

    }

这用法足够极简吧,直接在nuget里搜protobuf-net,加载到项目中即可。

2、转换的model类注意事项:

(1)汉字多的字段,尽量用GBK转化成二维数组,再存卡,能省30%的空间。

默认的utf-8编码,一个汉字是3字节,而GBK是2字节。像我这里,一个300字的诊断建议,utf-8编码占900字节,GBK占600字节,这对比很清楚吧?NFC卡内才多少空间啊?

简单点说,类里边,凡是有汉字存在的字段,我都用的是 byte[] ,而不是string。咱就是这么抠抠搜搜  (°ー°〃)

(2)float不要用,要用decimal。

虽然float是4字节;decimal 8字节,但是就得用decimal,这个原因下边讲读写的时候,会解释。

(3)int类型 ,bool类型

还是考虑卡内空间问题,分情况使用byte,uint16,short,uint32等等。

C#数据单位长度都是字节级,最小单位都是字节,像bool和byte都是占一个字节,这点没办法用bit级去搞小动作了,所以一些状态值字段,请用bool或byte,别用int,因为int是4字节....

3、分段读写

因为单次只能读写255字节,所以读写两个函数,都要逻辑判断,是否大于255。

(1)写:小于255字节的数据,直接单次写入,没啥说的。

        大于255的数据,要先切割数据,以255为单位,把数据切割成数份,并得出最后一组数据块的字节数。然后偏移写入。

比如,有500个字节的数据要写,假如你按每次写入200(用200举例是因为比255看起来更清晰)个数据,那么,

次数     偏移地址      长度      数据
1              0                200      aa...a
2            200              200      bb..b
3            400             100       cc...c

注意:无论如何 最后要多写入一个0,作为结尾。

因为NFC卡内容不会清理,只会逐个字节覆写。结尾写0,则读取的时候,检测到0,就意味着读取结束。

举例:我上次数据写了1000字节进去,我这次写500字节,其实卡里边600-1000的数据,还是存在的。

所以,我读卡的时候,每读一批数据,就检测下数据里有没有0,读到0就可以确认本次读取完毕,后边的不读了。

顺便填一下float的坑。这里不能用float,因为float里如果没有小数位,就会在它的前两个字节里有0存在,但decimal不会。这块,可以自己测一下,将float数据转二进制即可。

(2)读:主要思路就是:以255为单位,进行单次读取,找到0则结束,然后把读到的数据拼到一个字节数组里去。(其实,卡内每个文件都有大小限制,需要根据文件大小细化处理,否则会报越界)

android nfc 读CPU卡 nfc读写cpu卡_学习方法_03

 

读写这块说的有点啰嗦,但主要是说处理思路。如果有更好思路的大神,还请不吝赐教。

这块处理代码有点长,我是不想贴了,已经鬼扯了这么一大堆,再贴代码,这文章长度我都看不下去了。

如果实在不明白的,请留言或私信吧,我和你就着代码再解释。

最后说下FM1208的处理顺序作为结束:

  1. 连接 
  2. 寻卡 
  3. 复位 
  4. 密钥或口令验证
  5. 选主应用 
  6. 选文件
  7. 写文件 或读文件
  8. 断开 lc_exit