Modbus/TCPノードを試してみた

工業用製品などで使用されている通信規格であるModbus。仕様はオープンされているので、様々な機器で使用されています。マスタ/スレーブ形式で、1対1または1対多で接続され、マスタがスレーブのデータを読み書きするといった通信となっています。Modbusは物理レイヤとしては、RS-232CやRS-485、Ethernetなどで採用されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/66a8f77f-5234-235d-7055-28a376517781.png)


Google等で「Modbus」を検索すれば、いろいろ資料が公開されているので詳しくは検索してみて下さい。

# Modbus/TCPノード
enebularのDiscoverにはModbusスレーブとEthernetで通信するためのノードが有料プライベートノードとして公開されています。このノードは、読み出し系のファンクションコードに対応しています。

[Discover / modbus-tcp](https://enebular.com/discover/privatenode/91e7769d-1cb7-4f2e-8c0c-9c1b7d290b62)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/4acde5c1-2bfe-1af0-00a1-fce094c30f5a.png)

Modbus/TCPの入力と出力は以下の様になっています。Modbus通信に必要なパラメータはすべてノードの入力で設定します。

* 入力
    * ```server```　型：string  
通信対象のスレーブのIPアドレスを指定します
    * ```port```  型：number  
通信対象のスレーブのポート番号を指定します
    * ```timeOut```  型：number  
スレーブと通信を行う際のタイムアウト時間をmsで指定します
    * ```functionCode```  型：number  
スレーブの実行機能に対応したファンクションコードを指定します
        * 0x01 コイル読み込み
        * 0x02 入力ステータス読み込み
        * 0x03 保持レジスタ読み込み
        * 0x04 入力レジスタ読み込み
        * 0x2B デバイスID読み込み
    * ```address``` 型：number  
読み取り対象の開始アドレスを指定します。本項目は、ファンクションコード：0x2Bの場合に未使用
    * ```quantity```  型：number  
読み取り対象の範囲を指定します。本項目は、ファンクションコード：0x2Bの場合に未使用
    * ```readDeviIdCode```  型：number  
読み取り対象のReadDeviIdコードを指定します。本項目は、ファンクションコード：0x2Bの場合に使用
    * ```objectId```  型：number  
読み取り対象のオブジェクトIDを指定します。本項目は、ファンクションコード：0x2Bの場合に使用
* 出力（正常時）
    * ```payload.buffer```  型：buffer  
スレーブから受信したデータ。プロトコルデータ部をバイナリバッファで設定します。本項目は、ファンクションコード：0x2Bの場合に未使用
    * ```payload.data```  型：object
スレーブから受信したデータ。プロトコルデータ部をオブジェクトIDをキーとした連想配列で設定します。本項目は、ファンクションコード：0x2Bの場合に使用
    * ```server```  型：string  
通信対象のスレーブのIPアドレス  
    * ```port```  型：number  
通信対象のスレーブのポート番号  
    * ```statusCode```  型：number  
スレーブとの通信結果。未設定
* 出力（異常時）
    * ```payload.buffer```  型：buffer  
スレーブから受信したデータ。未設定
    * ```payload.data```  型：object  
スレーブから受信したデータ。未設定
    * ```server```  型：string  
通信対象のスレーブのIPアドレス
    * ```port```  型：number  
通信対象のスレーブのポート番号
    * ```statusCode```  型：number  
スレーブとの通信結果。スレーブからの例外レスポンスコードを設定します

# Modbusスレーブ機器を作って試す
Modbus対応のスレーブ機器が手元になかったため、[M5 ATOM Matrix](https://www.switch-science.com/catalog/6260/)を用意して、Arduinoでスケッチを作りました。
M5 ATOM Matrixにはボタンと加速度センサーが搭載されているので、その値をModbus経由で読み出せるようにします。

Modbusのレジスタのアドレスとして以下の様にしています。
100番地 ボタン
101番地 加速度X軸 上位16bit
102番地 加速度X軸 下位16bit
103番地 加速度Y軸 上位16bit
104番地 加速度Y軸 下位16bit
105番地 加速度Z軸 上位16bit
106番地 加速度Z軸 下位16bit

Arduino IDEで以下のライブラリとスケッチを用意して、M5 ATOM Matrixに書き込みました。

* [M5Atom](https://github.com/m5stack/M5Atom)
* [modbus-esp8266](https://github.com/emelianov/modbus-esp8266)

```c++
#include <M5Atom.h>
#include <WiFi.h>
#include <ModbusIP_ESP8266.h>

// Modbusのアドレス
const int SW_IREG = 100;
const int ACCX_H_IREG = 101;
const int ACCX_L_IREG = 102;
const int ACCY_H_IREG = 103;
const int ACCY_L_IREG = 104;
const int ACCZ_H_IREG = 105;
const int ACCZ_L_IREG = 106;

// Wi-Fi設定
const char WIFI_SSID[] = "ssid";
const char WIFI_PASS[] = "pass";

ModbusIP mb;
long ts;

void setup() {
  // M5 ATOM初期化
  M5.begin(true, false, false);
  // M5 ATOM内蔵加速度センサ初期化
  M5.IMU.Init();

  // Wi-Fi接続
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // Modbusスタート
  mb.server();
  // Modbus設定
  mb.addIreg(SW_IREG);
  mb.addIreg(ACCX_H_IREG);
  mb.addIreg(ACCX_L_IREG);
  mb.addIreg(ACCY_H_IREG);
  mb.addIreg(ACCY_L_IREG);
  mb.addIreg(ACCZ_H_IREG);
  mb.addIreg(ACCZ_L_IREG);
  
  ts = millis();
}

void loop() {
  int sw;
  static int before_sw = LOW;

  // Modbus周期処理
  mb.task();

  // スイッチ読み込み
  sw = M5.Btn.isPressed();
  if (sw != before_sw) {
    // スイッチ状態をModbusに反映
    mb.Ireg(SW_IREG, sw);
    before_sw = sw;
  }

  // 500ms周期処理
  if (millis() > ts + 500) {
    int value;
    float x=0, y=0, z=0;
    ts = millis();
    // 加速度センサ読み込み
    M5.IMU.getAccelData(&x, &y, &z);
    // 加速度センサをModbusに反映
    memcpy(&value,&x,4);
    mb.Ireg(ACCX_H_IREG, (uint16_t)((value & 0xFFFF0000) >> 16));
    mb.Ireg(ACCX_L_IREG, (uint16_t)(value & 0x0000FFFF));
    memcpy(&value,&y,4);
    mb.Ireg(ACCY_H_IREG, (uint16_t)((value & 0xFFFF0000) >> 16));
    mb.Ireg(ACCY_L_IREG, (uint16_t)(value & 0x0000FFFF));
    memcpy(&value,&z,4);
    mb.Ireg(ACCZ_H_IREG, (uint16_t)((value & 0xFFFF0000) >> 16));
    mb.Ireg(ACCZ_L_IREG, (uint16_t)(value & 0x0000FFFF));
  }
  delay(10);
  
  M5.update();
}
```
（Wi-Fiの設定は環境に合わせて下さい）

動作させると以下のようにIPアドレスが表示されるので、それを控えておいて下さい。

```
WiFi connected
IP address: 
192.168.2.236
```

IPアドレスを固定にする場合は、```WiFi.begin()```の前に以下を追加して下さい。

```cpp
const IPAddress ip(192,168,1,100);     // 使用するルータの範囲に合わせて下さい
const IPAddress gateway(192,168,1,1);  // 使用するルータのIPを設定
const IPAddress subnet(255,255,255,0); // 使用するルータのサブネットに合わせて下さい

WiFi.config(ip,gateway,subnet);
```

# Modbus/TCPノードを使ったフロー
1秒周期でModbusスレーブであるM5 ATOM Matrixからボタンの状態と加速度センサーの値を読み出し、JSONに変換してDebugノードで表示するフローです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/5c67da5f-bc58-0f99-06bf-29e35f86e7f5.png)

```json
[{"id":"93cd5a5.2c872a8","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"61a5999e.1973e8","type":"inject","z":"93cd5a5.2c872a8","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":360,"wires":[["be6a29d5.0196f8"]]},{"id":"89a1f8f4.f768c8","type":"debug","z":"93cd5a5.2c872a8","name":"","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"payload","targetType":"msg","x":1120,"y":360,"wires":[]},{"id":"be6a29d5.0196f8","type":"change","z":"93cd5a5.2c872a8","name":"","rules":[{"t":"set","p":"server","pt":"msg","to":"192.168.2.230","tot":"str"},{"t":"set","p":"port","pt":"msg","to":"502","tot":"num"},{"t":"set","p":"functionCode","pt":"msg","to":"4","tot":"num"},{"t":"set","p":"address","pt":"msg","to":"100","tot":"num"},{"t":"set","p":"quantity","pt":"msg","to":"7","tot":"num"},{"t":"set","p":"timeOut","pt":"msg","to":"500","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":360,"wires":[["e8016c9f.b29e6"]]},{"id":"57730195.163f8","type":"function","z":"93cd5a5.2c872a8","name":"バッファ列から各値に変換","func":"var btn = msg.payload.buffer.readUInt16BE(0);\nvar AccX = msg.payload.buffer.readFloatBE(2);\nvar AccY = msg.payload.buffer.readFloatBE(6);\nvar AccZ = msg.payload.buffer.readFloatBE(10);\n\nmsg.payload = {};\n\nmsg.payload.btn = btn;\nmsg.payload.AccX = AccX;\nmsg.payload.AccY = AccY;\nmsg.payload.AccZ = AccZ;\n\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":360,"wires":[["89a1f8f4.f768c8"]]},{"id":"e8016c9f.b29e6","type":"modbus-tcp","z":"93cd5a5.2c872a8","name":"","x":650,"y":360,"wires":[["57730195.163f8"]]}]
```

changeノードでModbus通信の各パラメータを設定し、ファンクションノードで取得したバッファ列からJSONオブジェクトに変換しています。

* changeノードのプロパティ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/424ff5c0-04ba-5e1a-27a3-c0d12093900f.png)

* functionノードのプロパティ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/060e43b2-6958-c304-b356-841da1cf108d.png)

フローを動作させる環境は、PC上のenebular editorでもRaspberry Pi上のenebular-agentでもOKです。条件としては、M5 ATOM Matrixと同じネットワーク内に繋がっていることです。

# 動作確認
M5 ATOM Matrixにスケッチを書き込み、動作させます。
フローのデバッグ出力に取得したボタンの状態と加速度センサーの値が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/ef27fbca-7b34-1449-01f6-32aa31505ce0.png)

M5 ATOM Matrixのボタンを押したり、傾けたりすると値が変化することが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109992/1edadc37-f6f6-cf1d-460b-7209d4f73f65.png)

# 最後に
紹介しているModbus/TCPノードは、工業用の製品との連携で使用された実績もあり、テストされたモジュールです。enebularのオフィシャルで公開しているアセットは、基本的に無料/有料に関わらずテストを行ってリリースしています。今後も利用価値の高いアセットを増やしていきたいと思っています。