菜鸟进场,方圆十里,寸草不生
这两天研究了NFC功能,网上查了很多的资料,不过感觉别人讲的都大同小异,但都缺了那么一点点火候,因为第一次接触有些概念是不清楚的,所以代码看上去很吃力,这个博客呢就是想整体的进行讲解一下,做一点点补充,算是做个笔记吧。
我用到的是nxp公司的S50芯片,网上也有很多卖的,但是叫法不一样,有的人叫M1智能卡芯片,有的叫复旦芯片(复旦什么科技公司生产的完全兼容S50的卡,可以理解为国产山寨),一般淘宝上买的都是这些,生活中使用的比如电梯卡,门禁卡也可能是属于这一类,价格大概5毛一张吧。
好了,先讲讲基本的东西吧
首先:
RFID: 叫射频识别技术,分为接触式(需要插卡)与非接触式(只需刷卡),NFC就是从这个技术发展而来的,包含多个频段,915MHz,125KHz,13.56MHz,2.4GHz等。
ID卡: 主要工作在125KHz,只有一个身份识别码,判断方式就是卡身有一串卡号,使用时需要联网进行操作。
IC卡: 主要工作在13.56MHz,里面有存储空间,可以进行读写,脱机工作(公交卡,门禁卡等)。
NFC: 叫近场通讯技术,只能工作在13.56MHz,所以能读取全部工作在这个频段的卡,是属于REID技术的,但是又有新的功能,可以理解为REID的子类。
以上概念了解了吧,所以我们开发NFC的话,只要不是设备与设备通信的话,那剩下的就是NFC来读取13.56MHz频段的IC卡或者ID卡。哦,还有NFC标签,这个我没有研究过,如果要用到这个的话就只有自己查了,这个靶向性比较好,容易查。
好吧,接下来说13.56频段的IC卡,由于不同的厂家生产的不同的芯片,数据格式与通信协议是不同的,所以需要对应,Android在这方面是有完整的底层支持的,你看到的NfcA、NfcB、NfcF、NfcV、IsoDep、Ndef这些就是对应不同的数据格式或者通讯协议的。举个例子吧,nxp公司的MIFARE Classic数据格式就是NfcA,MIFARE DESFire数据格式是IsoDep,二代身份证用的就是NfcB,Sony生产的Felica用的就是NfcF,德州仪器的VicinityCard卡用的是NfcV,。大概有个了解,在开发时通过自己手上的芯片类型再去定向搜索使用方法要容易一些。
好了,下面说我手上的MIFARE Classic S50芯片,读取所用到的类为MifareClassic,存储大小是1k,分16个扇区,每个扇区有4个块,每个块可以存储16个字节的数据。程序员一般从0开始数,第0扇区的第0块是厂家信息,这个无法写入。然后每个扇区的最后一个块(就是第3块)为密码块,每个密码块包含6个字节的密码A,4个字节的控制位(我现在手上只有一种卡,我不知道其他卡的控制位是否一样,但是按道理可能也许大概是一样的吧),6个字节的密码B,密码位是可以进行写入的,就是说可以自由设置扇区密码,一般新买的卡都是白卡,默认密码位ff ff ff ff ff ff(16进制)。可能不太清楚,截个图。
数据结构基本了解,下面开始讲解如何进行读取与写入:
读取:
添加权限
<uses-permission android:name="android.permission.NFC" />
设置页面属性和添加过滤器(本来还有一个什么过滤文件的,结果我死活加不上去,那就这样吧,不加了)
<activity
android:name=".NFCTest.NfcTestActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
</activity>
行,页面配置好了就可以进行页面的编写了。
首先进行判断,是否能否有NFC功能,如果有的话是否打开。
NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
if (null == adapter) {
Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show();
} else if (!adapter.isEnabled()) {
Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
// 根据包名打开对应的设置界面
startActivity(intent);
}
重写onNewIntent方法,每次刷卡的时候都会进入这个方法的。然后再这个方法里操作是读是写。
好,先看如何进行读取,我把注释都写代码里,方便一点。
//拿来装读取出来的数据,key代表扇区数,后面list存放四个块的内容
Map<String, List<String>> map = new HashMap<>();
//intent就是onNewIntent方法返回的那个intent
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
//如果当前IC卡不是这个格式的mfc就会为空
if (null != mfc) {
try {
//链接NFC
mfc.connect();
//获取扇区数量
int count = mfc.getSectorCount();
//用于判断时候有内容读取出来
boolean flag = false;
for (int i = 0; i < count; i++) {
//默认密码,如果是自己已知密码可以自己设置
byte[] bytes = {(byte) 0xff, (byte) 0xff, (byte) 0xff,
(byte) 0xff, (byte) 0xff, (byte) 0xff};
//验证扇区密码,否则会报错(链接失败错误)
//这里验证的是密码A,如果想验证密码B也行,将方法中的A换成B就行
boolean isOpen = mfc.authenticateSectorWithKeyA(i, bytes);
if (isOpen) {
//获取扇区里面块的数量
int bCount = mfc.getBlockCountInSector(i);
//获取扇区第一个块对应芯片存储器的位置
//(我是这样理解的,因为第0扇区的这个值是4而不是0)
int bIndex = mfc.sectorToBlock(i);
for (int j = 0; j < bCount; j++) {
//读取数据,这里是循环读取全部的数据
//如果要读取特定扇区的特定块,将i,j换为固定值就行
byte[] data = mfc.readBlock(bIndex+j);
list.add(byteToString(data));
}
flag = true;
}
map.put(i + "", list);
}
if (flag) {
//回调,因为我把方法抽出来了
callback.callBack(map);
} else {
callback.error();
}
} catch (Exception e) {
callback.error();
e.printStackTrace();
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
byteToString()方法,就是将16进制的数据转为字符串。
/**
* 将byte数组转化为字符串
*
* @param src
* @return
*/
public static String byteToString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
char[] buffer = new char[2];
for (int i = 0; i < src.length; i++) {
buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16);
buffer[1] = Character.forDigit(src[i] & 0x0F, 16);
System.out.println(buffer);
stringBuilder.append(buffer);
}
return stringBuilder.toString();
}
接下来就是写数据了 a是扇区,b是块
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
byte[] data = new byte[16];
if (null != mfc) {
try {
//连接NFC
mfc.connect();
//获取扇区数量
int count = mfc.getSectorCount();
//如果传进来的扇区大了或者小了直接退出方法
if (a > count - 1 || a < 0) {
callback.isSusses(false);
return;
}
//获取写的扇区的块的数量
int bCount = mfc.getBlockCountInSector(a);
//如果输入的块大了或者小了也是直接退出
if (b > bCount - 1 || b < 0) {
callback.isSusses(false);
return;
}
//将字符转换为字节数组,这样其实并不好,无法转换汉字,因为汉字转出来要占三个字节
for (int i = 0; i < 16; i++) {
if (i < str.length()) {
data[i] = (byte) str.charAt(i);
} else {
data[i] = (byte) ' ';
}
}
//验证扇区密码 bytes也是默认密码
boolean isOpen = mfc.authenticateSectorWithKeyA(a, bytes);
if (isOpen) {
int bIndex = mfc.sectorToBlock(a);
//写卡
mfc.writeBlock(bIndex + b, data);
}
callback.isSusses(true);
} catch (Exception e) {
e.printStackTrace();
callback.isSusses(false);
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来说如何设置密码,其实跟写数据是一样的,只是需要将格式设置一下然后写到特定的地方而已
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
byte[] data = new byte[16];
if (null != mfc) {
try {
mfc.connect();
if (password.length() != PASSWORD_LENTH) {
callback.isSusses(false);
return;
}
int count = mfc.getSectorCount();
if (a > count - 1 || a < 0) {
callback.isSusses(false);
return;
}
//将密码转换为keyA
for (int i = 0; i < password.length(); i++) {
data[i] = (byte) password.charAt(i);
}
//将密码转换为KeyB 我AB密码一样的,也可以不一样
for (int i = 0; i < password.length(); i++) {
data[i + password.length() + 4] = (byte) password.charAt(i);
}
//输入控制位
data[password.length()] = (byte) 0xff;
data[password.length() + 1] = (byte) 0x07;
data[password.length() + 2] = (byte) 0x80;
data[password.length() + 3] = (byte) 0x69;
//验证密码
boolean isOpen = mfc.authenticateSectorWithKeyA(a, bytes);
if (isOpen) {
int bIndex = mfc.sectorToBlock(a);
int bCount = mfc.getBlockCountInSector(a);
//写到扇区的最后一个块
mfc.writeBlock(bIndex + bCount - 1, data);
}
callback.isSusses(true);
} catch (Exception e) {
e.printStackTrace();
callback.isSusses(false);
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
好了,大概就是这样的,理解通这个了的话,S50芯片应该就没有问题了,到时候想存什么都可以了,数据大于扇区还可把两个扇区一起用啊,这样可玩性就多了。
最后我写了一个完成的demo,代码有点丑,但是功能能实现的,有兴趣的可以看看。
https://download.csdn.net/download/version1_0/10375350