串行编程/串行 Java
串行编程: 简介和 OSI 网络模型 -- RS-232 接线和连接 -- 典型的 RS232 硬件配置 -- 8250 UART -- DOS -- MAX232 驱动器/接收器系列 -- Windows 中的 TAPI 通信 -- Linux 和 Unix -- Java -- Hayes 兼容调制解调器和 AT 命令 -- 通用串行总线 (USB) -- 形成数据包 -- 错误纠正方法 -- 双向通信 -- 数据包恢复方法 -- 串行数据网络 -- 实际应用开发 -- 串行连接上的 IP
由于 Java 的平台无关性,串行接口很困难。串行接口需要一个标准化的 API,并具有特定于平台的实现,这对 Java 来说很困难。
不幸的是,Sun 并没有在 Java 中过多关注串行通信。Sun 定义了一个串行通信 API,称为 JavaComm,但 API 的实现不是 Java 标准版的一部分。Sun 为一些(但并非所有)Java 平台提供了一个参考实现。特别是,在 2005 年底,Sun 默默地撤回了对 Windows 的 JavaComm 支持。一些被遗漏平台的第三方实现是可用的。JavaComm 并没有看到多少维护活动,Sun 只执行了最低限度的维护,除了 Sun 显然回应了其 Sun Ray 瘦客户机买家的压力,并将 JavaComm 移植到此平台,同时放弃了对 Windows 的支持。
这种情况以及 Sun 最初没有为 Linux 提供 JavaComm 实现(从 2006 年开始,他们现在提供了)导致了免费软件 RxTx 库的开发。RxTx 可用于许多平台,不仅限于 Linux。它可以与 JavaComm 一起使用(RxTx 提供特定于硬件的驱动程序),也可以单独使用。当用作 JavaComm 驱动程序时,JavaComm API 和 RxTx 之间的桥接由 JCL(JavaComm for Linux)完成。JCL 是 RxTx 发行版的一部分。
Sun 对 JavaComm 和 JavaComm 特定编程模型的疏忽使得 JavaComm 备受批评。RxTx(如果不用作 JavaComm 驱动程序)提供了更丰富的接口,但该接口没有标准化。RxTx 支持的平台比现有的 JavaComm 实现更多。最近,RxTx 被采用以提供与 JavaComm 相同的接口,只是包名称与 Sun 的包名称不匹配。
那么,应用程序应该使用哪个库呢?如果需要最大程度的移植性(对于“最大程度”的某种价值),那么 JavaComm 是一个不错的选择。如果某个特定平台没有可用的 JavaComm 实现,但有一个 RxTx 实现,那么 RxTx 可以用作该平台上 JavaComm 的驱动程序。因此,通过使用 JavaComm,可以支持所有平台,这些平台要么直接由 Sun 的参考实现支持,要么由 RxTx 与 JCL 支持。这样,应用程序就不需要更改,并且可以针对单个接口(标准化的 JavaComm 接口)进行工作。
本模块讨论了 JavaComm 和 RxTx。它主要侧重于演示概念,而不是提供可运行的代码。那些想要盲目复制代码的人可以参考软件包附带的示例代码。那些想要了解他们正在做什么的人可能会在本模块中找到一些有用的信息。
还应考虑 jSSC(Java 简单串行连接器)
还有一个名为 jSerialComm 的库,它在 jar 文件中包含了所有平台特定的文件,这使其真正便携,因为不需要安装。
- 了解 串行通信和编程 的基础知识。
- 准备好要与之通信的设备(例如调制解调器)的文档。
- 设置所有硬件和测试环境
- 例如,使用终端程序手动与设备通信。这样做是为了确保测试环境设置正确,并且您已经理解了设备的命令和响应。
- 下载您要用于特定操作系统的 API 实现
- 阅读
- JavaComm 和/或 RxTx 安装说明(并遵循说明)
- API 文档
- 随附的示例源代码
JavaComm 和 RxTX 都存在一些安装上的问题。强烈建议您逐字逐句地按照安装说明进行操作。如果说明中指出将 jar 文件或共享库放到特定目录,那么请认真对待!如果说明中指出特定文件或设备需要具有特定所有权或访问权限,那么也请认真对待。许多安装问题仅仅是因为没有精确地按照说明操作。
尤其需要注意的是,某些版本的 JavaComm 附带两种安装说明。一种适用于 Java 1.2 及更高版本,另一种适用于 Java 1.1。使用错误的说明会导致安装失败。另一方面,某些版本的 RxTx(包括构建和软件包)附带的说明不完整。在这种情况下,需要获取 RxTx 的相应源代码分发版,其中应该包含完整的说明。
许多 Linux 发行版在它们的存储库中提供 RxTx 软件包(ArchLinux - 'java-rxtx',Debian/Ubuntu - 'librxtx-java'),这些软件包只包含库的平台特定部分,但通常可以直接使用。
此外,还需要注意,Windows JDK 安装通常会包含多达三个 VM,因此也包含三个扩展目录。
- 一个作为 JDK 的一部分,
- 一个作为 JDK 附带的私有 JRE 的一部分,用于运行 JDK 工具,以及
- 一个作为 JDK 附带的公共 JRE 的一部分,用于运行应用程序
有些人甚至声称在 \Windows 目录层次结构中还有一个第四个 JRE。
JavaComm 至少应该作为 JDK 和所有公共 JRE 中的扩展进行安装。
JavaComm 和 RxTx 都存在一个普遍的问题,即它们无法通过 Java WebStart 安装。
JavaComm 臭名昭著,因为它需要将名为 javax.comm.properties 的文件放到 JDK lib 目录中,而这无法通过 Java WebStart 完成。这一点尤其令人沮丧,因为该文件的存在是 JavaComm 中一些不必要的設計 / 决定的结果,本可以很容易地被 JavaComm 设计人员避免。Sun 一直拒绝糾正这个错误,声称该机制是必不可少的。在 JavaComm 中,他们是在撒谎,尤其是考虑到 Java 很久以前就有了专门用于此类目的的服务提供者架构。
属性文件的内容通常只是一行,即具有本机驱动程序的 Java 类名,例如:
driver=com.sun.comm.Win32Driver
以下是一个技巧,可以忽略那个脑残的属性文件,通过 WebStart 部署 JavaComm。它存在严重的缺点,并且可能无法在更新的 JavaComm 版本中使用(如果 Sun 真的决定发布新版本)。
首先,关闭安全管理器。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 需要更改串行设备的所有权和访问权限。这同样无法通过 WebStart 完成。
在程序启动时,您可以要求用户以超级用户身份执行必要的设置。
此外,RxTx 具有一个模式匹配算法,用于识别“有效”的串行设备名称。当想要使用非标准设备(例如 USB 转串行转换器)时,这通常会导致问题。可以通过系统属性覆盖此机制。有关详细信息,请参阅 RxTx 安装说明。
与 RxTx 和 JavaComm 相比,jSerialComm 在许多操作系统和平台(例如 Windows x86/x86_64、Linux x86/x86_64、ARM 甚至 android - 具体库 jar 文件中的完整列表)上无需任何更改即可使用。但是,它仍然需要访问设备的权限(有关详细信息,请访问 jSerialComm 主页)。
SerialPundit
SerialPundit 是另一个功能丰富的库,用于在 Java 中访问串行端口。它包括以下功能:检测 FTDI232 等 USB-UART 设备何时插入系统;自动识别操作系统和 CPU 架构;无需任何安装;全面记录;经过良好测试;以及支持/讨论组。
Java 中串行通信的官方 API 是 JavaComm API。该 API 不是标准 Java 2 版本的一部分。相反,API 的实现需要单独下载。不幸的是,JavaComm 没有得到 Sun 的太多关注,并且已经很长时间没有得到真正的维护。Sun 有时会进行一些微不足道的错误修复,但没有进行早已过期的主要大修。
本节介绍 JavaComm API 的基本操作。提供的源代码保持简单,以便演示重要要点。在实际应用中需要对其进行扩展。
本章中的源代码并非唯一的可用示例代码。JavaComm 下载包附带多个示例。这些示例几乎包含比 API 文档更详细的 API 使用信息。不幸的是,Sun 没有提供任何真正的教程或介绍性文本。因此,研究示例代码以了解 API 机制是值得的。但是,也应该研究 API 文档。但最好的方法是研究示例并使用它们。由于缺乏易于使用的应用程序以及人们难以理解 API 的编程模型,因此该 API 经常受到指责。该 API 实际上比它的名声要好,而且功能齐全。但仅此而已。
该 API 使用回调机制通知程序员有新的数据到达。最好研究这种机制,而不是依赖于轮询端口。与 Java 中的其他回调接口(例如 GUI 中的接口)不同,此接口只允许一个监听器监听事件。如果多个监听器需要监听串行事件,则主监听器需要以一种方式实现,以便将信息分发到其他辅助监听器。
Sun 的 JavaComm 网页指向 下载位置。在该位置,Sun 目前(2007 年)为 Solaris/SPARC、Solaris/x86 和 Linux x86 提供 JavaComm 3.0 实现。下载需要注册 Sun 在线帐户。下载页面提供了注册页面的链接。注册的目的尚不清楚。可以无需注册下载 JDK 和 JRE,但对于几乎微不足道的 JavaComm,Sun 引用了法律和政府对软件分发和出口的限制。
JavaComm 的 Windows 版本不再正式提供,Sun - 违反了他们自己的产品生命周期终结政策 - 没有将其提供给 Java 产品档案。但是,2.0 Windows 版本(javacom 2.0)仍然可以从 这里 下载。
按照下载包附带的安装说明进行操作。某些版本的 JavaComm 2.0 附带两种安装说明。两种说明中最明显的一种不幸的是错误的,它适用于古老的 Java 1.1 环境。引用同样古老的 Java 1.2 的信息(jdk1.2.html)才是正确的。
尤其是 Windows 用户通常不知道他们在多个位置(通常是三个到四个位置)安装了相同 VM 的副本。某些 IDE 也喜欢附带自己的私有 JRE/JDK 安装,一些 Java 应用程序也是如此。需要对每个 VM 安装(JDK 和 JRE)重复安装,这些 VM 应该与串行应用程序的开发和执行一起使用。
IDE 通常有 IDE 特定的方法来让 IDE 知道新库(类和文档)。通常像 JavaComm 这样的库不仅需要让 IDE 知道它本身,还需要让每个应该使用该库的项目都知道它。请阅读 IDE 的文档。需要注意的是,旧的 JavaComm 2.0 版本附带了 JavaDoc API 文档,这些文档采用历史上的 Java 1.0 JavaDoc 布局。一些现代的 IDE 不再支持这种结构,无法将其集成到他们的帮助系统中。在这种情况下,需要使用外部浏览器阅读文档(推荐的操作...)。
安装软件后,建议检查示例和 JavaDoc 目录。构建并运行一个示例应用程序以验证安装是否正确是很有意义的。示例应用程序通常需要一些小调整才能在特定平台上运行(例如,对硬编码的串口标识符进行更改)。在尝试示例应用程序时,最好有一些串行硬件,比如电缆、空闲调制解调器、分线盒、真正的调制解调器、程控交换机等。 Serial_Programming:RS-232 Connections 和 Serial_Programming:Modems and AT Commands 提供了一些关于如何设置串行应用程序开发环境的硬件部分的信息。
查找所需的串口
[edit | edit source]使用 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();<br>
//
// 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
//
...
找到端口标识符后,就可以使用它来获取所需的端口
//
// 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.
//
...
初始化串口
[edit | edit source]串口初始化非常简单。可以单独设置通信首选项(波特率、数据位、停止位、奇偶校验),也可以使用 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();
简单数据传输
[edit | edit source]简单数据写入
[edit | edit source]写入串口与基本的 Java IO 一样简单。但是,如果您使用 AT Hayes 协议,则需要注意一些注意事项。
- 不要在 OutputStream 上使用 println(或其他自动附加 "\n" 的方法)。AT Hayes 协议要求调制解调器将 "\r\n" 作为分隔符(无论底层操作系统如何)。
- 写入 OutputStream 后,如果调制解调器设置为回显命令行,则 InputStream 缓冲区将包含发送到它的命令(带换行符)的重复,以及另一个换行符(对“AT”命令的回答)。因此,在写入操作的一部分中,请确保从 InputStream 中清除这些信息(实际上可以用于错误检测)。
- 当使用 Reader/Writer(不是一个好主意)时,至少将字符编码设置为 US-ASCII,而不是使用平台的默认编码,这可能起作用也可能不起作用。
- 由于使用调制解调器时的主要操作是无损传输数据,因此应通过 InputStream/OutputStream(而不是 Reader/Writer)来处理与调制解调器的通信。
// 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 generates as output
简单数据读取(轮询)
[edit | edit source]如果您正确执行了写入操作(见上文),那么读取操作就像一个命令一样简单
// Read the response
String response = is.readLine(); // if you sent "AT" then response == "OK"
简单读/写问题
[edit | edit source]之前部分演示的从串口读/写数据的简单方法存在严重缺陷。这两种活动都是用阻塞 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 事件通知机制,则这不是严格必要的。所以总结
活动 | 架构 |
---|---|
读取 | 使用事件通知和/或单独线程 |
写入 | 始终使用单独线程,可以选择使用事件通知 |
以下部分提供一些详细信息。
事件驱动的串行通信
[edit | edit source]简介
[edit | edit source]JavaComm API 提供了一种事件通知机制来克服阻塞 I/O带来的问题。但是,按照 Sun 的典型方式,这种机制并非没有问题。
原则上,应用程序可以向特定的SerialPort
注册事件侦听器,以便随时了解该端口发生的重大事件。对于读取和写入数据的两个最有趣的事件类型是
javax.comm.SerialPortEvent.DATA_AVAILABLE
以及javax.comm.SerialPortEvent.OUTPUT_BUFFER_EMPTY
.
但也存在两个问题。
- 每个
SerialPort
只能注册一个事件侦听器。这迫使程序员编写“巨型”侦听器,根据事件类型进行区分。 OUTPUT_BUFFER_EMPTY
是一种可选的事件类型。Sun 在文档中隐晦地指出,并非所有 JavaComm 实现都支持生成此类型的事件。
在详细介绍之前,下一部分将介绍实现和注册串行事件处理程序的主要方法。请记住,一次只能有一个处理程序,并且它必须处理所有可能的事件。
设置串行事件处理程序
[edit | edit source] 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 */
数据写入
[edit | edit source] 本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
为写入设置单独的线程
[edit | edit source] 本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
使用单独的线程进行写入只有一个目的:避免在串行端口未准备好写入时整个应用程序阻塞。
一个简单、线程安全的环形缓冲区实现
[edit | edit source]使用单独的线程进行写入,与主应用程序线程分离,意味着需要一种方法将需要写入的数据从应用程序线程传递到写入线程。一个共享的、同步的数据缓冲区,例如一个byte[]
就可以。此外,还需要一种方法让主应用程序确定它是否可以写入数据缓冲区,或者数据缓冲区当前是否已满。如果数据缓冲区已满,则可能表示串行端口未准备好,并且输出数据已排队。主应用程序将不得不轮询共享数据缓冲区中新空间的可用性。但是,在主应用程序轮询之间,它可以执行其他操作,例如更新 GUI,提供一个可以中止发送的命令提示符等。
乍一看,PipedInputStream/PipedOutputStream
对似乎很适合这种通信。但如果管道流真的有用,Sun 就不可能是 Sun 了。如果对应的PipedOutputStream
没有及时清除,PipedInputStream
会阻塞。因此应用程序线程会阻塞。这正是使用单独线程想要避免的情况。java.nio.Pipe
也存在同样的问题。它的阻塞行为依赖于平台。而且,将 JavaComm 使用的经典 I/O 适配到 NIO 并不是一项愉快的任务。
在本文中,使用了一个非常简单的同步环形缓冲区来将数据从一个线程传递到另一个线程。在实际应用中,实现可能需要更加复杂。例如,在实际应用中,对缓冲区实现 OutputStream 和 InputStream 视图是有意义的。
环形缓冲区本身没什么特别的,在多线程方面也没有什么特殊属性。只是这个简单的數據结构在这里用来提供数据缓冲。实现方式保证了对该数据结构的访问是线程安全的。
/**
* Synchronized ring buffer.
* Suitable to hand over data from one thread to another.
**/
public '''synchronized''' 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.
* <nowiki><p></nowiki>
* 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 <code>off</code>.
* @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;
}
}
有了这个环形缓冲区,现在可以以受控的方式将数据从一个线程传递到另一个线程。任何其他线程安全的、非阻塞机制也可以。这里的关键点是,写入操作在缓冲区已满时不会阻塞,在没有要读取的数据时也不会阻塞。
将缓冲区与串行事件一起使用
[edit | edit source]在写入中使用 OUTPUT_BUFFER_EMPTY 事件
[edit | edit source]参考设置串行事件处理程序一节中介绍的事件处理程序骨架,现在可以使用一个简单、线程安全的环形缓冲区实现一节中的共享环形缓冲区来支持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) {
//TODO
}
本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
读取数据
[edit | edit source] 本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
以下示例假设数据的目的地是某个文件。只要数据可用,它就会从串行端口获取并写入文件。这是一个极其简化的视图,因为在实际应用中,需要检查数据以查找文件结束指示,例如,返回到调制解调器命令模式。
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();
}
在一个应用程序中处理多个端口
[edit | edit source] 本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
调制解调器控制
[edit | edit source]JavaComm 严格关注串行接口的处理以及通过该接口传输数据。它不了解也不提供对更高级协议的支持,例如,用于控制消费级调制解调器的 Hayes 调制解调器命令。这仅仅不是 JavaComm 的工作,也不是错误。
与任何其他特定串行设备一样,如果希望通过 JavaComm 控制调制解调器,则需要在 JavaComm 之上编写必要的代码。页面"Hayes 兼容调制解调器和 AT 命令"提供了处理 Hayes 调制解调器所需的必要基本通用信息。
一些操作系统,例如 Windows 或某些 Linux 发行版,提供了一种或多或少标准化的方式,用于为特定调制解调器类型或品牌配置操作系统的调制解调器控制命令。例如,Windows 调制解调器“驱动程序”通常只是注册表项,描述了特定调制解调器(实际驱动程序是通用串行调制解调器驱动程序)。JavaComm 本身没有访问此类操作系统特定数据的规定。因此,您必须提供一个单独的仅 Java 功能,允许用户为使用特定调制解调器配置应用程序,或者需要添加一些平台特定的(本机)代码。
RxTx
[edit | edit source] 本节为存根。 您可以通过 扩展它 来帮助 Wikibooks。 |
概述和版本
[edit | edit source]由于 Sun 没有为 Linux 提供 JavaComm API 的参考实现,因此人们为 Java 和 Linux 开发了 RxTx [1]。然后,RxTx 移植到其他平台。最新版本的 RxTx 已知可在 100 多个平台上运行,包括 Linux、Windows、Mac OS、Solaris 和其他操作系统。
RxTx 可以独立于 JavaComm API 使用,也可以用作 JavaComm API 的所谓的提供程序。为了做到后者,还需要一个名为 JCL 的包装器 [2]。JCL 和 RxTx 通常与 Linux/Java 发行版一起打包,或者 JCL 完全集成到代码中。因此,在尝试单独获取它们之前,值得查看 Linux 发行版的 CD。
由于 Sun 对 JavaComm API 的支持有限,而且文档不完善,因此似乎有一种趋势是放弃 JavaComm API,而直接使用 RxTx 而不是通过 JCL 包装器使用它。但是,RxTx 的文档非常稀疏。特别是,RxTX 人员喜欢把他们的版本和包内容弄得乱七八糟(例如,是否包含集成 JCL)。从 RxTx 版本 1.5 开始,RxTx 包含公共 JavaComm 类替换类。由于法律原因,它们不在java.comm
包中,而是在gnu.io
包中。但是,当前可用的两个 RxTx 版本的打包方式不同
- RxTx 2.0
- RxTx 版本,应该用作 JavaComm 提供程序。这个版本应该起源于 RxRx 1.4,这是在添加
gnu.io
包之前的 RxTx 版本。 - RxTx 2.1
- 具有完整
gnu.io
包替换java.comm
的 RxTx 版本。该版本应该起源于 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
[edit | edit source]因此,如果您属于因 Sun 放弃对 JavaComm 的 Windows 支持而感到失望的众多用户之一,您需要将 JavaComm 应用程序转换为 RxTx。如上所述,有两种方法可以做到这一点。两者都假设您首先成功安装了 RxTx 的版本。然后,您可以选择:
- 使用 RxTx 2.0 作为 JavaComm 提供程序
- 将应用程序移植到 RxTx 2.1
第一个选项已在前面解释过。第二个选项出奇地简单。要将某个应用程序从使用 JavaComm 移植到使用 RxTx 2.1,只需将应用程序源代码中所有对 `java.comm` 的引用替换为对 `gnu.io` 的引用。如果原始 JavaComm 应用程序编写正确,就无需再做其他操作了。
RxTx 2.1 甚至提供了 `contrib/ChangePackage.sh` 工具来执行 Unix 环境下源代码树的全局替换。在其他平台上,使用支持良好重构功能的 IDE 可以轻松实现全局替换。
- jSerialComm 主页 在 github 上 - 包含有关如何使用它的信息 - Javadoc
- Sun Java 通信 API
- Linux 上的 Java Comm 串口 API 操作指南
- jSSC - java 串口库。可在 Win32(Win98-Win7)、Win64(x86-64)、Linux x86、Linux x86-64 上运行
- RxTx 主页(文件只能通过 FTP 访问(2017 年 5 月))
- jRxTx 在 github 上 - RxTx 的一个新包装器,提供了一个“与 RXTX 相比,新的改进的 API”
- 非官方 Java Web Start/JNLP 常见问题解答 - 如何将 Web Start 和 Comm API 结合使用?
- SerialIO 提供其 SerialPort 软件包的免费试用版
- Ben Resner 提供了 其 SimpleSerial 软件包的免费下载 以及 一个没有 C++ 代码的更新版本
- gurux.serial.java 是一个易于使用的适用于 Windows 和 Linux 的开源串口库
串行编程: 简介和 OSI 网络模型 -- RS-232 接线和连接 -- 典型的 RS232 硬件配置 -- 8250 UART -- DOS -- MAX232 驱动器/接收器系列 -- Windows 中的 TAPI 通信 -- Linux 和 Unix -- Java -- Hayes 兼容调制解调器和 AT 命令 -- 通用串行总线 (USB) -- 形成数据包 -- 错误纠正方法 -- 双向通信 -- 数据包恢复方法 -- 串行数据网络 -- 实际应用开发 -- 串行连接上的 IP