跳轉至內容

序列埠程式設計/Java序列埠

來自 Wikibooks,開放世界中的開放書籍

使用Java進行序列埠通訊

[編輯 | 編輯原始碼]

由於Java的平臺無關性,序列埠介面很困難。序列埠介面需要一個標準化的API,以及平臺特定的實現,這對Java來說很難。

不幸的是,Sun 在Java中並沒有太關注序列埠通訊。Sun 定義了一個序列埠通訊API,稱為 JavaComm,但該API的實現不是Java標準版的一部分。Sun 為少數幾個,但不是所有Java平臺提供了參考實現。特別是,在2005年底,Sun 悄然撤回了對JavaComm對Windows的支援。一些遺漏平臺的第三方實現是可用的。JavaComm 在維護方面並沒有得到太多關注,Sun 只進行了最基本的維護,除了 Sun 似乎響應了其 Sun Ray 瘦客戶端買家的壓力,並將JavaComm 適應了這個平臺,同時放棄了對Windows的支援。

這種情況,以及 Sun 最初沒有為 Linux 提供JavaComm實現的事實(從2006年開始,他們現在有了),導致了免費軟體 RxTx 庫的開發。RxTx 可用於多個平臺,而不僅僅是 Linux。它可以與JavaComm結合使用(RxTx 提供特定於硬體的驅動程式),也可以單獨使用。當用作JavaComm驅動程式時,JavaComm API和RxTx之間的橋接由JCLJavaComm for Linux)完成。JCLRxTx分發的一部分。

Sun 對JavaComm的忽視以及JavaComm的特定程式設計模型,使JavaComm獲得了難以使用的聲譽。RxTx - 如果不用作JavaComm驅動程式 - 提供了一個更豐富的介面,但不是標準化的。RxTx 支援的平臺比現有的JavaComm實現更多。最近,RxTx 已經被採用,以提供與JavaComm相同的介面,只是包名不匹配 Sun 的包名。

那麼,在應用程式中應該使用哪個庫呢?如果需要最大的可移植性(對於“最大”的某些值而言),那麼JavaComm 是一個不錯的選擇。如果某個特定平臺沒有可用的JavaComm實現,但有RxTx實現,那麼RxTx 可以用作該平臺上JavaComm的驅動程式。因此,透過使用 JavaComm,可以支援所有直接由 Sun 的參考實現或由 RxTx 與 JCL 支援的平臺。這樣,應用程式就不需要更改,並且可以針對單個介面,即標準化的JavaComm介面進行工作。

本模組討論了JavaCommRxTx。它主要側重於演示概念,而不是提供可執行的程式碼。那些想要盲目複製程式碼的人可以參考包附帶的示例程式碼。那些想知道他們在做什麼的人可能在這個模組中找到一些有用的資訊。

jSSC (Java Simple Serial Connector) 也應該被考慮

還有一個名為jSerialComm t的庫,它將其所有平臺特定檔案包含在它的 jar 中,這使其真正可移植,因為不需要安裝。

  • 學習 序列埠通訊和程式設計 的基礎知識。
  • 準備好要與其通訊的裝置(例如,調變解調器)的文件。
  • 設定所有硬體和測試環境
  • 例如,使用終端程式手動與裝置通訊。這樣做是為了確保測試環境設定正確,並且你已經理解了裝置的命令和響應。
  • 下載要在特定作業系統上使用的 API 實現
  • 閱讀
    • JavaComm 和/或 RxTx 安裝說明(並按照說明進行操作)
    • API 文件
    • 附帶的示例原始碼

一般問題

[編輯 | 編輯原始碼]

JavaCommRxTX 都有一些安裝怪癖。強烈建議逐字逐句地遵循安裝說明。如果說明說 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 中。

對於 JavaCommRxTx 來說,一個普遍的問題是,它們抵抗透過 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 真的要釋出新版本的話。

首先,關閉安全管理器。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-to-serial 轉換器)時,這通常會導致問題。可以透過系統屬性覆蓋此機制。有關詳細資訊,請參見 RxTx 安裝說明。

jSerialComm
[編輯 | 編輯原始碼]

與 RxTx 和 JavaComm 相比,jSerialComm 在許多作業系統和平臺(例如 Windows x86/x86_64、Linux x86/x86_64、ARM 甚至 Android - 特定庫 jar 中的完整列表)上無需任何更改即可使用。但是它仍然需要訪問裝置的許可權(有關更多資訊,請參見 jSerialComm 主頁)。

SerialPundit

SerialPundit 是另一個功能豐富的庫,用於在 Java 中訪問序列埠。它包括諸如檢測何時將 FTDI232 等 USB-UART 裝置插入系統、自動識別作業系統和 CPU 架構、無需任何安裝、全面記錄、經過良好測試以及提供支援/討論組等功能。

JavaComm API

[編輯 | 編輯原始碼]

Java 中用於序列通訊的官方 API 是 JavaComm API。此 API 不是標準 Java 2 版本的一部分。相反,必須單獨下載 API 的實現。不幸的是,JavaComm 沒有得到 Sun 的太多關注,並且很長時間沒有得到維護。Sun 偶爾會進行一些微不足道的錯誤修復,但沒有進行長期需要的重大改進。

本節解釋了 JavaComm API 的基本操作。提供的原始碼保持簡單,以演示重要要點。在真實應用程式中使用時,需要對其進行增強。

本章中的原始碼不是唯一的示例程式碼。JavaComm 下載附帶幾個示例。這些示例包含的資訊幾乎比 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)重複安裝。

IDE 通常有特定於 IDE 的方式來讓 IDE 瞭解新的庫(類和文件)。通常,像 JavaComm 這樣的庫不僅需要讓 IDE 瞭解它本身,還需要讓每個打算使用該庫的專案都瞭解它。請閱讀 IDE 的文件。需要注意的是,舊版的 JavaComm 2.0 附帶了 JavaDoc API 文件,其結構採用的是歷史上的 Java 1.0 JavaDoc 佈局。一些現代 IDE 已經不再瞭解這種結構,無法將其整合到其幫助系統中。在這種情況下,需要使用外部瀏覽器來閱讀文件(推薦的操作...)。

安裝軟體後,建議檢查樣本和 JavaDoc 目錄。構建並執行一個樣本應用程式以驗證安裝是否正確是很有意義的。樣本應用程式通常需要進行一些小的調整才能在特定平臺上執行(例如,更改硬編碼的序列埠識別符號)。在嘗試執行樣本應用程式時,最好有一些序列硬體可用,比如電纜、零調變解調器、序列埠轉換器、真正的調變解調器、程控交換機等。 Serial_Programming:RS-232 ConnectionsSerial_Programming:Modems and AT Commands 提供了一些有關如何設定序列應用程式開發環境硬體部分的資訊。

查詢所需的序列埠

[編輯 | 編輯原始碼]

使用 JavaComm 對序列線路進行程式設計時,通常需要做的三件事是

  1. 列舉 JavaComm 可用的所有序列埠(埠識別符號),
  2. 從可用埠識別符號中選擇所需的埠識別符號,以及
  3. 透過埠識別符號獲取埠。

列舉和選擇所需的埠識別符號通常在一個迴圈中完成

 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
 //
 ...

注意
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 協議,則需要注意一些注意事項

  1. 不要在 OutputStream 上使用 println(或自動追加 "\n" 的其他方法)。調變解調器 AT Hayes 協議期望 "\r\n" 作為分隔符(無論底層作業系統是什麼)。
  2. 在寫入 OutputStream 後,如果調變解調器設定為回顯命令列,則 InputStream 緩衝區將包含傳送給它的命令的重複(帶換行符),以及另一個換行符(對“AT”命令的答覆)。因此,在寫入操作過程中,請確保從 InputStream 中清除這些資訊(實際上可以用於錯誤檢測)。
  3. 如果使用 Reader/Writer(不是一個好主意),至少將字元編碼設定為 US-ASCII,而不是使用平臺的預設編碼,該編碼可能有效也可能無效。
  4. 由於使用調變解調器時的主要操作是不經修改地傳輸資料,因此應透過 InputStream/OutputStream 處理與調變解調器的通訊,而不是使用 Reader/Writer。


Clipboard

待辦事項

  • 解釋如何在同一流上混合二進位制和字元 I/O
  • 修復示例以使用流


 // 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 系統(InputStreamOutputStream),但有所不同,如下文所述。

提到的“不同”之處在於,JavaComm 透過事件通知機制提供了一些對非同步 I/O 的有限支援。但是,Java 中在阻塞 I/O 系統之上實現非阻塞 I/O 的一般解決方案是使用執行緒。實際上,這對於序列寫入是可行的解決方案,強烈建議使用單獨的執行緒寫入序列埠 - 即使使用事件通知機制,如下文所述。

讀取也可以在單獨的執行緒中處理。但是,如果使用 JavaComm 事件通知機制,則沒有必要這樣做。因此,總結

活動 架構
讀取 使用事件通知和/或單獨執行緒
寫入 始終使用單獨執行緒,可以選擇使用事件通知

以下幾節將提供一些詳細資訊。

事件驅動的序列通訊

[編輯 | 編輯原始碼]

JavaComm API 提供了事件通知機制,以克服阻塞 I/O 帶來的問題。但是,以典型的 Sun 方式,這種機制並非沒有問題。

原則上,應用程式可以向特定的 SerialPort 註冊事件偵聽器,以便及時瞭解該埠上發生的重大事件。用於讀取和寫入資料的兩個最有趣的事件型別是

  • javax.comm.SerialPortEvent.DATA_AVAILABLE
  • javax.comm.SerialPortEvent.OUTPUT_BUFFER_EMPTY.

但也有兩個問題

  1. 每個 SerialPort 只能註冊一個事件偵聽器。這迫使程式設計師編寫“怪物”偵聽器,根據事件型別進行區分。
  2. 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 */

資料寫入

[編輯 | 編輯原始碼]
Clipboard

待辦事項


為寫入設定獨立執行緒
[編輯 | 編輯原始碼]

使用獨立執行緒進行寫入只有一個目的:避免當序列埠無法寫入時,整個應用程式阻塞。

一個簡單的、執行緒安全的環形緩衝區實現
[編輯 | 編輯原始碼]

使用獨立執行緒進行寫入,獨立於某些主應用程式執行緒,意味著需要一種方法將需要寫入的資料從應用程式執行緒傳遞到寫入執行緒。一個共享的、同步的資料緩衝區,例如 byte[],就可以實現。此外,還需要一種方法讓主應用程式確定它是否可以寫入資料緩衝區,或者資料緩衝區當前是否已滿。如果資料緩衝區已滿,則可能表明序列埠未準備好,輸出資料已排隊。主應用程式將必須輪詢共享資料緩衝區中新空間的可用性。然而,在主應用程式輪詢之間,它可以執行其他操作,例如更新 GUI、提供具有中止傳送功能的命令提示符等。

乍一看,PipedInputStream/PipedOutputStream 對似乎是這種通訊的不錯選擇。但是,如果管道流真正有用,Sun 就不應該是 Sun 了。PipedInputStream 阻塞,如果對應的 PipedOutputStream 未及時清除。因此,應用程式執行緒將被阻塞。這正是透過使用獨立執行緒想要避免的事情。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;
     }
 }

使用這個環形緩衝區,可以以受控的方式將資料從一個執行緒傳遞到另一個執行緒。任何其他執行緒安全、非阻塞機制也可以實現。這裡關鍵點在於,當緩衝區已滿時,寫入不會阻塞,當沒有內容可讀時,寫入也不會阻塞。

將緩衝區與序列事件一起使用
[編輯 | 編輯原始碼]
在寫入中使用 OUTPUT_BUFFER_EMPTY 事件
[編輯 | 編輯原始碼]

參考 設定序列事件處理程式 一節中介紹的事件處理程式框架,現在可以使用 一個簡單的、執行緒安全的環形緩衝區實現 一節中的共享環形緩衝區來支援 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
        
    }

資料讀取

[編輯 | 編輯原始碼]

以下示例假設資料的目標是某個檔案。每當資料可用時,就會從序列埠獲取資料並寫入檔案。這是一種極其簡化的檢視,因為在現實中,需要檢查資料以獲取檔案結束指示,例如,返回到調變解調器命令模式。

 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 的工作,也不是錯誤。

與任何其他特定序列裝置一樣,如果希望透過 JavaComm 控制調變解調器,則需要在 JavaComm 之上編寫必要的程式碼。頁面 “相容 Hayes 的調變解調器和 AT 命令” 提供了處理 Hayes 調變解調器所需的必要基本通用資訊。

某些作業系統,例如 Windows 或某些 Linux 發行版,提供了一種或多或少標準化的方法,用於為作業系統配置特定調變解調器型別或品牌的調變解調器控制命令。例如,Windows 調變解調器“驅動程式”通常只是登錄檔項,描述了特定調變解調器(實際驅動程式是通用的序列調變解調器驅動程式)。JavaComm 本身沒有訪問此類作業系統特定資料的規定。因此,您要麼需要提供一個獨立的僅限 Java 的工具來允許使用者為使用特定調變解調器配置應用程式,要麼需要新增一些平臺特定(原生)程式碼。

概述和版本

[編輯 | 編輯原始碼]

由於 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
RxTx 版本,帶有完整的 gnu.io 包,用於替換 java.comm。該版本應該起源於 RxTx 1.5,這是 gnu.io 支援開始的地方。

因此,如果要針對原始 JavaComm API 進行程式設計,則需要

  1. Sun 的通用 JavaComm 版本。截至撰寫本文時,這實際上是 Unix 軟體包(它包含對各種 Unix 版本的支援,如 Linux 或 Solaris)。即使在 Windows 上使用,也需要 Unix 軟體包來提供通用的 java.comm 實現。只有 Java 中實現的部分被使用,而 Unix 原生庫被忽略。
  2. 為了使用不同於 JavaComm 包提供的通用 JavaComm 版本的提供者,需要使用 RxTx 2.0。

但是,如果只是想針對 `gnu.io` 替換包進行程式設計,那麼

  • 只需要 RxTx 2.1。

將 JavaComm 應用程式轉換為 RxTx

[編輯 | 編輯原始碼]

因此,如果您屬於 Sun 停止對 JavaComm 的 Windows 支援後感到失望的大量人群,則需要將 JavaComm 應用程式轉換為 RxTx。如上所述,有兩種方法可以做到。兩者都假設您設法首先安裝了 RxTx 版本。然後,選項是:

  1. 使用 RxTx 2.0 作為 JavaComm 提供者
  2. 將應用程式移植到 RxTx 2.1

第一個選項已經解釋過。第二個選項出奇地簡單。要將某個應用程式從使用 JavaComm 移植到使用 RxTx 2.1,只需將應用程式原始碼中所有對 `java.comm` 的引用替換為對 `gnu.io` 的引用。如果原始 JavaComm 應用程式編寫得當,就無需再做任何操作。

RxTx 2.1 甚至提供了 `contrib/ChangePackage.sh` 工具來對 Unix 下的原始碼樹執行全域性替換。在其他平臺上,使用支援一組完善的重構功能的 IDE 可以輕鬆執行全域性替換。

另請參閱

[編輯 | 編輯原始碼]
華夏公益教科書