リアルタイムOSコントローラ e-RT3 （Elixirから制御編）

#1.はじめに

横河電機の「[リアルタイムOSコントローラ（e-RT3）](https://www.yokogawa.co.jp/solutions/products-platforms/control-system/real-time-os-controller/)」における入出力モジュールの制御を、並行処理や耐障害性、ホットデプロイなどの特徴を持つ[Elixir](https://ja.wikipedia.org/wiki/Elixir_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E))言語から制御する方法をまとめてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/2f2932c0-83b7-f87f-73c9-e1a9696e09c9.png)

目標としては、[前回記事](https://qiita.com/myasu/items/97ac50e968430487d5b5)のように、Elixirのシェルからコマンドで出力リレーの操作、入力の検知ができることとしています。

**リアルタイムOSコントローラ e-RT3 関連記事**

|||
|:--|:--|
|[第1回](https://qiita.com/myasu/items/057ce9ec2fa39148a5a3)|セットアップ編|
|[第2回](https://qiita.com/myasu/items/97ac50e968430487d5b5)|入出力ユニット編　(PythonとC言語から制御)|
|第3回（今回）|Elixirから制御編|
|[第4回](https://qiita.com/myasu/items/605587bd44576699c2f4)|ROS2から制御編|
|[第5回](https://qiita.com/myasu/items/d0f2c2277afdab9b7a20)|Rustから制御編|
|[第6回](https://qiita.com/myasu/items/8b32dc8e66ec4685973c)|Goから制御編|

#2.なぜElixir？

PLCの多くは[ラダーなどの言語](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%80%E3%83%BC%E3%83%BB%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF)で書かれています。
FA系では非常に馴染みのある言語ですが、C#やPythonなどの言語とは大分概念が違うので、IT系の方が触るとなると取っ掛かりに戸惑いがあります。

比較的新しいPLC（e-RT3など）では、LinuxやリアルタイムOS等が自由に動かせる環境が整ってきて、多くの高級言語も容易に扱う事ができるようになりました。

それらの言語の中で、長期に渡る安定動作や分散処理、RUN中書き込み[^run]など、PLCならではの機能を要求する場合に対応出来そうなのが、Elixirかなと考えています。

 - Elixirの特徴（分かり易い記事です）
   - [最速で知る！ ElixirプログラミングとErlang/OTPの始め方](https://eh-career.com/engineerhub/entry/2017/06/12/110000)
 - 組込み向けElixirの可能性について
   - [Nervesが開拓する「ElixirでIoT」の新世界](https://www.slideshare.net/takasehideki/nerveselixiriot)
   - [ElixirでIoT](https://qiita.com/takasehideki/items/94820516ec95b85bae32)

まだまだ作例が少ないのですが、上手くいけばラダーに変わる高級言語になり得るかと期待しているので、個人的に色々模索しています。
現状、筆者自身の組込ElixirのターゲットはRaspberryPiですが、同じDebian系のLinuxが使えるe-RT3でも行けるんじゃないか？と思って、今回の記事にチャレンジしてみました。

[^run]: [RUN中書き込みとは、PLCが動作中でもプログラム変更できる機能](https://www.niwakafa.com/entry/2017/10/11/%E3%80%90%E5%88%9D%E7%B4%9A%E7%B7%A8%E3%80%91PLC%28%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B5%29%E3%81%AE%E3%83%A2%E3%83%8B%E3%82%BF%E3%83%A2%E3%83%BC%E3%83%89%E3%80%81RUN%E4%B8%AD%E6%9B%B8%E3%81%8D#:~:text=RUN%E4%B8%AD%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF%E3%81%A8%E3%81%AF,%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%A7%E3%81%8D%E3%82%8B%E6%A9%9F%E8%83%BD%E3%81%A7%E3%81%99%E3%80%82)

##（参考）組込みElixirに関するコミュニティの紹介

 - [NervesJP](https://nerves-jp.connpass.com/)
   - 関数型言語ElixirによるIoT開発フレームワークNervesのコミュニティです
 - [kochi.ex -- Elixir community in Kochi](https://kochi-ex.connpass.com/)、[高知組み込み会](https://kochi-embedded-meeting.connpass.com/)
   - 高知エリアで活発に活動されている組込みElixirコミュニティです 
 - Qiitaに投稿されている記事
   - [Elixirタグ](https://qiita.com/tags/elixir) 
   - [Nervesタグ](https://qiita.com/tags/nerves) 
   - [e-RT3タグ](https://qiita.com/tags/e-rt3) 

#3.考え方

ここでは、NIFs[^nifs] という仕組みを使います。

[^nifs]: A NIF is a function that is implemented in C instead of Erlang. [NIFs - Erlang](http://erlang.org/doc/tutorial/nif.html)

大ざっぱな操作の流れのイメージは下記の通りです。

[Elixir]
　↓
[nifm3.c] -> [relay.c] -> [libm3.so] -> e-RT3のIOユニット

|名称|役割|
|:--|:--|
|libm3.so|e-RT3のIOユニット制御のライブラリ（メーカから提供）|
|relay.c|`libm3.so`のAPIを操作するC言語のソース|
|nifm3.c|Elixirから`relay.c`の関数を呼ぶためのラッパー（NIFs）|

**libm3.so**の配置状況

```bash:コマンドライン
$ ls -l /usr/local/lib
total 692
lrwxrwxrwx 1 root root      19 Mar 21  2020 libert3dgc.so -> libert3dgc.so.1.1.1
lrwxrwxrwx 1 root root      19 Mar 21  2020 libert3dgc.so.1 -> libert3dgc.so.1.1.1
-rw-r--r-- 1 root root  662644 Mar 17  2020 libert3dgc.so.1.1.1
lrwxrwxrwx 1 root root      14 Mar  5  2019 libm3.so -> libm3.so.1.0.1
lrwxrwxrwx 1 root root      14 Mar  5  2019 libm3.so.1 -> libm3.so.1.0.1
-rw-r--r-- 1 root root   36348 Mar  5  2019 libm3.so.1.0.1
```

#4.Elixirのインストール

別記事を参照してください。

 - [e-RT3にElixirをインストール](https://qiita.com/myasu/items/057ce9ec2fa39148a5a3#4elixir)


#5.ソースコード

長くなるので、主要な部分だけ抜粋してます。
[全てのソースファイルはGithubに置いてあります](https://github.com/trihome/eRT3_nifm3)ので、こちらを参照してください。（ファイル置いただけですので、読みにくくてすみません･･･）

##(1)NIFs

`static ErlNifFunc nif_funcs`のところで、Elixir側から呼び出す関数名と、C言語側（`relayio.c`）との対応付けをしています。

```cpp:nifm3.c
#include <stdio.h>
#include <erl_nif.h>

#include "relayio.h"

（・・・省略・・・）

/**
 * @brief 出力リレー書き込み
 * @param env nif指定の引数
 * @param argc nif指定の引数
 * @param argv nif指定の引数
 * @return ERL_NIF_TERM 
 */
static ERL_NIF_TERM _writeM3OutRelayP(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
    int unit, slot, pos, data;
    int ret = -1;

    //引数を取り出し
    enif_get_int(env, argv[0], &unit);
    enif_get_int(env, argv[1], &slot);
    enif_get_int(env, argv[2], &pos);
    enif_get_int(env, argv[3], &data);

    //処理
    ret = WriteOutRelay1(unit, slot, pos, data);
    if (ret == 0)
        //正常なら、得た値を代入
        ret = data;

    //読み込み結果を返す
    //エラー時は負の値
    return enif_make_uint(env, ret);
}

/**
 * @brief define the array of ErlNifFunc beforehand
 * 
 */
static ErlNifFunc nif_funcs[] =
    {
        // {erl_function_name, erl_function_arity, c_function}
        {"readinrelay", 4, _readM3InRelay},
        {"readinrelay", 3, _readM3InRelayP},
        {"readoutrelay", 4, _readM3OutRelay},
        {"writeoutrelay", 5, _writeM3OutRelay},
        {"writeoutrelay", 4, _writeM3OutRelayP},
};

ERL_NIF_INIT(Elixir.NifTest, nif_funcs, NULL, NULL, NULL, NULL)

```

##(2)API呼び出しのためのC言語ソース

ここは、前記事の[「リアルタイムOSコントローラ e-RT3 （入出力ユニット編）」](https://qiita.com/myasu/items/97ac50e968430487d5b5#3c%E8%A8%80%E8%AA%9E%E7%89%88)を参考に書いています。

```cpp:relayio.c
#include <stdio.h>
#include <ert3/m3lib.h>
#include <errno.h>

#include "relayio.h"

（・・・省略・・・）

/**
 * @brief 出力リレー書き込み
 * 
 * @param unit ユニット番号を指定（0～7）
 * @param slot スロット番号を指定（1～16）
 * @param pos 出力リレー番号を指定（1～64）
 * @param data 書き込みデータ
 * @return int 
 */
unsigned int WriteOutRelay1(int unit, int slot, int pos, unsigned short data)
{
    unsigned int result = -1;

    //API呼び出し
    result = writeM3OutRelayP(unit, slot, pos, data);

    if (result != 0)
        //エラーメッセージ
        result = errcommon((char *)__func__, __LINE__, result);
    else
        //返り値にスロット番号・ポジション・データを付加
        result = (slot * 1000 + pos * 10 + data);

    //書き込み結果を返す
    return result;
}

（・・・省略・・・）

```

```cpp:relayio.h
#ifndef RELAYIO_H
#define RELAYIO_H

int ReadInRelayCommon(int , int , int , int , int*);
unsigned int ReadOutRelayCommon(int , int , int , int , unsigned int*);
unsigned int WriteOutRelay1(int unit, int slot, int pos, unsigned short data);
unsigned int WriteOutRelay(int unit, int slot, int pos, unsigned int data, unsigned int mask);
int errcommon(char *func, int line, int result);

#endif
```

##(3)Makefile

関連するソースファイルをまとめてビルドします。

```makefile:Makefile
#----------------------------------------------------------
# @file Makefile
# @author myasu
# @brief nif for e-RT3 API Makefile
# @version 0.1
# @date 2020-08-10
# 
# @copyright Copyright (c) 2020
#----------------------------------------------------------

CC				= gcc
CFLAGS			= -O4 -Wall -I/usr/local/include -shared -fPIC -I$(ERL_INCLUDE_PATH)
ERL_INCLUDE_PATH=$(shell elixir -e 'IO.puts [:code.root_dir, "/erts-", :erlang.system_info(:version), "/include"]')
DEST			= /usr/local/bin
LDFLAGS			= -L/usr/local/lib
LIBS			= -lm -lm3
OBJS			= nifm3.o relayio.o relayio.h
PROGRAM			= nifm3.so

all:			$(PROGRAM)

$(PROGRAM):		$(OBJS)
				$(CC) $(OBJS) $(CFLAGS) $(LDFLAGS) $(LIBS) -o $(PROGRAM)

clean:;			rm -f *.o *.c.* *~ $(PROGRAM)

install:		$(PROGRAM)
				install -s $(PROGRAM) $(DEST)
```

##(4)Elixir側の実装

```elixir:nif_test.ex
defmodule NifTest do
  @moduledoc """
  Documentation for `NifTest`.
  Elixirからのm3ライブラリへのアクセスサンプル
  """
  @on_load :init

  @doc """
  init
  初期化
  """
  def init do
    #nifのロード
    :erlang.load_nif("./nifm3", 0)
  end

（・・・省略・・・）

  @doc """
  writeoutrelay
  出力リレー書き込み（1ビット単位）
  ## Parameter
    - 1:ユニット番号を指定（0～7）
    - 2:出力リレーを設置のスロット番号を指定（1～16）
    - 3:出力リレー番号を指定（1～32）
    - 4:書き込みデータ（0 / 1）
  ## Examples
      iex> NifTest.writeoutrelay(0,2,1,1)   #出力リレー1だけON
      iex> NifTest.writeoutrelay(0,2,10,0)  #出力リレー10だけOFF
  """
  def writeoutrelay(_, _, _, _) do
    "writeoutrelay/4 : NIF library not loaded"
  end
end
```

####補足

`@on_load :init`は、Elixirのビルドの段階[^onload] で展開されます。（実行時ではない）

[^onload]: https://hexdocs.pm/elixir/Module.html#module-on_load

##(5)ビルド

```bash:コマンドライン
$ make
gcc -O4 -Wall -I/usr/local/include -shared -fPIC -I/home/ert3/.asdf/installs/erlang/22.0.7/erts-10.4.4/include   -c -o relayio.o relayio.c
gcc nifm3.o relayio.o relayio.h -O4 -Wall -I/usr/local/include -shared -fPIC -I/home/ert3/.asdf/installs/erlang/22.0.7/erts-10.4.4/include -L/usr/local/lib -lm -lm3 -o nifm3.so
```

##(6)実行例

モジュールの配置状態は、[第1回の記事](https://qiita.com/myasu/items/057ce9ec2fa39148a5a3#2%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E8%A8%AD%E5%AE%9A)と同じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/07310cb4-3547-cd92-0155-208fdd9a24ed.png)

|スロット番号|モジュール|
|:--:|:--:|
|1|(CPU)|
|2|出力リレー32ch|
|3|入力リレー32ch|
|4|(ADコンバータ)|
|5|(DAコンバータ)|

```bash:コマンドライン
ert3@ert3u:~/gitwork/elixir/nifm3$ iex nif_test.ex 
Erlang/OTP 22 [erts-10.4.4] [source] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]

Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

```

###リレー出力

####①出力リレーをまとめてON/OFF

```Elixir:iexシェル
#出力リレーをまとめてON/OFF
iex(1)> NifTest.writeoutrelay(0,2,1,0xffffffff,0xffffffff)
4294967295
iex(2)> NifTest.writeoutrelay(0,2,1,0x00000000,0xffffffff)
0
```

上記を実行したときの、出力の状態。
全てOFF状態→ON状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/94b040ad-abf2-a4b8-4ba2-d176f6832f07.png)

####②出力リレーの一部だけONにして、その状態を読み取り

```Elixir:iexシェル
#特定の出力リレー1つだけ(ch1)をON/OFF
iex(3)> NifTest.writeoutrelay(0,2,1,1)                    
iex(4)> NifTest.writeoutrelay(0,2,1,0)

#特定の出力リレー1つだけ(ch32)をON/OFF
iex(5)> NifTest.writeoutrelay(0,2,32,1)                    
iex(6)> NifTest.writeoutrelay(0,2,32,0)

#特定の出力リレー2つ（ch1, ch2）をONにして、そのリレーの状態を読み取り
iex(15)> NifTest.writeoutrelay(0,2,1,0x00000011,0xffffffff)
17
iex(16)> NifTest.readoutrelay(0,2,1,2)                     
17

#特定の出力リレー2つ（ch1, ch32）をONにして、そのリレーの状態を読み取り
iex(7)> NifTest.writeoutrelay(0,2,1,1)                    
iex(8)> NifTest.writeoutrelay(0,2,32,1)                    
iex(9)> NifTest.readoutrelay(0,2,1,2)                     
2147483649
```

上記を実行したときの、出力の状態。
ch1と32がON、残りは全てOFF状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/a7561592-5ba6-34ff-ac95-761e3dedc166.png)

###リレー入力

####③入力リレーをまとめて読み取り

```Elixir:iexシェル
#入力リレーをまとめて読み取り(1～32)
iex(2)> NifTest.readinrelay(0,3,1,2)
4294967295
iex(3)> NifTest.readinrelay(0,3,1,2)
0
```

上記を実行したときの、入力（スイッチ）の状態。
全てON状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/72df1ea7-b0a1-982f-a52b-0299bdc5a7a8.png)

####④特定の入力リレー1つだけを読み取り

```Elixir:iexシェル
#特定の入力リレー1つだけ(ch1→ON)を読み取り
iex(4)> NifTest.readinrelay(0,3,1)  
1

#特定の入力リレー1つだけ(ch32→ON)を読み取り
iex(5)> NifTest.readinrelay(0,3,32)
1

#特定の入力リレー1つだけ(ch2→OFF)を読み取り
iex(6)> NifTest.readinrelay(0,3,2)
0
```

上記を実行したときの、入力（スイッチ）の状態。
ch1と32がON、残りは全てOFF状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/c6ba5318-507d-6969-16a3-dcd523bd558f.png)


#6.おわりに

ElixirのNIFsを扱う勉強を兼ねて、試してみました。
今回は、とりあえず”Lチカ”する程度の内容で留めていますが、FA機器の制御をするまで発展させたいと思っています。（実験が纏まるまで、少々お待ちください･･･）



#参考資料

 - 本記事のソースコード
   - https://github.com/trihome/eRT3_nifm3
 - NIF入門
   - [Using C from Elixir with NIFs](https://andrealeopardi.com/posts/using-c-from-elixir-with-nifs/)
   - [NIFsの初歩的な使い方](https://qiita.com/sym_num/items/7c86c5dd298fd29f4ec3)
   - [Elixir で NIF を利用するサンプル](https://gist.github.com/mattsan/1d0fc1a02965f19aa53db0eb5cc5376b)
 - e-RT3 m3ライブラリの使い方
   - [e-RT3 plusの新機能について](https://www.softech.co.jp/mm_161005_firm.htm)
   - [e-RT3 2.0を使った開発方法について](https://www.softech.co.jp/mm_100526_plc.htm)