ElixirでMODBUS（MELSEC-FX5U/Q ～PC間通信）

#１．はじめに

> この記事は[NervesJP Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nervesjp)の6日目の記事です。
5日目は @torifukukaiou さんの[rclexから始めるROS 2](https://qiita.com/torifukukaiou/items/9802f2f26f15778679d7)でした。

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

**（2022年2月：MELSEQ-Qで扱う例を追記しました）**

### 関連記事

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

#２．構成

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)

なんかｴﾗｰ出てるけど･･･

#３．MELSEC FX5U PLC側の準備

GXWork3を起動して、PLCと通信できる状態にします。

##（１）ユニットパラメータの設定

- ①「ナビゲーション」→「ユニットパラメータ」→「Ethernetポート」を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/7d8a1131-db61-0308-7312-618df9b52a22.png)

- ②「IPアドレス設定」に、PLCのIPアドレスを指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/28248073-8be4-a056-6391-4b4900a2acb7.png)

- ③「相手機器接続構成設定」をダブルクリックで開く　→　「ユニット一覧」の「MODBUS/TCP接続機器」をウィンドウ下部のイラストにドラッグ＆ドロップ　→　「設定を反映して保存」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/07cd0ba8-afae-0d6d-3e4c-004b0db8ead9.png)
※この図の「コネクションNo.`1`」が、後述の`UNITID=0x00` に対応します。

- ④「ユニットパラメータ」の「デバイス割り付け」→「詳細設定」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/ff032000-cf72-1804-c90f-cdee5a6db974.png)

- ⑤「MODBUSデバイス割り付けパラメータ」はデフォルトのままでOKです。（そのままOKボタンを押す）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/6306cc9a-a13a-75e1-6b32-d8fb6cea59f0.png)

以上で最小限の設定は完了です。

PLCへの書き込みで「PCパラメータ」をPLCに転送します。電源を入れ直すと、設定が反映されます。

##（２）動作確認用のラダー

PLC側の状態を取得するときの確認のため、簡単なラダーを準備しておきます。

- Y0を2秒おきにON/OFF
- D0を1秒ごとにインクリメント
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/c3f5a657-f459-6324-aa03-6e15c29742b8.png)

PLCへの書き込みで「PCパラメータ」をPLCに転送します。

最後に、PCとPLCが通信できていることを確認するため、パソコン側からPLCにPINGを送ってみます。

```sh:コマンドライン
$ 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ミリ秒
```

#４．PC側の準備

Elixirの[インストール参考](https://elixir-lang.jp/install.html)

```sh:コマンドライン
$ 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)
```

##（１）modbusライブラリ

今回使用するライブラリはこちらです。[^1]
[^1]: メンテが途絶えて久しいようなのでこちらでもOK。https://hex.pm/packages/modbux

https://hex.pm/packages/modbus

```elixir:通信用の関数
{: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で使えるものだけ抜粋）

- https://hexdocs.pm/modbus/Modbus.Tcp.Master.html#exec/3

|機能|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|



```elixir:基本のコード
#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})
```


##（２）コードの作成

MODBUSでお話しするだけの、簡単なコードを作ってみます。

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

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

必要なライブラリを追加します。

```elixir:mix.exs
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
```

ライブラリを取得、ビルド。

```sh:コマンドライン
$ mix hex.local
$ mix deps.get
$ mix deps.compile
```

#５．実験

iexを立ち上げて下準備

```elixir:コマンドライン
$ 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>}
```

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

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

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

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

```elixir:iex
# 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
```

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

##（２）出力Yリレーの状態を取得

startRegisterが`0x00`(=Y0)から８ビット分読み込んでみます。

```elixir:iex
# 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]}
```

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

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

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

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

```elixir:iex
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に書き換えます。

```elixir:iex
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に書き換えます。

```elixir:iex
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に戻って、そこからラダーのフリック動作でインクリメントしていることが分かります。


#６．MELSEC-Q PLCを使う場合

(2022/2追記)

**＜今回トライした環境＞**

スロット＃１の「QJ71MT91」（MODBUS/TCP）で試してみます。

- IOの割り付け例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/84493790-e605-315a-0c9e-a5e1381630cb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/c76f627a-605d-b83f-af28-8a95ce794084.png)

##（１）インテリジェント機能ユニットの設定

- ①「ナビゲーション」→「インテリジェント機能ユニット」→「QJ71MT91」→「スイッチ設定」を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/d9de41df-65e9-c56f-8f4d-e7565d13adea.png)

- ②「スイッチ設定」を画像に倣って設定
  - IPアドレスは各自の環境に合わせてください
  - 「○○パラメータ起動方法」
     - 「ユーザ設定パラメータで起動」に指定
  - 「RUN中書き込み許可・禁止設定」
     - **禁止：**MODBUS経由でのレジスタ・コイルは読み出しだけ可能です。（書き込み命令は無視されてタイムアウトします：詳細は後述します）
     - **許可：（こっちに設定）**MODBUS経由でのレジスタ・コイルの読み書きが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/8bcd6812-1b69-3700-3c23-674fa788d159.png)

- ③保存して、PLCにパラメータを転送する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/6418d42b-8b4c-50b2-65be-c53e344ddf66.png)

- ④全ての電源をOFFして、再度ONすることで、変更したパラメータを反映できます。（保存、転送だけでは、インテリジェント機能ユニットのパラメータ変更は反映されないので注意）

##（２）Wレジスタ、Bレジスタの読み書き例

###コード

```elixir:lib/mb.ex
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
  # ユニット番号（ベースボード１枚なら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の読み書き

```elixir:コマンドライン
$ 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)> 
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/6cbf4211-2fd7-4762-210d-0a66f9184b96.png)

- レジスタWの読み書き

```elixir:コマンドライン
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
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/9037fddd-8a1b-161b-a67a-364765450f1e.png)


##（３）ハマリどころのメモ

###①レジスタの読み込みはできるけど、書き込みができない

MELSEQ-QのMODBUS/TCPユニット（QJ71MT91）へ書き込みしたところ、タイムアウトしてしまう。（PINGは通るし、ソースコード中のinit関数も通っている）

```elixir:コマンドライン
#読み込みは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ランプが点灯しているので、詳細情報を確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/11b72fb7-14b1-674c-e686-387b57cf7523.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/a250d00f-2d0f-b828-eada-c1e227b12ef0.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/6ed8a08b-2cc8-c1b4-5b25-730522dc783a.png)

####処置

先ほど書いたとおり、QJ71MT91のスイッチ設定のなかで「RUN中書き込み許可」に設定が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/72301c64-8ba0-2379-9709-39c99d1786a0.png)


※変更したら、全てのパラメータ情報（PLC本体、インテリジェントバイスも）をPLCに転送して、PLCの電源を入れ直して新しいパラメータを反映させます。


#７．まとめ

めっちゃ久しぶりにQiitaの記事を書きました･･･

今年も[kochi-ex](https://qiita.com/organizations/kochi-ex)さんの勉強会に参加させて頂いて、その中で取り組んだ課題になります。
相変わらず、PLC周りの設定はググっても情報が出てこないので変なところで苦労しました･･･
これから「PLCとElixirを繋いで、ナウでヤングなプロダクトを作るぜ！」とアップをはじめている同志諸君の参考になれば幸いです。

もう一つのライブラリ[modbux](https://hex.pm/packages/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
  - https://github.com/samuelventura/modbus/