RustでUARTのシリアル通信を受信する方法：ArduinoのUSBから文字を受ける

こんにちは👋
ますます寒くなっておりますが、筆者はワクワクしています。

# 概要

Rustの `serialport`のクレートを使って、UART法のシリアル通信を行う方法を解説します。

## モチベーション

筆者は、基礎データがわからないサーミスタの温度対電気抵抗のデータを取得して記録した上、最小二乗法で気温を推定する数学式を作りたいのですが、そのために抵抗データと気温データをArduinoから受信してCSVに記録する必要があります。

RustでArduinoから受信したデータに対して複雑な処理を書くのがより楽なので、そのような方法を探していました。

他の方ももしかして、実験データをRustで処理したいのかもしれませんので共有します。

# UARTとは？

UARTは、古くからある簡単なシリアル通信方法の一つです。UARTの特徴は、非同期的であることです。

多くのシリアル通信は、コントローラー側のCPUクロック数が通信のスピードを決めるのですが、UARTはクロック数を使わず、通信する端末の間で**ボーレート**という通信のテンポを決め合って、例えクロック数が違ってもそのボーレート以上にはデータを通信しないから、高いクロック数のマイコンが弱いマイコンを追い抜くようなことがないのです。

UARTはその単純さがメリットでマイコン同士の通信で広く使われています。UARTの通信にも電磁場干渉によるデータ崩壊を防ぐロジックもあります。

UARTについてもっと知りたければ以下の記事をご参照ください。非常に興味深いのでお勧めします。

https://jp.rs-online.com/web/content/discovery/ideas-and-advice/uart-guide?srsltid=AfmBOopAo3TXi9ejVnWmDJNOU51eByskjBufbInFsJ4-vWWosp8BIZwS

# RustでUARTのシグナルを受信するには？

Rustで受信する以前に、パソコンが受信するモデムが必要です。USBで接続するシリアルモデムを通販で売っているので必要であればご用意ください。

https://amzn.asia/d/3MrKmQc

筆者は今回の記事ではArduino Unoを使っているので、Unoの基盤に内蔵されているUSBシリアルコントローラーを使います。

脱線ですが、Arduino UnoにはハードウェアのUARTチップが一つ用意されており、それがUSBとの通信に選任しています。Unoのデジタルピンの0と1にUARTのTX（送信）とRX（受信）が書かれており、実際、もう一つのUARTとして使えるのですが、こちらはソフトでエミュレートしているUARTであり、ハードウェアレベルのUARTではないのです。

USBにシリアルコントローラーもしくはArduinoを繋いだ上、進めてください。

## serialportでシリアルモデムを調べる

まずは新規なプロジェクトをCargoで作成した上、`serialport`を追加してください。

https://crates.io/crates/serialport

それから、以下のコードで接続されているシリアルモデムをリストアップしましょう。

```rust
    let ports = serialport::available_ports().expect("No ports found!");
    
    println!("Ports:");
    for port in ports.iter() {
        println!("{}, {:?}", port.port_name, port.port_type);
    }
```

筆者だと以下の出力になります：

```
/dev/cu.debug-console, PciPort
/dev/tty.debug-console, PciPort
/dev/cu.Bluetooth-Incoming-Port, PciPort
/dev/tty.Bluetooth-Incoming-Port, PciPort
/dev/cu.usbmodem2101, UsbPort(UsbPortInfo { vid: 9025, pid: 67, serial_number: Some("34333323832351710210"), manufacturer: Some("Arduino (www.arduino.cc)"), product: None })
/dev/tty.usbmodem2101, UsbPort(UsbPortInfo { vid: 9025, pid: 67, serial_number: Some("34333323832351710210"), manufacturer: Some("Arduino (www.arduino.cc)"), product: None })
```

他の`PciPort`と言っているのはおそらくMacBook内蔵のものでしょうが、`manufacturer`がArduinoのものが目当てです。

`cu`と`tty`のどちらを使えばいいか悩むかもしれませんが、`tty`は端末を複数のプロセスでシェアできるようにするもののようです。`cu`は他のプロセスが使えないようにするものです。`cu`で受信していると、他のプロセスが同じモデムを使おうとすると`busy`になってしまいます。

今回は`cu`を使います。

## シリアルモデムを取得する

上記のリストから使いたいモデムが見つかったら、それを使って以下のコードを書きます。ベタ書きが嫌だったら`manufacturer`等で選択するようにしていただければと思います。

```rust
    let ports = serialport::available_ports().expect("No ports found!");
    
    let arduino_port = match ports
        .iter()
        .find(|p| &p.port_name == "/dev/cu.usbmodem2101")
    {
        Some(p) => p,
        None => panic!("Arduino not connected."),
    };
```

それからserialportのSerialPortオブジェクトを作ります。

```rust 
    let port = serialport::new(&arduino_port.port_name, 9600)
        .timeout(Duration::from_millis(2000))
        .open()
        .expect("failed to open port on arduino");
```

## シリアルから文字列を受信する

UARTのシリアルからバイト（`u8`）を受信できるのですが、今回はASCIIのバイトのみなので、Rustでは`String`をバッファーにします。

```rust
    let mut buf = String::with_capacity(1024);

    let mut reader = std::io::BufReader::new(port);
```

このバッファーを使って一行ずつ受信したバイトを読みます。つまり、マイコンから開業の`\n`が来るまで、表示しないということです。特にこの書き方にする必要はないのですが、便利なのでこのようにしました。

```rust
 loop {
        match reader.read_line(&mut buf) {
            Ok(_res) => {
                println!("{}", buf);

                buf.clear();
            }
            Err(ref err) if err.kind() == std::io::ErrorKind::TimedOut => {}
            Err(err) => {
                dbg!(err);
                break;
            }
        }
    }
```

`Err`の`std::io::ErrorKind::TimedOut`を無視しているのは、相手のデバイスが何も送信していない時に起きるエラーなので、今回は特に気にしないという意味です。`_res`は、開業まで何バイト受信したのかの数値です。

loopに戻る前にバッファーをリセットしています。これで綺麗に表示されます。

```
13571.43,14769.98,17.8
13625.87,14769.98,17.8
```

筆者のArduinoからは接続されている二つのサーミスタの抵抗値が句読点入りで送信されています。最後に、DHT気温センサーが記録したデータも送っています。本記事には関係ないのですが、参考までに以下のコードで記録しています。`csv`というクレートでCSVに受信したデータを書き込んでいます。

```rust
fn main() {
    let date_stamp = chrono::Utc::now().format("%Y-%m-%d_%H%M%S");
    let file_name = format!("./readings/sensor_readings_{}.csv", date_stamp);

    let path = Path::new(&file_name);
    let f = File::create_new(path).expect("failed to create file");

    let mut csv_wtr = csv::Writer::from_writer(f);

    let ports = serialport::available_ports().expect("No ports found!");

    println!("Ports:");
    for port in ports.iter() {
        println!("{}, {:?}", port.port_name, port.port_type);
    }

    let arduino_port = match ports
        .iter()
        .find(|p| &p.port_name == "/dev/cu.usbmodem2101")
    {
        Some(p) => p,
        None => panic!("Arduino not connected."),
    };

    let port = serialport::new(&arduino_port.port_name, 9600)
        .timeout(Duration::from_millis(2000))
        .open()
        .expect("failed to open port on arduino");

    let mut buf = String::with_capacity(1024);

    let mut reader = BufReader::new(port);

    loop {
        match reader.read_line(&mut buf) {
            Ok(_res) => {
                let mut values = buf.split(",").filter_map(|s| s.trim().parse::<f64>().ok());

                let thermistor_1_resistance =
                    values.next().expect("missing therm 1 value").to_string();
                let thermistor_2_resistance =
                    values.next().expect("missing therm 2 value").to_string();
                let temp = values.next().expect("missing temp value").to_string();

                println!(
                    "Therm 1: {}Ω, Therm 2: {}Ω, Temp: {} C",
                    thermistor_1_resistance, thermistor_2_resistance, temp
                );

                csv_wtr
                    .write_record(&[thermistor_1_resistance, thermistor_2_resistance, temp])
                    .expect("Failed to write record to csv");
                csv_wtr.flush().expect("flush failed");

                buf.clear();
            }
            Err(ref err) if err.kind() == std::io::ErrorKind::TimedOut => {}
            Err(err) => {
                dbg!(err);
                break;
            }
        }
    }
}
```

CSVは以下のようなデータを記録してくれています。

```csvs
14830.1,14890.51,16.4
15073.53,14830.1,16.4
14830.1,14830.1,16.3
14769.98,14769.98,16.3
14710.15,14710.15,16.3
14532.37,14532.37,16.3
14532.37,14415.27,16.3
16030.54,14473.68,16.5
15384.62,14591.34,16.6
```

# まとめ

RustでマイコンのUARTから送信されたデータを受信する方法を紹介しましたが、いかがでしたでしょうか？

Arduinoのコード自体はC++で書いていますが、少なくともCSVを書くロジックはRustの方が楽なので、こうして使い分けができるといいです。ArduinoをRustで書くこともできるのですが、筆者はC++の勉強のためにそこはあえてC++で苦戦しています。Rustのありがたさがわかります。

他の週末科学者のための共有でしたが、LabBaseにはこうした趣味でもプログラミングに対して熱心な仲間がいるのです:grinning: