VISA/SCPIを調べる その1 プロトコル

概要

VISA/SCPIを使った自動制御をためすために調べてみました。

VISAとは?

上記はNational Instrumentsのページですが、このNI-​VISA​が一番使われているライブラリとなります。​​シリアル、USB、イーサネット、​GPIBなどの複数のインターフェースを同じAPIで操作するための仕組みです。VISAを利用すると機材を新しいものに変更した場合でも、過去と同じようなAPIで連携することができます。

SCPIとは?

VISAはインターフェース層の定義でしたが、SCPIは実際にVISAで通信をするためのプロトコルです。テキストベースのコマンドで、制御元からコマンドを投げると端末側から返事がくるような形になっています。共通コマンドがありますが、機材によって微妙にコマンドが異なります。

MEASure:VOLTage:AC?

上記のコマンドだとACの電圧を取得するコマンドになります。先頭から4文字が大文字で、それ以降は小文字になることが多いようです。最後に?があると機材から返事があるという目印になります。機材によりACとDCがある場合、複数の電圧があるなどで、細かいコマンドが変わってきます。

SCPIのプロトコル

基本はテキストで送信するので、シンプルなプロトコルになります。コマンドの終わりは利用しているインターフェースによって異なるのですが、シリアルやイーサネットの場合には’\r'(LF)が使われている事が多いです。その他;(セミコロン)があると複数コマンドを1行で送付した場合の区切り文字となります。

コマンドは:区切りで階層を表現して、スペースを明けてから設定値などを指定します。

INPut:STATe:TRIGgered OFF

上記の場合にはコマンドが「INPut:STATe:TRIGgered」で設定値が「OFF」になります。入力トリガーをOFFにする設定です。

とりあえず組んでみた

#include <WiFi.h>
#include <WiFiClient.h>

WiFiServer server(5025);

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();

  WiFi.begin();
  Serial.print("WiFi.begin()");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi Connected.");
  Serial.printf("IP Address  : ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("server.begin()");
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("New Client.");
    while (client.connected()) {
      input(&client);
    }
    Serial.println("Close Client.");
  }

  delay(1);
}

void input(Stream *client) {
  static char input[256] = {};
  static int pos = 0;
  static int ret = 0;

  while (client->available()) {
    uint8_t c = client->read();
    if (c == '\n' || c == ';' || c == 0 || 250 < pos) {
      // command run
      command(client, input);

      // reset
      memset(input, 0, sizeof(input));
      pos = 0;
    } else {
      input[pos] = c;
      pos++;
    }
  }
}

void command(Stream *client, char* input) {
  Serial.printf("get: [%s]\n", input);
  for (int i = 0; i < strlen(input); i++) {
    Serial.printf("%02X ", input[i]);
  }
  Serial.println();

  int returnFlag = input[strlen(input) - 1] == '?';

  char* commandTemp = strtok(input, ":");
  String command1 = commandTemp;
  commandTemp = strtok(NULL, ":");
  String command2 = commandTemp;
  commandTemp = strtok(NULL, ":");
  String command3 = commandTemp;
  commandTemp = strtok(NULL, ":");
  String command4 = commandTemp;

  command1.toUpperCase();
  command2.toUpperCase();
  command3.toUpperCase();
  command4.toUpperCase();

  command1.trim();
  command2.trim();
  command3.trim();
  command4.trim();
  Serial.printf("command1: [%s]\n", command1);
  Serial.printf("command2: [%s]\n", command2);
  Serial.printf("command3: [%s]\n", command3);
  Serial.printf("command4: [%s]\n", command4);

  String returnStr = "";
  if (command1 == "*IDN?") {
    uint8_t mac[6];
    esp_read_mac(mac, ESP_MAC_WIFI_STA);
    char mac_str[18];
    snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

    returnStr = String("ESP32,VISA Test,") + mac_str + "," + __DATE__"\n";
  } else if (command1 == "TIME?") {
    returnStr = String(millis());
  } else {
    // Unknown command
    if (returnFlag) {
      returnStr = "999";
    }
  }

  if (returnStr != "") {
    Serial.printf("returnStr: [%s]\n", returnStr);
    returnStr = returnStr + "\n";
    client->write(returnStr.c_str());
  }
}

Wi-Fiで待ち受けて、「*IDN?」コマンドで端末情報を返して、「TIME?」コマンドで起動経過時間を返却するだけの処理となります。結構短く作れますね。ArduinoはシリアルもWi-Fiもほぼ同じクラスになっていますので、シリアル通信に変更する場合にもすぐに変更が可能です。とはいえ、コマンドのパースはちょっと面倒ですね。

ライブラリがあった!

さがしてみたら、そのものズバリのライブラリがありました。。。

#include "Vrekrer_scpi_parser.h"

SCPI_Parser scpi;

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();

  scpi.RegisterCommand("*IDN?", &Identify);
  scpi.RegisterCommand("TIME?", &Time);
  scpi.SetErrorHandler(&Error);
}

void loop() {
  scpi.ProcessInput(Serial, "\n");
  delay(1);
}

void Identify(SCPI_C commands, SCPI_P parameters, Stream& interface) {
  interface.println("Vrekrer,Arduino SCPI Dimmer,#00,v0.4.2");
}

void Time(SCPI_C commands, SCPI_P parameters, Stream& interface) {
  interface.println(millis());
}

void Error(SCPI_C commands, SCPI_P parameters, Stream& interface) {
}

シリアルでの通信例ですが、かなりシンプルになりました。RegisterCommandでコマンドを登録して、コールバックする仕組みですね。今風のバインディング方法です。ちょっと負荷テストをしたところ、おかしな返信があった気がしますが概ね普通に動いています。

まとめ

んー、外部ライブラリ依存をするかはちょっと悩み中です。新規に作るのであれば既存ライブラリを利用したほうがいいと思います。自分で一度作っておくと既存ライブラリの内部構造もわかりやすくなります。

適当に組んでみても、Excelとかからも呼び出せますしPythonからも呼び出せました。

コメント