ロータリーエンコーダのデータをシリアル通信で取得する方法について

# はじめに

はじめまして。とある大学でUnityを使用した福祉関係の研究をしている学部4年生です。自分の備忘録的にまとめたものが誰かの役に少しでも立てばと思い、今回初めて投稿いたします。つたない文章になると思いますが、ご覧いただけると幸いです！

# 目的

Arduinoに接続したロータリーエンコーダをコントローラとして使用し、回転数をプレイヤーの移動に反映させることが今回の最終目的ですが、今回は試験として、ArduinoとPythonを使用して入力データのCSV出力まで行いたいと思います。

# エンコーダ

今回は、Arduino互換のスターターキットに入っていたKY-040というロータリーエンコーダを使用します。

## エンコーダの仕様

動作電圧：5V

パルスサークル：20

GND：接地端子

"+"：5V電源

CLK：エンコーダピンA

DT：エンコーダーピンB

SW：Selectスイッチ(シャフトを押し込むと動作)

# ソースコード

色々な参考サイトに転がっているロータリーエンコーダの値読み取りを行っているスクリプトを試してみたところ、ゆっくりだと正しく動作をするが、少し早めにエンコーダを回してみると値が変更されなかったり、うまくいかないことが多かったのですが、原因はシリアル通信とエンコーダの値を毎回読み取る処理の重たさが原因であったと考えられます。

今回は、丸ちゃんの技術ノート様(https://maruchan-note.com/2022/07/01/20220701/)
を参考にさせてもらいながらソースコードを記述しました。

下記のように割り込み処理で値の変更をするattachInterrupt関数を使用したエンコーダの値の取得するスクリプトに変更したところ、値が飛んだりすることなく正確に取得することができるようになりました。

```arduino
const int pinA = 3;       // KY-040のCLKにつなぐ(割り込みPin)
const int pinB = 4;       // KY-040のDTにつなぐ
int encoderPosCount = 0;  //カウント数

unsigned long time = 0;

void setup() {
  pinMode(pinA, INPUT);
  pinMode(pinB, INPUT);
  attachInterrupt(digitalPinToInterrupt(pinA), pulseCounter, CHANGE);  // pinAの値が変化したときに割り込み処理を実行する
  Serial.begin(9600);
}

void loop() {
  time = millis();
  Serial.print(time);
  Serial.print(" , ");
  Serial.println(encoderPosCount);
}

void pulseCounter() {
  if (digitalRead(pinA) ^ digitalRead(pinB))  //排他的論理和
  {
    encoderPosCount++;  // カウンターを加算
  } else {
    encoderPosCount--;  // カウンターを減算
  }
}
```

# データの取得

データはシリアル通信経由でプログラム開始時からの時間[ミリ秒]とカウンタの値を下記のような形で出力し、その値をpython経由でCSVファイルに書き込む処理を行っています。

```arduino
Serial.print(time);
Serial.print(" , ");
Serial.println(encoderPosCount);
//シリアルモニタには " 0 , 0 " の形で表示されています
```

## Pythonスクリプトの実装の流れ

### モジュールのインストール

まずは必要なモジュールをインストールしましょう。

- pyserial：シリアル通信用のモジュール
- csv：CSV形式のファイルを扱うためのモジュール(Python標準で入っているのでインストール不要です)

```python

import serial
import csv

try:
    # Create a CSV file
    csv_file = open('data.csv', 'w', newline='')
    print("CSV file created")
    csv_writer = csv.writer(csv_file)

    # Open the serial port
    serial_port = 'COM3'
    ser = serial.Serial(serial_port, 9600)
    print(f"Serial port {serial_port} opened")

    # Read data from the serial port and write it to the CSV file
    while True:

        data = ser.readline().decode().strip()  # Read a line of data from the serial port
        if data:
            print(f"Data read from serial port: {data}")
            csv_writer.writerow([data])
            print(f"Data written to CSV: {data}")
            csv_file.flush()  # Flush the CSV file to write the data to disk

except serial.SerialException as e:
    print(f"Error opening serial port: {e}")
except Exception as e:
    print(f"An error occurred: {e}")

finally:
    # Close the CSV file and serial port
    if 'csv_file' in locals():
        csv_file.close()
        print("CSV file closed")
    if 'ser' in locals():
        ser.close()
        print("Serial port closed")
```

## **コードの説明**

1. **モジュールのインポート**:
    
    ```python
    import serial
    import csv
    ```
    
    `serial`モジュールと`csv`モジュールをインポートします。
    
2. **CSVファイルの作成**:
    
    ```python
    csv_file = open('data.csv', 'w', newline='')
    print("CSV file created")
    csv_writer = csv.writer(csv_file)
    ```
    
    `data.csv`という名前のCSVファイルを作成し、書き込みモードで開きます。`csv.writer`を使ってCSVファイルに書き込むためのオブジェクトを作成します。
    
3. **シリアルポートのオープン**:
    
    ```python
    serial_port = 'COM3'
    ser = serial.Serial(serial_port, 9600)
    print(f"Serial port {serial_port} opened")
    ```
    
    `COM3`ポートを9600ボーで開く設定になっています。`serial.Serial`を使ってシリアルポートを開きます。
    
4. **データの読み取りとCSVファイルへの書き込み**:
    
    ```python
    # Read data from the serial port and write it to the CSV file
        while True:
    
            data = ser.readline().decode().strip()  # Read a line of data from the serial port
            if data:
                print(f"Data read from serial port: {data}")
                csv_writer.writerow([data])
                print(f"Data written to CSV: {data}")
                csv_file.flush()  # Flush the CSV file to write the data to disk
    ```
    
    `while`ループ内でシリアルポートからデータを読み取り、それをデコードして余分な空白を取り除きます。データが存在する場合、そのデータをCSVファイルに書き込みます。
    
5. **例外処理**:
    
    ```python
    except serial.SerialException as e:
        print(f"Error opening serial port: {e}")
    except Exception as e:
        print(f"An error occurred: {e}")
    ```
    
    シリアルポートを開く際に発生したエラーやその他の例外をキャッチし、それぞれのエラーメッセージを表示します。
    
6. **リソースのクリーンアップ**:
    
    ```python
    finally:
        # Close the CSV file and serial port
        if 'csv_file' in locals():
            csv_file.close()
            print("CSV file closed")
        if 'ser' in locals():
            ser.close()
            print("Serial port closed")
    ```
    
    `finally`ブロックでは、CSVファイルとシリアルポートを閉じます。`locals()`関数を使って、`csv_file`と`ser`がローカルスコープに存在するかどうかを確認し、それぞれを閉じる処理を行います。これにより、リソースのリークを防ぐことができます。
    
# 注意
ArduinoIDEなどでシリアルモニタを開いている場合、Pythonのスクリプトがシリアルポートを開くことができず、エラーになってしまうので、必ずPythonスクリプトの実行前にシリアルモニタを閉じるようにしましょう。
# おわりに

いかがだったでしょうか？今回はArduinoに接続されたロータリーエンコーダの値をシリアル通信で読み取りCSV形式のデータに出力する方法をご紹介しました。
エンコーダの種類はほかにもいろいろありますが、今回のスクリプトの応用で色々なデータをarduinoから送ることができますので、試してみると面白いかもしれませんね。

最後までご覧いただきありがとうございました。