こんにちは👋
ますます寒くなっておりますが、筆者はワクワクしています。
概要
Rustの serialport
のクレートを使って、UART法のシリアル通信を行う方法を解説します。
モチベーション
筆者は、基礎データがわからないサーミスタの温度対電気抵抗のデータを取得して記録した上、最小二乗法で気温を推定する数学式を作りたいのですが、そのために抵抗データと気温データをArduinoから受信してCSVに記録する必要があります。
RustでArduinoから受信したデータに対して複雑な処理を書くのがより楽なので、そのような方法を探していました。
他の方ももしかして、実験データをRustで処理したいのかもしれませんので共有します。
UARTとは?
UARTは、古くからある簡単なシリアル通信方法の一つです。UARTの特徴は、非同期的であることです。
多くのシリアル通信は、コントローラー側のCPUクロック数が通信のスピードを決めるのですが、UARTはクロック数を使わず、通信する端末の間でボーレートという通信のテンポを決め合って、例えクロック数が違ってもそのボーレート以上にはデータを通信しないから、高いクロック数のマイコンが弱いマイコンを追い抜くようなことがないのです。
UARTはその単純さがメリットでマイコン同士の通信で広く使われています。UARTの通信にも電磁場干渉によるデータ崩壊を防ぐロジックもあります。
UARTについてもっと知りたければ以下の記事をご参照ください。非常に興味深いのでお勧めします。
RustでUARTのシグナルを受信するには?
Rustで受信する以前に、パソコンが受信するモデムが必要です。USBで接続するシリアルモデムを通販で売っているので必要であればご用意ください。
筆者は今回の記事ではArduino Unoを使っているので、Unoの基盤に内蔵されているUSBシリアルコントローラーを使います。
脱線ですが、Arduino UnoにはハードウェアのUARTチップが一つ用意されており、それがUSBとの通信に選任しています。Unoのデジタルピンの0と1にUARTのTX(送信)とRX(受信)が書かれており、実際、もう一つのUARTとして使えるのですが、こちらはソフトでエミュレートしているUARTであり、ハードウェアレベルのUARTではないのです。
USBにシリアルコントローラーもしくはArduinoを繋いだ上、進めてください。
serialportでシリアルモデムを調べる
まずは新規なプロジェクトをCargoで作成した上、serialport
を追加してください。
それから、以下のコードで接続されているシリアルモデムをリストアップしましょう。
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
等で選択するようにしていただければと思います。
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オブジェクトを作ります。
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
をバッファーにします。
let mut buf = String::with_capacity(1024);
let mut reader = std::io::BufReader::new(port);
このバッファーを使って一行ずつ受信したバイトを読みます。つまり、マイコンから開業の\n
が来るまで、表示しないということです。特にこの書き方にする必要はないのですが、便利なのでこのようにしました。
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に受信したデータを書き込んでいます。
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は以下のようなデータを記録してくれています。
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にはこうした趣味でもプログラミングに対して熱心な仲間がいるのです