⚙️

Rust: 測定器を自動化してみた(VISA)

2022/09/29に公開

測定器の自動化は今まで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 IOCommand ExpertはRustで動かなかった場合のデバッグに便利です。
ここでの説明は省略しますが、私はInteractive IOでは簡単なコマンドの確認(IDN確認)を行ったり、Command Expertでコマンド仕様を確認し、比較的複雑なコマンド確認を行ったりしています。測定器からの戻り値がバイナリの場合は、Interactive IOだと良くわからないので、Command Expertで確認した方が良いでしょう。

altテキスト

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)?;
  • コマンド送信
    コマンドはTrait std::io::Writeを使用し、write_allメソッドで送信します。
    queryの型は、[u8]なので、b"*IDN?として、bをつけてあげる必要があります。
instr.write_all(query)?;
  • 戻り値受信
    測定器側からの戻り値の受信はTrait std::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 IOCommand 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に保存していますので、参考にしてください。
https://github.com/TatsuyaYoke/visa-app-rs

GitHubで編集を提案

Discussion