MELSOFT MaiLabのデモ機を製作してみた① ~M5Stack + InfluxDB v2でセンサデータを時系列保存する編~

目次

はじめに

「MELSOFT MaiLabのデモ機を製作してみた」では、三菱電機のデータ分析ツール「MELSOFT MaiLab 」を実際に体感できるデモ機を作成する様子を複数回に渡り紹介していきます。

MELSOFT MaiLabはシーケンサからデータを取り込んで判断させるのに最適ですが、CSV形式のデータも取り込むことが可能です。そのため今回はCSV出力の下準備として、当社でもよく使用する時系列データベースInfluxDB v2(以下InfluxDB) に、取り扱いセンサーの種類が豊富なM5Stack(本記事ではM5 Stack Dialを使用します。以下 M5 Dial)で取得したデータを蓄積する方法を紹介します。

余談ではありますが、作成したデモ機は2024年5月に開催された「MEX2024」の当社ブースにて展示しました。ブースの様子や実際のデモ機の動作は、株式会社エニイワイヤ様作成の動画にて確認できます(エニイワイヤ様、ありがとうございました)

こちらのデモでは、手動で金属に穴をあける際に、綺麗に穴あけができる動作とそうでない動作をMELSOFT MaiLabで判定させる様子を確認することができます。

MEX2024で紹介したデモ機一式は、当社のソリューション類を展示するショールームにて展示を行なっておりますので、機会がありましたらぜひ見学にいらしてください。

準備

システム構成

今回はボール盤に各種センサー(電流、荷重、回転数、温度、距離)を取り付け、M5Dialを通しWi-Fi経由でサーバPCのInfluxDBにデータを送信および蓄積することを目指します。図1は構成のイメージです。

図1 全体構成
InfluxDB

InfluxData社(アメリカ)が提供する、オープンソースの時系列データベースの1つです。

リンク: https://www.influxdata.com

M5 Stack

M5Stack Technology 社(中国)が展開する、比較的安価に手に入る小型のマイコンやセンサー等のブランドです。

リンク: https://m5stack.com

M5Stack Dial

M5Stackシリーズの中でも珍しい、円形タッチスクリーンを備えたマイコンです。

リンク: https://docs.m5stack.com/en/core/M5Dial

実装

環境構築

各構築は、どの順番から行なっても問題ありません。

InfluxDBのセットアップ

サーバーPCにInfluxDBをインストールします。詳しいセットアップ手順は、本記事では割愛させていただきます。

サーバーPCにNTPサーバとしての機能を持たせる

センサデータをInfluxDBに蓄積する関係上、データ取得時間の管理が非常に重要になってきます。M5DialのWi-Fiはローカル環境のWi-Fiに接続しているため、自身で時刻同期ができません。そのため起動時に、NTPサーバーに問い合わせて時刻同期を行う必要があります。

Arduino IDE の準備

開発PCにM5Dialのプログラミングを行う環境を構築します。今回はArduinoIDEを利用しプログラミングしましたが、お好みでVSCodeのPlatformIO等を利用してもよいと思います。

ボール盤へセンサーを取り付け

今回は図2のように、ボール盤の各箇所にセンサーを取り付けました。

図2 センサー取り付けの様子

プログラム作成

本項では、それぞれのM5Dialで作成したプログラムの一部を紹介します。
※プログラムの全様は、別途GitHubでの公開を検討しています。

参考にしたサイト:
 https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino
 https://docs.m5stack.com/en/core/M5Dial


動作イメージを図3に示します。

図3 各M5Dialの動作イメージ

メインM5Dialの動作をプログラミングする

メインのM5Dialの動作を言語化すると、以下のようになります。

  1.  加工準備が整い次第、物理ボタンを押下する。
  2.  各センサM5DIALに、InfluxDBデータ書き込み開始の指令を送る。
  3.  ~ ここで穴あけ加工動作 ~
  4.  穴の加工精度について評価を行う。(現在はOK・NG・TESTの3種類)
     その後、評価決定のため物理ボタンを押下する。
  5.  各センサM5DIALに、InfluxDBへの書き込み停止の指令を送る。
  6.  穴の加工精度の評価をInfluxDBに書き込む。

今回は「各センサM5DIALに、InfluxDBへの書き込み開始/停止の指令を送る」部分のプログラムの一部を紹介します。

InfluxDBへデータ書き込み(通常)

InfluxDBへ、ライブラリを利用し文字列データを書き込みます。加工の開始/停止タイミング、および加工精度を記録するだけなので時間指定はせず、関数を呼び出した時間を使用する構成にしています。

#include <InfluxDbClient.h>

// 本プログラム動作に必要なライブラリは以下
//#include <M5Dial.h>
//#include <WiFi.h>
//#include <Wire.h>
//#include <HTTPClient.h>

// InfluxDB 設定 --------------------
#define INFLUXDB_URL "http://192.168.xxx.xxx:8086/"     // NTPサーバと同じIPアドレス
#define INFLUXDB_TOKEN "your-influxdb-token"
#define INFLUXDB_ORG "your-influxdb-org"
#define INFLUXDB_BUCKET "your-influxdb-bucket"
#define WRITE_PRECISION WritePrecision::S   // 精度:秒
#define MAX_BATCH_SIZE 15                  // 一度に書き込まれるポイントの数
#define WRITE_BUFFER_SIZE 30               // バッファー内の最大ポイント数(リトライ含む)
InfluxDBClient InfluxDB_client(INFLUXDB_URL, INFLUXDB_ORG, INFLUXDB_BUCKET, INFLUXDB_TOKEN);
Point status("influxdb-point-name");


void InfluxDB_setup() {  // InfluxDBの接続チェック
  while (!InfluxDB_client.validateConnection()) {
    Serial.print("InfluxDB connection failed: ");
    delay(500);
  }
  Serial.print("Connected to InfluxDB: ");
  Serial.println(InfluxDB_client.getServerUrl());
}

void write_InfluxDB(String st) {  // InfluxDBへ書き込む
  status.clearFields();           // ポイントを空にする
  status.addField("Status", st);  
  status.setTime();                                             // 現在時刻をセット
  InfluxDB_client.writePoint(status);                           // InfluxDBに送信
  Serial.println(InfluxDB_client.pointToLineProtocol(status));  // ポイントの内容表示
  // Write point
  if (!InfluxDB_client.writePoint(status)) {
    Serial.print("InfluxDB write failed: ");
    Serial.println(InfluxDB_client.getLastErrorMessage());
  }
}

void btn_process(int mode) {  // ボタン押下時の処理(※画面表示系の処理は割愛)
  int _responcecode = -1;
  switch (mode) {
    case 1:  // メイン→センサ待機(自動で測定中へ)
      write_InfluxDB("start");
      display_mode = 2;
      break;
    case 2:  // 測定中→終了
      write_InfluxDB("stop");
      display_mode = 3;
      break;
    case 3:  // 終了→メイン
      display_mode = 1;
      break;
  }
  delay(100);
}

void setup() {
  InfluxDB_setup()
 // その他セットアップ処理は割愛
}

long oldPosition = -999;
void loop() {
  M5Dial.update();  // 処理の更新
  if (M5Dial.BtnA.wasPressed()) { // ボタン押下処理
    btn_process(display_mode);
  }
}

各センサのM5Dialの動作をプログラミングする

各センサのM5Dialの動作を言語化すると、以下のようになります。

  1.  通常時はセンサ値を取得し、画面に表示する。
  2.  メインM5DialからInfluxDBへの書き込み指令を受信すると、InfluxDBへデータを書き込む。
     ※画面表示は継続する。
  3.  ~ ここで穴あけ加工動作 ~
  4.  メインM5DialからInfluxDBへの書き込み停止指令を受信すると、InfluxDBへデータを書き込むプロセス 
     が完了次第、データの書き込みを停止し、通常時の動作へ戻る。

センサが5種類あるので、本記事ではM5Stack用ADCユニットv1.1(ADコンバータ/電流値取得に使用)を例に、プログラムを作成していきます。ほとんどメインM5Dialと同様のコードになりますが、以下の部分が異なります。

異なるポイント
  • InfluxDBへデータ書き込み(ミリ精度/時間指定)
  • コアを「データストア」と「まとめて送信」で使い分け、リソースの排他制御(セマフォ)を意識

以下、各ポイントを中心にプログラムを紹介していきます。

InfluxDBへデータ書き込み(ミリ精度/時間指定)

ハイライト部分が、メインM5Dialとは異なる設定になります。

#include <InfluxDbClient.h>

// InfluxDB 設定 --------------------
#define INFLUXDB_URL "http://192.168.xxx.xxx:8086/"     // NTPサーバと同じIPアドレス
#define INFLUXDB_TOKEN "your-influxdb-token"
#define INFLUXDB_ORG "your-influxdb-org"
#define INFLUXDB_BUCKET "your-influxdb-bucket"
#define WRITE_PRECISION WritePrecision::MS  // 精度:ミリ秒
#define MAX_BATCH_SIZE 15                   // 一度に書き込まれるポイントの数
#define WRITE_BUFFER_SIZE 30                // バッファー内の最大ポイント数(リトライ含む)
InfluxDBClient InfluxDB_client(INFLUXDB_URL, INFLUXDB_ORG, INFLUXDB_BUCKET, INFLUXDB_TOKEN);
Point status("influxdb-point-name");

実際の動作は排他制御にも関係するので、次節で紹介します。

コアを「データストア」と「まとめて送信」で使い分け、リソースの排他制御(セマフォ)を意識

M5 StampS3はデュアルコアを採用しています。Queue.h ライブラリを利用し、InfluxDBのMAX_BATCH_SIZE分のデータのやり取りをコア間で行うようにしました。また、書き込み時にキューを占有し続けると新規センサデータをストアできないので、キューのデータコピーを利用しています。

#include "Queue.h"

// マルチスレッド 設定 --------------------
TaskHandle_t thp[2];              // マルチスレッドのタスクハンドル格納用
SemaphoreHandle_t taskSemaphore;  // セマフォ(排他制御)用
bool f_write_point = 0;           // 【フラグ】クエリ→ポイント
bool f_write_InfluxDB = 0;        // 【フラグ】ポイント→InfluxDB
// キュー 設定 --------------------
struct queue_struct { // 書き込むキューの構造
  uint16_t adc_result;
  String timestamp;
};
Queue<queue_struct> queue_sensordata(MAX_BATCH_SIZE);
Queue<queue_struct> copy_queue_sensordata(MAX_BATCH_SIZE);

void setup() {
  // 諸々設定(省略)

  // セマフォ&タスク立ち上げ
  taskSemaphore = xSemaphoreCreateBinary();
  xSemaphoreGive(taskSemaphore);
  xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, taskSemaphore, 4, &thp[0], 0);  // データストア
  xTaskCreatePinnedToCore(Core0b, "Core0b", 4096, taskSemaphore, 3, &thp[1], 0);  // InfluxDBへまとめて書き込み
}

void loop() {
  // センサ値取得、画面描画処理(省略)

  if (isOrder == 1) {                                // APIでInfluxDBモードがONになった場合
    M5Dial.Display.drawString("!DB Connecting!", center_x, center_y + 30);
    if (count_datastock == MAX_BATCH_SIZE) {         // メモリ最大キュー数の場合
      f_write_point = 1;                             // 【フラグ】書き込み指示ON
      count_datastock = 0;
    } else {
      String unixtime = calc_timestamp();
      //取得情報をキュー
      xSemaphoreTake(taskSemaphore, portMAX_DELAY);  // セマフォ取得
      queue_sensordata.push(queue_struct{ adc_mA, unixtime });
      xSemaphoreGive(taskSemaphore);                 // セマフォ解放
      Serial.println(adc_mA);                        // キューした情報の表示
      count_datastock++;
    }
  } else {                                           // 残存データがある場合
    if (count_datastock > 0) {
      M5Dial.Display.drawString("!DB Connecting!", center_x, center_y + 30);
      f_write_point = 1;
      count_datastock = 0;
    }
  }
  delay(300);
}

少しセマフォから脱線しますが、今回InfluxDBの時間指定にUnixTimeを使用しています(上記ハイライト部分)。ミリ秒のUnixTimeだったので、計算方法を少し工夫しました。

String calc_timestamp() {  // InfluxDBに渡すタイムスタンプを作成
  // 現在時刻(UNIXTIME)とmillisを取得
  time_t time_now = time(nullptr);
  unsigned long millis_now = millis();

  // 基準値との差を取得し、オフセットを考慮。ミリ秒を0埋めし3桁にする。
  unsigned long diff_millis = millis_now - reference_millis;
  int diff_mS = (diff_millis - Ofset_mS) % 1000;
  char c_diff_mS[3];
  sprintf(c_diff_mS, "%03d", diff_mS);

  // String型に変換し、ミリ秒を考慮したUNIXTIME(String型)を返す。
  String s_diff_mS = c_diff_mS;
  String return_timestamp = (String)time_now + s_diff_mS;
  Serial.println(return_timestamp);

  return return_timestamp;
}

各コアの動作は以下のようになっています。キューから1つずつ取り出し、InfluxDBの入力形式に直して書き込みを行なう構成です(ハイライト部分)。

void Core0a(void* args) {                        // [タスク]キューのコピー
  while (1) {
    delay(1);                                    // コアタスクがバッティングしないように
    if (f_write_point) {
      // Queueをコピー
      SemaphoreHandle_t semaphore = (SemaphoreHandle_t)args;
      xSemaphoreTake(semaphore, portMAX_DELAY);  // セマフォ取得
      Serial.println("query copy start.");
      copy_queue_sensordata = queue_sensordata;
      queue_sensordata.clear();
      xSemaphoreGive(semaphore);                 // セマフォ解放
      Serial.println("query copy end.");
      f_write_InfluxDB = 1;
      f_write_point = 0;
    }
  }
}

void Core0b(void* args) {                        // [タスク]InfluxDBへバッチ書き込み
  while (1) {
    delay(1);                                    // コアタスクがバッティングしないように
    if (f_write_InfluxDB) {
      // QueueをInfluxDB形式へ
      SemaphoreHandle_t semaphore = (SemaphoreHandle_t)args;
      xSemaphoreTake(semaphore, portMAX_DELAY);  // セマフォ取得
      Serial.println("Write InfluxDB start.");
      if (client.isBufferEmpty()) {
        int count = copy_queue_sensordata.count();
        for (int i = 0; i < count; i++) {
          queue_struct item = copy_queue_sensordata.pop();
          Point sensor(INFLUXDB_POINT);
          sensor.addField("ADC_mA", item.adc_result);
          sensor.setTime(item.timestamp);
          client.writePoint(sensor);
        }
        copy_queue_sensordata.clear();
        xSemaphoreGive(semaphore);              // セマフォ解放

        Serial.println("Write InfluxDB end.");
        M5Dial.Speaker.tone(8000, 20);
        f_write_InfluxDB = 0;
      }
    }
  }
}

おわりに

M5Stack Dial+InfluxDBでセンサデータを時系列保存するテクニックについて紹介しました。
指摘事項、掘り下げてほしい箇所等がございましたら、お気軽に問合せフォームやコメントにてご連絡ください。

  • URLをコピーしました!

この記事を書いた人

普段はAI・データ分析に関する業務を行なっています。
MELSOFT MaiLab、AzureOpenAIService、Azure、PowerBI 他

目次