使用Java Applet和Java串口编程API实现电子称称重功能

Java Applet技术应该算是Java语言诞生之初在Web领域唯一的一个卖点,有一段时间非常流行Applet做的动画,聊天室和游戏。但是由于Applet自身存在的一些问题,比如要求浏览器装有JRE,很多用户不愿意去装;开发困难,没有好的IDE,性能差等;导致Applet慢慢地被Java在服务器端的优秀表现所淹没。
Java串口编程使用的是javax.comm包下的SerialPortEventListener接口。javax.comm是当年Sun公司提供的(现在已经属于Oracle公司)用于开发平台独立的通讯应用程序的扩展API。
废话不多说了,直接进入主题,先说说怎么开发一个Applet的Hello World程序。

1. Applet Hello World


 首先介绍下在Eclipse环境下如何编写一个Applet程序。 
import java.applet.Applet;
import java.awt.Graphics;

public class HelloWorld extends Applet {
    @Override
    public void init() {
        System.out.println("init...");
    }

    @Override
    public void start() {
        System.out.println("start...");
    }

    @Override
    public void stop() {
        System.out.println("stop...");
    }

    @Override
    public void destroy() {
        System.out.println("destroy...");
    }

    public void paint(Graphics g) {
        g.drawString("Hello World!", 5, 35);
    }
}

任何一个Applet程序须继承java.applet.Applet类。在Eclipse运行上面代码的时候(Run as Java Applet),会发现代码运行的顺序与Servlet类似(准确地说,应该是Servlet与Applet类似,因为先有Applet),控制台依次输出“init… start…”,然后弹出Applet绘制的一个小窗口,如下图:
使用Java Applet和Java串口编程API实现电子称称重功能_第1张图片
当关闭Applet的时候,控制台依次输出“stop… destroy…”。


下面介绍下如何将这个最简单的Applet嵌入到HTML页面里。
在普通的HTML页面中添加标签,然后指定code属性为刚刚编写的Applet的.class文件全路径(有包名的话需要指定包路径)。
 
  

<html>
<head>
    <title>HelloWorld.htmltitle>
head>

<body>
    <applet code="HelloWorld.class" width="200" height="100"> applet>
body>
html>

将上面编写的HelloWorld.java和HelloWorld.html文件复制到JDK\bin目录下,打开命令行窗口,也切换到这个目录下,执行以下两个命令,会发现这个弹出Hello World窗口,说明Applet已经在HTML里面正常执行。

...\jdk1.7.0_13\bin>javac HelloWorld.java
...\jdk1.7.0_13\bin>appletviewer HelloWorld.html

2.使用SerialPortEventListener实现读取串口中数据的功能:

import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.Enumeration;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.comm.CommDriver;
import javax.comm.CommPortIdentifier;
import javax.comm.SerialPort;
import javax.comm.SerialPortEvent;
import javax.comm.SerialPortEventListener;
import javax.swing.JApplet;

/**
 * 电子称Applet
 *
 * @author songkaojun 2015-11-18
 *
 */
public class ScaleApplet extends JApplet {

    private static final long serialVersionUID = -5163984585837742255L;

    private String driverName = "com.sun.comm.Win32Driver";

    // 目前支持常用的十个端口
    private static final String[] PORT_NAMES = new String[] { "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9" };

    // 电子称数据前缀
    private static final String SCALE_DATA_PREFIX = "wn";

    // 电子称数据后缀(单位)
    private static final String SCALE_DATA_POSTFIX = "kg";

    private static final String SCALE_DATA_FORMAT = "#0.00";

    // 丢弃的不稳定数据的长度
    private static final long ABANDON_UNSTABLE_DATA_LENGTH = 6;

    private static final int STREAM_TRY_COUNT = 100;

    private static final int PORT_NUM = 2000;

    private static final int MAX_QUEUE_LENGTH = 10;

    // 读取到的重量数据队列
    private BlockingQueue weightQueue;

    // 异常信息队列
    private BlockingQueue msgQueue;

    static {
        System.setSecurityManager(null);// 禁用安全管理器(必须写)
    }

    /**
     * 装载和初始化驱动程序,即载入win32comm.dll文件 初始化阻塞队列,最大长度为Integer.MAX_VALUE
     *
     */
    @Override
    public void init() {
        try {
            System.loadLibrary("win32com");
            CommDriver driver = (CommDriver) Class.forName(this.driverName).newInstance();
            driver.initialize();
            System.out.println("init...");
            this.weightQueue = new LinkedBlockingQueue(ScaleApplet.MAX_QUEUE_LENGTH);
            this.msgQueue = new LinkedBlockingQueue();
        } catch (Exception e) {
            this.msgQueue.add(e.getMessage());
            System.err.println(e);
        }
    }

    @Override
    public void start() {
        System.out.println("start...");
        Thread serialPortCheckThread = new Thread(new SerialPortCheckThread());
        serialPortCheckThread.start();

        // 测试代码,上线时候需要去掉
        // while (true) {
        // String weight = this.getWeight();
        // String msg = this.getMsg();
        // try {
        // Thread.sleep(10);
        // } catch (InterruptedException e) {
        // e.printStackTrace();
        // }
        // if (weight != null) {
        // System.out.println(weight);
        // }
        // if (msg != null) {
        // System.out.println("msg:");
        // System.out.println(msg);
        // }
        // }
    }

    @Override
    public void destroy() {
    }

    public String getWeight() {
        String weight = null;
        try {
            weight = this.weightQueue.poll();
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
        return weight;
    }

    public String getMsg() {
        return this.msgQueue.poll();
    }

    /**
     * 串口检测线程 循环检测所有端口,如果可以读出数据则通知串口读取线程(SerialPortReaderThread)进行读取,自己进入等待状态。
     *
     * @author songkaojun
     *
     */
    class SerialPortCheckThread implements Runnable {

        private CommPortIdentifier portIdentifier;

        @Override
        public void run() {
            @SuppressWarnings("rawtypes")
            Enumeration ports = CommPortIdentifier.getPortIdentifiers();
            Thread serialPortReaderThread;
            while (ports.hasMoreElements()) {
                this.portIdentifier = (CommPortIdentifier) ports.nextElement();
                if (this.portIdentifier.getPortType() == CommPortIdentifier.PORT_SERIAL) {
                    for (String portName : ScaleApplet.PORT_NAMES) {
                        if (this.portIdentifier.getName().equals(portName)) {

                            serialPortReaderThread = new Thread(new SerialPortReaderThread(this.portIdentifier));
                            serialPortReaderThread.start();

                            try {
                                synchronized (serialPortReaderThread) {
                                    serialPortReaderThread.wait();
                                }
                            } catch (InterruptedException e) {
                                ScaleApplet.this.msgQueue.add(e.getMessage());
                                System.err.println(e.getMessage());
                            }
                        }
                    }
                }
            }
        }

    }

    /**
     *
     * 串口读取线程
     * 当串口检测线程(SerialPortCheckThread)发现可以读取到数据的串口时,本线程进行读取,然后校验数据类型是否是电子称的数据
     * 如果不是,则通知串口检测线程检测下一个串口 如果是电子称的数据,则读出数据放入weightQueue队列中
     *
     * @author songkaojun
     *
     */
    class SerialPortReaderThread implements Runnable, SerialPortEventListener {

        private CommPortIdentifier portIdentifier;

        private SerialPort serialPort;

        private InputStream is;

        private StringBuilder weight = new StringBuilder();

        public SerialPortReaderThread(CommPortIdentifier portIdentifier) {
            this.portIdentifier = portIdentifier;
        }

        @Override
        public void run() {
            this.openSerialPort();
        }

        private final void openSerialPort() {
            try {
                this.serialPort = (SerialPort) this.portIdentifier.open("ScaleApplet", ScaleApplet.PORT_NUM);
                this.is = this.serialPort.getInputStream();
                this.serialPort.addEventListener(this);
                this.serialPort.notifyOnDataAvailable(true);
                this.serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
                if (!this.isStreamAvailable()) {
                    this.wakeCheckThread();
                }
            } catch (Exception e) {
                ScaleApplet.this.msgQueue.add(e.getMessage());
                System.err.println(e);
            }
        }

        /**
         * available()方法返回的值是inputstream在不被阻塞的情况下一次可以读取到的数据估算长度。
         * 如果数据还没有传输过来,那么这个inputstream势必会被阻塞,从而导致available返回0
         * 由于电子称传输过来的数据不是连续的,所以在使用available判断是否有数据传输过滤的时候需要进行多次判断,
         * 如果有一次读取到了数据,就说明有数据
         *
         * @return
         * @throws IOException
         * @throws InterruptedException
         */
        private final boolean isStreamAvailable() throws IOException, InterruptedException {
            int bytesAvailable = 0;
            int tryCount = 0;
            while ((bytesAvailable == 0) && (tryCount < ScaleApplet.STREAM_TRY_COUNT)) {
                bytesAvailable = this.is.available();
                tryCount++;
                Thread.sleep(5);
            }
            if (bytesAvailable > 0) {
                return true;
            }
            return false;
        }

        /**
         * 关闭流和串口,并通知串口检测线程继续检测其他串口
         *
         */
        private final void wakeCheckThread() {
            this.close();
            synchronized (this) {
                this.notify();
            }
        }

        @Override
        public void serialEvent(SerialPortEvent event) {
            switch (event.getEventType()) {
            case SerialPortEvent.DATA_AVAILABLE:
                try {
                    long count = 0L;
                    while (true) {
                        int b = this.is.read();
                        if ((b == 10) || (b == 13)) {
                            if ((this.weight != null) && (this.weight.length() != 0)) {
                                // 对于刚开始读取的不稳定数据进行丢弃
                                if (count < ScaleApplet.ABANDON_UNSTABLE_DATA_LENGTH) {
                                    count++;
                                    this.weight.delete(0, this.weight.length());
                                    continue;
                                }
                                count++;

                                if (this.isScaleData(this.weight.toString())) {
                                    String filterWeight = this.filterPreZero(this.filterUnit(this.weight.toString()));
                                    if ((filterWeight != null) && !filterWeight.equals("")) {
                                        try {
                                            // 防止消费者一段时间不消费导致队列堆积
                                            if (ScaleApplet.this.weightQueue.size() < ScaleApplet.MAX_QUEUE_LENGTH) {
                                                ScaleApplet.this.weightQueue.add(filterWeight);
                                            }
                                        } catch (Exception e) {
                                            System.err.println(e.getMessage());
                                        }
                                    }
                                } else {
                                    // 格式不正确,检测下一个串口
                                    this.wakeCheckThread();
                                }
                                this.weight.delete(0, this.weight.length());
                            }
                        } else {
                            this.weight.append(new String(new byte[] { (byte) b }));
                        }
                    }
                } catch (IOException e) {
                    ScaleApplet.this.msgQueue.add(e.getMessage());
                    System.err.println(e);
                }
                break;
            }
        }

        private final void close() {
            try {
                if (this.is != null) {
                    this.is.close();
                }
                if (this.serialPort != null) {
                    this.serialPort.close();
                }
            } catch (IOException e) {
                ScaleApplet.this.msgQueue.add(e.getMessage());
                System.out.println(e);
            }
        }

        /**
         * 过滤掉前缀和单位,只留下数字
         *
         * @param inputWeight
         * @return
         */
        private final String filterUnit(String inputWeight) {
            if (!inputWeight.startsWith(ScaleApplet.SCALE_DATA_PREFIX) || !inputWeight.endsWith(ScaleApplet.SCALE_DATA_POSTFIX)) {
                return "";
            }
            return inputWeight.substring(inputWeight.indexOf(ScaleApplet.SCALE_DATA_PREFIX) + 2, inputWeight.indexOf(ScaleApplet.SCALE_DATA_POSTFIX));
        }

        private final String filterPreZero(String inputWeight) {
            if ((inputWeight == null) || inputWeight.equals("")) {
                return "";
            }
            return new DecimalFormat(ScaleApplet.SCALE_DATA_FORMAT).format(Double.parseDouble(inputWeight));
        }

        /**
         * 根据格式判断数据是不是电子称数据
         *
         * @param inputWeight
         * @return
         */
        private final boolean isScaleData(String inputWeight) {
            if ((inputWeight == null) || inputWeight.equals("")) {
                return true;
            }
            if (inputWeight.startsWith(ScaleApplet.SCALE_DATA_PREFIX) && inputWeight.endsWith(ScaleApplet.SCALE_DATA_POSTFIX)) {
                return true;
            }
            return false;
        }

    }

}


<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HelloWorld! Applettitle>
<script type="text/javascript">
    function getWeightRepeatable() {
           window.setInterval("setWeight()",100); 
    }
    function setWeight(){
        var weight = document.scaleApplet.getWeight();
        if(weight!=null&&weight!=''){
            document.getElementById("weightSpan").innerHTML=weight;
        }
    }
script>
head>

<body onload="getWeightRepeatable();">
    <applet name="scaleApplet" code="ScaleApplet.class" 
        archive="ScaleApplet.jar,javax.comm.jar"  width="100" height="100">
    applet>
    <span id="weightSpan">0.00span>
body>
html>
上面的代码写完之后,还不能立即运行,因为Java使用被称作“沙箱模型”的安全访问机制,所以为了能够获得对客户端资源的访问权限,需要进行以下操作:

1)、编译:

\jdk1.7.0_13\bin>javac ScaleApplet.java -classpath javax.comm.jar

2)、打包:

\jdk1.7.0_13\bin>jar -cf ScaleApplet.jar ScaleApplet.class ScaleApplet$SerialPortCheckThread.class ScaleApplet$SerialPortReaderThread.class

3)、使用keytool工具生成密匙库 :

\jdk1.7.0_13\bin>keytool -genkey -keystore ScaleApplet.store -alias sa
输入密钥库口令:
您的名字与姓氏是什么?
  [Unknown]:  songkaojun
您的组织单位名称是什么?
  [Unknown]:  explink
您的组织名称是什么?
  [Unknown]:  explink
您所在的城市或区域名称是什么?
  [Unknown]:  chaoyang
您所在的省/市/自治区名称是什么?
  [Unknown]:  beijing
该单位的双字母国家/地区代码是什么?
  [Unknown]:  CN
CN=songkaojun, OU=explink, O=explink, L=chaoyang, ST=beijing, C=CN是否正确?
  []:  y

输入  的密钥口令
        (如果和密钥库口令相同, 按回车):

4)、使用keytool工具导出签名时用到的证书 :

\jdk1.7.0_13\bin>keytool -export -keystore ScaleApplet.store -alias sa -file ScaleApplet.cert
输入密钥库口令:
存储在文件 <ScaleApplet.cert> 中的证书

5)、使用jarsigner工具签名jar压缩文档

\jdk1.7.0_13\bin>jarsigner -keystore ScaleApplet.store ScaleApplet.jar sa
输入密钥库的密码短语:

警告:
签名者证书将在六个月内过期。

更详细的操作请参考“applet应用程序的数字签名应用实战”

对于存储到msgQueue变量中的电子称数据,如何在网页中将其获取到呢?其实很简单,可以通过Javascript代码来获取:

var weight = document.scaleApplet.getWeight();

scaleApplet是applet的name,getWeight()是ScaleApplet类的public方法(必须是public的方法,或者public的字段)。
最后,部署的目录结构如下图所示:
使用Java Applet和Java串口编程API实现电子称称重功能_第2张图片
启动服务器,通过火狐浏览器访问(Chrome浏览器从45版本开始不支持Java了),可以看到电子称的示数已经显示在网页上了。
使用Java Applet和Java串口编程API实现电子称称重功能_第3张图片

你可能感兴趣的:(Java)