本示例使用的设备: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: //返回有效数据长度为1
switch (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+"成功!卡号:"+cardnohex
break;
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: //返回有效数据长度为53
for(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: //旧版本设备返回有效数据长度为54
for(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个0
mypicckey = 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个0
mypicckey = 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个0
mypicckey = 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文库