LoginSignup
14
4

More than 1 year has passed since last update.

RustでMODBUS(FX5U~PC間通信)

Last updated at Posted at 2021-12-11

1.はじめに

この記事はRust Advent Calendar 2021 (カレンダー2)の11日目の記事です。

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

関連記事

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

2.構成

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

PLC側をMODBUSスレーブ、PC側をMODBUSマスターとします。
image.png

FX5Uシーケンサです。

image.png

3.PLC側の準備

設定の詳細は、別記事 を参照してください。

4.PC側の準備

Rustのインストール参考

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

(1)modbusクレート

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

通信用の関数
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]

(2)コードの作成

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

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

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

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

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 = "*"

とりあえず実行。

コマンドライン
$ 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!

5.実験

(1)出力YリレーをON・OFF

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

出力Y startRegister
Y0 0x00
Y1 0x01
Y2 0x02
Y3 0x03
・・・

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

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を1つ操作
    //操作対象 → 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が4回点滅します。

コマンドライン
$ 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

(3)データレジスタDの読み書き

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

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

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

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の値が変化していることが分かります。

コマンドライン
$ 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

6.まとめ

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

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

参考資料

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4