#1.はじめに
この記事はNervesJP Advent Calendar 2021の6日目の記事です。
5日目は @torifukukaiou さんのrclexから始めるROS 2でした。
FA機器を遠隔制御する通信規格「MODBUS」でイーサネット通信に対応した「MODBUS/TCP」を使って、ElixirからPLCを遠隔制御するテストをしてみました。
(2022年2月:MELSEQ-Qで扱う例を追記しました)
関連記事
使用言語 | |
---|---|
Elixir | こちらの記事 |
Rust | https://qiita.com/myasu/items/935d46644a37420bd75c |
#2.構成
PC側のOSはWindows10で、UbuntuLinuxはVirtualBoxの上で動かしてます。
GXWorksからPLCへの書き込みは、USBを使わずLAN経由でもOKです。
PLC側をMODBUSスレーブ、PC側をMODBUSマスターとします。
FX5Uシーケンサです。(何故か手元に・・・)
なんかエラー出てるけど・・・
#3.MELSEC FX5U PLC側の準備
GXWork3を起動して、PLCと通信できる状態にします。
##(1)ユニットパラメータの設定
-
③「相手機器接続構成設定」をダブルクリックで開く → 「ユニット一覧」の「MODBUS/TCP接続機器」をウィンドウ下部のイラストにドラッグ&ドロップ → 「設定を反映して保存」
※この図の「コネクションNo.1
」が、後述のUNITID=0x00
に対応します。
以上で最小限の設定は完了です。
PLCへの書き込みで「PCパラメータ」をPLCに転送します。電源を入れ直すと、設定が反映されます。
##(2)動作確認用のラダー
PLC側の状態を取得するときの確認のため、簡単なラダーを準備しておきます。
PLCへの書き込みで「PCパラメータ」をPLCに転送します。
最後に、PCとPLCが通信できていることを確認するため、パソコン側からPLCにPINGを送ってみます。
$ ping 192.168.5.41
PING 192.168.5.41 (192.168.5.41) 56(84) バイトのデータ
64 バイト応答 送信元 192.168.5.41: icmp_seq=1 ttl=63 時間=25.3ミリ秒
64 バイト応答 送信元 192.168.5.41: icmp_seq=2 ttl=63 時間=0.873ミリ秒
64 バイト応答 送信元 192.168.5.41: icmp_seq=3 ttl=63 時間=0.970ミリ秒
^C
--- 192.168.5.41 ping 統計 ---
送信パケット数 3, 受信パケット数 3, パケット損失 0%, 時間 2043ミリ秒
rtt 最小/平均/最大/mdev = 0.873/9.051/25.311/11.497ミリ秒
#4.PC側の準備
Elixirのインストール参考
$ elixir --version
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Elixir 1.11.4 (compiled with Erlang/OTP 23)
##(1)modbusライブラリ
今回使用するライブラリはこちらです。1
{:ok, pid} = Master.start_link([ip: {192, 168, 5, 41}, port: 502])
Master.exec(pid, {functionCode, unitId, startRegister, Value})
引数 | 意味 | 今回のFX5Uでの例 |
---|---|---|
ip | 対象となるPLCのIPアドレス | 192.168.5.41 |
port | 接続先ポート | 502 |
functionCode | 操作コマンド | (補足は後述のコードで) |
unitId | ユニットID(コネクションNo) | 0x00 |
startRegister | 対象のレジスタの開始位置 | 0x00(補足は後述のコードで) |
Value (read系) | 読み込みビット数 | (補足は後述のコードで) |
Value (write系) | コイル:1→ON, 0→OFF、レジスタ→値 | - |
Value (write系複数ビット) | 出力のON/OFF(リスト型で渡せます) | 例→[0, 1, 0, 1] |
functionCode
に記述する内容は以下の通りです。(FX5U PLCで使えるものだけ抜粋)
機能 | atom | (略) |
---|---|---|
コイルYの読み込み | :rc | read coils |
コイルYへ書き込み | :fc | force coils |
コイルXの読み込み | :ri | read inputs |
データレジスタDの読み込み | :rhr | read holding registers |
データレジスタDへ書き込み | :phr | preset holding registers |
startRegister
に記述する内容は以下の通りです。
(前述のGXWorks2/3「MODBUSデバイス割り付けパラメータ」にデフォルトで指定されるアドレスです。よく使いそうなモノだけ抜粋)
項目 | functionCodeのatom | デバイス | MODBUS先頭デバイス番号 |
---|---|---|---|
コイル | :rc / :fc | Y0 | 0 |
〃 | 〃 | M0 | 8192 |
〃 | 〃 | SM0 | 20480 |
〃 | 〃 | B0 | 30720 |
入力 | :ri | X0 | 0 |
保持レジスタ | :rhr / :phr | D0 | 0 |
〃 | 〃 | SD0 | 20480 |
〃 | 〃 | W0 | 30720 |
#start link
{:ok, pid} = Master.start_link([ip: {192, 168, 5, 41}, port: 502])
#Y1コイル書き込み
# ユニットID↓ ↓MODBUS先頭デバイス番号
Master.exec(pid, {:fc, 0x00, 0x01, 1})
#Y1~Y4まで複数同時にコイル書き込み
# ↓配列でまとめて指定も可能
Master.exec(pid, {:fc, 0x00, 0x01, [1, 0, 1, 0]})
#D0レジスタから8ワード読み込み
Master.exec(pid, {:rhr, 0x00, 0x00, 8})
#D1レジスタに1ワード書き込み
Master.exec(pid, {:phr, 0x00, 0x01, 0x12})
##(2)コードの作成
MODBUSでお話しするだけの、簡単なコードを作ってみます。
#作業ディレクトリを作成
$ mkdir work
$ cd work
#プロジェクト作成
$ mix new modbus_prac
$ cd modbus_prac
必要なライブラリを追加します。
defmodule ModbusPrac.MixProject do
use Mix.Project
・・・(省略)・・・
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
# ↓↓追加
{:modbus, "~> 0.3.7"}
]
end
end
ライブラリを取得、ビルド。
$ mix hex.local
$ mix deps.get
$ mix deps.compile
#5.実験
iexを立ち上げて下準備
$ iex -S mix
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Compiling 1 file (.ex)
Interactive Elixir (1.11.4) - press Ctrl+C to exit (type h() ENTER for help)
#予め以下の操作をしておいてpidを準備
iex(1)> alias Modbus.Tcp.Master
Modbus.Tcp.Master
iex(2)> {:ok, pid} = Master.start_link(ip: {192, 168, 5, 41}, port: 502)
{:ok, #PID<0.209.0>}
##(1)出力YリレーをON・OFF
GXWorksでのリレーYの割当は以下のようになってます。
出力Y | startRegister |
---|---|
Y0 | 0x00 |
Y1 | 0x01 |
Y2 | 0x02 |
Y3 | 0x03 |
・・・ |
Y0はラダーの方で2secのフリック制御をしているので、ここではY1~Y4までを同時にON・OFFしてみます。
# Y1~4 ON
iex(3)> Master.exec(pid, {:fc, 0x00, 0x01, [1, 1, 1, 1]})
:ok
# Y1~4 OFF
iex(4)> Master.exec(pid, {:fc, 0x00, 0x01, [0, 0, 0, 0]})
:ok
##(2)出力Yリレーの状態を取得
startRegisterが0x00
(=Y0)から8ビット分読み込んでみます。
# Y0リレーが点滅している
iex(5)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
iex(6)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}
# Y1~4 ON
iex(7)> Master.exec(pid, {:fc, 0x00, 0x01, [1, 1, 1, 1]})
:ok
# 読み込むと、Y1~4がONになっている(フリックしているY0もたまたまONになっている)
iex(8)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [1, 1, 1, 1, 1, 0, 0, 0]}
# Y1~4 OFF
iex(9)> Master.exec(pid, {:fc,0x00, 0x01, [0, 0, 0, 0]})
:ok
# 読み込むと、Y1~4がONになっている(フリックしているY0もたまたまONになっている)
iex(10)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}
##(3)データレジスタDの読み書き
GXWorksでのデータレジスタDの割当は以下のようになってます。
データレジスタ | startRegister |
---|---|
D0 | 0x00 |
D1 | 0x01 |
D2 | 0x02 |
D3 | 0x03 |
・・・ |
D0はラダーの方で1秒ごとにインクリメントしているので、ここではD0~D7までをまとめて読み込んでみます。D0に相当する0ビット目の値が変化しています。
iex(11)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14874, 0, 0, 0, 0, 0, 0, 0]}
iex(12)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14878, 0, 0, 0, 0, 0, 0, 0]}
iex(13)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14880, 0, 0, 0, 0, 0, 0, 0]}
D1=10, D2=16に書き換えます。
iex(14)> Master.exec(pid, {:phr, 0x00, 0x01, [10, 16]})
:ok
iex(15)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [15171, 10, 16, 0, 0, 0, 0, 0]}
D0~D2を0に書き換えます。
iex(16)> Master.exec(pid, {:phr, 0x00, 0x00, [0, 0, 0]})
:ok
iex(17)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}
iex(18)>
D0も0に戻って、そこからラダーのフリック動作でインクリメントしていることが分かります。
#6.MELSEC-Q PLCを使う場合
(2022/2追記)
<今回トライした環境>
スロット#1の「QJ71MT91」(MODBUS/TCP)で試してみます。
##(1)インテリジェント機能ユニットの設定
-
②「スイッチ設定」を画像に倣って設定
- IPアドレスは各自の環境に合わせてください
- 「○○パラメータ起動方法」
- 「ユーザ設定パラメータで起動」に指定
- 「RUN中書き込み許可・禁止設定」
- **禁止:**MODBUS経由でのレジスタ・コイルは読み出しだけ可能です。(書き込み命令は無視されてタイムアウトします:詳細は後述します)
- **許可:(こっちに設定)**MODBUS経由でのレジスタ・コイルの読み書きが可能です。
-
④全ての電源をOFFして、再度ONすることで、変更したパラメータを反映できます。(保存、転送だけでは、インテリジェント機能ユニットのパラメータ変更は反映されないので注意)
##(2)Wレジスタ、Bレジスタの読み書き例
###コード
defmodule Mb do
@moduledoc """
MODBUS / TCP Master test
for MELSEC Q
2022/2/4-5
"""
alias Modbus.Tcp.Master
# 通信相手のIPアドレスとポート
@target_address {192, 168, 5, 51}
@target_port 502
# ユニット番号(ベースボード1枚なら0x00で固定でOK)
@unit_no 0x00
# 先頭MODBUSデバイス番号
# コイル系
@c_Y 0
@c_M 8192
@c_SM 20480
@c_B 30720
# 入力系
@i_X 0
# 入力レジスタ系(MELSEC系は無し)
# 保持レジスタ系
@r_D 0
@r_SD 20480
@r_W 30720
@r_SW 40960
@doc """
Initialize
"""
def init do
Master.start_link(ip: @target_address, port: @target_port, timeout: 2000)
end
@doc """
コイル読み込み
"""
def coilread(addr \\ 0x0) do
# 初期化
{:ok, pid} = init()
# 開始のメッセージ
IO.puts("* coil read [start addr: #{addr}]")
# ループ開始
coilread_loop(pid, addr)
end
@doc """
コイル読み込み・ループ
"""
def coilread_loop(pid, addr, count \\ 0) do
# 先頭アドレスaddrを起点に8ワード読み込み
Master.exec(pid, {:rc, @unit_no, @c_B + addr, 8})
|> IO.inspect(label: " [#{count}] > ")
Process.sleep(500)
# 決まった回数だけ繰り返し
if count < 4 do
coilread_loop(pid, addr, count + 1)
end
end
@doc """
コイル書き込み
"""
def coilwrite(data \\ [1, 1, 1, 1], addr \\ 0x0) do
# 初期化
{:ok, pid} = init()
# 開始のメッセージ
IO.puts("* coil write [start addr: #{addr}] -> data: #{inspect(data)}")
# 書き込み
Master.exec(pid, {:fc, @unit_no, @c_B + addr, data})
|> IO.inspect(label: " > ")
end
@doc """
レジスタ読み込み
"""
def regread(addr \\ 0x0) do
# 初期化
{:ok, pid} = init()
# 開始のメッセージ
IO.puts("* register read [start addr: #{addr}]")
# ループ開始
regread_loop(pid, addr)
end
@doc """
レジスタ読み込み・ループ
"""
def regread_loop(pid, addr, count \\ 0) do
# 先頭アドレスaddrを起点に8ワード読み込み
Master.exec(pid, {:rhr, @unit_no, @r_W + addr, 8})
|> IO.inspect(label: " [#{count}] > ")
Process.sleep(500)
# 決まった回数だけ繰り返し
if count < 4 do
regread_loop(pid, addr, count + 1)
end
end
@doc """
レジスタ書き込み
"""
def regwrite(data \\ [0x6800, 0x4A08, 0x68A3, 0x4A4A, 0x6AAA], addr \\ 0x0) do
# 初期化
{:ok, pid} = init()
# 開始のメッセージ
IO.puts("* register write [start addr: #{addr}] -> data: #{inspect(data)}")
# 書き込み
Master.exec(pid, {:phr, @unit_no, @r_W + addr, data})
|> IO.inspect(label: " > ")
end
end
###実行例
- コイルBの読み書き
$ iex -S mix
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.11.4) - press Ctrl+C to exit (type h() ENTER for help)
#コイルB0~B3まで1を書き込み
iex(1)> Mb.coilwrite
* coil write [start addr: 0] -> data: [1, 1, 1, 1]
> : :ok
:ok
#コイルB0~B8まで読み込み
iex(2)> Mb.coilread
* coil read [start addr: 0]
[0] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
[1] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
[2] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
[3] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
[4] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
nil
iex(3)>
- レジスタWの読み書き
iex(3)> Mbm2.regwrite
* register write [start addr: 0] -> data: [26624, 18952, 26787, 19018, 27306]
> : :ok
:ok
iex(4)> Mbm2.regread
* register read [start addr: 0]
[0] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
[1] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
[2] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
[3] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
[4] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
nil
##(3)ハマリどころのメモ
###①レジスタの読み込みはできるけど、書き込みができない
MELSEQ-QのMODBUS/TCPユニット(QJ71MT91)へ書き込みしたところ、タイムアウトしてしまう。(PINGは通るし、ソースコード中のinit関数も通っている)
#読み込みはOK
iex(1)> Master.exec(pid, {:rhr, @unit_no, @r_W, 8})
1 > : {:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
2 > : {:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
・・・
#書き込みがタイムアウト?
iex(2)> Master.exec(pid, {:phr, @unit_no, @r_W, [24576, 18952, 26787, 19018, 27306]})
12:27:19.491 [error] GenServer #PID<0.207.0> terminating
** (MatchError) no match of right hand side value: {:error, :timeout}
(modbus 0.3.7) lib/master.ex:115: anonymous fn/3 in Modbus.Tcp.Master.exec/3
(elixir 1.11.4) lib/agent/server.ex:16: Agent.Server.handle_call/3
(stdlib 3.14.2.1) gen_server.erl:715: :gen_server.try_handle_call/4
(stdlib 3.14.2.1) gen_server.erl:744: :gen_server.handle_msg/6
(stdlib 3.14.2.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.191.0>): {:get_and_update, #Function<0.72871438/1 in Modbus.Tcp.Master.exec/3>}
State: {#Port<0.12>, 0}
Client #PID<0.191.0> is alive
・・・
ユニットのERRランプが点灯しているので、詳細情報を確認。
↓
↓
####処置
先ほど書いたとおり、QJ71MT91のスイッチ設定のなかで「RUN中書き込み許可」に設定が必要です。
※変更したら、全てのパラメータ情報(PLC本体、インテリジェントバイスも)をPLCに転送して、PLCの電源を入れ直して新しいパラメータを反映させます。
#7.まとめ
めっちゃ久しぶりにQiitaの記事を書きました・・・
今年もkochi-exさんの勉強会に参加させて頂いて、その中で取り組んだ課題になります。
相変わらず、PLC周りの設定はググっても情報が出てこないので変なところで苦労しました・・・
これから「PLCとElixirを繋いで、ナウでヤングなプロダクトを作るぜ!」とアップをはじめている同志諸君の参考になれば幸いです。
もう一つのライブラリmodbuxを使う方は、きっと @kikuyuta センセが記事を書いてくださると思うので!請うご期待!!!
(そして来年はもっとアウトプットしますので、今後ともごひいきに・・・
#参考資料
- https://www.softech.co.jp/mm_210804_tr.htm
- https://qiita.com/orangeman1226/items/162dcd9d72b0c44efdf5
- https://kt2525family.com/cmelsecteletester/
- https://hexdocs.pm/modbus/api-reference.html
-
メンテが途絶えて久しいようなのでこちらでもOK。https://hex.pm/packages/modbux ↩