首頁  >  文章  >  Java  >  詳細解讀Java的串口編程

詳細解讀Java的串口編程

高洛峰
高洛峰原創
2016-12-19 16:52:361634瀏覽

常見問題

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.

Clipboard
 

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 generates as output

   

简单的数据读取(轮询)

如果你正确的使用了写操作(如上所述),读操作只需简单的一条命令。

// 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替换包,那么你只需要将一个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(整合開發環境)來完成。



更多詳細解讀Java的串口程式設計相關文章請關注PHP中文網!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn