jSerialComm を用いた簡単なプログラム 〜 Arduino と接続して LED の点灯消灯
ここでは jSerialComm ライブラリを利用してシリアル通信を行う簡単なプログラムを作り、 シリアルポートを選んで、オープンして、使い終わったらクローズするといった、基本的な操作を行う方法をみていきましょう。
シリアル通信する対象は Arduino として、Java プログラムから LED の点灯と消灯ができるか試してみましょう。
1. jSerialComm とは?
jSerialComm はクロスプラットフォームで利用可能なシリアル通信用のライブラリです。2019年現在 Windows 7 以降、 macOS X 10.4 以降及び Linux 等で利用可能です。
ライセンスは LGPL で、誰でもフリーで利用できます。
2. jSerialComm で Arduino とシリアル通信してみる
それでは早速、プログラムを作っていきましょう。
今回作るプログラムは JavaFX の GUI プログラムです。Arduino とシリアル通信で接続します。 画面上の ON ボタンを押すと Arduino に接続された LED が点灯、OFF ボタンを押すと消灯するようにします。
Arduino というのは AVR マイクロコントローラを備えた、シングルボードマイクロコントローラです。 Arduino にはいくつかバリエーションがありますが、今回使うのは Arduino UNO Rev 3 というタイプの Arduino で USB ケーブルで PC と接続できシリアル通信できます。
その他 Arduino については、「Arduino 入門」をみてください。
2-1. Arduino に接続した LED の ON/OFF
今回は Arduino の 10 番のデジタルピンから 470 Ω の抵抗を介して青色の LED を接続します。 この 10 番ピンを HIGH/LOW を切り替えることによって、LED を点灯したり消灯したりします。
絵で描くと次のようになります。(概念図であって必ずしも GND が D10 の下にあるわけではありませんが)
LED についての補足説明は「LED を点灯させる」をみてください。
Arduino 側のコードは次のようにします。
const int PIN_LED = 10; int incomingByte = 0; void setup() { pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, LOW); Serial.begin(9600); } void loop() { if (Serial.available() > 0 ) { incomingByte = Serial.read(); if( incomingByte == 97 ){ digitalWrite(PIN_LED, HIGH); } else if( incomingByte == 122 ){ digitalWrite(PIN_LED, LOW); } } }
1バイト受け取り、それが10進数にして 97 なら D10 番ピンを HIGH にして、 122 なら D10 番ピンを LOW にします。
ちなみに 97 とか 122 というは、それぞれ文字 'a' と 'z' の ASCII コードです。 ですから文字で言えば Arduino が 'a' を受け取ったら LED を点灯し、 'z' を受け取ったら消灯することと同じです。
Arduino の IDE はフリーでダウンロードできます。Arduino IDE を使って、 上のコードをコンパイルして、Arduino にアップロードすれば Arduino 側は準備完了です。
2-2. jSerialComm の利用設定
さて、本題の Java 側のシリアル通信のプログラミングに戻りましょう。
Java の IDE は JetBrains 社の IntelliJ IDE (2019.2) で、JDK 11 (Amazon Corretto) を利用しています。また、 JavaFX は Gluon です。
環境については特にこの組み合わせが必要なわけではありません。ただ、念のため同様の環境で試す場合のため、それぞれ参考となるページを紹介しておきます。
まず、IntelliJ で JavaFX プログラムのテンプレートで新規プロジェクトを作成し、JavaFX を JDK11 で動かすための基本的な設定をします。
IntelliJ で JavaFX 11 アプリケーションを開発するための設定
次に jSerialComm.jar をダウンロードして、プロジェクトに取り込みましょう。本家サイトから jSerialComm の JAR ファイルをダウンロードします。ファイル名は jSerialComm-2.5.2.jar のようにバージョン番号が付きます。
次に IntelliJ の
メニューの を選択します。このサイトでは Mac OS を利用しています。Windows でも同様のメニューがありますので、適宜読み替えてください。
を選択して にダウンロードした JAR ファイルを追加してください。
これで jSerialComm を利用した JavaFX プログラムを開発する準備ができました。
2-3. JavaFX を使ってプログラムを作成する
IntelliJ で JavaFX テンプレートでプロジェクトを作成すると、sample パッケージ内に Main.java と Controller.java と sample.fxml が作成されます。
このうちパッケージ名と fxml ファイル名を myapp に変更しました。その上で、それぞれの3つのファイルは次のようになります。
Main.javaは次の通りです。JavaFX の典型的なスタートアップコードです。
プライマリステージの OnHidden のイベントハンドラで、コントローラのクリーンアップメソッド cleanup() を呼ぶことにしました。
package myapp; import javafx.application.Application; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; import javafx.stage.WindowEvent; import javafx.fxml.FXMLLoader; public class Main extends Application { @Override public void start(Stage primaryStage) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("myapp.fxml")); BorderPane root = (BorderPane) loader.load(); Controller controller = loader.getController(); Scene scene = new Scene(root, 450, 350); primaryStage.setScene(scene); primaryStage.setResizable(false); primaryStage.setTitle("Serial Port Test"); primaryStage.setOnHidden(new EventHandler<WindowEvent>() { @Override public void handle(WindowEvent event) { controller.cleanup(); } }); primaryStage.show(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { launch(args); } }
Controller.javaは次の通りです。ここが jSerialComm を使う場所です。
package myapp; import com.fazecast.jSerialComm.SerialPort; import com.fazecast.jSerialComm.SerialPortDataListener; import com.fazecast.jSerialComm.SerialPortEvent; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; public class Controller { enum UIState { PORT_OPEN, PORT_CLOSE } SerialPort serialPort; boolean portFound = false; Integer[] baudRateOptions = {9600, 14400, 19200, 28800, 38400, 57600, 115200}; @FXML private Button turnOnButton; @FXML private Button turnOffButton; @FXML private ChoiceBox<String> portChoiceBox; @FXML private ChoiceBox<String> baudChoiceBox; @FXML private Button openButton; @FXML private Button refreshButton; @FXML private Button closeButton; @FXML void initialize() { updatePortChoiceBoxe(); // ボーレート ChoiceBox for (int b : baudRateOptions) { baudChoiceBox.getItems().add(Integer.toString(b)); } updateUI(UIState.PORT_CLOSE); } @FXML void handleCloseButtonAction(ActionEvent event) { closeSerialPort(); } @FXML void handleOpenButtonAction(ActionEvent event) { if (!portFound || serialPort != null) { return; } // ポート名 String portName = portChoiceBox.getValue(); for (SerialPort sp : SerialPort.getCommPorts()) { if (sp.getSystemPortName().equals(portName)) { serialPort = sp; break; } } // ボーレート int baudRate = Integer.parseInt(baudChoiceBox.getValue()); System.out.println("Opening " + serialPort.getSystemPortName() + " Baud Rage " + baudRate); serialPort.setBaudRate(baudRate); serialPort.addDataListener(new SerialPortDataListener() { @Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; } @Override public void serialEvent(SerialPortEvent event) { try { int evt = event.getEventType(); System.out.println("Event " + evt + " received"); if (evt == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) { int bytesToRead = serialPort.bytesAvailable(); if (bytesToRead == -1) { System.out.println("-1 means port is closed."); Platform.runLater(new Runnable() { @Override public void run() { closeSerialPort(); updatePortChoiceBoxe(); } }); return; } System.out.println(bytesToRead + " byte(s) available."); byte[] newData = new byte[bytesToRead]; serialPort.readBytes(newData, bytesToRead); String s = new String(newData, "UTF8"); System.out.println("Received [" + s + "]"); } } catch (Exception ex) { System.err.println(ex.getMessage()); closeSerialPort(); } } }); if (!serialPort.openPort()) { showAlertMessage("Unable to open the port.", "Error", AlertType.ERROR); return; } updateUI(UIState.PORT_OPEN); } @FXML void handleRefreshButtonAction(ActionEvent event) { updatePortChoiceBoxe(); } @FXML void handleTurnOffButtonAction(ActionEvent event) { System.out.println("Turn Off clicked"); sendByte((byte) 122); } @FXML void handleTurnOnButtonAction(ActionEvent event) { System.out.println("Turn ON clicked"); sendByte((byte) 97); } void cleanup() { closeSerialPort(); } void closeSerialPort() { if (serialPort != null) { if (serialPort.isOpen()) { serialPort.closePort(); } serialPort = null; } updateUI(UIState.PORT_CLOSE); } void sendByte(byte b) { byte[] buff = new byte[1]; buff[0] = b; serialPort.writeBytes(buff, 1); } void showAlertMessage(String message, String title, AlertType alertType) { Alert alert = new Alert(alertType); alert.setTitle(title); alert.setContentText(message); alert.showAndWait(); } void updatePortChoiceBoxe() { if (serialPort != null) { return; } portFound = false; portChoiceBox.getItems().clear(); for (SerialPort sp : SerialPort.getCommPorts()) { portChoiceBox.getItems().add(sp.getSystemPortName()); portFound = true; } // UI コンポーネントの更新 openButton.setDisable(!(portFound)); closeButton.setDisable(true); if (portFound) { portChoiceBox.setValue(SerialPort.getCommPorts()[0].getSystemPortName()); baudChoiceBox.setValue(Integer.toString(baudRateOptions[0])); } } void updateUI(UIState uiState) { boolean isOpen = (uiState == UIState.PORT_OPEN); turnOffButton.setDisable(!isOpen); turnOnButton.setDisable(!isOpen); closeButton.setDisable(!isOpen); openButton.setDisable(isOpen); portChoiceBox.setDisable(isOpen); baudChoiceBox.setDisable(isOpen); refreshButton.setDisable(isOpen); } }
やりたいことは、はじめの Arduino のコードと整合するように、ON ボタンを押した時にシリアル通信で接続された Arduino に対して文字 'a' (ASCII コードで 97) を、 OFF ボタンを押した時に 'z' (ASCII コードで 122) をそれぞれ送ることだけです。
ボーレートは Arduino 側で 9600 とハードコードしているので、わざわざユーザーに選択させなくても良いのですが、 このプログラムを土台として他にも拡張できるように考え、選択可能にしています。使うときはボーレードは 9600 を選択してください。
シリアルポート serialPort のデータリスナーにイベントハンドラをセットしていますが、 シリアルイベントのデータ利用可能タイプのイベントでは、ポートの利用可能バイトが -1 となることで、予期せずシリアルポートが閉じられたことを検出できます。
今回は Arduino 側からは何も送信していませんが、受け取ったデータを標準出力に出力するようにもしています。これも雛形としての役目のためです。
FXML による画面のレイアウト
最後に myapp.fxml です。こちらは次のような画面が実現できるようにしているだけです。 ID の設定やイベントハンドラなど、Controller.java と @FXML で繋がる部分だけ注意してください。
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.Insets?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.ChoiceBox?> <?import javafx.scene.control.Label?> <?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.RowConstraints?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.text.Font?> <BorderPane prefHeight="191.0" prefWidth="405.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myapp.Controller"> <center> <VBox alignment="CENTER" BorderPane.alignment="CENTER"> <children> <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0"> <children> <Button fx:id="turnOnButton" mnemonicParsing="false" onAction="#handleTurnOnButtonAction" text="ON"> <HBox.margin> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> </HBox.margin> <font> <Font size="48.0"/> </font> </Button> <Button fx:id="turnOffButton" mnemonicParsing="false" onAction="#handleTurnOffButtonAction" text="OFF"> <HBox.margin> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> </HBox.margin> <font> <Font size="48.0"/> </font> </Button> </children> </HBox> </children> </VBox> </center> <bottom> <VBox prefHeight="97.0" prefWidth="405.0" BorderPane.alignment="CENTER"> <children> <GridPane prefHeight="124.0" prefWidth="405.0"> <columnConstraints> <ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="196.0" minWidth="10.0" prefWidth="66.0"/> <ColumnConstraints hgrow="SOMETIMES" maxWidth="345.0" minWidth="10.0" prefWidth="339.0"/> </columnConstraints> <rowConstraints> <RowConstraints minHeight="30.0" prefHeight="30.0" vgrow="SOMETIMES"/> <RowConstraints minHeight="30.0" prefHeight="30.0" vgrow="SOMETIMES"/> <RowConstraints minHeight="30.0" prefHeight="30.0" vgrow="SOMETIMES"/> <RowConstraints minHeight="30.0" prefHeight="30.0" vgrow="SOMETIMES"/> </rowConstraints> <children> <HBox GridPane.columnIndex="1"> <children> <ChoiceBox fx:id="portChoiceBox" prefHeight="24.0" prefWidth="208.0"/> <Button fx:id="refreshButton" mnemonicParsing="false" onAction="#handleRefreshButtonAction" text="Refresh"> <HBox.margin> <Insets left="5.0"/> </HBox.margin> </Button> </children> <padding> <Insets bottom="10.0" top="10.0"/> </padding> </HBox> <Label text="Port"> <GridPane.margin> <Insets right="10.0"/> </GridPane.margin> </Label> <Label text="Baud" GridPane.rowIndex="1"> <GridPane.margin> <Insets right="10.0"/> </GridPane.margin> </Label> <ChoiceBox fx:id="baudChoiceBox" prefWidth="150.0" GridPane.columnIndex="1" GridPane.rowIndex="1"/> <HBox GridPane.columnIndex="1" GridPane.rowIndex="2"> <children> <Button fx:id="openButton" mnemonicParsing="false" onAction="#handleOpenButtonAction" text="Open"> <HBox.margin> <Insets right="10.0"/> </HBox.margin> </Button> <Button fx:id="closeButton" mnemonicParsing="false" onAction="#handleCloseButtonAction" text="Close"/> </children> <padding> <Insets top="10.0"/> </padding> </HBox> </children> </GridPane> </children> </VBox> </bottom> </BorderPane>
以上が揃えばプログラムがビルドできるはずです。
3. Mac と Windows で動作確認
IntelliJ の
ボタンをクリックして、プログラムを実行します。macOS 上で実行すると Arduino のシリアルポート名は cu.usbmodem142401 のような名前になります。
ボーレートは 9600 のまま、 をクリックすると、Arduino とのシリアルポートがオープンします。 、 をクリックして LED がついたり消えたりすれば成功です。
Windows での動作確認
JAR ファイルを作成して、コマンドプロンプト (Windows の場合) から実行します。
jar を実行するので java -jar JAR ファイルのパス を指定する他、 JDK 11 を使っているので --add-modules と --module-path を指定します。
このビデオのように ON を押した時に LED が点灯し、OFF を押した時に LED が消灯すれば OK です。