本示例使用的设备:RS232串口RFID NFC IC卡读写器可二次开发编程发卡器USB转COM-淘宝网 (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串口读写器示例 </title><script> window.onload = function() {document.getElementById('butt_openserial').hidden=true;document.getElementById('butt_closeserial').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:break;case 2: //读取M1卡序列号的回应case 3: //读取M1卡扇区数据的回应case 4: //写M1卡扇区数据的回应case 5: //修改M1卡扇区数密钥的回应switch (getdata[0]){case 1: //返回有效数据长度为1switch (getdata[1]){case 8:dispstr = "未寻到卡!" ; break;case 9:dispstr = "两张以上卡片同时在感应区,发生冲突!" ; break;case 10:dispstr = "无法选择激活卡片!" ; break;case 11:dispstr = "密码装载失败,卡片序列号已知!" ; break;default:dispstr = "操作卡失败,返回代码:" + getdata[1].ToString() ; break;}var label_disp = document.getElementById('label_disp');label_disp.innerText = dispstr;break; case 5: //返回有效数据长度为5 for(i=2;i<6;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}if(SendCode==2){dispstr ="读卡序列号";}else if(SendCode==3){dispstr ="读扇区数据";}else if(SendCode==4){dispstr ="写扇区数据";}else if(SendCode==5){dispstr ="修改扇区密码";} switch (getdata[1]){case 0: dispstr=dispstr+"成功!卡号:"+cardnohexbreak;case 1:dispstr = dispstr+",密码认证成功,但读写扇区内容失败!卡号:" + cardnohex;break; case 2:dispstr = dispstr+",第0块操作成功,但第1、2块操作失败,仅扇区内容前16个字节的数据有效!!卡号:" + cardnohex;break; case 3:dispstr = dispstr+",第0、1块操作成功,但第2块操作失败,仅扇区内容前32个字节的数据有效!!卡号:" + cardnohex;break; case 12:dispstr = dispstr+",密码认证失败,卡号:" + cardnohex;break;default:dispstr = dispstr+",操作卡失败,返回代码:" + getdata[1].ToString() ; break; }var label_disp = document.getElementById('label_disp');label_disp.innerText = dispstr;break;case 53: //返回有效数据长度为53for(i=2;i<6;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}dispstr = "读M1卡扇区数据成功,卡号:" + cardnohex;for(i=6;i<DataPoint-1;i++){datahex=datahex+getdata[i].toString(16).padStart(2, '0').toUpperCase()+" ";}var label_disp = document.getElementById('label_disp');label_disp.innerText = dispstr;RWM1Data.value=datahex;break;case 54: //旧版本设备返回有效数据长度为54for(i=2;i<6;i++){cardnohex=cardnohex+getdata[i].toString(16).padStart(2, '0').toUpperCase();}dispstr = "读M1卡扇区数据成功,卡号:" + cardnohex;for(i=7;i<DataPoint-1;i++){datahex=datahex+getdata[i].toString(16).padStart(2, '0').toUpperCase()+" ";}var label_disp = document.getElementById('label_disp');label_disp.innerText = dispstr;RWM1Data.value=datahex;break; }break;}DataPoint=0;}} async function listenReceived(){if (reading){console.log("On reading.");return;}reading=true;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();} }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 Request(){if ((port == null) || (!port.writable)) {alert("请选择并打开与发卡器相连的串口!");return;} const outputData = new Uint8Array(3);outputData[0]=0x01; //指令长度outputData[1]=0xf0; //功能码outputData[2]=0xf0; //校验码 var sendhex="";for(i=0;i<3;i++){sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";} SendData.value=sendhex;ReceiveData.value="";SendCode=2;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 ReadM1Card(){if ((port == null) || (!port.writable)) {alert("请选择并打开与发卡器相连的串口!");return;} if (selinoutkey.selectedIndex==1){myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN + EXTERNKEY; //外部密钥认证}else {myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN;} //已装载到发卡器内部密钥认证myareano = selareano.selectedIndex; //指定为本次读取第8区,十进制authmode = selauthmode.selectedIndex; //指定密码模式,十进制,大于0表示用A密码认证,推荐用A密码认证mypiccserial = "00000000"; //指定本次操作的卡序列号,十六进制,未知时可指定为8个0mypicckey = authkey0.value.trim(); //指定卡片密码,十六进制,FFFFFFFFFFFF为卡片厂家出厂密码if (!isHex(mypicckey) || mypicckey.length!=12) {textarea.value = "认证密钥输入错误,请输入正确的12位16进制认证密钥!";authkey0.focus();authkey0.select();return;} const outputData = new Uint8Array(16);outputData[0]=0x0e; //指令长度outputData[1]=0x78; //功能码outputData[2]=myctrlword; //控制位 outputData[3]=0x00; //四字节本次操作卡卡号,全部取0表示可操作任意卡outputData[4]=0x00;outputData[5]=0x00;outputData[6]=0x00;outputData[7]=myareano; //扇区号outputData[8]=authmode; //密钥认证方式 for(i=0;i<6;i++){ //6字节密钥outputData[9+i]=parseInt(mypicckey.substr(i*2,2),16);}var crc=0;for (i=1;i<15;i++){crc=crc^outputData[i];}outputData[15]=crc; var sendhex="";for(i=0;i<16;i++){sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";} SendData.value=sendhex;ReceiveData.value="";RWM1Data.value="";SendCode=3;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 WriteM1Card(){if ((port == null) || (!port.writable)) {alert("请选择并打开与发卡器相连的串口!");return;} if (selinoutkey.selectedIndex==1){myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN + EXTERNKEY; //外部密钥认证}else {myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN;} //已装载到发卡器内部密钥认证myareano = selareano.selectedIndex; //指定为本次读取第8区,十进制authmode = selauthmode.selectedIndex; //指定密码模式,十进制,大于0表示用A密码认证,推荐用A密码认证mypiccserial = "00000000"; //指定本次操作的卡序列号,十六进制,未知时可指定为8个0mypicckey = authkey0.value.trim(); //指定卡片密码,十六进制,FFFFFFFFFFFF为卡片厂家出厂密码if (!isHex(mypicckey) || mypicckey.length!=12) {textarea.value = "认证密钥输入错误,请输入正确的12位16进制认证密钥!";authkey0.focus();authkey0.select();return;} mypiccdata=RWM1Data.value.trim();mypiccdata=mypiccdata.replace(/\s/g, "");if (isHex(mypiccdata)){if(mypiccdata.length<96){if (confirm("写卡数据不足一扇区48个字节,是否要后面补0写入?")) { while (mypiccdata.length<96){mypiccdata=mypiccdata+"0";}}else{return;} }}else{alert("请输入96位16进制写卡数据!");return;}const outputData = new Uint8Array(64);outputData[0]=0x3e; //指令长度outputData[1]=0x69; //功能码outputData[2]=myctrlword; //控制位 outputData[3]=0x00; //四字节本次操作卡卡号,全部取0表示可操作任意卡outputData[4]=0x00;outputData[5]=0x00;outputData[6]=0x00;outputData[7]=myareano; //扇区号outputData[8]=authmode; //密钥认证方式 for(i=0;i<6;i++){ //6字节密钥outputData[9+i]=parseInt(mypicckey.substr(i*2,2),16);}for(i=0;i<48;i++){ //48字节写卡数据outputData[15+i]=parseInt(mypiccdata.substr(i*2,2),16);}var crc=0;for (i=1;i<63;i++){crc=crc^outputData[i];}outputData[63]=crc; var sendhex="";for(i=0;i<64;i++){sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";} SendData.value=sendhex;ReceiveData.value="";SendCode=4;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 changecardkeyex(){if ((port == null) || (!port.writable)) {alert("请选择并打开与发卡器相连的串口!");return;} if (selinoutkey.selectedIndex==1){myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN + EXTERNKEY; //外部密钥认证}else {myctrlword = BLOCK0_EN + BLOCK1_EN + BLOCK2_EN;} //已装载到发卡器内部密钥认证myareano = selareano.selectedIndex; //指定为本次读取第8区,十进制authmode = selauthmode.selectedIndex; //指定密码模式,十进制,大于0表示用A密码认证,推荐用A密码认证mypiccserial = "00000000"; //指定本次操作的卡序列号,十六进制,未知时可指定为8个0mypicckey = authkey0.value.trim(); //指定卡片密码,十六进制,FFFFFFFFFFFF为卡片厂家出厂密码if (!isHex(mypicckey) || mypicckey.length!=12) {textarea.value = "认证密钥输入错误,请输入正确的12位16进制认证密钥!";authkey0.focus();authkey0.select();return;} mypicckeyA = newkeya.value.trim(); //新A密钥if (!isHex(mypicckeyA) || mypicckeyA.length!=12) {textarea.value = "新A密钥输入错误,请输入正确的12位16进制新A密钥!";newkeya.focus();newkeya.select();return;} mypiccctr = cardctr.value.trim(); //新控制位,出厂为FF078069,if (!isHex(mypiccctr) || mypiccctr.length!=8) {textarea.value = "新控制位输入错误,请输入正确的8位16进制控制位!";cardctr.focus();cardctr.select();return;}mypicckeyB = newkeyb.value.trim(); //新B密钥if (!isHex(mypicckeyB) || mypicckeyB.length!=12) {textarea.value = "新B密钥输入错误,请输入正确的12位16进制新B密钥!";newkeyb.focus();newkeyb.select();return;}mypicckey_new=mypicckeyA+mypiccctr+mypicckeyB;switch (selchangekey.selectedIndex){case 0:mypicckey_new=mypicckey_new+"00";break;case 1:mypicckey_new=mypicckey_new+"02";break; default:mypicckey_new=mypicckey_new+"03";break; } const outputData = new Uint8Array(33);outputData[0]=0x1f; //指令长度outputData[1]=0xf1; //功能码outputData[2]=myctrlword; //控制位 outputData[3]=0x00; //四字节本次操作卡卡号,全部取0表示可操作任意卡outputData[4]=0x00;outputData[5]=0x00;outputData[6]=0x00;outputData[7]=myareano; //扇区号outputData[8]=authmode; //密钥认证方式 for(i=0;i<6;i++){ //6字节密钥outputData[9+i]=parseInt(mypicckey.substr(i*2,2),16);}for(i=0;i<17;i++){ //6字节新A钥+4字节控制位+6字节新B钥+1字节密钥修改类型outputData[15+i]=parseInt(mypicckey_new.substr(i*2,2),16);}var crc=0;for (i=1;i<32;i++){crc=crc^outputData[i];}outputData[32]=crc; var sendhex="";for(i=0;i<33;i++){sendhex=sendhex+outputData[i].toString(16).padStart(2, '0').toUpperCase()+" ";} SendData.value=sendhex;ReceiveData.value="";SendCode=5;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="423" 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"><input name="butt_request" type="submit" id="butt_request" style="width:100%" onclick="Request()" value="仅读取M1卡序列号" /></td><td><label style="color:blue;" name="label_disp" id="label_disp"></label></td></tr><tr><td height="36"><input name="butt_readm1" type="submit" id="butt_readm1" style="width:100%" onclick="ReadM1Card()" value="读取M1卡扇区数据" /></td><td><p>扇区号:<select style="color:blue;" name="selareano" id="selareano"><option selected="selected">第0扇区</option><option>第1扇区</option><option>第2扇区</option><option>第3扇区</option><option>第4扇区</option><option>第5扇区</option><option>第6扇区</option><option>第7扇区</option><option>第8扇区</option><option>第9扇区</option><option>第10扇区</option><option>第11扇区</option><option>第12扇区</option><option>第13扇区</option><option>第14扇区</option><option>第15扇区</option> </select><label for="changeauthkey"></label><select style="color:blue;" name="selinoutkey" id="selinoutkey"><option>内部密钥认证</option><option selected="selected">外部密钥认证</option></select><label for="newkey"></label><select style="color:blue;" name="selauthmode" id="selauthmode"><option>B密钥认证</option><option selected="selected">A密钥认证</option> </select>,认证密钥:<label for="authkey0"></label><input style="color:blue;text-align:center;" name="authkey0" type="text" id="authkey0" value="FFFFFFFFFFFF" size="12" maxlength="12" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/></p> </td></tr><tr><td height="69"><input name="butt_writem1" type="submit" id="butt_writem1" style="width:100%" onclick="WriteM1Card()" value="写数据到M1卡扇区" /></td><td><textarea style="width:800px;color:red;" name="RWM1Data" id="RWM1Data" cols="100" rows="4" ></textarea></td></tr> <tr><td height="36"><input style="width:100%" name="butt_changecardkeyex" type="submit" id="butt_changecardkeyex" onclick="changecardkeyex()" value="修改M1卡扇区密钥" /></td><td><select style="color:blue;" name="selchangekey" id="selchangekey"><option>只修改A密钥</option><option>只修改AB密钥</option><option selected="selected">修改AB密钥及控制位</option></select>,新A密钥<input style="color:red;text-align:center;" name="newkeya" type="text" id="newkeya" value="FFFFFFFFFFFF" size="12" maxlength="12" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/>,控制位<input style="color:red;text-align:center;" name="cardctr" type="text" id="cardctr" value="FF078069" size="8" maxlength="8" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/>,B密钥<input style="color:red;text-align:center;" name="newkeyb" type="text" id="newkeyb" value="FFFFFFFFFFFF" size="12" maxlength="12" onkeyup="this.value=this.value.replace(/[^0-9a-fA-F]/g,'')"/></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>
源码下载:WebSerialapi浏览器串口通讯读写NFC标签资源-CSDN文库