在浏览器中和本地计算机串口进行通讯

在浏览器中和本地计算机串口进行通讯

Applet+Javascript实现B/S读取串口信息

(一) 背景 
(二) Applet实现思路 
(三) Java Windows串口编程简介 
(四) Applet + Javascript实现细节 
(五) Applet数字签名制作步骤 
(六) 参考资料 

背景 
根据公司项目的要求,需要达到以下的功能:在B/S构架的WEB应用中,能够在客户端的浏览器中,通过串口把公交卡信息读到浏览器中的各个输入区域。
根据前期项目已经开发的功能,我们已经完成了对公交卡芯片的写入,主要包含多种信息,如卡片格式化、目录结构的建立、金额写入,在WEB应用中,应用的方式类似以下描述:在浏览器中,打开的HTML页面包括各种输入区域,当公交卡靠近读卡器感应区,读卡器会读取到卡片信息然后通过串口发送给浏览器,自动填入相应的输入区域中。读卡器接在客户端计算机的串口上。
本功能的程序已经开发实现,在摸索的过程中,发现Internet上这类技术的可用资料非常少,并且大多都是英文的资料,阅读起来很费劲,所以把这个技术以文挡方式描述清楚,以方便日后使用。

Applet实现思路 
根据上面的需求描述知道,要满足需求,需要完成2个步骤:首先,必须有程序通过和本地计算机进行串口通讯,通过串口读出读卡器读取出来的卡信息(IO操作);第二,读出的卡信息,必须能够送到浏览器脚本语言控制区,通过脚本语言把信息分到各个输入区域中。
Java处理IO操作有先天的优势,所以上述第一个环节,我们理所当然的想到了使用Java进行串口通讯读出读卡器中所取到的卡信息,但串口是位于客户端计算机上,在B/S结构的WEB应用中,因此,采用Applet的Java代码进行串口通讯。


 

Applet实现需要考虑的问题是,因为Applet的安全性要求,在普通情况下,Applet是不允许访问本地计算机的资源的,比如,一个Applet不能去访问本地的文件系统,否则,随便在Internet上放一个Applet,把客户端的C盘文件全部删除,那还得了!但是,Applet的设计者并没有全部抑制这些功能,通过其他机制,是可以访问到本地的资源的,串口作为本地资源,就需要用到这种机制,这种机制就是数字签名。对Applet进行数字签名后,可以访问到本地资源。

Java Windows串口编程简介 
Java的IO操作有先天的优势,Java的IO操作是Java的核心之一,串口通讯作为Java IO操作中的一种,已经被封装的非常良好,使用Java进行串口通讯,程序员的工作变得非常简单。
Java进行串口通讯,可以归结成为以下5个过程:
1、          得到串口
2、          OPEN串口
3、          设置串口
4、          得到输入(输出)流,通过流操作,进行串口通讯
5、          CLOSE串口
串口操作包位于comm.jar,这个一个扩展的java包,在标准的JDK中并没有包含这个包,需要额外下载,另外,Java串口通讯还需要用到win32com.dll,在Sun下载comm.jar时,会包含这个dll。
串口通讯的5个过程,在Java 代码中,实现如下:
//第一步:获取串口
CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier("COM1");
//第二步:OPEN串口
SerialPort      serialPort = (SerialPort)portId.open("Serial_Communication", 2000);
//第三步:设置串口
serialPort.setSerialPortParams(
9600,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
//第四步:获取IO流,进行流操作
InputStream is = serialPort.getInputStream();
……
//第五步:关闭串口
serialPort.close();
Java串口通讯的资料在Internet中可以找到很多,本文的重点不在于此,各位需要了解更多这方面的资料,可以在Internet中得到。

Applet + Javascript实现细节 
根据以上的描述,来看看实现的细节:
串口通讯有个特点,连接上去以后,它发送过来的信息,程序是不知道什么时候是结束的,因为连接上串口以后,串口的输入输出流一直是间断进行数据传送的,永远没有结束(代码中就是InputStream.read()永远不会有-1),除非人为中断它,否则它一直保持连接。如果只有在Applet的主线程中进行串口通讯,那么输入输出流操作永远也不能退出,其他代码永远也没有机会运行。在Applet的主线程中启动工作线程,该线程专门负责读串口信息,把读到的信息送到Applet的属性msg中,msg将被javascript读走。另外,javascript可以和applet通讯,但applet不能和javascript通讯(我目前的理解是如此,不知道有没有错误?),所以,applet是否读到信息,不能主动通知javascript,必须由javascript每隔一段时间来查看。

Applet代码如下:

package com;

import java.io.File;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Enumeration;
import java.util.List;

import javax.swing.JApplet;

import com.Tx800B.ReadCardNumberCallback;

import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;

public class CardApplet extends JApplet implements Runnable, SerialPortEventListener {
	private static final long serialVersionUID = 1L;
	private static final String PORT_NAME = "COM3";
	private static final String LIB_PATH = "D:\\rxtxSerial.dll";
	
	private boolean hasDll = false;
	private boolean hasPort = false;
	private boolean isRead = false;
	private Dequemsg = new ArrayDeque<>();
	private SerialPort sp;
	private Tx800B tx800B;
	
	public String getMsg(){
		return msg.poll();
	}
	
	public void addMsg(String data){
		msg.add(data);
	}
	
	static {
		//System.setSecurityManager(null);
	}

	public void init() {
		addMsg("------------------初始化-----------------------");
		//加载库
		loadDll();
	}

	public void start() {
		addMsg("------------------程序启动-----------------------");
		//启动线程,定时检测串口
	    Thread t = new Thread(this);
	    t.start();
	}
	
	public void stop() {
		addMsg("------------------停止运行-----------------------");
		//关闭串口,IO
		clear();
	}
	
	public void clear(){
		if(sp != null){
			try {
				if(sp.getOutputStream() != null){
					sp.getOutputStream().close();
				}
			} catch (IOException e) {
			}
			try {
				if(sp.getInputStream() != null){
					sp.getInputStream().close();
				}
			} catch (IOException e) {
			}
			sp.close();
		}
	}
	
	public void loadDll(){
		File f = new File(LIB_PATH);
		if(f.exists()){
			System.load(f.getAbsolutePath());
			hasDll = true;
			addMsg("加载库:" + f.getAbsolutePath());
		}else{
			addMsg("文件不存在:" + f.getAbsolutePath());
		}
	}
	
	public void startComm(){
		if(hasDll){
			Listports = listPorts();
			for(CommPortIdentifier port : ports){
				if(PORT_NAME.equals(port.getName())){
					hasPort = true;
					initPort(port);
					return;
				}
			}
			hasPort = false;
		}else{
			loadDll();
		}
	}
	
	private void initPort(CommPortIdentifier port){
		clear();
		try{
			addMsg("初始化串口: " + port.getName());
			sp = (SerialPort) port.open("Serial_Communication", 2000);
			sp.addEventListener(this);
			sp.notifyOnDataAvailable(true);
			sp.setSerialPortParams(9600,                
				    SerialPort.DATABITS_8,
		            SerialPort.STOPBITS_1,
		            SerialPort.PARITY_NONE);
			tx800B = new Tx800B(null, 
				new ReadCardNumberCallback() {
					public void onReadCardNumber(int cardTagType, int sak, byte[] serialNumber) {
						addMsg("卡序列号:" + bytes2int(serialNumber));
					}
				}
			);
			isRead = true;
		}catch(Exception e){
			isRead = false;
			addMsg("串口初始化失败: " + e.getMessage());
		}
	}
	
	private ListlistPorts() {
		//读取目前所有连接的串口
		Listports = new ArrayList<>();
		Enumeration en = CommPortIdentifier.getPortIdentifiers();
		while (en.hasMoreElements()) {
			CommPortIdentifier portId = (CommPortIdentifier) en.nextElement();
			if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
				ports.add(portId);
			}
		}
		return ports;
	}
	
	@Override
	public void serialEvent(SerialPortEvent event) {
		//接收串口发过来的数据
		switch(event.getEventType()) {
        case SerialPortEvent.DATA_AVAILABLE:
            try {
            	int available = sp.getInputStream().available();
                if (available > 0) {
                	byte[] readBuffer = new byte[available];
                	sp.getInputStream().read(readBuffer, 0, available);
                	addMsg("接收到数据:" + printHexString(readBuffer));
                	tx800B.parser(readBuffer);
                }
            } catch (Exception e) {
            	addMsg("接收数据失败: " + e.getMessage());
            }
            break;
        }
	}

	public void run() {
		while(true){
			try {
				Thread.sleep(2000);
				if(!hasPort){
					//如果未发现串口,每2秒检测一次
					startComm();
				}
			} catch (Exception e) {
				addMsg("运行错误: " + e.getMessage());
			}
		}
	}
	
	public static String printHexString(byte[] b) {
		String hex1 = "";
		for (int i = 0; i < b.length; i++) {
			String hex = Integer.toHexString(b[i] & 0xFF) + " ";
			if (hex.length() == 2) {
				hex = '0' + hex;
			}
			hex1 += hex;
		}
		return hex1.trim();
	}
	
	public static byte[] toHexByte(String hex) {
		if(hex != null && hex.length() > 0){
			String[] ds = hex.split(" ");
			byte[] bs = new byte[ds.length];
			for(int i = 0; i < ds.length; i++){
				bs[i] = (byte)printDecimalString(ds[i]);
			}
			return bs;
		}
		return null;
	}

	public static int printDecimalString(String hex) {
		int decimal = Integer.parseInt(hex, 16);
		return decimal;
	}
	
	//高位在前,低位在后
    public static int bytes2int(byte[] bytes){
        int result = 0;
        if(bytes.length == 4){
            int a = (bytes[0] & 0xff) << 24;
            int b = (bytes[1] & 0xff) << 16;
            int c = (bytes[2] & 0xff) << 8;
            int d = (bytes[3] & 0xff);
            result = a | b | c | d;
        }
        return result;
    }
    
    private void write(byte[] data){
		if(!hasDll){
			addMsg("请下载DLL到指定路径:" + LIB_PATH);
			return;
		}
		if(!hasPort){
			addMsg("未发现串口");
			return;
		}
		if(!isRead){
			addMsg("串口初始化失败");
			return;
		}
		try {
			addMsg("发送数据: " + printHexString(data));
			sp.getOutputStream().write(tx800B.sendHex(data));
			sp.getOutputStream().flush();
		} catch (Exception e) {
			addMsg("发送数据失败: " + e.getMessage());
		}
	}
	
    /**
     * 发送指令
     * @param cmd
     */
	public void send(String cmd){
		switch (cmd) {
		case "GET_CARD_SEQUENCE":
			write(toHexByte("20 00 10 01 00 ee 03"));
			break;
		default:
			break;
		}
	}
    
}

下面是HTML代码:applet.html

<!DOCTYPE html>
<html>
	<head>
		<style type="text/css">
			#send_txt{width:500px;height:100px;}
			.cmd_list{padding:10px 0;}
			.cmd_list button{margin-left:5px;}
		</style>
		<script type="text/javascript" src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script>
		<script>
			jQuery(function($){
				
				function receiveMsg(){
					var msg = cardReader.getMsg();
					if($.trim(msg).length>0){
						var $ul = $("#msg_list")
						$('<li></li>').html(msg).appendTo($ul);
					}
				}
				setInterval(receiveMsg, 500);
				
				/**
				* 下面是发送读卡/写卡指令
				**/
				$("#read_card_btn").bind("click", function(){
					cardReader.send('GET_CARD_SEQUENCE');
				});
			});
		</script>
	</head>
	<body>
		<OBJECT id="cardReader" name="cardReader" code="com.CardApplet" codebase="." archive="CardApplet.jar" width="110" height="110">
		</OBJECT>
		<div class="cmd_list">
			<button id="read_card_btn">读取卡序列号</button>
		</div>
		<ul id="msg_list">
			<li>消息列表</li>
		</ul>
	</body>
</html>

Applet数字签名制作步骤 

第一步:产生jar包,命令:

jar cvf Applet.jar *


第二步:为jar包文件创建keystore和keys。其中,keystore将用来存放密匙(private keys)和公共钥匙的认证,alias为别名,命令: 

keytool -genkey -keystore Applet.keystore -alias Applet


此命令生成了一个名为LocalFileApplet.keystore的keystore文件,接着这条命令,系统会问你好多问题,比如你的公司名称,你的地址,你要设定的密码等等,都由自己的随便写。密码将在第三步和第四步中使,其他信息提供给客户端用户,知道这个数字签名是谁提供的。

 

第三步:对jar包进行签名,命令:

jarsigner -keystore Applet.keystore Applet.jar Applet

 

第四步:导出认证文件,命令:

keytool -export -keystore Applet.keystore -alias Applet -file Applet.cer



这个步骤将生成Applet.cer文件,发布时,把该文件同jar文件和applet.html文件放在同一个目录即可。


参考资料

http://www.sace.cn/exams/it/java/view/5044.html

http://www.yesky.com/105/1867605.shtml

附件(解压到D盘下,用IE浏览器打开,导入证书,并设置Java控制面板添加例外站点列表)

    demo.zip

  1. 解压到D盘下

  2. 导入证书CardApplet.cer到IE受信任证书列表中

  3. 到控制面板-Java添加到例外站点列表

  4. 打开IE浏览器运行activeX.html

打赏

您看完此文章的心情是

  • 0人

  • 鼓掌

    0人

  • 草泥马

    0人

  • 愤怒

    0人

  • 鄙视

    0人

评论

    暂无评论...