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 の下にあるわけではありませんが)

Arduino と LED の配線図

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 のようにバージョン番号が付きます。

jSerialComm のダウンロード

次に IntelliJ の File メニューの Project Structure... を選択します。

このサイトでは Mac OS を利用しています。Windows でも同様のメニューがありますので、適宜読み替えてください。

Modules を選択して Dependencies にダウンロードした 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 の Run ボタンをクリックして、プログラムを実行します。

macOS 上で実行すると Arduino のシリアルポート名は cu.usbmodem142401 のような名前になります。

ボーレートは 9600 のまま、Open をクリックすると、Arduino とのシリアルポートがオープンします。 ONOFF をクリックして LED がついたり消えたりすれば成功です。

Windows での動作確認

JAR ファイルを作成して、コマンドプロンプト (Windows の場合) から実行します。

jar を実行するので java -jar JAR ファイルのパス を指定する他、 JDK 11 を使っているので --add-modules--module-path を指定します。

このビデオのように ON を押した時に LED が点灯し、OFF を押した時に LED が消灯すれば OK です。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Java 入門