はじめに
こちらのデモでは、手動で金属に穴をあける際に、綺麗に穴あけができる動作とそうでない動作をMELSOFT MaiLabで判定させる様子を確認することができます。
準備
システム構成
今回はボール盤に各種センサー(電流、荷重、回転数、温度、距離)を取り付け、M5Dialを通しWi-Fi経由でサーバPCのInfluxDBにデータを送信および蓄積することを目指します。図1は構成のイメージです。
- InfluxDB
-
InfluxData社(アメリカ)が提供する、オープンソースの時系列データベースの1つです。
- M5 Stack
-
M5Stack Technology 社(中国)が展開する、比較的安価に手に入る小型のマイコンやセンサー等のブランドです。
リンク: https://m5stack.com
- M5Stack Dial
-
M5Stackシリーズの中でも珍しい、円形タッチスクリーンを備えたマイコンです。
実装
環境構築
各構築は、どの順番から行なっても問題ありません。
- InfluxDBのセットアップ
-
サーバーPCにInfluxDBをインストールします。詳しいセットアップ手順は、本記事では割愛させていただきます。
- サーバーPCにNTPサーバとしての機能を持たせる
-
センサデータをInfluxDBに蓄積する関係上、データ取得時間の管理が非常に重要になってきます。M5DialのWi-Fiはローカル環境のWi-Fiに接続しているため、自身で時刻同期ができません。そのため起動時に、NTPサーバーに問い合わせて時刻同期を行う必要があります。
- Arduino IDE の準備
-
開発PCにM5Dialのプログラミングを行う環境を構築します。今回はArduinoIDEを利用しプログラミングしましたが、お好みでVSCodeのPlatformIO等を利用してもよいと思います。
- ボール盤へセンサーを取り付け
-
今回は図2のように、ボール盤の各箇所にセンサーを取り付けました。
プログラム作成
本項では、それぞれのM5Dialで作成したプログラムの一部を紹介します。
※プログラムの全様は、別途GitHubでの公開を検討しています。
参考にしたサイト:
https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino
https://docs.m5stack.com/en/core/M5Dial
動作イメージを図3に示します。
メインM5Dialの動作をプログラミングする
メインのM5Dialの動作を言語化すると、以下のようになります。
- 加工準備が整い次第、物理ボタンを押下する。
- 各センサM5DIALに、InfluxDBデータ書き込み開始の指令を送る。
- ~ ここで穴あけ加工動作 ~
- 穴の加工精度について評価を行う。(現在はOK・NG・TESTの3種類)
その後、評価決定のため物理ボタンを押下する。 - 各センサM5DIALに、InfluxDBへの書き込み停止の指令を送る。
- 穴の加工精度の評価を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の動作を言語化すると、以下のようになります。
- 通常時はセンサ値を取得し、画面に表示する。
- メインM5DialからInfluxDBへの書き込み指令を受信すると、InfluxDBへデータを書き込む。
※画面表示は継続する。 - ~ ここで穴あけ加工動作 ~
- メイン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でセンサデータを時系列保存するテクニックについて紹介しました。
指摘事項、掘り下げてほしい箇所等がございましたら、お気軽に問合せフォームやコメントにてご連絡ください。