引言
由于java的平台无关特性使得串口编程很困难。因为串口需要一个与特定平台实现的标准的API,而这对于java来说很困难。
不幸的是,Sun在java的串口通信上没有太多关注。Sun已经定义了一个叫做JavaComm的串口通信API,但它的实现却不是java SE(标准版)的一部分。Sun只为少数java平台提供了一个参考实现。特别是在2005年后Sun悄悄地退出了对Windows平台的JavaComm支持。那些被漏掉的平台可以使用第三方实现。除了Sun实施过的几次小的维护外,JavaComm几乎没有被维护过,但那也只是Sun为了应付购买他们Sun Ray瘦客户端和在此平台采用JavaComm而放弃Windows支持的用户所施加的压力。
在这种情况下,而且在Sun起初没有为Linux提供JavaComm实现的现实下(从2006开始才有的),导致了免费软件RxTx库的开发。RxTx支持许多平台而不仅仅是Linux。它可以与JavaComm结合起来使用(RxTx提供了特定硬件驱动),也可以独立使用。当作为JavaComm的驱动时,通过JCL(JavaComm for Linux)将JavaComm与RxTx桥接起来。JCL是RxTx发布的一部分。
Sun对JavaComm的忽视和JavaComm的特殊的编程模型为JavaComm赢得了不实用的坏名声。而RxTx-如果不作为JavaComm的驱动-提供了丰富的接口,但却不是标准化的。RxTx比已存在的JavaComm实现支持更多的平台。最近,RxTx经过修改已经提供了和JavaComm相同的接口,只是包名与Sun的包名不同。
所以,应用程序到底应该使用哪个库呢?如果需要较大的移植性,JavaComm是较好的选择。如果某一特定平台没有JavaComm实现但有RxTx实现的话,RxTx可以作为JavaComm的驱动使用。所以,通过使用JavaComm,一个应用可以通过Sun提供的参考实现或与JCL结合的Rxtx实现来支持所有平台。这样应用程序可以工作在一个接口上而不需要改动,即标准的JavaComm接口。
本文讨论了JavaComm 和 RxTx。 它主要演示了概念而不是准备运行的代码。那些只想拷贝代码的人可以参考与这些包在一起的示例代码。那些想知道他们在干什么的人在这篇文件章中可能会找到一些有用的信息。
jSSC (Java 简单串口接口)也会被提及。
准备
- 了解串口通信和编程的基础.
- 准备好你想与之通信的设备的文档(例如调制解调器)
- 搭建好所有硬件和测试环境
- 使用一个终端程序来手动地与设备通信。这用来确保测试环境搭建已正确搭建,并保证你已经理解了与该设备交互的命令和响应。
- 下载你想使用的与你的操作系统相符的API实现
- 阅读
- JavaComm 和/或 RxTx安装说明(并按照它说的去做)
- API文档
- 示例代码片段
安装
常见问题
JavaComm 和 RxTX 安装时有一些与众不同的地方。强烈建议按照安装说明一点点的安装。如果安装说明要求一个jar文件或一个共享库必须在某一特定的文件夹下,那这就意味着需要严肃对待。如果说明要求一个特定的文件或设备需要拥有一个特定的所有权或访问权,这也意味着需要严肃处理。很多安装问题都只是因为没有按照安装说明要求的去做而引起的。
特别要注意的是一些版本的JavaComm会带有两个安装说明。一个用于java 1.2及以后的版本,一个用于java 1.1版本。使用错误的安装说明会导致不能工作的安装结果。另一方面,TxTx的一些版本/构件/包会包含不完全的说明。在这种情况下需要获得相关的RxTx发布的源码,它包含了完整的安装说明。
另外要注意Windows的Jdk安装程序会包含三个java虚拟机,因此会有三个扩展文件夹。
- 一个作为JDK的组成部分。
- 一个作为与运行JDK工具的JDK一起的私有JRE的一部分。
- 一个作为与运行应用程序的JDK一起的公共JRE的一部分。
更有甚者甚至会有第4个jre,它存在于\Windows的目录结构中。 JavaComm应该作为扩展被安装到JDK和所有公共JRE中。
Webstart
JavaComm
关于JavaComm和RxTx的一个常见问题是它们不支持通过Java WebStart进行安装:JavaComm的臭名昭著是因为需要将一个称为javax.comm.properties的文件放到JDK lib目录下,而这是不能通过Java WebStart完成的。很令人沮丧的是,对于该文件的需要是JavaComm中一些不必要的设计/决定所导致的恶果,而JavaComm的设计者们可以很容易地避免这种事情。Sun固执地拒绝修正这个错误,他们强调这个机制是必不可少的。他们是在睁着眼说瞎话,特别是当提及JavaComm时,因为Java在很长一段时间内拥有一个专门用于此类意图的服务提供者架构。
这个属性文件中的内容只有一行,即提供本地驱动的java类名称。
driver=com.sun.comm.Win32Driver
以下是一个可以通过Web Start部署JavaComm而无视那个伤脑筋的属性文件的技巧。但它有严重的缺陷,并且在部署较新的JavaComm时可能会失败-如果Sun会做一个新版本的话。
首先,关闭安全管理器(security manager)。Sun的一些蠢货程序员觉得一遍又一遍地检查可怕的javax.comm.properties文件的存在是很酷的事情,特别是当它最初已经被加载完成之后。这只是单纯地检查文件是否存在而不为其他原因。
System.setSecurityManager(null);
然后,当初始化JavaComm API时,手动初始化驱动。
String driverName = "com.sun.comm.Win32Driver"; // or get as a JNLP property CommDriver commDriver = (CommDriver)Class.forName(driverName).newInstance(); commDriver.initialize();
RxTx
RxTx在某些平台上需要改变串口设备的所有权和访问权。这也是无法通过WebStart完成的事。
在程序启动时你应该要求用户作为超级用户来执行必要的设置。特别的,RxTx有一个模式匹配算法来验证“合法”的串口设备名。当某人想使用不标准的设备,例如USB转串口转换器(USB-to-serial converter)时,这常会把事情弄砸。这个机制可以被系统属性屏蔽掉。详情参照RxTx的安装说明。
JavaComm API
引言
Java官方串口通信API是JavaComm API。这个API不是Java 2标准版的组成部分,因而此API的实现需要单独下载。不幸的是,JavaComm没有获得Sun足够的重视,实际的维护时间也不是很长。Sun只是偶尔修复一些不重要的bug,却没有做过一些早已过期的重要检修。
本节阐述JavaComm API的基本操作。所提供的源码保持简化以展示重点,在实际应用中使用需要完善。
这章的源码并不是唯一可用的示例代码。很多例子中都包含JavaComm下载。这些例子几乎包括比其API文档更多的关于如何使用它的信息。不幸的是,Sun公司没有任何真正的教程或一些说明文档。因此,要理解这个API的机制,学习这些示例代码是值得的,也仍需要学习这个API文档。但最好的方法是,学习这些例子并运用它们。由于缺少易用的应用以及理解这些API的编程模型有困难,API通常备受抨击。相比其名气和功能,这个API更好,但仅此而已。
该API采用回调机制通知程序员有新数据到来。这也是学习这一机制的好主意,而不是依赖询问端口。不像Java中的其他回调接口(如:在图形界面),这个接口只允许一个监听器监听事件。如果多个监听器请求监听几个事件,主监听器必须通过分派信息给其他二级监听器的方式来实现。
下载与安装
下载
Sun公司的JavaComm网页指向下载地址。在这个地址下,Sun当前(2007年)提供了支持Solaris/SPARC、Solaris/x86已经Linux x86的JavaComm 3.0版本。下载需要注册一个Sun公司的账户。下载页提供了注册页的链接。注册的目的并不清楚。在为注册时,用户可下载JDK和JREs,但对于这几乎微不足道的JavaComm,Sun公司在软件分销和出口方面却援引法律条文和政府限制。
官方已不再提供JavaComm的Windows版本,并且Sun已经违背了他们自己的产品死亡策略-不能在Java产品集中下载。但仍可以从这下载2.0的Windows版本(javacom 2.0).
安装
按照与下载一起的安装说明进行安装。一些版本的JavaComm 2.0会包含两个安装说明。这两个说明间最明显的区别是错误的那个是用于古老的Java1.1环境的,而适用于Java 1.2(jdk1.2.html)的那个才是正确的。
Windows用户可能不会意识到他们在不同的地方(一般是3到4个)安装了同一个VM的副本。一些IDE和Java应用程序可能也会带有他们自己的私有JRE/JDK。所以JavaComm需要重复安装到这些VM(JDK和JRE)中,这样才能够开发和执行串口应用程序。
IDE 都有代表性的IDE的方式来得知一个新的库(类和文档)。通常一个库想JavaComm不仅需要被IDE识别,而且每个使用该库的项目也应当识别。阅读IDE的文档,应该注意老的JavaComm 2.0 版本以及JavaDoc API文档使用的是Java 1.0 的Java Doc 布局。一些现代的IDE已经不再认识这些结构并不能将JavaComm2.0的文档集成到他们的帮助系统中了。在这种情况下需要一个外部的浏览器来阅读文档(推荐活动)
一旦软件安装完成,它便会推荐测试样例和JavaDoc 目录。构建并运行样例应用来确认安装是否正确时很有道理的。样例程序通常需要一些小的调整以便运行在特别的平台上(像改写硬编码的com端口标识符)。在运行一个样例程序时最好有一些串行硬件,想cabling,零调制解调器,接线盒,一个真正的猫,PABX以及其他可用的设备。
Serial_Programming:RS-232 Connections 和Serial_Programming:Modems and AT Commands 提供了一些怎样搭建串行应用开发环境的信息。
找到预期的串口
当用JavaComm串行编程时首先要做的三件事
- 枚举JavaComm能访问的所有串口(端口标识)
- 从能访问的端口标识中选择预期的端口标识
- 通过端口标识取得端口
枚举和选择期望的端口标识在同一个循环中完成:
import javax.comm.*;
import java.util.*;
...
//
// Platform specific port name, here= a Unix name
//
// NOTE: On at least one Unix JavaComm implementation JavaComm
// enumerates the ports as "COM1" ... "COMx", too, and not
// by their Unix device names "/dev/tty...".
// Yet another good reason to not hard-code the wanted
// port, but instead make it user configurable.
//
String wantedPortName = "/dev/ttya";
//
// Get an enumeration of all ports known to JavaComm
//
Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
//
// Check each port identifier if
// (a) it indicates a serial (not a parallel) port, and
// (b) matches the desired name.
//
CommPortIdentifier portId = null; // will be set if port found
while (portIdentifiers.hasMoreElements())
{
CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
pid.getName().equals(wantedPortName))
{
portId = pid;
break;
}
}
if(portId == null)
{
System.err.println("Could not find serial port " + wantedPortName);
System.exit(1);
}
//
// Use port identifier for acquiring the port
//
...
注意:
JavaComm会从与其绑定的特定平台相关的驱动中获得一个默认的可访问串口标识列表。这个列表实际上不能通过JavaComm进行配置。方法CommPortIdentifier.addPortName()是有误导性的,因为驱动类是与平台相关的,而且它们的实现不是公共API的组成部分。依赖于驱动,这个端口列表可能会在驱动中进行配置/扩展。所以,如果JavaComm没有找到某一特定端口,对驱动进行一些改动有时会有所帮助。
某端口标识符一旦被找到,就可以用它取得期望的端口:
//
// Use port identifier for acquiring the port
//
SerialPort port = null;
try {
port = (SerialPort) portId.open(
"name", // Name of the application asking for the port
10000 // Wait max. 10 sec. to acquire port
);
} catch(PortInUseException e) {
System.err.println("Port already in use: " + e);
System.exit(1);
}
//
// Now we are granted exclusive access to the particular serial
// port. We can configure it and obtain input and output streams.
//
...
初始化串口
串口的初始化是很直观的。可以逐个地设置通信参数(波特率,数据位,停止位,奇偶校验),也可以使用方便的setSerialPortParams(...)方法一下把他们搞定。
作为初始化的一部分,通信的输入输出流可以在如下的示例中配置。
import java.io.*;
...
//
// Set all the params.
// This may need to go in a try/catch block which throws UnsupportedCommOperationException
//
port.setSerialPortParams(
115200,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
//
// Open the input Reader and output stream. The choice of a
// Reader and Stream are arbitrary and need to be adapted to
// the actual application. Typically one would use Streams in
// both directions, since they allow for binary data transfer,
// not only character data transfer.
//
BufferedReader is = null; // for demo purposes only. A stream would be more typical.
PrintStream os = null;
try {
is = new BufferedReader(new InputStreamReader(port.getInputStream()));
} catch (IOException e) {
System.err.println("Can't open input stream: write-only");
is = null;
}
//
// New Linux systems rely on Unicode, so it might be necessary to
// specify the encoding scheme to be used. Typically this should
// be US-ASCII (7 bit communication), or ISO Latin 1 (8 bit
// communication), as there is likely no modem out there accepting
// Unicode for its commands. An example to specify the encoding
// would look like:
//
// os = new PrintStream(port.getOutputStream(), true, "ISO-8859-1");
//
os = new PrintStream(port.getOutputStream(), true);
//
// Actual data communication would happen here
// performReadWriteCode();
//
//
// It is very important to close input and output streams as well
// as the port. Otherwise Java, driver and OS resources are not released.
//
if (is != null) is.close();
if (os != null) os.close();
if (port != null) port.close();
简单数据传输
简单地写入数据
将数据写入到串口与基本的java IO一样简单。但在你使用AT Hayes 协议时仍有一些注意事项:
- 不要在输出流(OutputStream)中使用prinln(或其他自动附加"\n"的方法)。调制解调器的AT Hayes协议使用"\r\n"作为分隔符(而不考滤底层的操作系统)。
- 写入输出流之后,如果调制解调器设置了回显命令行,输入流的缓冲区会存有发送的指令的复述(有换行)和另一个换行("AT"指令的响应)。所以做为写操作的一部分,要确保清理输入流中的这种信息(实际上它可以用于查错)。
- 当使用Reader/Writer(不是个好主意)时,最少要设置字符编码为US-ASCII而不是使用系统平台的默认编码,否则程序可能不会运行。
- 因为使用调制解调器的主要操作是传输原始数据,与调制解调器的通信应该使用输入/输出流,而不是Reader/Writer.
To do:
|
// Write to the output
os.print("AT");
os.print("\r\n"); // Append a carriage return with a line feed
is.readLine(); // First read will contain the echoed command you sent to it. In this case: "AT"
is.readLine(); // Second read will remove the extra line feed that AT generat
简单的数据读取(轮询)
如果你正确的使用了写操作(如上所述),读操作只需简单的一条命令。
// Read the response String response = is.readLine(); // if you sent "AT" then response == "OK"
简单读写的问题
上一节中演示的简单串口读写有很严重的缺陷。所有的操作都是通过阻塞I/O完成的。这意味着当
- 没有可读数据时,或
- 输出缓冲区满(设备不能接受更多数据)时
读写方法(在前面示例中的是os.print()或is.readLine())不会返回, 导致应用程序被暂停。更准确地说,读写线程被阻塞了。如果那个线程是应用程序主线程的话,应用程序会停止直到阻塞条件被释放(即有可读数据到达或设备重新接受数据)。
除非应用程序是最原始的那种,否则程序被阻塞是绝不允许的。例如,最起码也要能让用户取消通信操作。这需要使用非阻塞I/O或异步I/O。然而JavaComm是基于Java的标准阻塞I/O系统(InputStream,OutputStream)的,但可以采用稍后展示的一个变形技巧。
所谓的"变形技巧"是JavaComm通过事件通知机制为异步I/O提供的有限的支持。但在Java中要在阻塞I/O的基础上实现非阻塞I/O的常用解决方案是使用线程。对于串口写操作这个方案是切实可行的,强烈建议使用一个单独的线程对串口进行写操作-尽管已经使用了事件通知机制,这稍后会做出解释。
读操作也应该在一个单独的线程中进行处理,但如果采用了JavaComm的事件通知机制这也不是必须的。总结:
操作 架构
读操作 使用事件通知和/或单独线程
写操作 都要使用单独线程,可选用事件通知机制
接下来的部分会介绍一些其他细节。
事件驱动串行通信
引言
JavaComm API提供了事件通知机制以克服阻塞I/O带来的问题。但在这个典型的Sun方式中这个机制也有问题的。
原则上一个应用程序可以注册事件监听器到一个特定的串口以接收发生在这个端口上的重要事件的通知。读写数据的两个最有意思的事件类型是
- javax.comm.SerialPortEvent.DATA_AVAILABLE和
- javax.comm.SerialPortEvent.OUTPUT_BUFFER_EMPTY.
但这也带来了两个问题:
- 每个串口只能注册一个事件监听器。这会强制程序员编写"巨大"的监听器,它以接收到的事件类型来区分要进行的操作。
- OUTPUT_BUFFER_EMPTY是一个可选的事件类型。Sun在文档中隐晦地提到JavaComm的实现不一定都会支持产生这个事件类型。
在进行详细讨论前,下一节将会演示实现和注册一个串口事件处理器的主要方式。要记住一个串口只能有一个事件处理器,而且它要处理所有可能的事件。
设置串行事件处理器
import javax.comm.*;
/**
* Listener to handle all serial port events.
*
* NOTE: It is typical that the SerialPortEventListener is implemented
* in the main class that is supposed to communicate with the
* device. That way the listener has easy access to state information
* about the communication, e.g. when a particular communication
* protocol needs to be followed.
*
* However, for demonstration purposes this example implements a
* separate class.
*/
class SerialListener implements SerialPortEventListener {
/**
* Handle serial events. Dispatches the event to event-specific
* methods.
* @param event The serial event
*/
@Override
public void serialEvent(SerialPortEvent event){
//
// Dispatch event to individual methods. This keeps this ugly
// switch/case statement as short as possible.
//
switch(event.getEventType()) {
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
outputBufferEmpty(event);
break;
case SerialPortEvent.DATA_AVAILABLE:
dataAvailable(event);
break;
/* Other events, not implemented here ->
case SerialPortEvent.BI:
breakInterrupt(event);
break;
case SerialPortEvent.CD:
carrierDetect(event);
break;
case SerialPortEvent.CTS:
clearToSend(event);
break;
case SerialPortEvent.DSR:
dataSetReady(event);
break;
case SerialPortEvent.FE:
framingError(event);
break;
case SerialPortEvent.OE:
overrunError(event);
break;
case SerialPortEvent.PE:
parityError(event);
break;
case SerialPortEvent.RI:
ringIndicator(event);
break;
<- other events, not implemented here */
}
}
/**
* Handle output buffer empty events.
* NOTE: The reception of this event is optional and not
* guaranteed by the API specification.
* @param event The output buffer empty event
*/
protected void outputBufferEmpty(SerialPortEvent event) {
// Implement writing more data here
}
/**
* Handle data available events.
*
* @param event The data available event
*/
protected void dataAvailable(SerialPortEvent event) {
// implement reading from the serial port here
}
}
监听器一旦实现,即可用来监听特定的串口事件。要做到如此,需要为串口添加一个监听器实例。此外,每个事件类型的接收需要进行单独申请。
SerialPort port = ...;
...
//
// Configure port parameters here. Only after the port is configured it
// makes sense to enable events. The event handler might be called immediately
// after an event is enabled.
...
//
// Typically, if the current class implements the SerialEventListener interface
// one would call
//
// port.addEventListener(this);
//
// but for our example a new instance of SerialListener is created:
//
port.addEventListener(new SerialListener());
//
// Enable the events we are interested in
//
port.notifyOnDataAvailable(true);
port.notifyOnOutputEmpty(true);
/* other events not used in this example ->
port.notifyOnBreakInterrupt(true);
port.notifyOnCarrierDetect(true);
port.notifyOnCTS(true);
port.notifyOnDSR(true);
port.notifyOnFramingError(true);
port.notifyOnOverrunError(true);
port.notifyOnParityError(true);
port.notifyOnRingIndicator(true);
<- other events not used in this example */
数据写入
使用单独分离的进程进行数据写入只有一个目的:避免整个应用程序块由于某一个串口未准备好写数据而锁定。
一个简单的,线程安全的环形缓冲区实现
使用一个独立于主程序线程的线程进行写操作,表明需要某种方式将要写入的数据从主应用线程(主线程)提交给写线程。这可以采用一个共享的异步事件缓冲区,例如一个byte数组。另外,主程序还需要某种方式决定是否可以往数据缓冲区中写数据或者数据缓冲区是否已经满了。如果数据缓冲区已满,表明串口还没有准备好写操作,并且要输出的数据正在排队。主程序需要在共享数据缓冲区中轮询可用的新的空闲空间。然而,在主程序轮询的间隙可以做些其他的事,例如更新用户界面(GUI),提供一个可以退出发送数据的命令提示等等。
乍一看PipedInputStream/PipedOutputStream对于这种通信是一个不错的主意。但如果管道流真的有用的话那Sun就不是Sun了。如果与之对应的PipedOutputStream没有及时清理的话,PipedInputStream会发生阻塞,进而会阻塞应用程序线程。就算使用独立线程也避免不了。而java.nio.Pipe也有与此相同的问题。它的阻塞行为与平台相关。而将JavaComm使用的传统I/O改为NIO也不是很好。
在本文中采用了一个很简单的同步的环形缓冲区来进行线程间数据传递。在现实世界中的应用程序很可能会使用更加复杂的缓冲区实现。例如在一个现实世界的实现需要以输入输出流的视角操作缓冲区。
如此一个环形缓冲器并没有什么特别的,在线程处理方面,也没有特别的属性。它只是用来这里用来提供数据缓冲的一个简单数据结构。这里已经实现了该缓冲器,以确保访问该数据结构是线程安全的。
/**
* Synchronized ring buffer.
* Suitable to hand over data from one thread to another.
**/
public class RingBuffer {
/** internal buffer to hold the data **/
protected byte buffer[];
/** size of the buffer **/
protected int size;
/** current start of data area **/
protected int start;
/** current end of data area **/
protected int end;
/**
* Construct a RingBuffer with a default buffer size of 1k.
*/
public RingBuffer() {
this(1024);
}
/**
* Construct a RingBuffer with a certain buffer size.
* @param size Buffer size in bytes
*/
public RingBuffer(int size) {
this.size = size;
buffer = new byte[size];
clear();
}
/**
* Clear the buffer contents. All data still in the buffer is lost.
*/
public void clear() {
// Just reset the pointers. The remaining data fragments, if any,
// will be overwritten during normal operation.
start = end = 0;
}
/**
* Return used space in buffer. This is the size of the
* data currently in the buffer.
* <p>
* Note: While the value is correct upon returning, it
* is not necessarily valid when data is read from the
* buffer or written to the buffer. Another thread might
* have filled the buffer or emptied it in the mean time.
*
* @return currently amount of data available in buffer
*/
public int data() {
return start <= end
? end - start
: end - start + size;
}
/**
* Return unused space in buffer. Note: While the value is
* correct upon returning, it is not necessarily valid when
* data is written to the buffer or read from the buffer.
* Another thread might have filled the buffer or emptied
* it in the mean time.
*
* @return currently available free space
*/
public int free() {
return start <= end
? size + start - end
: start - end;
}
/**
* Write as much data as possible to the buffer.
* @param data Data to be written
* @return Amount of data actually written
*/
int write(byte data[]) {
return write(data, 0, data.length);
}
/**
* Write as much data as possible to the buffer.
* @param data Array holding data to be written
* @param off Offset of data in array
* @param n Amount of data to write, starting from .
* @return Amount of data actually written
*/
int write(byte data[], int off, int n) {
if(n <= 0) return 0;
int remain = n;
// @todo check if off is valid: 0= <= off < data.length; throw exception if not
int i = Math.min(remain, (end < start ? start : buffer.length) - end);
if(i > 0) {
System.arraycopy(data, off, buffer, end, i);
off += i;
remain -= i;
end += i;
}
i = Math.min(remain, end >= start ? start : 0);
if(i > 0 ) {
System.arraycopy(data, off, buffer, 0, i);
remain -= i;
end = i;
}
return n - remain;
}
/**
* Read as much data as possible from the buffer.
* @param data Where to store the data
* @return Amount of data read
*/
int read(byte data[]) {
return read(data, 0, data.length);
}
/**
* Read as much data as possible from the buffer.
* @param data Where to store the read data
* @param off Offset of data in array
* @param n Amount of data to read
* @return Amount of data actually read
*/
int read(byte data[], int off, int n) {
if(n <= 0) return 0;
int remain = n;
// @todo check if off is valid: 0= <= off < data.length; throw exception if not
int i = Math.min(remain, (end < start ? buffer.length : end) - start);
if(i > 0) {
System.arraycopy(buffer, start, data, off, i);
off += i;
remain -= i;
start += i;
if(start >= buffer.length) start = 0;
}
i = Math.min(remain, end >= start ? 0 : end);
if(i > 0 ) {
System.arraycopy(buffer, 0, data, off, i);
remain -= i;
start = i;
}
return n - remain;
}
}
通过使用该环形缓冲器,你现在可以以一种可控的方式从一个线程提交数据到另一个线程。当然,其他线程安全、非阻塞式的方法同样可以。这里的关键点在于当缓冲区已满或者缓冲区为空时,数据的读写不会造成堵塞。
根据在 "建立一个串口事件处理器"小节演示的事件处理器的轮廓,你可以使用在"一个简单的,线程安全的环形缓冲区实现"小节中介绍的共享环形缓冲区以支持OUTPUT_BUFFER_EMPTY事件。不是所有的JavaComm实现都支持这个事件,所以这段代码可能永远也不会被调用。但如果可以,它是确保最佳数据吞吐量的一部分,因为它可以使串口不会长时间处于空闲状态。
事件监听器的轮廓需要提供一个outputBufferEmpty()方法,它的实现如下:
RingBuffer dataBuffer = ... ;
/**
* Handle output buffer empty events.
* NOTE: The reception is of this event is optional and not
* guaranteed by the API specification.
* @param event The output buffer empty event
*/
protected void outputBufferEmpty(SerialPortEvent event) {
}
下面的示例假设数据的目的地是某个文件。当数据到达时它会被从串口中取出并写入目的文件。这只是个精简化的视图,因为实际上你需要检查数据的EOF标识以将调制解调器(通常称为“猫”)重置为命令模式。
import javax.comm.*;
...
InputStream is = port.getInputStream();
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("out.dat"));
/**
* Listen to port events
*/
class FileListener implements SerialPortEventListener {
/**
* Handle serial event.
*/
void serialEvent(SerialPortEvent e) {
SerialPort port = (SerialPort) e.getSource();
//
// Discriminate handling according to event type
//
switch(e.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE:
//
// Move all currently available data to the file
//
try {
int c;
while((c = is.read()) != -1) {
out.write(c);
}
} catch(IOException ex) {
...
}
break;
case ...:
...
break;
...
}
if (is != null) is.close();
if (port != null) port.close();
}
调制解调器控制
JavaComm主要关心的是一个串口的处理和串口上数据的传送。它不懂或者提供对高层协议的支持,比如Hayes调制解调指令通常用来控制客户级的猫。这不是JavaComm的任务,也就不是一个bug。
如同其他特别的串行设备,如果希望由JavaComm控制一个猫,那么就得在JavaComm上写必要的代码。页面"Hayes-compatible Modems and AT Commands"提供了处理Hayes猫的必要的基本信息。
一些操作系统,像Windows或某一Linux对于如何配置一个特别类型或牌子的猫的控制命令提供了一个或多或少标准的方式。例如,Windows猫的“驱动”通常只是注册入口,描述一个个别的猫(真正的驱动是一个通用的串行调制解调驱动)。JavaComm没法获取这样的操作系统的具体的数据。因此,要么必须提供一个单独的java工具来允许用户为使用个别的猫去配置一个应用,要么就添加一些相应平台的(本地的)代码。
RxTx
概述与版本
由于Sun没有为Linux提供JavaComm的参考实现,人们为java和linux开发了RxTx。后来RxTx被移植到了其他平台。最新版本的RxTx已知可运行在100种以上平台,包括Linux, Windows, Mac OS, Solaris 和其他操作系统。
RxTx可以独立于JavaComm API使用,也可以作为所谓的Java Comm API服务者。如果采用后者还需要一个称为JCL的封装包。JCL和RxTx通常与Linux/Java发行版打包在一起,或者JCL完全与代码集成在一起。所以,在一个个地下载他们之前,看一看Linux发行版的CD是值得的。
由于Sun对JavaComm的有限的支持和不适当的文档,放弃JavaComm API,转而直接使用RxTx而不是通过JCL封装包似乎成为了一种趋势。然而RxTx的文档是很稀少的。特别是RxTx开发者喜欢将他们的版本和包内容弄得一团糟(例如使用或未使用集成的JCL)。从1.5版本开始,RxTx包含了公共JavaComm类的替代类。由于法律原因,他们没有在java.comm包中,而是在gui.io包下。然而现存的两个版本的打包内容有很大差别。
- RxTx 2.0
- 这个版本的RxTx 主要用作JavaComm提供者。它应该源自于RxRx 1.4,这是RxTx添加gui.io包之前的版本。
- RxTx 2.1
- 这个版本的RxTx包含了一个完整的代替java.comm的gnu.io包。它应该源自于RxTx 1.5,这是支持gnu.io的起始版本。
因此,如果你想对原始的JavaComm API 编程的话你需要
- Sun JavaComm 通用版。撰写本文时实际上就是Unix包(包含对各种类Unix系统的支持,像Linux或Solaris)即使在Windows上,这个Unix包也是需要用来提供java.comm的通用实现的。只用用Java实现那部分会被用到,然而Unix的本地库会被忽略的。
- RxTx 2.0, 为了能在JavaComm通用版本下有不同的提供者,不同于JavaComm包下的那个。然而,如果你只想用gnu.io替换包,那么你只需要
- RxTx 2.1
将一个JavaComm应用转换成RxTx应用
如果你是对Sun公司放弃使JavaComm支持Windows这一行为感到失望的众多成员中的一个,那么你应该将你的JavaComm应用转到RxTx上来。如你在上面所看到的,这里有两种方式来完成这件事,假设你已经安装了RxTx的某一版本,那么下面的选项可选其一:
- 使用RxTx 2.0作为JavaComm接口的实现
- 将应用移植到RxTx 2.1环境上
上面的第一项在前面已经解释,第二项也相当简单。对于需要将JavaComm应用移植到RxTx 2.1上来的人,只需要将应用源代码中所有对“java.comm”包的引用换成“gnu.io”包,如果原始的JavaComm应用编写恰当,这里就没有其他的事情需要去做。
在Unix平台上,RxTx 2.1甚至提供了工具“contrib/ChangePackage.sh”去在源代码树形结构中执行全局的替换,这样的替换在其他的平台很容易使用支持重构功能的IDE(集成开发环境)来完成。