Rust: 測定器を自動化してみた(VISA)
測定器の自動化は今までPythonを使ってやっていましたが、コードを書いていて楽しいRustに置き換えようと思います。
今回は、試しにKeysightのオシロスコープ(DSOX1204A)のスクリーンショットと波形をcsvで取得する簡単な自動化のソフトを作ってみました。
準備
- ping
USB接続の場合は不要な作業になりますが、LAN接続の場合は、まずはpingチェックをしましょう。測定器側及びPCのIPアドレス以下の通り設定します。サブネットマスクは一般的に255.255.255.0
とし、上位3つは同じ数字にして、残りをかぶらないような数字(1-254)で割り当てます(複数の測定器を自動に制御する場合は、同じネットワーク内にいれて制御すると思いますが、それらの測定器のアドレスもかぶらないようにしてください)。
PC: 192.168.3.10
DSOX1204A: 192.168.3.242
測定器とPCを接続したら、PCでターミナルを起動し、以下のコマンドで応答があるか確認を行います。
ping 192.168.3.242
- Keysight IO Libraries Suite
Keysight IO Libraries Suite
を使用して、VISAアドレスの設定・確認や簡単なコマンドを確認します。このツールはKeysightのホームページから無料でダウンロードできます。
起動すると、下図のようなアプリが起動するので、画面の左上にある+Add
ボタンや隣の更新ボタンで機器の追加とVISAアドレスの設定を行います。
Keysightの測定器は更新ボタンを押すだけで自動で設定されるのですが、DSOX1204Aは自動検出されなかったので、+Add
ボタンからマニュアルで設定しました。下図の通り、緑色のチェックボタンになったら無事接続されています。また、VISAアドレスは下図の中央にあるTCPIP0::192.168.3.242::hislip0::INSTR
になります。
Interactive IO
やCommand Expert
はRustで動かなかった場合のデバッグに便利です。
ここでの説明は省略しますが、私はInteractive IO
では簡単なコマンドの確認(IDN確認)を行ったり、Command Expert
でコマンド仕様を確認し、比較的複雑なコマンド確認を行ったりしています。測定器からの戻り値がバイナリの場合は、Interactive IO
だと良くわからないので、Command Expert
で確認した方が良いでしょう。
Dependencies
本記事で使用するCrate
は以下になりますので、Cargo.toml
に記載しておきましょう。
[dependencies]
visa-rs = "0.4.0"
csv = "1.1"
IDN
まずは、VISAのHello World的な存存であるIDN
確認を実施してみましょう!
以下がコードの全容です。
use std::error::Error;
use std::ffi::CString;
use std::fs::File;
use std::io::{BufReader, Read, Write};
use visa_rs::{flags::AccessMode, DefaultRM, Instrument, TIMEOUT_IMMEDIATE};
fn query(mut instr: &Instrument, query: &[u8]) -> Result<String, Box<dyn Error>> {
instr.write_all(query)?;
let mut buf_reader = BufReader::new(instr);
let mut buf = String::new();
let _ = buf_reader.read_to_string(&mut buf);
Ok(buf)
}
fn idn(visa_address: &str) -> Result<(), Box<dyn Error>> {
let rm = DefaultRM::new()?;
let rsc = CString::new(visa_address)?.into();
let instr = rm.open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)?;
let response = query(&instr, b"*IDN?")?;
println!("{:?}", response.trim());
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let visa_address = "TCPIP0::192.168.3.242::hislip0::INSTR";
idn(visa_address)?;
Ok(())
}
- 接続
以下の3行のコードが、測定器との接続をする設定です。visa_address
の箇所に、先ほど確認したVISAアドレスを設定します。
let rm = DefaultRM::new()?;
let rsc = CString::new(visa_address)?.into();
let instr = rm.open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)?;
- コマンド送信
コマンドはTraitstd::io::Write
を使用し、write_all
メソッドで送信します。
query
の型は、[u8]
なので、b"*IDN?
として、b
をつけてあげる必要があります。
instr.write_all(query)?;
- 戻り値受信
測定器側からの戻り値の受信はTraitstd::io::{BufReader, Read}
を使用します。
戻り値が文字列の場合は以下の通りですが、バイナリの場合はやり方が異なります(後述)。
let mut buf_reader = BufReader::new(instr);
let mut buf = String::new();
let _ = buf_reader.read_to_string(&mut buf);
スクリーンショット
use std::error::Error;
use std::ffi::CString;
use std::fs::File;
use std::io::{BufReader, Read, Write};
use visa_rs::{flags::AccessMode, DefaultRM, Instrument, TIMEOUT_IMMEDIATE};
fn query(mut instr: &Instrument, query: &[u8]) -> Result<String, Box<dyn Error>> {
instr.write_all(query)?;
let mut buf_reader = BufReader::new(instr);
let mut buf = String::new();
let _ = buf_reader.read_to_string(&mut buf);
Ok(buf)
}
fn get_screenshot(visa_address: &str, filename: &str) -> Result<(), Box<dyn Error>> {
let rm = DefaultRM::new()?;
let rsc = CString::new(visa_address)?.into();
let mut instr = rm.open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)?;
instr.write_all(b":HARDcopy:INKSaver OFF")?;
instr.write_all(b":DISP:DATA? PNG, COLor")?;
let mut buf_reader = BufReader::new(instr);
let mut buf = Vec::new();
while buf.is_empty() {
let _ = buf_reader.read_to_end(&mut buf);
}
let mut file = File::create(filename)?;
file.write_all(&buf[10..buf.len() - 1])?;
file.flush()?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let visa_address = "TCPIP0::192.168.3.242::hislip0::INSTR";
get_screenshot(visa_address, "waveform.png")?;
Ok(())
}
:DISP:DATA? PNG, COLor
の戻り値は文字列ではなく、バイナリで返ってくるので、以下のようにしてあげる必要があります。while
ループにいれてbuf
の長さを確認しているのは、戻り値が返ってくるまでに数秒かかるため、待機しなければ、戻り値を取得できませんでした。現状だと、戻り値がなければ永久にループを抜けないので、タイムアウトなどの工夫が必要ですね。。。
let mut buf_reader = BufReader::new(instr);
let mut buf = Vec::new();
while buf.is_empty() {
let _ = buf_reader.read_to_end(&mut buf);
}
簡易的な対策として、以下のようにして待機時間を設けるのも有りかと。
use std::{thread, time};
let mut buf_reader = BufReader::new(instr);
let mut buf = Vec::new();
thread::sleep(time::Duration::from_secs(2));
let _ = buf_reader.read_to_end(&mut buf);
本題とはずれますが、画像を保存するときにバイナリ配列をスライスしているのは、バイナリブロックのヘッダやフッタを除くためです。
Interactive IO
で確認すると分かりますが、データの長さなどが付随されています。おそらくieee 488.2
のバイナリブロックのフォーマットなのだと思います。
file.write_all(&buf[10..buf.len() - 1])?;
CSV取得
やっていることは上記の組合せで難しいことはししていません。
Interactive IO
やCommand Expert
で動作確認しながら、同じシーケンスで書いていくだけです。
use std::error::Error;
use std::ffi::CString;
use std::fs::File;
use std::io::{BufReader, Read, Write};
use visa_rs::{flags::AccessMode, DefaultRM, Instrument, TIMEOUT_IMMEDIATE};
fn query(mut instr: &Instrument, query: &[u8]) -> Result<String, Box<dyn Error>> {
instr.write_all(query)?;
let mut buf_reader = BufReader::new(instr);
let mut buf = String::new();
let _ = buf_reader.read_to_string(&mut buf);
Ok(buf)
}
fn get_csv(visa_address: &str, filename: &str, ch: u64) -> Result<(), Box<dyn Error>> {
let rm = DefaultRM::new()?;
let rsc = CString::new(visa_address)?.into();
let mut instr = rm.open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)?;
instr.write_all(b":WAVeform:POINts:MODE RAW")?;
instr.write_all(b":WAVeform:POINts 10240")?;
instr.write_all(format!(":WAVeform:SOURce CHANnel{}", ch).as_bytes())?;
instr.write_all(b":WAVeform:FORMat BYTE")?;
let preamble_string = query(&instr, b":WAVeform:PREamble?")?;
let preamble: Vec<f64> = preamble_string
.as_str()
.trim()
.split(',')
.map(|x| x.trim().parse().unwrap_or(0.0))
.collect();
let x_increment = preamble[4];
let x_origin = preamble[5];
let y_increment = preamble[7];
let y_origin = preamble[8];
let y_ref = preamble[9];
instr.write_all(b":WAVeform:DATA?")?;
let mut buf_reader = BufReader::new(instr);
let mut buf = Vec::new();
while buf.is_empty() {
let _ = buf_reader.read_to_end(&mut buf);
}
let mut wtr = csv::Writer::from_path(filename)?;
wtr.write_record(["time", "data"])?;
for (i, value) in buf[10..buf.len() - 1].iter().enumerate() {
let time = x_origin + i as f64 * x_increment;
let data = (*value as f64 - y_ref) * y_increment + y_origin;
wtr.write_record([time.to_string(), data.to_string()])?;
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let visa_address = "TCPIP0::192.168.3.242::hislip0::INSTR";
get_csv(visa_address, "waveform.csv", 1)?;
Ok(())
}
まとめ
あまり参考になる記事がなかったので、かなり手探りでしたが、なんとかRustで測定の自動化ソフトを作ることができました。
Rustの記事はまだまだ多くないので、Rust人口を増やすために、マイナーな内容でもどんどん投稿していこうかなと思います!
コードは以下のGithubに保存していますので、参考にしてください。
Discussion