本示例使用的发卡器: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,AUTH0myctrlword = 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>