最近一门课要求编写一个上位机串口通信工具,我基于Java编写了一个带有图形界面的简单串口通信工具,下面详述一下过程,供大家参考 ^_^
一:
首先,你需要下载一个额外的支持Java串口通信操作的jar包,由于java.comm比较老了,而且不支持64位系统,这里推荐Rxtx这个jar包(32位/64位均支持)。
官方下载地址:http://fizzed.com/oss/rxtx-for-java (注:可能需要FQ才能下载)
不能FQ的童鞋,可以在这里下载:
http://files.cnblogs.com/files/Dreamer-1/mfz-rxtx-2.2-20081207-win-x86.zip (32位)
http://files.cnblogs.com/files/Dreamer-1/mfz-rxtx-2.2-20081207-win-x64.zip (64位)
二:
下载解压jar包并在 Java Build Path 下引入:
注:如果运行过程中抛出 java.lang.UnsatisfiedLinkError 错误或 gnu.io 下的类找不到,请将rxtx解压包中的 rxtxParallel.dll,rxtxSerial.dll 这两个文件复制到 C:\Windows\System32 目录下即可解决该错误。
三:
关于该jar包的使用,我写了一个SerialTool.java类,该类提供关于串口通信的各简单服务,代码如下(注意该类位于 serialPort 包里):
注:该类方法中 throw 的 Exception 都是我自定义的 Exception,之所以这么做是为了方便在主程序中进行相应处理,下面贴其中一个Exception出来给大家做下说明:
(注意我所有自定义的 Exception 都放在 serialException 包里)
package serialException; public class SerialPortParameterFailure extends Exception { /** * */ private static final long serialVersionUID = 1L; public SerialPortParameterFailure() {} @Override public String toString() { return "设置串口参数失败!打开串口操作未完成!"; } }
每个自定义的Exception类我都重写了它的 toString() 方法,便于主程序捕捉到该Exception后打印对应的错误信息
其中在serialException包里还有一个专门将接收到的Exception对象内的错误信息提取出来转换成字符串并返回的类,代码如下:
package serialException; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; /** * 负责将传入的Exception中的错误信息提取出来并转换成字符串; * @author zhong * */ public class ExceptionWriter { /** * 将Exception中的错误信息封装到字符串中并返回该字符串 * @param e 包含错误的Exception * @return 错误信息字符串 */ public static String getErrorInfoFromException(Exception e) { StringWriter sw = null; PrintWriter pw = null; try { sw = new StringWriter(); pw = new PrintWriter(sw); e.printStackTrace(pw); return "\r\n" + sw.toString() + "\r\n"; } catch (Exception e2) { return "出错啦!未获取到错误信息,请检查后重试!"; } finally { try { if (pw != null) { pw.close(); } if (sw != null) { sw.close(); } } catch (IOException e1) { e1.printStackTrace(); } } } }
四:
主程序类的使用,Client.java里含有程序的入口地址(main方法),它的作用是显示一个欢迎界面并调用DataView.java这个类进行实际的串口数据显示。
Client.java代码如下:
package serialPort; import java.awt.Color; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Image; import java.awt.Label; import java.awt.Panel; import java.awt.Toolkit; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JOptionPane; import serialException.ExceptionWriter; /** * 主程序 * @author zhong * */ public class Client extends Frame{ /** * */ private static final long serialVersionUID = 1L; /** * 程序界面宽度 */ public static final int WIDTH = 800; /** * 程序界面高度 */ public static final int HEIGHT = 620; /** * 程序界面出现位置(横坐标) */ public static final int LOC_X = 200; /** * 程序界面出现位置(纵坐标) */ public static final int LOC_Y = 70; Color color = Color.WHITE; Image offScreen = null; //用于双缓冲 //设置window的icon(这里我自定义了一下Windows窗口的icon图标,因为实在觉得哪个小咖啡图标不好看 = =) Toolkit toolKit = getToolkit(); Image icon = toolKit.getImage(Client.class.getResource("computer.png")); //持有其他类 DataView dataview = new DataView(this); //主界面类(显示监控数据主面板) /** * 主方法 * @param args // */ public static void main(String[] args) { new Client().launchFrame(); } /** * 显示主界面 */ public void launchFrame() { this.setBounds(LOC_X, LOC_Y, WIDTH, HEIGHT); //设定程序在桌面出现的位置 this.setTitle("CDIO工程项目"); //设置程序标题 this.setIconImage(icon); this.setBackground(Color.white); //设置背景色 this.addWindowListener(new WindowAdapter() { //添加对窗口状态的监听 public void windowClosing(WindowEvent arg0) { //当窗口关闭时 System.exit(0); //退出程序 } }); this.addKeyListener(new KeyMonitor()); //添加键盘监听器 this.setResizable(false); //窗口大小不可更改 this.setVisible(true); //显示窗口 new Thread(new RepaintThread()).start(); //开启重画线程 } /** * 画出程序界面各组件元素 */ public void paint(Graphics g) { Color c = g.getColor(); g.setFont(new Font("微软雅黑", Font.BOLD, 40)); g.setColor(Color.black); g.drawString("欢迎使用上位机实时监控系统", 45, 190); g.setFont(new Font("微软雅黑", Font.ITALIC, 26)); g.setColor(Color.BLACK); g.drawString("Version:1.0 Powered By:ZhongLei", 280, 260); g.setFont(new Font("微软雅黑", Font.BOLD, 30)); g.setColor(color); g.drawString("————点击Enter键进入主界面————", 100, 480); //使文字 "————点击Enter键进入主界面————" 黑白闪烁 if (color == Color.WHITE) color = Color.black; else if (color == color.BLACK) color = Color.white; } /** * 双缓冲方式重画界面各元素组件 */ public void update(Graphics g) { if (offScreen == null) offScreen = this.createImage(WIDTH, HEIGHT); Graphics gOffScreen = offScreen.getGraphics(); Color c = gOffScreen.getColor(); gOffScreen.setColor(Color.white); gOffScreen.fillRect(0, 0, WIDTH, HEIGHT); //重画背景画布 this.paint(gOffScreen); //重画界面元素 gOffScreen.setColor(c); g.drawImage(offScreen, 0, 0, null); //将新画好的画布“贴”在原画布上 } /* * 内部类形式实现对键盘事件的监听 */ private class KeyMonitor extends KeyAdapter { public void keyReleased(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_ENTER) { //当监听到用户敲击键盘enter键后执行下面的操作 setVisible(false); //隐去欢迎界面 dataview.setVisible(true); //显示监测界面 dataview.dataFrame(); //初始化监测界面 } } } /* * 重画线程(每隔250毫秒重画一次) */ private class RepaintThread implements Runnable { public void run() { while(true) { repaint(); try { Thread.sleep(250); } catch (InterruptedException e) { //重画线程出错抛出异常时创建一个Dialog并显示异常详细信息 String err = ExceptionWriter.getErrorInfoFromException(e); JOptionPane.showMessageDialog(null, err, "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); } } } } }
运行截图:
注:实际运行过程中最下面的“点击Enter键进入主界面”有一个一闪一闪的效果(是通过每隔一段时间重画一次界面,让这句话以白黑两色反复交替出现实现的),双缓冲方式利于解决重画时界面闪烁的问题(如果不使用双缓冲方式的话相当于每次重画时是在旧界面上一点一点画上新东西,而双缓冲实质上是通过先在内存中直接画好一张新界面图,然后一次性直接用新界面覆盖掉旧界面)
DataView.java代码如下:(该类用于实时显示串口数据)
简单说明:
硬件设备每隔一段时间通过串口发送一次数据到计算机,该串口工具成功连接至硬件设备并添加监听后,会在每次接收到数据时解析数据并更新界面;
你在使用时很可能需求跟我不一样,该类仅供参考,实际使用中你很可能需要重新制作数据显示界面以及数据解析方式
package serialPort; import java.awt.Button; import java.awt.Choice; import java.awt.Color; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Image; import java.awt.Label; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.List; import java.util.TooManyListenersException; import javax.swing.JOptionPane; import gnu.io.SerialPort; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import serialException.*; /** * 监测数据显示类 * @author Zhong * */ public class DataView extends Frame { /** * */ private static final long serialVersionUID = 1L; Client client = null; private List<String> commList = null; //保存可用端口号 private SerialPort serialPort = null; //保存串口对象 private Font font = new Font("微软雅黑", Font.BOLD, 25); private Label tem = new Label("暂无数据", Label.CENTER); //温度 private Label hum = new Label("暂无数据", Label.CENTER); //湿度 private Label pa = new Label("暂无数据", Label.CENTER); //压强 private Label rain = new Label("暂无数据", Label.CENTER); //雨量 private Label win_sp = new Label("暂无数据", Label.CENTER); //风速 private Label win_dir = new Label("暂无数据", Label.CENTER); //风向 private Choice commChoice = new Choice(); //串口选择(下拉框) private Choice bpsChoice = new Choice(); //波特率选择 private Button openSerialButton = new Button("打开串口"); Image offScreen = null; //重画时的画布 //设置window的icon Toolkit toolKit = getToolkit(); Image icon = toolKit.getImage(DataView.class.getResource("computer.png")); /** * 类的构造方法 * @param client */ public DataView(Client client) { this.client = client; commList = SerialTool.findPort(); //程序初始化时就扫描一次有效串口 } /** * 主菜单窗口显示; * 添加Label、按钮、下拉条及相关事件监听; */ public void dataFrame() { this.setBounds(client.LOC_X, client.LOC_Y, client.WIDTH, client.HEIGHT); this.setTitle("CDIO工程项目"); this.setIconImage(icon); this.setBackground(Color.white); this.setLayout(null); this.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent arg0) { if (serialPort != null) { //程序退出时关闭串口释放资源 SerialTool.closePort(serialPort); } System.exit(0); } }); tem.setBounds(140, 103, 225, 50); tem.setBackground(Color.black); tem.setFont(font); tem.setForeground(Color.white); add(tem); hum.setBounds(520, 103, 225, 50); hum.setBackground(Color.black); hum.setFont(font); hum.setForeground(Color.white); add(hum); pa.setBounds(140, 193, 225, 50); pa.setBackground(Color.black); pa.setFont(font); pa.setForeground(Color.white); add(pa); rain.setBounds(520, 193, 225, 50); rain.setBackground(Color.black); rain.setFont(font); rain.setForeground(Color.white); add(rain); win_sp.setBounds(140, 283, 225, 50); win_sp.setBackground(Color.black); win_sp.setFont(font); win_sp.setForeground(Color.white); add(win_sp); win_dir.setBounds(520, 283, 225, 50); win_dir.setBackground(Color.black); win_dir.setFont(font); win_dir.setForeground(Color.white); add(win_dir); //添加串口选择选项 commChoice.setBounds(160, 397, 200, 200); //检查是否有可用串口,有则加入选项中 if (commList == null || commList.size()<1) { JOptionPane.showMessageDialog(null, "没有搜索到有效串口!", "错误", JOptionPane.INFORMATION_MESSAGE); } else { for (String s : commList) { commChoice.add(s); } } add(commChoice); //添加波特率选项 bpsChoice.setBounds(526, 396, 200, 200); bpsChoice.add("1200"); bpsChoice.add("2400"); bpsChoice.add("4800"); bpsChoice.add("9600"); bpsChoice.add("14400"); bpsChoice.add("19200"); bpsChoice.add("115200"); add(bpsChoice); //添加打开串口按钮 openSerialButton.setBounds(250, 490, 300, 50); openSerialButton.setBackground(Color.lightGray); openSerialButton.setFont(new Font("微软雅黑", Font.BOLD, 20)); openSerialButton.setForeground(Color.darkGray); add(openSerialButton); //添加打开串口按钮的事件监听 openSerialButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { //获取串口名称 String commName = commChoice.getSelectedItem(); //获取波特率 String bpsStr = bpsChoice.getSelectedItem(); //检查串口名称是否获取正确 if (commName == null || commName.equals("")) { JOptionPane.showMessageDialog(null, "没有搜索到有效串口!", "错误", JOptionPane.INFORMATION_MESSAGE); } else { //检查波特率是否获取正确 if (bpsStr == null || bpsStr.equals("")) { JOptionPane.showMessageDialog(null, "波特率获取错误!", "错误", JOptionPane.INFORMATION_MESSAGE); } else { //串口名、波特率均获取正确时 int bps = Integer.parseInt(bpsStr); try { //获取指定端口名及波特率的串口对象 serialPort = SerialTool.openPort(commName, bps); //在该串口对象上添加监听器 SerialTool.addListener(serialPort, new SerialListener()); //监听成功进行提示 JOptionPane.showMessageDialog(null, "监听成功,稍后将显示监测数据!", "提示", JOptionPane.INFORMATION_MESSAGE); } catch (SerialPortParameterFailure | NotASerialPort | NoSuchPort | PortInUse | TooManyListeners e1) { //发生错误时使用一个Dialog提示具体的错误信息 JOptionPane.showMessageDialog(null, e1, "错误", JOptionPane.INFORMATION_MESSAGE); } } } } }); this.setResizable(false); new Thread(new RepaintThread()).start(); //启动重画线程 } /** * 画出主界面组件元素 */ public void paint(Graphics g) { Color c = g.getColor(); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 温度: ", 45, 130); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 湿度: ", 425, 130); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 压强: ", 45, 220); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 雨量: ", 425, 220); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 风速: ", 45, 310); g.setColor(Color.black); g.setFont(new Font("微软雅黑", Font.BOLD, 25)); g.drawString(" 风向: ", 425, 310); g.setColor(Color.gray); g.setFont(new Font("微软雅黑", Font.BOLD, 20)); g.drawString(" 串口选择: ", 45, 410); g.setColor(Color.gray); g.setFont(new Font("微软雅黑", Font.BOLD, 20)); g.drawString(" 波特率: ", 425, 410); } /** * 双缓冲方式重画界面各元素组件 */ public void update(Graphics g) { if (offScreen == null) offScreen = this.createImage(Client.WIDTH, Client.HEIGHT); Graphics gOffScreen = offScreen.getGraphics(); Color c = gOffScreen.getColor(); gOffScreen.setColor(Color.white); gOffScreen.fillRect(0, 0, Client.WIDTH, Client.HEIGHT); //重画背景画布 this.paint(gOffScreen); //重画界面元素 gOffScreen.setColor(c); g.drawImage(offScreen, 0, 0, null); //将新画好的画布“贴”在原画布上 } /* * 重画线程(每隔30毫秒重画一次) */ private class RepaintThread implements Runnable { public void run() { while(true) { //调用重画方法 repaint(); //扫描可用串口 commList = SerialTool.findPort(); if (commList != null && commList.size()>0) { //添加新扫描到的可用串口 for (String s : commList) { //该串口名是否已存在,初始默认为不存在(在commList里存在但在commChoice里不存在,则新添加) boolean commExist = false; for (int i=0; i<commChoice.getItemCount(); i++) { if (s.equals(commChoice.getItem(i))) { //当前扫描到的串口名已经在初始扫描时存在 commExist = true; break; } } if (commExist) { //当前扫描到的串口名已经在初始扫描时存在,直接进入下一次循环 continue; } else { //若不存在则添加新串口名至可用串口下拉列表 commChoice.add(s); } } //移除已经不可用的串口 for (int i=0; i<commChoice.getItemCount(); i++) { //该串口是否已失效,初始默认为已经失效(在commChoice里存在但在commList里不存在,则已经失效) boolean commNotExist = true; for (String s : commList) { if (s.equals(commChoice.getItem(i))) { commNotExist = false; break; } } if (commNotExist) { //System.out.println("remove" + commChoice.getItem(i)); commChoice.remove(i); } else { continue; } } } else { //如果扫描到的commList为空,则移除所有已有串口 commChoice.removeAll(); } try { Thread.sleep(30); } catch (InterruptedException e) { String err = ExceptionWriter.getErrorInfoFromException(e); JOptionPane.showMessageDialog(null, err, "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); } } } } /** * 以内部类形式创建一个串口监听类 * @author zhong * */ private class SerialListener implements SerialPortEventListener { /** * 处理监控到的串口事件 */ public void serialEvent(SerialPortEvent serialPortEvent) { switch (serialPortEvent.getEventType()) { case SerialPortEvent.BI: // 10 通讯中断 JOptionPane.showMessageDialog(null, "与串口设备通讯中断", "错误", JOptionPane.INFORMATION_MESSAGE); break; case SerialPortEvent.OE: // 7 溢位(溢出)错误 case SerialPortEvent.FE: // 9 帧错误 case SerialPortEvent.PE: // 8 奇偶校验错误 case SerialPortEvent.CD: // 6 载波检测 case SerialPortEvent.CTS: // 3 清除待发送数据 case SerialPortEvent.DSR: // 4 待发送数据准备好了 case SerialPortEvent.RI: // 5 振铃指示 case SerialPortEvent.OUTPUT_BUFFER_EMPTY: // 2 输出缓冲区已清空 break; case SerialPortEvent.DATA_AVAILABLE: // 1 串口存在可用数据 //System.out.println("found data"); byte[] data = null; try { if (serialPort == null) { JOptionPane.showMessageDialog(null, "串口对象为空!监听失败!", "错误", JOptionPane.INFORMATION_MESSAGE); } else { data = SerialTool.readFromPort(serialPort); //读取数据,存入字节数组 //System.out.println(new String(data)); // 自定义解析过程,你在实际使用过程中可以按照自己的需求在接收到数据后对数据进行解析
if (data == null || data.length < 1) { //检查数据是否读取正确 JOptionPane.showMessageDialog(null, "读取数据过程中未获取到有效数据!请检查设备或程序!", "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); } else { String dataOriginal = new String(data); //将字节数组数据转换位为保存了原始数据的字符串 String dataValid = ""; //有效数据(用来保存原始数据字符串去除最开头*号以后的字符串) String[] elements = null; //用来保存按空格拆分原始字符串后得到的字符串数组 //解析数据 if (dataOriginal.charAt(0) == '*') { //当数据的第一个字符是*号时表示数据接收完成,开始解析 dataValid = dataOriginal.substring(1); elements = dataValid.split(" "); if (elements == null || elements.length < 1) { //检查数据是否解析正确 JOptionPane.showMessageDialog(null, "数据解析过程出错,请检查设备或程序!", "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); } else { try { //更新界面Label值 /*for (int i=0; i<elements.length; i++) { System.out.println(elements[i]); }*/ //System.out.println("win_dir: " + elements[5]); tem.setText(elements[0] + " ℃"); hum.setText(elements[1] + " %"); pa.setText(elements[2] + " hPa"); rain.setText(elements[3] + " mm"); win_sp.setText(elements[4] + " m/s"); win_dir.setText(elements[5] + " °"); } catch (ArrayIndexOutOfBoundsException e) { JOptionPane.showMessageDialog(null, "数据解析过程出错,更新界面数据失败!请检查设备或程序!", "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); } } } } } } catch (ReadDataFromSerialPortFailure | SerialPortInputStreamCloseFailure e) { JOptionPane.showMessageDialog(null, e, "错误", JOptionPane.INFORMATION_MESSAGE); System.exit(0); //发生读取错误时显示错误信息后退出系统 } break; } } } }
运行截图:
整个项目源码打包下载:http://files.cnblogs.com/files/Dreamer-1/serialMonitor.rar