RustでMODBUS（FX5U～PC間通信）

#１．はじめに

> この記事は[Rust Advent Calendar 2021 （カレンダー２）](https://qiita.com/advent-calendar/2021/rust)の11日目の記事です。

FA機器を遠隔制御する通信規格「MODBUS」でイーサネット通信に対応した「MODBUS/TCP」を使って、RustからPLCを遠隔制御するテストをしてみました。

### 関連記事

|使用言語||
|---|---|
|Rust|こちらの記事|
|Elixir|https://qiita.com/myasu/items/6d342ea875ee71a723c7|

#２．構成

PC側のOSはWindows10で、UbuntuLinuxはVirtualBoxの上で動かしてます。
GXWorksからPLCへの書き込みは、USBを使わずLAN経由でもOKです。

PLC側をMODBUSスレーブ、PC側をMODBUSマスターとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/a367411a-f610-5ee6-8223-4a2f0d6b6d95.png)

FX5Uシーケンサです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/4079163f-0600-0699-0583-39be437b07a0.png)

#３．PLC側の準備

設定の詳細は、[別記事](https://qiita.com/myasu/items/6d342ea875ee71a723c7#%EF%BC%93plc%E5%81%B4%E3%81%AE%E6%BA%96%E5%82%99) を参照してください。

#４．PC側の準備

Rustの[インストール参考](https://www.rust-lang.org/ja/tools/install)

```sh:コマンドライン
$ rustc --version
rustc 1.54.0 (a178d0322 2021-07-26)
```

##（１）modbusクレート

今回使用するクレートはこちらです。

https://crates.io/crates/modbus

```rust:通信用の関数
pub fn new(addr: &str) -> Result<Transport>

//出力リレーYの状態読み込み
fn read_coils(&mut self, address: u16, quantity: u16) -> Result<Vec<Coil>>
//出力リレーYの操作
fn write_multiple_coils(&mut self, address: u16, coils: &[Coil]) -> Result<()>

//入力Xの状態読み込み
fn read_discrete_inputs(&mut self, address: u16, quantity: u16) -> Result<Vec<Coil>>

//データレジスタDの読み込み
fn read_holding_registers(&mut self, address: u16, quantity: u16) -> Result<Vec<u16>>
//データレジスタDの書き込み
fn write_multiple_registers(&mut self, address: u16, values: &[u16]) -> Result<()>
```

|引数|意味|今回のFX5Uでの例|
|---|---|--|
|addr|対象となるPLCのIPアドレス|192.168.5.41|
|address|対象のレジスタの開始位置|0x00|
|quantity (read系)|読み込みビット数|（後述のコードで）|
|Coil (write系)|出力のON/OFF（リスト型で渡せます）|例→[Coil::Off, Coil::On, Coil::Off, Coil::On]|

##（２）コードの作成

MODBUS経由でPLCを読み書きする、簡単なコードを作ってみます。

```sh:コマンドライン
#作業ディレクトリを作成
$ mkdir work
$ cd work

#プロジェクト作成
$ cargo new modbus_prac
$ cd modbus_prac
```

必要なクレートを追加します。

```rust:Cargo.toml
[package]
name = "modbus_intro"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
// ↓↓追加
modbus = "*"
```

とりあえず実行。

```sh:コマンドライン
$ cargo run
   Compiling modbus_intro v0.1.0 (modbus_intro)
    Building [========================>    ] 8/9: modbus_intro(bin)                                               

    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/modbus_intro`
Hello World!
```

#５．実験

##（１）出力YリレーをON・OFF

GXWorksでのリレーYの割当は以下のようになってます。

|出力Y|startRegister|
|---|---|
|Y0|0x00|
|Y1|0x01|
|Y2|0x02|
|Y3|0x03|
|･･･||

Y0はラダーの方で2secのフリック制御をしているので、ここではY1～Y4までを同時にON・OFFしてみます。

```rust:main.rs
use modbus::tcp;
use modbus::{Client, Coil};
use std::thread;
use std::time::Duration;

const IPADDRPLC: &str = "192.168.5.41";

///エントリーポイント
fn main() {
    println!(" ---  modbus test  ---");

    for n in 0..5 {
        //回数を表示
        println!(" > Count {:?}", n);

        //ウェイト
        thread::sleep(Duration::from_millis(10));

        //出力リレーYの操作(OFF→ON)と状態表示
        out_relay(Coil::On, IPADDRPLC);
        thread::sleep(Duration::from_millis(10));
        read_relay(IPADDRPLC);

        thread::sleep(Duration::from_millis(1000));

        //出力リレーYの操作(ON→OFF)と状態表示
        out_relay(Coil::Off, IPADDRPLC);
        thread::sleep(Duration::from_millis(10));
        read_relay(IPADDRPLC);
    }
    println!(" > end");
}

/// 出力リレーYの状態取得
/// -
/// Arguments
/// * `ipaddr` - 制御対象のPLCのIPアドレス
fn read_relay(ipaddr: &str) {
    println!(" --- > Relay Read");
    let mut client = tcp::Transport::new(ipaddr).unwrap();
    //出力Yを読み込み
    //対象 → Y0～7
    match client.read_coils(0x00, 7) {
        Ok(data) => {
            println!(" ---  > Relay Y0 - Y7 / {:?}", data)
        }
        Err(err) => {
            eprintln!("{}", err.to_string())
        }
    }
}

/// 出力リレーYの操作
/// -
/// Arguments
/// * `val` - コイルのON/OFF
/// * `ipaddr` - 制御対象のPLCのIPアドレス
fn out_relay(val: Coil, ipaddr: &str) {
    println!(" --- > Relay write");

    //PLCに接続
    let mut client = tcp::Transport::new(ipaddr).unwrap();

    //出力Yを１つ操作
    //操作対象 → Y1をON
    match client.write_single_coil(0x01, val) {
        Ok(_) => {}
        Err(err) => {
            eprintln!("{}", err.to_string());
            return;
        }
    }

    //出力Yを複数操作
    //操作対象 → Y2～3をON
    match client.write_multiple_coils(0x02, &[val, val]) {
        Ok(_) => {}
        Err(err) => {
            eprintln!("{}", err.to_string());
            return;
        }
    }

    //閉じる
    match client.close() {
        Ok(_) => {}
        Err(err) => {
            eprintln!("{}", err.to_string());
            return;
        }
    }

    //
    println!(" ---  > Relay Y / {:?}", val)
}
```

実行すると、Y1～Y3が４回点滅します。

```sh:コマンドライン
$ cargo run
   Compiling modbus_intro v0.1.0 (/home/myasu/gitwork/rust/modbus_intro)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/modbus_intro`
 ---  modbus test  ---
 > Count 0
 --- > Relay write
 ---  > Relay Y / On
 --- > Relay Read
 ---  > Relay Y0 - Y7 / [Off, On, On, On, Off, Off, Off, Off]
 --- > Relay write
 ---  > Relay Y / Off
 --- > Relay Read
 ---  > Relay Y0 - Y7 / [On, Off, Off, Off, Off, Off, Off, Off]
 > Count 1
 --- > Relay write
 ---  > Relay Y / On
 --- > Relay Read
 ---  > Relay Y0 - Y7 / [On, On, On, On, Off, Off, Off, Off]
 --- > Relay write
 ---  > Relay Y / Off
 --- > Relay Read
 ---  > Relay Y0 - Y7 / [Off, Off, Off, Off, Off, Off, Off, Off]
・・・（省略）・・・ 
 > end
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/182f3ac4-2368-76fd-1afe-e3baaef6d527.png)

##（３）データレジスタDの読み書き

GXWorksでのデータレジスタDの割当は以下のようになってます。

|データレジスタ|startRegister|
|---|---|
|D0|0x00|
|D1|0x01|
|D2|0x02|
|D3|0x03|
|･･･||

D0はラダーの方で１秒ごとにインクリメントしているので、ここではD0～D7までをまとめて読み込んでみます。D0に相当する0ビット目の値が変化しています。

```rust:main.rs
use modbus::tcp;
use modbus::{Client, Coil};
use std::thread;
use std::time::Duration;

const IPADDRPLC: &str = "192.168.5.41";

///エントリーポイント
fn main() {
    println!(" ---  modbus test  ---");

    for n in 0..5 {
        //回数を表示
        println!(" > Count {:?}", n);

        //ウェイト
        thread::sleep(Duration::from_millis(1000));

        //データレジスタDの変更と状態表示
        write_register(n * 10, IPADDRPLC);
        thread::sleep(Duration::from_millis(10));
        read_register(IPADDRPLC);
    }
    println!(" > end");
}

/// データレジスタDの状態取得
/// -
/// Arguments
/// * `ipaddr` - 制御対象のPLCのIPアドレス
fn read_register(ipaddr: &str) {
    println!(" --- > Register Read");
    let mut client = tcp::Transport::new(ipaddr).unwrap();
    //データレジスタDを読み込み
    //対象 → D0～7
    match client.read_holding_registers(0x00, 8) {
        Ok(data) => {
            println!(" ---  > Registers D0 - D7 / {:?}", data)
        }
        Err(err) => {
            eprintln!("{}", err.to_string())
        }
    }
}

/// データレジスタDに値を書き込み
/// -
/// Arguments
/// * `val` - データレジスタにお設定する値
/// * `ipaddr` - 制御対象のPLCのIPアドレス
fn write_register(val: u16, ipaddr: &str) {
    println!(" --- > Register Write");
    let mut client = tcp::Transport::new(ipaddr).unwrap();
    //データレジスタDに書き込み
    //対象 → D1～3（さらにレジスタ毎にインクリメント）
    match client.write_multiple_registers(0x01, &[val, val + 1, val + 2]) {
        Ok(_) => {
            println!(" ---  > Registers D1 - D3 / {:?}", val)
        }
        Err(err) => {
            eprintln!("{}", err.to_string())
        }
    }
}

```

実行すると、D0がラダーでインクリメントされている様子と、D1～D3の値が変化していることが分かります。

```sh:コマンドライン
$ cargo run
   Compiling modbus_intro v0.1.0 (/home/myasu/gitwork/rust/modbus_intro)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/modbus_intro`
 ---  modbus test  ---
 > Count 0
 --- > Register Write
 ---  > Registers D1 - D3 / 0
 --- > Register Read
 ---  > Registers D0 - D7 / [356, 0, 1, 2, 0, 0, 0, 0]
 > Count 1
 --- > Register Write
 ---  > Registers D1 - D3 / 10
 --- > Register Read
 ---  > Registers D0 - D7 / [357, 10, 11, 12, 0, 0, 0, 0]
 > Count 2
 --- > Register Write
 ---  > Registers D1 - D3 / 20
 --- > Register Read
 ---  > Registers D0 - D7 / [358, 20, 21, 22, 0, 0, 0, 0]
 > Count 3
 --- > Register Write
 ---  > Registers D1 - D3 / 30
 --- > Register Read
 ---  > Registers D0 - D7 / [359, 30, 31, 32, 0, 0, 0, 0]
 > Count 4
 --- > Register Write
 ---  > Registers D1 - D3 / 40
 --- > Register Read
 ---  > Registers D0 - D7 / [360, 40, 41, 42, 0, 0, 0, 0]
 > end
```

#６．まとめ

先日書いたElixir版に引き続いて、Rustでも書いてみました。

ご参考になれば幸いです。


#参考資料

- https://crates.io/crates/modbus
- https://hirschenberger.github.io/modbus-rs/modbus/