在java程序中,为了实现同本地系统的硬件端口(RS-232标准串口/IEEE1284标准并口)通讯,目前有两个方法,第一个就是以前说过的,使用JNI技术自己写一个与java程序相关的dll/so文件,这个文件可以直接和本地系统通信,从而管理本地机器的端口,不过使用java程序独自实现一个比较完善的端口管理解决方案对个人而言是一个花费巨大且不实际的想法.第二个方法就是使用sun公司提供的comm包,这套API是用于开发平台独立的通讯应用程序的扩展API,但是这个包并未包含在sun的j2se包中,而是以独立jar包形式发布在java.sun.com网站上,下面仅讨论使用comm包管理本地机器端口的方法.
comm包目前有三种系统的版本,分别适用于x86和Sparc结构下的Solaris系统,以及x86下的Linux系统,x86下的windows系统,但是在sun的网站上面,仅提供了Windows和Solaris平台下的两个版本,如果需要linux下的comm包,需要从第三方网站下载,据说这个地址http://www.geeksville.com/~kevinh/linuxcomm.html可以下载(但是我费了很长时间并没有从这里下载下来,而是从CSDN下载中心找到的).
适用于不同系统平台下的comm.jar包含的本地接口文件是不一样的,windows平台的包除API外提供的文件是javax.comm.properties,win32com.dll两个文件,linux平台的包提供的是javax.comm.properties,libLinuxSerialParallel.so,libLinuxSerialParallel_g.so三个文件,其中javax.comm.properties记录了comm API的驱动名,winddow平台的是com.sun.comm.Win32Driver,linux平台的是com.sun.comm.LinuxDriver,而dll和so文件则提供了本地驱动接口.
运行java程序需要JRE来运行,在安装完jdk后会出现两套jre,一套是独立的一套包含于jdk目录内,这个要弄清楚,javax.comm.properties文件必须放到运行当前java程序的jre的lib目录下(一般放在javahome/jre/lib下即可),而so和dll文件所在的目录需要被映射到系统的path中,一般装完jdk后,javahome会被自动映射到系统path中,所以把so或dll文件放到javahome/bin目录下即可,如果so/dll文件的路径搞错,会有异常抛出,如在windows下的话错误信息如下:
1 Error loading win32com: java.lang.UnsatisfiedLinkError: no win32com in java.library.path.
同时这个错误会继而抛出javax.comm.NoSuchPortException异常,所以在发现有NoSuchPortException异常时要仔细检查,不一定是没有某一个端口才会抛出NoSuchPortException的异常.
在使用这个包之前要先检查一下是否缺少某些文件,我原先使用的一个comm.jar包里面只有javax\comm\下的各个类,而没有com\sun\comm\下的各个类,所以总是报java.lang.ClassNotFoundException: com.sun.comm.Win32Driver这个异常,刚开始是以为两个文件放错了位置,后来发现更改了几次路径,还是出这个错误,后来检查包,发现comm.jar包里面没有com\sun\comm目录,真正的Win32Driver.class是放在这个目录下的,linux平台的类是LinuxDriver.class,它负责与本地系统进行通信来管理设备.
comm.jar包的位置很自由,只要保证在java程序的classpath的映射中即可.
在window系统中,dos下有一个查看或配置本机所有端口参数的工具,可以使用mode /?命令查看此命令的帮助信息.
在linux系统中,可以使用命令cat /proc/devices来查看本机所有设备.如果是普通串口设备,设备名前缀为ttyS,第一串口为ttyS0,第二串口为ttyS1,依次类推,并口则是以lp开头,从0开始顺延名称.linux下配置端口参数的命令是minicom -s 端口名
一,初始化comm驱动(可能不是必须的)
使用如下方式初始化comm的驱动(windows系统):
javax.comm.CommDriver commDriver = (javax.comm.CommDriver) Class.forName( "com.sun.comm.Win32Driver" ).newInstance();
commDriver.initialize();
为了维护程序的移值性,可以根据系统类型来判断使用何种驱动名:
String osName = System.getProperty( "os.name" );
String driverName = null;
if ( osName.startsWith("Windows") )
{
System.out.println( "当前操作系统是Windows操作系统......" );
driverName = "com.sun.comm.Win32Driver";
}
else
driverName = "com.sun.comm.LinuxDriver";
javax.comm.CommDriver commDriver = (javax.comm.CommDriver) Class.forName( driverName ).newInstance();
commDriver.initialize();
执行initialize()会把当前设备上的所有端口注册给CommPortIdentifier,需要注意的是,我使用的这个版本的comm在这个地方有个BUG,就是每执行一次initialize()方法,就会把当前设备上的所有端口注册给CommPortIdentifier一遍,而CommPortIdentifier会把每一次注册的端口都当作不同的端口分配CommPortIdentifier对象,但是它们的端口名和类型是相同的,所以BUG不影响设备的正常连接和通信,只不过如果使用使用下面的方法遍历设备上的所有端口时会让人产生迷惑:
Enumeration portEnum = CommPortIdentifier.getPortIdentifiers();
Enumeration容器中存放的全都是绑定了端口的CommPortIdentifier对象,每执行一次commDriver.initialize()方法,这个容器中的数量就会增加实际端口数量的一倍.而CommPortIdentifier.getPortIdentifiers()这个方法内部也会再调用一次commDriver.initialize(),所以最后的数量应该是(commDriver.initialize()的次数+1)*实际端口数.
但不管怎么样,使用如下方法:
CommPortIdentifier portId = (CommPortIdentifier) CommPortIdentifier.getPortIdentifier( portName );
取绑定了名为portName的端口的CommPortIdentifier实例时只会取到一个,如果有同一个端口注册给了不同的CommPortIdentifier实例,现在搞不清楚这里是怎么取的其中某一个实例.
鉴于这种情况,不必使用初始化方法,直接使用
Enumeration portEnum = javax.comm.CommPortIdentifier.getPortIdentifiers();
javax.comm.CommPortIdentifier portId = (CommPortIdentifier) CommPortIdentifier.getPortIdentifier( portName );
这两个方法返回的结果总是正确的(假设端口存在).
文档上对initialize()的说明是:will be called by the CommPortIdentifier's static initializer
二,查看本机所有端口名及其类型
CommPortIdentifier类有两个很有用的方法,getName()和getPortType()方法,分别返回被绑定的当前端口的名称和类型,getPortType()返回的是一个int变量值,在CommportIdentifier的定义中,对类型的定义如下:
public static final int PORT_SERIAL=1;
public static final int PORT_PARALLEL=2;
即整数1代表串口,整数2代表并口.
java.util.Enumeration portEnum = javax.comm.CommPortIdentifier.getPortIdentifiers();
while ( portEnum.hasMoreElements() )
{
javax.comm.CommPortIdentifier portId = (CommPortIdentifier) portEnum.nextElement();
String portName = portId.getName();
int portType = portId.getPortType();
i++;
System.out.println( "第[" + i + "]个端口,名称[" + portName + "],类型[" + (portType==CommPortIdentifier.PORT_SERIAL?"RS-232串口":"IEEE 1284并口") + "]" );
}
这里需要注意的是,Enumeration只是一个接口,getPortIdentifiers()返回的对象是实现了此接口的StringTokenizer实例,每使用一次此实例的nextElement()方法,就会从此实例中去掉当前的元素,这和Iterator接口是不同的,虽然Iterator的Enumeration功能重复,但是Iterator接供了可选的移除操作方法,所以上面的循环完成后,之后从portEnum里是取不出任何元素的.
三,把java程序绑定到端口
javax.comm.CommPort comPort = null
javax.comm.SerialPort sPort = null;
javax.comm.ParallelPort pPort = null;
java.io.OutputStream outStream = null;
java.io.InputStream inputStream = null;
CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier( String portName );
comPort = portId.open( String appName, int outTime );
上面这句代码作用是为一个应用程序实例绑定此端口,appName为java实例名称,outTime为端口打开时阻塞等待时间,单位是ms,在调用CommPort的close()方法之前此端口被名为appName的实例程序独占
switch ( portId.getPortType() )
{
case CommPortIdentifier.PORT_SERIAL:
//在判断当前端口类型为串口后把绑定产生的CommPort实例强制转换为SerialPort(串口)实例
sPort = (SerialPort) comPort;
//以下是串口封装类SerialPort类中的方法
//注册帧错误事件,true为有效
sPort.notifyOnFramingError( true );
//注册数据丢失错误事件,true为有效
sPort.notifyOnOverrunError( true );
//初始化串口,参数 波特率/数据位/停止位/奇偶校验
sPort.setSerialPortParams( int BAUD, int DATABITS, int STOPBITS, int PARITY_TYPE );
//设置流控制
sPort.setFlowControlMode( int FLOWCONTROL );
//获得输出流,可以此流为载体向串口设备写入数据
outStream = sPort.getOutputStream();
//获得输入流,可以此流为载体从串口设备读取数据
inputStream = sPort.getInputStream();
break;
case CommPortIdentifier.PORT_PARALLEL:
//在判断当前端口类型为并口后把绑定产生的CommPort实例转换为ParallelPort(并口)实例
ParallelPort pPort = (ParallelPort) comPort;
//获得输出流,可以此流为载体向并口设备写入数据
outStream = pPort.getOutputStream();
//获得输入流,可以此流为载体从并口设备读取数据
inputStream = pPort.getInputStream();
//取得当前并口的工作模式
int mode = pPort.getMode();
//自动判断并口模式;
switch ( mode )
{
case ParallelPort.LPT_MODE_ECP://Extended Capabilities Ports,扩展功能端口
System.out.println( "Mode is: ECP" );
break;
case ParallelPort.LPT_MODE_EPP://Enhanced Parallel Ports,增强并行口
System.out.println( "Mode is: EPP" );
break;
case ParallelPort.LPT_MODE_NIBBLE://Bi-tronics
System.out.println( "Mode is: Nibble Mode." );
break;
case ParallelPort.LPT_MODE_PS2://Bi-directional
System.out.println( "Mode is: Byte mode." );
break;
case ParallelPort.LPT_MODE_SPP://标准模式
System.out.println( "Mode is: Compatibility mode." );
break;
default:
throw new IllegalStateException( "Parallel mode " + mode + " invalid." );
}
break;
default:
throw new IllegalStateException( "Unknown port type " + portId );
}
在上面的程序中,根据端口的不同类型而执行了不同的初始化及注册工作,并获得不同的输入输出流.CommPort类只是一个抽象类,而SerialPort/ParallelPort均是CommPort类的子类,所有在端口类型确定后可以直接把CommPort类强制转换为相应的并口或串口类.
串口与并口初始化的原理不同,所以串口类和并口类的初始化方法及提供的其它功能方法都有所不同.
1>串口
A.串口的监听
串口类中,在得到SerialPort实例后,可以对此串口注册一系列的通知,包括错误及其它事件的通知,比如上面的代码中的notifyOnFramingError(boolean)方法,即给串口注册了成帧误差事件通知,当向端口写入或从端口读取连续的二进制位流时如果数据产生误差,且参数设置为true时,会有一个通知发送给串口监听类,而notifyOnOverrunError方法的作用则是当接收设备不能按数据发送速率接收数据而造成数据丢失时会有一个相应的通知发送给串口监听类,在API中可以查到有10个注册通知的方法,分别对应了SerialPortEvent的十种类型的事件,对应关系如下:
通知方法 事件值 事件类型
notifyOnBreakInterrupt(boolean) SerialPortEvent.BI 通讯中断
notifyOnCarrierDetect(boolean) erialPortEvent.CD 载波检测
notifyOnCTS(boolean) SerialPortEvent.CTS 清除发送
notifyOnDataAvailable(boolean) SerialPortEvent.DATA_AVAILABLE 有数据到达
notifyOnDSR(boolean) SerialPortEvent.DSR 数据设备准备好
notifyOnFramingError(boolean) SerialPortEvent.FE 帧错误
notifyOnOutputEmpty(boolean) erialPortEvent.OUTPUT_BUFFER_EMPTY 输出缓冲区已清空
notifyOnOverrunError(boolean) SerialPortEvent.OE 溢位错误
notifyOnParityError(boolean) SerialPortEvent.PE 奇偶校验错误
notifyOnRingIndicator(boolean) SerialPortEvent.RI 振铃指示
使用上述方法注册了通知之后,还要实现串口的监听接口SerialPortEventListener,实现此接口的方法
public void serialEvent( SerialPortEvent arg0 ){}
通过SerialPortEvent的getEventType()方法可以得到当前事件的变量值,然后与上表中的事件值对比,即可知道当前串口上产生了哪种类型的事件.
使用SerialPort类的addEventListener( SerialPortEventListener arg0)方法为串口添加事件监听.
B.串口的初始化参数
串口通信一般需要设置五个参数,波特率/数据位/停止位/奇偶校验/流控制,在初始化的API方法中,参数均为int类型,这些参数要根据连到此串口上的具体设备的参数要求设置,如果未按要求设置,一些容错性能比较好的设备可能不会影响数据正常传输,容错性能较差的设备可就不行了,我曾经在做串口显示屏数据传输程序时就碰见过这个问题,有一种设备数据位和停止位的参数值传反了,而且流控制没有设置,竟然都可以正常显示,而另外几款显示屏就不行了,有时候因为流控制没有设置正确也会造成无法与程序正常传输数据的问题.
在comm的SerialPort类中,已经定义好了一套参数,这套定义包含了一些常用的参数值,在编写程序时可以根据具体的设备使用具体的参数值,各参数定义如下:
数据位:取决于传送数据的编码类型/通信协议/设备
public static final int DATABITS_5=5;
public static final int DATABITS_6=6;
public static final int DATABITS_7=7;
public static final int DATABITS_8=8;
停止位:是数据包中的最后一位,标志数据结束,维护设备传输的同步
public static final int STOPBITS_1=1;
public static final int STOPBITS_2=2;
public static final int STOPBITS_1_5=3;
奇偶校验:检错方式,这个参数可以默认为0,即不进行检错
public static final int PARITY_NONE=0;
public static final int PARITY_ODD=1;
public static final int PARITY_EVEN=2;
public static final int PARITY_MARK=3;
public static final int PARITY_SPACE=4;
流控制
public static final int FLOWCONTROL_NONE=0;
public static final int FLOWCONTROL_RTSCTS_IN=1;
public static final int FLOWCONTROL_RTSCTS_OUT=2;
public static final int FLOWCONTROL_XONXOFF_IN=4;
public static final int FLOWCONTROL_XONXOFF_OUT=8;
另外说一下波特率,它是指每秒钟串口同设备传送的bit的数量,据我写过通信程序的设备来看,波特率一般都是9600或2400两种.
C.流控制的问题
流控制可以控制数据传输的进程,防止数据的丢失.PC机中常用的两种流控制是硬件流控制(包括RTS/CTS、DTR/CTS等)和软件流控制XON/XOFF(继续/停止),是靠电平的高低位来识别信号的,相关知识可以查阅微机原理/数电/模电等书籍.需要注意的是设置流控制并是像设置数据位/停止位/奇偶校验那样全放在setSerialPortParams()方法里,而是使用一个单独的方法setFlowControlMode( int FLOWCONTROL )来设置,这个地方一定要根据实际设备要求来设置,对容错性能不太好的设备尤其要如此.
D.串口类中还有其它一些设置串口参数的方法,如setDTR(boolean) 等,可以查阅API文档.
2>并口
A.初始化:
并口的初始化比较简单,不需要单独的初始化方法,在通过CommPortIdentifier的getPortType方法判断了端口类型为并口类型以后,就可以把通过CommPortIdentifier的open产生的CommPort直接转换为并口类ParallelPort的对象实例.
B.并口工作模式:
并口有一个setMode( int mode )方法可以设置并口的工作模式,在comm的ParallelPort类中定义了几种工作模式,可以根据具体设置的要求设置相应的值(一般在程序中不需要显式的去调用这个方法设置,默认即可):
public static final int LPT_MODE_ANY=0;
public static final int LPT_MODE_SPP=1;
public static final int LPT_MODE_PS2=2;
public static final int LPT_MODE_EPP=3;
public static final int LPT_MODE_ECP=4;
public static final int LPT_MODE_NIBBLE=5;
ANY:comm中的自适应模式;
NIBBLE:Bi-directional模式的一种,即Bi-tronics,IEEE-1284标准,此模式下数据双向并行传输,每次四个bit,一般见于惠普的并口设备;
PS2:即Bi-directional模式,此模式下允许主机和外设双向通信,但同一时间内只能在一个方向上传输;
SPP:标准的工作模式,此模式下数据单向半双工传输,速率为15k/s,支持的外设比较广泛,一般被设置为默认工作模式;
EPP:增强型工作模式,此模式下数据双向半双工传输,速率500k/s~2m/s,有两种标准EPP1.7和EPP1.9;
ECP:扩充型工作模式,此模式下数据双向全双工传输,速率2m/s;
在EPP/ECP/SPP三种模式中,传输速度依次为ECP>EPP>SPP.PS2.
可以使用ParallelPort的getMode()方法判断当前端口处于哪种工作模式下.
C.并口的监听
并口上的事件类型比较少,只有两种,同理对应的通知方法也只有两个,对应如下:
通知方法 事件值 事件类型
notifyOnBuffer(boolean) ParallelPortEvent.PAR_EV_BUFFER 并口输出缓冲区满
notifyOnError(boolean) ParallelPortEvent.PAR_EV_BUFFER 并口发生错误
在使用上述通知方法设置事件通知有效后,实现关口监听接口ParallelPortEventListener和其中的方法:
public void parallelEvent( javax.comm.ParallelPortEvent arg0 ){}
使用ParallelPortEvent的getEventType()方法得到事件值,然后与上述的事件值对比,即可知道此时当前并口上发生的事件.
使用ParallelPort类的addEventListener( ParallelPortEventListener arg0)方法为并口添加事件监听.
3>CommPortOwnershipListener监听接口
这个监听负责判断使用某个端口名称得到的端口实例当前的状态,此类定义了三个静态的final整型变量来标志三种端口状态:
CommPortOwnershipListener.PORT_OWNED 标志当前端口被占用,这个状态当一个设备得到了端口使用权时端口由UNOWNED变为此状态
CommPortOwnershipListener.PORT_UNOWNED 标志当前端口空闲,未被任何程序占用,也没有程序请求此端口使用权
CommPortOwnershipListener.PORT_OWNERSHIP_REQUESTED 标志当前端口被争用,即一个程序占用此端口,而另一个程序请求使用此端口,如果占用端口的程序监听到此事件,可以调用CommPort的close()方法来释放端口并把占用权交给请求占用权的程序.
如果要注册此监听,需要实现此接口及其方法public void ownershipChange(int eventId){},使用 CommPortIdentifier的addPortOwnershipListener(CommPortOwnershipListener arg0)的方法、使用SerialPort/ParallelPort的addEventListener()方法均可以为当前端口注册此监听,eventId值对应于CommPortOwnershipListener的API中定义的三种事件的值.
4>输入输出流
无论是并口还是串口,初始化及参数设置工作完成后,使用各自的getOutputStream()和getInputStream()方法获取输出和输入流了,这里的输入输出流是java.io中的标准输入输出流,之后的读取或写入工作同以文件为对象的读取或写入工作相似,可以把设备看作外部文件,同文件操作不同的是,设备端有未知性,比如如果一个设备连接有误,但是程序开始通过某个端口向此设备进行读取和写入的工作,这时候程序就会发生阻塞,看起来就像死机一样,因为程序本身没有错,它只管去控制端口,并不管设备状况,所以最好这个地方重启一个线程来判断程序阻塞时间,如果阻塞时间超过设定的时间,就视为此设备未连接或连接错误等状况,然后使用此线程关闭输入输出流,并释放此端口,使得程序可以继续无阻塞的走下去.
一般使用OutputStream.write(byte[] b)和InputStream.inputStream.read( byte[] b )两个方法来写入或读取数据,常用的设备一般有打印机/显示屏/扫描仪/RF枪/读卡器等.
使用输入流读取数据时,comm本身提供了两种轮询方式去读取数据,但是我觉得重开一个线程是最好的解决方式.
四,关闭流,释放端口资源
在comm的API中,CommPortIdentifier和CommPort似乎分工很明确,端口的注册、查找、初始化以及为端口绑定应用程序并为此程序打开端口、监听端口占用状态等端口管理工作由CommPortIdentifier负责,而CommPort则是管理实际的数据传输等工作,比如使用CommPort获取输入输出流.按这种分工,按说关闭端口的工作也应该交给CommPortIdentifier,但是事实不是这样,CommPort负责关闭端口释放资源的工作.在关闭端口之前,记得先关闭输入输出流.
如果当前端口没有关闭,另外一个实例程序去直接使用它时,会抛出javax.comm.PortInUseException异常.
更多交流、更多了解:回钦波 QQ:444084929