🌮

Sysmac向けMQTT通信ライブラリとDocker上に構築した監視ツールによるモニタリング

2025/01/05に公開

はじめに

本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioとコントローラ(NX1またはNX5)、Docker Desktopを使用します。

今回は、NX向けに作成したMQTT PUBLISHサービスとDocker上に構築した監視ツールを組み合わせます。MQTT通信ライブラリの具体的な使用例です。次のようなダッシュボードを作成します。


InfluxDBのDashbords

監視ツールとネットワークに障害が生じなければ、全サイクルの送出データを取りこぼすことなく収集できます。Composeファイルは複雑ではないので、少し手を加えればRancherやPodmanでも使用できます。

Sysmacプロジェクト

以下にSysmacプロジェクトと監視ツールがあります。

https://github.com/kmu2030/NXMonitorWithInfluxDB

Line protocol形式のデータ生成には以下を使用します。

https://github.com/kmu2030/LineProtocolLib

概要

Docker上の監視ツールは、Mosquitto + Telegraf(MQTT Consumer)+ InfluxDBで構築します。可視化にGrafanaを合わせてもよいです。パーソナルツールという位置づけで使用するため、Docker Composeを使用します。もし、イントラ向けに公開するのであればオーケストレーションツールを使用することを検討します。

監視ツールは、手元のノートPC、場内あるいは別拠点のデスクにある端末、間借りした組織内サーバのどこでも稼働することができます。ただし、ブローカーまでの遅延は問題になります。構成や使用方法によってはネットワークトラフィックが増え、お叱りを受けることになります。気を付けましょう。

複数で同一対象の監視を行うのであれば、MQTTプロトコルを活かし、それぞれで監視ツールを運用し、メッセージを購読して監視するようにします。ハブとなるブローカーを用いるか、コントローラとの通信遅延において最も有利にある端末でブローカーを稼働させ、他はそこに接続します。MQTTプラットフォームの使用もよいですが、通信量が多くなるため使い方によってはコストが大きくなります。今回の用途であれば、すぐに数GBは消費します。

ブローカーは匿名許可かつ非暗号化で構成しています。よって、イントラネットでも不用意に機微な情報を流さないようにします。MQTTSとして運用することもできますが、経路中でTLSインスペクションが行われるようであれば、関係各位と調整します。

可視化はInfluxDBのData Explorerを使用します。使用方法については、公式ドキュメントを参照しつつ触れて慣れるのがよいです。現状、Fluxの積極的な使用は難しい状況にあるため、必要に応じて学習するのが適当です。クエリを組むことで、タイミングチャートのように1サイクル単位での信号変化の確認から、OEEのような生産指標の確認もできます。本格運用やパーソナルツールではない用途を想定する場合、Grafana等の可視化ツールを検討します。

監視ツールは、Sysmac Studioのデータトレースを置き換えるものではありません。装置の立ち上げやトラブル対処にはデータトレースのほうがよいです。監視ツールは、立ち上げ後のいわゆる調整や、性能評価での使用を対象としています。生産に関連して使用するのであれば、可用性を考慮した構成と環境で運用する必要があります。

扱わないこと

次の事項は情報が豊富かつ、PLC固有ではないため扱いません。

  • Docker Composeの使い方
  • Telegrafの使い方
  • InfluxDBの使い方
  • Mosquittoの使い方

使用手順

決まった手順はありませんが、参考に使用手順を示します。

1. 監視ツールの立ち上げ

まず、Docker Composeで監視ツールを立ち上げます。monitor-toolsフォルダをdockerコマンドを実行可能なターミナルで開き、次のコマンドを実行します。

docker compose --profile collect up -d

これで終わりです。問題があればログを確認するだけです。

2. ダッシュボードの作成

監視ツールが立ち上がったら、InfluxDBのダッシュボードを作成します。InfluxDB CLIからダッシュボードを作成できればよいのですが、リストア以外に手が無いため今回は手動で行います。Web APIがあるので、そちらを使う手もあります。InfluxDBのVolumeを破棄しない限り、この作業は一度だけです。次のように、ダッシュボードのテンプレートファイルを適用します。テンプレートはmonitor-toolsフォルダ内にあります。InfluxDBのユーザー/パスワードは、helltaker/chocolatepancakesです。


Dashbordsへのテンプレートインポート

3. コントローラの立ち上げ

監視ツールの準備ができたら、コントローラを立ち上げます。

  1. プロジェクトのファイルハッシュを確認
    プロジェクトファイルが破損しているとどうにもなりません。
  2. プロジェクトのコントローラ形式を使用するコントローラに合わせる
    古いNX1はMQTT通信ライブラリを使用できません。使用可能なバージョンは、MQTT通信ライブラリのマニュアルを確認してください。
  3. POU/プログラム/NotifyStatsのPUBLISHサービス設定の変更
    ブローカーのアドレス、ポート番号、クライアントIDを使用する環境に合わせます。ブローカーは、監視ツールとして立ち上がるMosquittoです。
  4. 構成・設定/コントローラ設定/内臓EtherNet/IPポート設定の確認
    ブローカーへ到達できる設定に変更します。
  5. コントローラ時計を合わせる
    接続するSysmac Studioが動作している端末の時計が適切であれば、それを反映します。NTPを使用する場合、同期したか確認します。
  6. プロジェクトをコントローラにダウンロード
    プログラムを運転モードで動作させ、イベントログにエラーがないか確認します。

構成

監視ツールは、Composeファイルが主たる構成要素です。.envファイルでホスト環境との競合解消とデータ収集対象の設定を行います。シークレット情報は、secretsディレクトリにまとめてあります。NX上のデータ生成コードは、メッセージへの出力フォーマットがLine protocolである以外、大きな変更はありません。

Composeファイル

Composeファイルは次になります。Compose限定であれば、secretsを使用する必要はありません。profiles属性を使用しています。データ収集を行う場合、--profile collectを指定して開始します。

compose.yml
services:
  mosquitto:
    image: "eclipse-mosquitto:2"
    ports:
      - ${MOSQUITTO_MQTT_PORT:-1883}:1883
      - ${MOSQUITTO_WEBSOCKET_PORT:-9001}:9001
    volumes:
      - type: bind
        source: ./mosquitto
        target: /mosquitto
    profiles:
      - collect

  influxdb:
    image: "influxdb:2"
    ports:
      - ${INFLUX_PORT:-8086}:8086
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME_FILE: /run/secrets/influxdb2-admin-username
      DOCKER_INFLUXDB_INIT_PASSWORD_FILE: /run/secrets/influxdb2-admin-password
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN_FILE: /run/secrets/influxdb2-admin-token
      DOCKER_INFLUXDB_INIT_ORG: ${INFLUX_ORGANIZATION:-monitor}
      DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUX_BUCKET:-machine}
    secrets:
      - source: influxdb2-admin-username
        mode: 0444
      - source: influxdb2-admin-password
        mode: 0444
      - source: influxdb2-admin-token
        mode: 0444
    volumes:
      - type: volume
        source: influxdb2-data
        target: /var/lib/influxdb2
      - type: volume
        source: influxdb2-config
        target: /etc/influxdb2

  telegraf-mqtt:
    image: "telegraf:1.33"
    depends_on:
      - mosquitto
      - influxdb
    environment:
      - TELEGRAF_HOSTNAME
      - INFLUX_ORGANIZATION
      - INFLUX_BUCKET
      - TELEGRAF_MQTT_SUB_TOPICS
    volumes:
      - type: bind
        source: ./telegraf/mqtt.telegraf.conf
        target: /etc/telegraf/telegraf.conf
        read_only: true
    secrets:
      - source: influx_token
        mode: 0444
    profiles:
      - collect

secrets:
  influxdb2-admin-username:
    file: ./secrets/.influxdb2-admin-username
  influxdb2-admin-password:
    file: ./secrets/.influxdb2-admin-password
  influxdb2-admin-token:
    file: ./secrets/.influxdb2-admin-token
  influx_token:
    file: ./secrets/.influxdb2-admin-token

volumes:
  influxdb2-data:
  influxdb2-config:

secrets

次のsecretsがあります。平文のテキストです。オーケストレーションツールを使用する場合、それぞれのツールで良いとされているプラクティスに従います。

項目 ファイル
Influx管理者ユーザー secrets/.influxdb2-admin-username helltaker
Influx管理者パスワード secrets/.influxdb2-admin-password chocolatepancakes
Influx管理者トークン secrets/.influxdb2-admin-token THVjaWZlciwgQ0VPIG9mIEhlbGw=

.envファイル

.envファイルでホスト環境と競合しそうな設定とデータ収集対象を変更できます。何らかのFAエンジニアリングツールをインストールしたことがある場合、競合するサービスが稼働している可能性があります。例えば、InfluxDBはエンジニアリングツールのバックエンドとして使用されていることがあります。その場合、InfluxDBのデフォルトポートが使用できず、監視ツールが正常に立ち上がりません。

.env
# Mosquitto settings
MOSQUITTO_MQTT_PORT=1883
MOSQUITTO_WEBSOCKET_PORT=9001

# InfluxDB settings
INFLUX_PORT=8086
INFLUX_ORGANIZATION=monitor
INFLUX_BUCKET=machine

# Telegraf settings
TELEGRAF_HOSTNAME=localhost
TELEGRAF_MQTT_SUB_TOPICS=machine/#

その他の設定ファイル

Mosquitto、Telegrafの設定ファイルがあります。Telegrafは、Composeファイルの内容に合わせてあるので、Composeファイルの内容を変更したら見直します。

データ生成コード

データの生成元となるコントローラ側のコードは次のものです。MQTT PUBLISHサービスで作成したものをLine protocol形式で出力するようにしました。

POU/プログラム/NotifyStatsWithLineProtocol
IF P_First_Run THEN
  // Configure mqtt pub service.
  iServiceName := 'mqtt_pub_1';
  // Connection
  iMqttSettings.BrokerAdr := 'YOUR_BROKER_ADR';
  iMqttSettings.BrokerPort := 1883;
  iMqttSettings.ClientID := 'YOUR_CLIENT_ID';
  iMqttSettings.CleanSession := TRUE;
  iMqttSettings.KeepAlive := 60; //sec
  iMqttSettings.Timeout := 5; //sec
  iMqttSettings.DiscardMsgTime := 1000; //ms
  // Auth, Encrypt
  iMqttSettings.UserName := '';
  iMqttSettings.Password := '';
  iMqttSettings.TLSUse := FALSE;
  iMqttSettings.TLSSessionName := '';
  // Will
  iMqttSettings.WillFlag := TRUE;
  iMqttSettings.WillTopic
    := CONCAT('notify/offline/', iServiceName);
  iMqttSettings.WillMsg
    := CONCAT('[info]$L',
              'type = "machine/service"$L',
              CONCAT('entity = "',iServiceName, '"$L'),
              'origin = "broker"$L',
              'message = "offline"$L');
  iMqttSettings.WillQos := 1;
  iMqttSettings.WillRetain := FALSE;
  // Auto ping
  iMqttSettings.AutoPing := TRUE;
  iMqttSettings.AutoPingInterval := TIME#10s;
  iMqttSettings.AutoPingTimeout := 1000; //ms
  
  iMqttPubService.Settings := iMqttSettings;
  
  // Configure data collection.
  MqttPubRequest_Init(iPubRequest);
  iUtcOffset := TIME#+9h;
  iTrushMessageBufferUsageRate := REAL#0.80;
  iMaxThroughput := REAL#950.0;
END_IF;

// Add task stats to the request.
GetMyTaskStatus(
  LastExecTime=>iLastExecTime,
  MaxExecTime=>iMaxExecTime,
  ExecCount=>iExecCount,
  Exceeded=>iExceeded,
  ExceedCount=>iExceedCount);

Clear(iBuffer);
Clear(iHead);
LP_BEGIN(Measurement:='stats',
         Tags:='type=task,entity=CommTask',
         Buffer:=iBuffer, Head:=iHead);
LP_BOOL(Key:='exceeded', Value:=iExceeded,
        Buffer:=iBuffer, Head:=iHead);
LP_UDINT(Key:='exceed_count', Value:=iExceedCount,
         Buffer:=iBuffer, Head:=iHead);
LP_UDINT(Key:='exec_count', Value:=iExecCount,
         Buffer:=iBuffer, Head:=iHead);
LP_TIME(Key:='last_exec_time', Value:=iLastExecTime,
        Buffer:=iBuffer, Head:=iHead);
LP_TIME(Key:='max_exec_time', Value:=iMaxExecTime,
        Buffer:=iBuffer, Head:=iHead);
LP_END(Timestamp:=GetTime(), UtcOffset:=iUtcOffset,
       Buffer:=iBuffer, Head:=iHead);

MqttPubRequest_AddBytes(Request:=iPubRequest,
                        Content:=iBuffer, Head:=0, Size:=iHead);

// Add mqtt pub service stats to the request.
CASE iMqttPubService.State OF
  MQTT_PUB_SRV_STATE_ACTIVE:
    Clear(iBuffer);
    Clear(iHead);
    LP_BEGIN(Measurement:='stats',
             Tags:=CONCAT('type=service,entity=', iServiceName),
             Buffer:=iBuffer, Head:=iHead);
    LP_UINT(Key:='latency', Value:=iMqttPubService.Latency,
            Buffer:=iBuffer, Head:=iHead);
    LP_UINT(Key:='throughput', Value:=iMqttPubService.Throughput,
            Buffer:=iBuffer, Head:=iHead);
    iThroughputUsage
      := TO_REAL(iMqttPubService.Throughput) / iMaxThroughput;
    LP_REAL(Key:='throughput_usage', Value:=iThroughputUsage,
            Buffer:=iBuffer, Head:=iHead);
    iBufferUsage
      := TO_REAL(MqttPubRequest_MessageSize(iPubRequest) + 22)
        / TO_REAL(MqttPubRequest_MessageCapacity(iPubRequest));
    LP_REAL(Key:='buffer_usage', Value:=iBufferUsage,
            Buffer:=iBuffer, Head:=iHead);
    LP_END(Timestamp:=GetTime(), UtcOffset:=iUtcOffset,
           Buffer:=iBuffer, Head:=iHead);
    
    MqttPubRequest_AddBytes(Request:=iPubRequest,
                            Content:=iBuffer, Head:=0, Size:=iHead);
END_CASE;

// Control mqtt pub service.
CASE iMqttPubService.State OF
  MQTT_PUB_SRV_STATE_STOP:
    // If the service is stopped, it will start immediately.
    iMqttPubService.Enable := TRUE;

  MQTT_PUB_SRV_STATE_ERROR:
    // If an error occurs, the service will be stopped
    // regardless of the content.
    iMqttPubService.Enable := FALSE;
    iMqttPubService.Publish := FALSE;

  MQTT_PUB_SRV_STATE_INACTIVE,
  MQTT_PUB_SRV_STATE_NO_CONNECTION:
    // When the buffer capacity exceeds the threshold value
    // while data cannot be sent, the data is discarded.
    iBufferUsage
      := TO_REAL(MqttPubRequest_MessageSize(iPubRequest))
        / TO_REAL(MqttPubRequest_MessageCapacity(iPubRequest));
    IF iBufferUsage > iTrushMessageBufferUsageRate THEN
      MqttPubRequest_Init(iPubRequest);
    END_IF;

  MQTT_PUB_SRV_STATE_ACTIVE:
    IF NOT MqttPubRequest_IsEmpty(iPubRequest) THEN
      // Add timestamp info to the request.    
      MqttPubRequest_Build(
        Request:=iPubRequest,
        Topic:='machine/stats',
        QoS:=0,
        RetainFlag:=FALSE,
        Timeout:=0);
      iMqttPubService.Request := iPubRequest;
      iMqttPubService.Publish := TRUE;
      
      // Clear the request and start the next collection.
      MqttPubRequest_Init(iPubRequest);
    END_IF;
    
  MQTT_PUB_SRV_STATE_PUBLISHED:
    // After sending the message, drop the flag.
    iMqttPubService.Publish := FALSE;
END_CASE;
iMqttPubService();

稼働

監視ツールが正常に稼働し、コントローラが運転を始めると、次のようなメッセージが延々とNXからブローカー、Telegraf経由でInfluxDBに投げられます。

stats,type=task,entity=CommTask exceeded=F,exceed_count=0u,exec_count=19025721u,last_exec_time=1006540i,max_exec_time=1227185i  1736000445580098695
stats,type=task,entity=CommTask exceeded=F,exceed_count=0u,exec_count=19025722u,last_exec_time=429230i,max_exec_time=1227185i  1736000445584069130
stats,type=task,entity=CommTask exceeded=F,exceed_count=0u,exec_count=19025723u,last_exec_time=419960i,max_exec_time=1227185i  1736000445588105280
stats,type=task,entity=CommTask exceeded=F,exceed_count=0u,exec_count=19025724u,last_exec_time=439705i,max_exec_time=1227185i  1736000445592061605
stats,type=task,entity=CommTask exceeded=F,exceed_count=0u,exec_count=19025725u,last_exec_time=483105i,max_exec_time=1227185i  1736000445596061095
stats,type=service,entity=mqtt_pub_1 latency=39u,throughput=178u,throughput_usage=1.873684e-01,buffer_usage=1.166136e-02  1736000445596182080

これを、InfluxDBのダッシュボードで表示すると次のようにリアルタイム監視が出来るようになります。


InfluxDBのDashbords

10秒ごとにPingを使用しているので、タスク実行時間(CommTask exec time)に角が生えているのが分かります。ウィンドウ期間すべてのデータに抜けが無く、かつ、全てのデータをプロットしているためこのような表示になります。

可視化したデータの目視確認は、事後の状況確認には有効ですが、リアルタイム監視には通知機能を使用したほうがよいです。ダッシュボードの高頻度な更新は端末への負担も大きく、それっぽい見た目を提供するに留まります。ダッシュボードを実際に使用するのであれば、カンバンのようなシンプルな構造にします。

まとめ

"NXでMQTTが簡単に使えますよ"では面白くないので、過去使用したモニタリングツールを思い出しながら合わせてみました。現在のFA向けコントローラの多くは潜在的に情報システムとの十分な接続性を有し、かつ、処理能力もマイコン程度にはあります。Docker ComposeやInfluxDB、Telegrafについては殆ど省きました。とても扱いきれる分量ではありません。このようなシステムを簡単に構築できるとなれば、IT分野とFA分野のエンジニアの交流を作る具体的なトピックになります。業務上も色々と面白いことができるでしょう

Discussion