本示例使用的发卡器:RS232串口USB转COM读写器IC卡发卡器WEB浏览器二次开发JS编程SDK-淘宝网 (taobao.com)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Serial串口读写器Ntag卡示例 </title>
<script>
window.onload = function() {
document.getElementById('butt_openserial').hidden=true;
document.getElementById('butt_closeserial').hidden=true;
document.getElementById('dispauthkey').hidden=true;
document.getElementById('authkey0').hidden=true;
}
if ('serial' in navigator){
}else{
alert('您的浏览器不支持 Web Serial API,暂无法使用以下功能!');
}
navigator.serial.onconnect =function event(){
console.log("Serial port connected: ", event.target);
}
navigator.serial.ondisconnect =function event(){
console.log("Serial port disconnected: ", event.target);
}
var BLOCK0_EN = 0x01;//读第一块的(16个字节)
var BLOCK1_EN = 0x02;//读第二块的(16个字节)
var BLOCK2_EN = 0x04;//读第三块的(16个字节)
var NEEDSERIAL = 0x08;//仅读指定序列号的卡
var EXTERNKEY = 0x10;//用明码认证密码,产品开发完成后,建议把密码放到设备的只写区,然后用该区的密码后台认证,这样谁都不知道密码是多少,需要这方面支持请联系
var NEEDHALT = 0x20; //读/写完卡后立即休眠该卡,相当于这张卡不在感应区。要相重新操作该卡必要拿开卡再放上去
var port = null;
var reader = null;
var reading = false;
const getdata=new Uint8Array(1000); //接收串口返回的数据
var DataPoint=0; //接收数据指针
var SendCode=0; //已发送的指令代码
function isUIntNum(val) {
var testval = /^\d+$/; // 非负整数
return (testval.test(val));
}
function isHex(val) {
var testval = /^(\d|[A-F]|[a-f])+$/; // 十六进制数判断
return (testval.test(val));
}
async function SelectSerial(){
try{
port =await navigator.serial.requestPort(); // 弹出系统串口列表对话框,选择一个串口进行连接
ports =await navigator.serial.getPorts(); // 获取已连接的授权过的设备列表
document.getElementById('butt_openserial').hidden=false;
}
catch (e)
{
console.log(e);
}
}
function updateInputData(data) {
let array = new Uint8Array(data); // event.data.buffer就是接收到的inputreport包数据了
//let hexstr = "";
for (const data of array) {
//hexstr += (Array(2).join(0) + data.toString(16).toUpperCase()).slice(-2) + " "; // 将字节数据转换成(XX )形式字符串
getdata[DataPoint]=data;
DataPoint=DataPoint+1;
}
var crc=0;
for(i=1;i<DataPoint;i++){ //校验接收数据,同时也解决数据分包上传的问题
crc=crc^getdata[i];
}
if (crc==0 && DataPoint>1){
let hexstr = "";
for (i=0;i<DataPoint;i++){
hexstr=hexstr+getdata[i].toString(16).padStart(2, '0').toUpperCase()+" ";
}
ReceiveData.value += hexstr;
var dispstr="";
var cardnohex="";
var datahex="";
switch (SendCode) {
case 1: //驱动发卡器响声的回应
dispstr = "发卡器已执行响声指令!" ;
label_disp.innerText=dispstr;
break;
case 6: //读Ntag卡的回应
switch (getdata[1]){
case 0x00:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
for (i=11;i<DataPoint-1;i++){datahex=datahex+getdata[i].toString(16).padStart(2, '0').toUpperCase()+" ";}
dispstr= "读Ntag卡成功,卡号:" + cardnohex ;
RWdata.value=datahex;
break;
case 0x08:
dispstr= "读卡失败,未寻到卡!"
break;
case 0x09:
dispstr= "读卡失败!两张以上卡片同时在感应区发生冲突!"
break;
case 0x0c:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
dispstr= "卡密码认证失败,卡号:" + cardnohex +",读取块数据失败!";
break;
case 0x0d:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
dispstr= "可能要带密码操作,卡号:" + cardnohex +",读取块数据失败!";
break;
default:
dispstr= "读卡失败,返回错误代码:"+getdata[1].toString();
}
label_disp.innerText=dispstr;
break;
case 7: //写Ntag卡的回应
case 8: //初始化Ntag卡的回应
switch (getdata[1]){
case 0x00:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
dispstr= "写Ntag卡成功,卡号:" + cardnohex ;
break;
case 0x08:
dispstr= "读卡失败,未寻到卡!"
break;
case 0x09:
dispstr= "写卡失败!两张以上卡片同时在感应区发生冲突!"
break;
case 0x0c:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
dispstr= "卡密码认证失败,卡号:" + cardnohex +",写块数据失败!";
break;
case 0x0e:
for (i=2;i<=8;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}
dispstr= "可能要带密码操作,卡号:" + cardnohex +",写块数据失败!";
break;
default:
dispstr= "写卡失败,返回错误代码:"+getdata[1].toString();
}
label_disp.innerText=dispstr;
break;
}
DataPoint=0; //数据接收指针置0
}
}
async function listenReceived(){
if (reading){
console.log("On reading.");
return;
}
reading=true;
while (port.readable && reading) {
reader = port.readable.getReader();
try {
updateInputData(value);
} catch (e) {
alert(e);
} finally {
reader.releaseLock();
}
}
await port.close(); // 关闭串口
port = null;
alert("串口已关闭!");
}
async function OpenSerial(){
if (port==null){
alert('请先选择要操作的串口号!');
return;
}else{
document.getElementById('butt_closeserial').hidden=false;
var baudSelected = parseInt(document.getElementById("select_btn").value);
await port.open({
baudRate: baudSelected,
});
listenReceived();
alert('串口打开成功!');
}
}
async function CloseSerial(){
if ((port == null) || (!port.writable)) {
alert("请选择并打开与发卡器相连的串口!");
return;
}
if (reading) {
reading = false;
reader?.cancel();
}
document.getElementById('butt_openserial').hidden=true;
document.getElementById('butt_closeserial').hidden=true;
}
function selecheckauthkey(){
if (checkauth.checked){
document.getElementById('dispauthkey').hidden=false;
document.getElementById('authkey0').hidden=false;
}else{
document.getElementById('dispauthkey').hidden=true;
document.getElementById('authkey0').hidden=true;
}
}
async function beep(){
if ((port == null) || (!port.writable)) {
alert("请选择并打开与发卡器相连的串口!");
return;
}
var beepdelay=parseInt(document.getElementById("beepdelay").value);
const outputData = new Uint8Array(5);
outputData[0]=0x03;
outputData[1]=0x0f;
outputData[2]=beepdelay % 256;
outputData[3]=beepdelay / 256;
outputData[4]=outputData[1] ^ outputData[2] ^outputData[3];
var sendhex="";
for(i=0;i<5;i++){
sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";
}
SendData.value=sendhex;
ReceiveData.value="";
SendCode=1;
DataPoint=0;
const writer = port.writable.getWriter();
await writer.write(outputData); // 发送数据
writer.releaseLock();
}
async function piccinit_ntag(){
if ((port == null) || (!port.writable)) {
alert("请选择并打开与发卡器相连的串口!");
return;
}
myctrlword = 0; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
mypiccserial = "00000000000000"; //指定序列号,00000000000000 表示任意 NTAG卡。
if (checkauth.checked) { //指定密码,NTAG21x卡密码为4个字节,卡出厂时密码功能不启用,这样无需密码也能读写卡
myctrlword = EXTERNKEY; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
mypicckey = authkey0.value.trim();
if (!isHex(mypicckey) || mypicckey.length!=8) {
alert( "卡片认证密钥输入错误,请输入8位16进制密钥!");
authkey0.focus();
authkey0.select();
return;
}
}
else {
mypicckey = "00000000";
}
//数据准备
if (selonoff.selectedIndex == 0) { //开启密码保护功能,写保护功能生效,但读保护需要下面的数据设定
newkeystr = newkey.value.trim(); //取新密码
if (!isHex(newkeystr) || newkeystr.length != 8) {
alert( "新密钥输入错误,请输入8位16进制新密钥!");
newkey.focus();
newkey.select();
return;
}
strls1 = protectpageno.value.trim();//起始保护页号
if (!isUIntNum(strls1)) {
alert("起始保护页号输入错误!");
protectpageno.focus();
protectpageno.select();
return;
}
strls1 = "0" + parseInt(strls1).toString(16);
beginpage = strls1.substring(strls1.length - 2);
//计数器
strls1 = keyerrortimes.value.trim();//允许密码错误次数
if (!isUIntNum(strls1)) {
alert("允许密码错误次数输入错误!");
protectpageno.focus();
protectpageno.select();
return;
}
i = parseInt(strls1);
i = i % 8;
if (checkreadon.checked) {
i = i + 128;
}
strls1 = "0" + i.toString(16);
authfail = strls1.substring(strls1.length - 2);
packstr = packcode.value.trim(); //取PACK码
if (!isHex(packstr) || packstr.length != 4) {
alert( "PACK密钥确认码输入错误,请输入4位16进制PACK密钥确认码!");
packcode.focus();
packcode.select();
return;
}
mypiccdata = "000000";
mypiccdata = mypiccdata +beginpage;
mypiccdata = mypiccdata + authfail;
mypiccdata = mypiccdata + "000000";
mypiccdata = mypiccdata + newkeystr; //4字节新密码
mypiccdata = mypiccdata + packstr; //2字节PACK确认码
mypiccdata = mypiccdata + "0000";
myctrlword = myctrlword + 0x01; //更新控制字
myctrlword = myctrlword + 0x02; //更新控制字
myctrlword = myctrlword + 0x04; //更新控制字
}
else {
mypiccdata = "000000FF"; //MIRROR,RFUI,MIRROR_PAGE,AUTH0
myctrlword = myctrlword + 0x01; //更新控制字
mypiccdata = mypiccdata + "000000000000000000000000";
myctrlword = myctrlword + 0x02; //更新控制字
}
const outputData = new Uint8Array(31);
outputData[0]=0x1d; //指令数据长度
outputData[1]=0x16; //功能码
outputData[2]=myctrlword; //控制位
for (i=0;i<7;i++){ //7字节本次操作卡UID,7字节全部取0表示可操作任意ntag标签
outputData[3+i]=parseInt(mypiccserial.substr(i*2,2),16);
}
for (i=0;i<4;i++){ //4字节卡片认证密钥
outputData[10+i]=parseInt(mypicckey.substr(i*2,2),16);
}
for (i=0;i<16;i++){ //16字节初化数据
outputData[14+i]=parseInt(mypiccdata.substr(i*2,2),16);
}
var crc=0;
for (i=1;i<30;i++){crc=crc^outputData[i];}
outputData[30]=crc; //指令信息累加和校验位
var sendhex="";
for(i=0;i<31;i++){
sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";
}
SendData.value=sendhex;
ReceiveData.value="";
SendCode=8;
DataPoint=0;
var label_disp = document.getElementById('label_disp');
label_disp.innerText = "";
const writer = port.writable.getWriter();
await writer.write(outputData); // 发送数据
writer.releaseLock();
}
async function readcard_ntag(){
if ((port == null) || (!port.writable)) {
alert("请选择并打开与发卡器相连的串口!");
return;
}
mypiccserial = "00000000000000"; //指定序列号,未知卡序列号时可指定为14个0,因为NTAG21x卡是7个字节的卡序列号
if (checkauth.checked) { //指定密码,NTAG21x卡密码为4个字节,卡出厂时密码功能不启用,这样无需密码也能读写卡
myctrlword = EXTERNKEY; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
mypicckey = authkey0.value.trim();
if (!isHex(mypicckey) || mypicckey.length!=8) {
alert( "卡片认证密钥输入错误,请输入8位16进制密钥!");
authkey0.focus();
authkey0.select();
return;
}
}
else {
mypicckey = "00000000";
myctrlword = 0; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
}
myblockaddr = ntagstartno.value.trim(); //读写起始页
if (!isUIntNum(myblockaddr)) {
alert( "读写起始页输入错误!");
ntagstartno.focus();
ntagstartno.select();
return;
}
myblocksize = ntagpagenumber.value.trim(); //读写页数
if (!isUIntNum(myblocksize)) {
alert( "读写页数输入错误!");
ntagpagenumber.focus();
ntagpagenumber.select();
return;
}else if(myblocksize>12){
alert( "每次最多读取12块数据!");
ntagpagenumber.focus();
ntagpagenumber.select();
ntagpagenumber.value="12";
return;
}
const outputData = new Uint8Array(17);
outputData[0]=0x0f; //指令数据长度
outputData[1]=0x1b; //功能码
outputData[2]=myctrlword; //控制位
for (i=0;i<7;i++){ //7字节本次操作卡UID,7字节全部取0表示可操作任意ntag标签
outputData[3+i]=parseInt(mypiccserial.substr(i*2,2),16);
}
for (i=0;i<4;i++){ //4字节卡片认证密钥
outputData[10+i]=parseInt(mypicckey.substr(i*2,2),16);
}
outputData[14]=myblockaddr; //读卡起始块
outputData[15]=myblocksize; //读卡总块数
var crc=0;
for (i=1;i<16;i++){
crc=crc^outputData[i];
}
outputData[16]=crc; //指令信息累加和校验位
var sendhex="";
for(i=0;i<17;i++){
sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";
}
SendData.value=sendhex;
ReceiveData.value="";
RWdata.value="";
SendCode=6;
DataPoint=0;
var label_disp = document.getElementById('label_disp');
label_disp.innerText = "";
const writer = port.writable.getWriter();
await writer.write(outputData); // 发送数据
writer.releaseLock();
}
async function writecard_ntag(){
if ((port == null) || (!port.writable)) {
alert("请选择并打开与发卡器相连的串口!");
return;
}
mypiccserial = "00000000000000"; //指定序列号,未知卡序列号时可指定为14个0,因为NTAG21x卡是7个字节的卡序列号
if (checkauth.checked) { //指定密码,NTAG21x卡密码为4个字节,卡出厂时密码功能不启用,这样无需密码也能读写卡
myctrlword = EXTERNKEY; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
mypicckey = authkey0.value.trim();
if (!isHex(mypicckey) || mypicckey.length!=8) {
alert( "卡片认证密钥输入错误,请输入8位16进制密钥!");
authkey0.focus();
authkey0.select();
return;
}
}
else {
mypicckey = "00000000";
myctrlword = 0; //指定控制字,无需密码为0,当需要密码时为EXTERNKEY;
}
myblockaddr = ntagstartno.value.trim(); //读写起始页
if (!isUIntNum(myblockaddr)) {
alert( "读写起始页输入错误!");
ntagstartno.focus();
ntagstartno.select();
return;
}
myblocksize = ntagpagenumber.value.trim(); //读写页数
if (!isUIntNum(myblocksize)) {
alert( "读写页数输入错误!");
ntagpagenumber.focus();
ntagpagenumber.select();
return;
}else if(myblocksize>11){
alert( "每次最多写入11块数据!");
ntagpagenumber.focus();
ntagpagenumber.select();
ntagpagenumber.value="11";
return;
}
var datalen=myblocksize*4;
mypiccdata = RWdata.value.trim(); //写卡数据
mypiccdata=mypiccdata.replace(/\s/g, "");
if (!isHex(mypiccdata)) {
alert( "写卡数据输入错误,请输入"+(datalen*2).toString()+"位16进制写卡数据!");
RWdata.focus();
RWdata.select();
return;
}else if(mypiccdata.length<datalen*2){
if (confirm("写卡数据不足,是否要后面补0写入?")) {
while (mypiccdata.length<datalen*2){
mypiccdata=mypiccdata+"0";
}
}else{
return;
}
}
const outputData = new Uint8Array(datalen+17);
outputData[0]=15+datalen; //指令数据长度
outputData[1]=0x1c; //功能码
outputData[2]=myctrlword; //控制位
for (i=0;i<7;i++){ //7字节本次操作卡UID,7字节全部取0表示可操作任意ntag标签
outputData[3+i]=parseInt(mypiccserial.substr(i*2,2),16);
}
for (i=0;i<4;i++){ //4字节卡片认证密钥
outputData[10+i]=parseInt(mypicckey.substr(i*2,2),16);
}
outputData[14]=myblockaddr; //写卡起始块
outputData[15]=myblocksize; //写卡总块数
for (i=0;i<datalen;i++){ //写卡数据
outputData[16+i]=parseInt(mypiccdata.substr(i*2,2),16);
}
var crc=0;
for (i=1;i<16+datalen;i++){
crc=crc^outputData[i];
}
outputData[16+datalen]=crc; //指令信息累加和校验位
var sendhex="";
for(i=0;i<17+datalen;i++){
sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";
}
SendData.value=sendhex;
ReceiveData.value="";
SendCode=7;
DataPoint=0;
var label_disp = document.getElementById('label_disp');
label_disp.innerText = "";
const writer = port.writable.getWriter();
await writer.write(outputData); // 发送数据
writer.releaseLock();
}
</script>
<style>
th {
font-family:楷体;
background-color:#F6FAFF;
color:blue;
}
td {
font-family:楷体;
background-color:#F6FAFF;
}
</style>
</head>
<body>
<table width="950" height="428" align="center">
<tr>
<td width="120" height="50">
<input name="btnSelect" type="submit" id="btnSelect" style="width:100%" onclick="SelectSerial()" value="选择串口" />
</td>
<td width="800">波特率:<label for="select_btn"></label>
<select name="select_btn" id="select_btn">
<option>1200</option>
<option>4800</option>
<option>9600</option>
<option>14400</option>
<option selected="selected">19200</option>
<option>38400</option>
<option>43000</option>
<option>57600</option>
<option>115200</option>
<option>128000</option>
<option>230400</option>
<option>256000</option>
<option>460800</option>
<option>921600</option>
<option>1382400</option>
</select>
数据位:
<select name="select_btn2" id="select_data">
<option>8</option>
<option>7</option>
<option>6</option>
<option>5</option>
</select>
停止位:
<select name="select_btn3" id="select_stop">
<option>1</option>
<option>1.5</option>
<option>2</option>
</select>
校验位:
<select name="select_btn4" id="select_mark">
<option>None 无</option>
<option>Odd 奇</option>
<option>Even 偶</option>
<option>Mask 常1</option>
<option>Space 常0</option>
</select>
<input name="butt_openserial" type="submit" id="butt_openserial" style="width:80px" onclick="OpenSerial()" value="打开串口" />
<input name="butt_closeserial" type="submit" id="butt_closeserial" style="width:80px" onclick="CloseSerial()" value="关闭串口" />
</td>
</tr>
<tr>
<td height="36" >
<input name="butt_beep" type="submit" id="butt_beep" style="width:100%" onclick="beep()" value="驱动发卡器响声" />
</td>
<td>响声延时:
<input style="color:blue;text-align:center;" name="beepdelay" type="text" id="beepdelay" value="30" size="5" maxlength="4" onkeyup="this.value=this.value.replace(/\D/g,'')"/>
毫秒</td>
</tr>
<tr>
<td height="36"> </td>
<td><label style="color:blue;" name="label_disp" id="label_disp"></label></td>
</tr>
<tr>
<td height="36"> </td>
<td>
<input type="checkbox" name="checkauth" id="checkauth" onchange="selecheckauthkey()"/>
选择先认证卡片密钥再继续以下的操作
<label name="dispauthkey" id="dispauthkey">,16进制卡片认证密钥:</label>
<input style="color:blue;text-align:center;" name="authkey0" type="text" id="authkey0" value="12345678" size="8" maxlength="8" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/>
</td>
</tr>
<tr>
<td height="69"><input style="width:120px" name="butt_piccinit_ntag" type="submit" id="butt_piccinit_ntag" onclick="piccinit_ntag()" value="初始化Ntag卡" /></td>
<td><p>
<label for="rwtext"></label>
<select style="color:blue;" name="selonoff" id="selonoff">
<option>开启卡片密钥保护功能</option>
<option selected="selected">取消卡片密钥保护功能</option>
</select>
,从:
<input style="color:blue;text-align:center;" name="protectpageno" type="text" id="protectpageno" value="20" size="4" maxlength="4" onkeyup="this.value=this.value.replace(/\D/g,'')"/>
页开始有密钥保护功能,
<input type="checkbox" name="checkreadon" id="checkreadon" />
选择开启读操作密钥保护。</p>
<p>新密钥:
<input style="color:blue;text-align:center;" name="newkey" type="text" id="newkey" value="12345678" size="8" maxlength="8" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/>
,允许密钥认证失败次数:
<input style="color:blue;text-align:center;" name="keyerrortimes" type="text" id="keyerrortimes" value="0" size="2" maxlength="2" onkeyup="this.value=this.value.replace(/\D/g,'')"/>
,PACK密钥确认码:
<input style="color:blue;text-align:center;" name="packcode" type="text" id="packcode" value="1234" size="4" maxlength="4" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/>
</p>
<p style="color:red;">警告:当密钥认证失败次数取值0表示不限制次数,认证密钥操作失败大于设置值时卡片将会报废!</p></td>
</tr>
<tr>
<td height="36"><p>
<input style="width:120px" name="butt_readcard_ntag" type="submit" id="butt_readcard_ntag" onclick="readcard_ntag()" value="轻松读Ntag卡" />
</p>
<p> </p>
<p>
<input style="width:120px" name="butt_writecard_ntag" type="submit" id="butt_writecard_ntag" onclick="writecard_ntag()" value="轻松写Ntag卡" />
</p></td>
<td><p>读写起始页:
<input style="color:blue;text-align:center;" name="ntagstartno" type="text" id="ntagstartno" value="4" size="4" maxlength="4" onkeyup="this.value=this.value.replace(/\D/g,'')"/>
,读写页数:
<input style="color:blue;text-align:center;" name="ntagpagenumber" type="text" id="ntagpagenumber" value="10" size="2" maxlength="2" onkeyup="this.value=this.value.replace(/\D/g,'')"/>
,每次最多读12页、写11页。</p>
<p>
<textarea style="width:800px;color:red;font-family:楷体;" name="RWdata" id="RWdata" cols="100" rows="4" ></textarea>
</p></td>
</tr>
<tr>
<td height="70" scope="row"><p align="center">发送的数据</p></td>
<td><textarea style="width:800px;color:blue;" name="SendData" id="SendData" cols="100" rows="4" ></textarea></td>
</tr>
<tr>
<td height="70" scope="row"><p align="center">接收的数据</p></td>
<td><textarea style="width:800px" name="ReceiveData" id="ReceiveData" cols="100" rows="4" ></textarea></td>
</tr>
</table>
</body>
</html>