[PR]記事内のアフィリエイトリンクから収入を得る場合があります
スポンサーリンク

【IoT】プレス機1ショットごとにタイムをグーグルスプレッドシートに送る

IoTでESP32からGSSへデータ転送

IoTについていろいろとYouTubeでも見ているうちに旭鉄工というメーカーが中小企業ながらIoTでかなりリードしているという印象を受けた。

その会社はなにをやっているかというと金属加工をしている会社だが、それぞれの機械の要所にセンサー(リードスイッチや光センサーやら)をつけてデータをクラウドに上げて生産性をモニターしているというのだ。

そしてその実現方法といえばだいたい今自分がハマっているマイコンボードのレベルに近いようでこれは自分でできるんじゃね?って勝手に想像して始めてみたのだ。

詳細を述べる。

スポンサーリンク

▼Amazonや秋月電子で買うのが早いが安さでいったらやはりアリエク

概要

メーカーにはいろいろな機械が置いてあるが自分の勤務先にもほとんど手仕事だけどいくつか機械がある。

その機械の一つでも二つでも旭鉄工社のような装置をつけてサイクルタイムを監視することができれば今後こういう生産監視が肝なんですよと会社にわからせることができよう。

処理の流れ

まず会社にある卓上でもないけど比較的小さいプレス機にリードスイッチを取り付けて、その先にマイコンボードESP32を取り付けて作ってみた。

機器構成とフローチャートは以下のようになる。

  1. プレス機にスイッチ3箇所取り付ける(①上死点②下死点③途中))
  2. 上記3つのスイッチとESP32のポートをつなげ、ESP32でどのスイッチが接続されたか常時監視する
  3. 後述するプログラムをESP32に打ち込んで待つ
  4. やがてプレス機で作業するとA(40mm)、B(60mm)の2種類の部品をいずれか加工しているデータがグーグルスプレッドシートに反映される
  5. Spreadsheet側にもプログラム(GAS)が仕込んであり、ESP32側からは実際にはこのGASプログラム(ウェブアプリ形式にしてある)をコールするとともにデータを後ろにくっつけて渡す
  6. グーグルスプレッドシートのデータを分析し何をすれば時間が短縮されるかは自分たちで考える

ソフトウェア編

ESP32に入れるプログラム

この内容はバグも含んでいるかもしれないし、環境によってはちゃんと動かない場合もあるかもしれないが無保証なので質問はしないでください。

// 参考サイト
// https://qiita.com/Ninagawa_Izumi/items/cc9b960535ec87b7c2e3
// https://qiita.com/Ninagawa_Izumi/items/f75400b579ef6fbcbdf4
// ↓ボード書き込み前にここ確認
#define MotorRoom
/* atMYHOME    // 配置場所ごとに変えることで近いSSIDに接続する
 * HeadQuater
 * MotorRoom
 */
// ↓ボード書き込み前にここ確認 
#define SHEETID 1   // ← 対応するマイコンボードごとにSHEETID変える
/*  0=ローターかしめ
 *  1=シャフト圧入  
 *  2:テストシート
 */
#include <SPI.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <HTTPClient.h>
//#define LOCAL_DEBUG
//#define CLOUD_DEBUG
#define TIMER_FUNCTION
#if defined(TIMER_FUNCTION)
#define SLEEP_START_Hr   8    // ディープスリープする時刻←スリープから
                              // ちゃんと起きなかったので実際には指定時刻にリスタートさせてる
#endif
#include "private.h"         
const char* ssid = SSID_NAME;
const char* pass = PASSWORD;

#define BROWN_LINE_0  25    // 上死点スイッチ
#define ORANGE_LINE_1 26    // 60mm 〃
#define GREEN_LINE_1  27    // 60mm 〃
#define BLUE_LINE_2   13    // 40mm 〃
#define LED_BUILTIN   2

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
volatile int timeCounter1;
//WiFiServer server(80);
const String host = "https://script.google.com/macros/s/" + 
  TOKEN + "/exec";

unsigned long gPreMillis = 0; // 直前CPU時間
unsigned long gNowMillis = 0; // 現在CPU時間
char gStringDate[11];         // 年月日
char gStringTime[9];          // 時分秒
int gSwitchState60 = 1;       // 60mm加工状態
int gSwitchState40 = 0;       // 40mm加工状態
String gModelName = "Start";  // モデル名初期値

#define         JST 3600*9  // 日本標準時間の定義
void DisplayDate(char *);
int wifiDisconnectF = 0;

TaskHandle_t thp[2];        // マルチスレッドのタスクハンドル格納用
QueueHandle_t xQueue_1;     // Queueを使う準備 [キューハンドル名]
int   gShotNum = -1;         // プレスのショット回数
struct QueueData {          // キューに渡す構造体
  int shotnum;              // ショットナンバー
  float cyclTime;           // サイクルタイム
};

#if defined(TIMER_FUNCTION) // タイマー割り込みで日付時刻取得 
void kickTask1() 
{
  char buf[100];
  char stringTime2[9];
  DisplayDate(gStringDate);
  DisplayTime(stringTime2);
  #if SHEETID == 2
  sprintf(buf,"GREEN:%d ORANGE:%d BLUE:%d BROWN:%d",digitalRead(GREEN_LINE_1),
                                                    digitalRead(ORANGE_LINE_1),
                                                    digitalRead(BLUE_LINE_2),                                                    
                                                    digitalRead(BROWN_LINE_0)
                                                    );
  Serial.println(buf);
  #endif
}
void IRAM_ATTR onTimer(){
  if(WiFi.status() != WL_CONNECTED){
    wifiDisconnectF ++;    
  }else{
    wifiDisconnectF = 0;
  }
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);  
}
#endif
void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  pinMode(LED_BUILTIN, OUTPUT); // on-board LED
  pinMode(BROWN_LINE_0, INPUT_PULLUP);
  pinMode(ORANGE_LINE_1, INPUT_PULLUP);
  pinMode(GREEN_LINE_1, INPUT_PULLUP);
  pinMode(BLUE_LINE_2, INPUT_PULLUP);
  // Connect to WiFi

  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, pass);
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    i ++;
    if(i > 20){
      ESP.restart();
    }
  }
  Serial.println("connected");
  digitalWrite(LED_BUILTIN, LOW);
  /////////////////// Arduino OTA 初期設定 //////////////
    // Port defaults to 3232
  // ArduinoOTA.setPort(3232);

  // Hostname defaults to esp3232-[MAC]
   ArduinoOTA.setHostname(ESP32DEVICENAME);

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";

      // NOTE: if updating SPIFFS this would be the place to unmount
      // SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
  ArduinoOTA.begin();
  //////////////////////////////////////////////////////////////////
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

// ここの下数行はタイマー割り込みルーチンを設定するお決まりの命令
#if defined(TIMER_FUNCTION)
  timer = timerBegin(0, 80, true); // 1us
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, TIMER_INTERVAL, true);//5秒
  timerAlarmEnable(timer);
#endif
  // 時刻をJSTに補正する呪文
  configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
  delay(3000);      //ディレイ3秒ぐらいとらないと最初の年取得がおかしい
                    // 3秒でもだめなとき多々ある
  DisplayDate(gStringDate); // 1回ここで日付取得してみてもだめなときある
  gPreMillis = millis();    // 最初のCPU時間取得
  ////////////////////////////////////////////////////////////////////////////
  xQueue_1 = xQueueCreate( 10, sizeof(QueueData) );
  //xQueueCreate( [待ち行列の席数], [一席あたりのバイト数] );

  //スレッドの準備 プレススイッチ検知とクラウドに上げる2つの並列処理定義
  xTaskCreatePinnedToCore(EventOccurrence, "EventOccurrence", 4096, NULL, 1, &thp[1], 1);
  xTaskCreatePinnedToCore(FromQueToWeb, "FromQueToWeb", 8192, NULL, 2, &thp[0], 0);
  //xTaskCreatePinnedToCore(
  //   [タスク名], "[タスク名]", 
  //   [スタックメモリサイズ(4096or8192)], [NULL], 
  //   [タスク優先順位(1-24)] 大きいほど優先順位が高い,
  //   [宣言したタスクハンドルのポインタ(&thp[0])], [CoreID(0or1)]); 
}

void loop() {//メインループ
  ArduinoOTA.handle();          // OTAの命令を入れておく
  delay(1);
  if(timeCounter1 >= 1){        // タイマー処理1回待つ
    // タイマー割り込み経過回数
    portENTER_CRITICAL(&timerMux);
    timeCounter1 = 0;
    portEXIT_CRITICAL(&timerMux);
    kickTask1();  // この処理内で時刻取得処理呼んで指定時刻だったら再起動
  }
  if(wifiDisconnectF >= 3){
    ESP.restart();
  }
}
// プレス機が動いていずれかのスイッチが検知したらそれぞれの処理をする
// メインのloop()とは別で動いている
void EventOccurrence(void *args) {//発生スレッド ①
  QueueData data1;                // キューバッファ確保
  while (1) {
    if( ( digitalRead(GREEN_LINE_1 ) )&&  // 上死点スイッチオフ
        (!digitalRead(ORANGE_LINE_1) )&&  // 60mmスイッチオン
        ( digitalRead(BLUE_LINE_2  ) ) ){ // 40mmスイッチオフ
    // 60mmのスイッチオンになって他のスイッチはオフであるか
    // つまり背の高い部品をプレスしたか判断
      digitalWrite(LED_BUILTIN, HIGH);
      if(gSwitchState40 == 0){            // 40mm状態がオフ
        gSwitchState60 = 1;               // 60mm状態にする
                                          // gSwitchState40はすでに0
        gModelName = MODEL2;              // 作業モデル名(60mm)決定
      }
      delay(50);
      digitalWrite(LED_BUILTIN, LOW);
    }
    if( ( digitalRead(GREEN_LINE_1 ) )&&  // 上死点スイッチオン
        ( digitalRead(ORANGE_LINE_1) )&&  // 60mmスイッチオフ
        (!digitalRead(BLUE_LINE_2  ) ) ){ // 40mmスイッチオン
      digitalWrite(LED_BUILTIN, HIGH);
      // 40mmスイッチONのとき
      gSwitchState40 = 1;                 // 40mm状態オンにする
      gSwitchState60 = 0;                 // 60mm状態オフにする
      gModelName = MODEL1;                // 作業モデル名(40mm)決定
      delay(50);
      digitalWrite(LED_BUILTIN, LOW);
    }
    if( (!digitalRead(GREEN_LINE_1 ) )&&  // 上死点スイッチオン
        ( digitalRead(ORANGE_LINE_1) )&&  // 60mmスイッチオフ
        ( digitalRead(BLUE_LINE_2  ) ) ){ // 40mmスイッチオフ
      digitalWrite(LED_BUILTIN, HIGH);
      // 上死点スイッチ・ON状態
      if((gSwitchState60)||(gSwitchState40)){
      // いずれかの状態がオンであるなら
        gSwitchState60 = 0;               // 60mm状態をクリア
        gSwitchState40 = 0;               // 40mm状態をクリア
        DisplayTime(gStringTime);         // 現在時刻取得
        gNowMillis = millis();            // CPU時間取得
        data1.cyclTime = ((float)gNowMillis - (float)gPreMillis) / 1000;
        // 現在CPU時間-前回CPU時間=所要時間として計算し構造体に入れる
        gShotNum++;                       // ショット番号加算
        data1.shotnum = gShotNum;         // ショット番号も構造体に
        delay(1);
        xQueueSend( xQueue_1, &data1, 0 );// 構造体をキューに入れる
        //xQueueSend( [キューハンドル名], 
        //            [送る値], 
        //            [待ち行列の席が空くまで待つ時間]);
        delay(1);
        gPreMillis = gNowMillis;          // 前回のCPU時間を今回のもので
                                          // 置き換える
      }
      digitalWrite(LED_BUILTIN, LOW);
    }
    delay(1);
  }
}
// キューを監視しあれば取り出しGSSに書き込む処理を呼ぶ
// 同じくメインのloop()とは無関係に動いている
void FromQueToWeb(void *args) {//スレッド ②
  QueueData data1;                        // キュー受け取りバッファ確保
  while (1) {
    /* メッセージ受信待ち */
    if(xQueueReceive( xQueue_1, &data1, 0 )){      
      //xQueueSend([キューハンドル名], [データを受信するアドレス], [キュー空きを待つ最大時間。portMAX_DELAYで永久待ち])
      delay(1);
      #if SHHEID == 2
      Serial.println(host + "?" + "SheetID=" + SHEETID + "&status=" + gModelName + 
        "&progress=" + data1.cyclTime + "&stringDate=" + gStringDate +
        "&stringTime=" + gStringTime + "&shotnumber=" + data1.shotnum
        );
      #endif
      HTTPClient http;                    // お決まりの呪文
      http.begin(host + "?" + "SheetID=" + SHEETID + "&status=" + gModelName + 
        "&progress=" + data1.cyclTime + "&stringDate=" + gStringDate +
        "&stringTime=" + gStringTime + "&shotnumber=" + data1.shotnum
        );
        // http.begin(ホストアドレス("https://script.google.com/macros/s/" +   TOKEN + "/exec")
        // その後ろに"?SheetID=0&status=40mm&&progress=10.00&stringDate=2022/09/25&&stringTime=12:34:56&shotnumber=1
        // みたいな感じでパラメーターが続く
      delay(1);
      int status_code = http.GET();       // 無事送れたか確認
      #if defined(CLOUD_DEBUG)
      Serial.printf("get request: status code = %d\r\n", status_code);
      #endif
      http.end();
    }
    delay(1);
  }
}

上から30行目ぐらいにあるTOKENというのは後述するGoogleAppScriptをデプロイ(アクティベートする意味?)したときに発行されるトークンを入れる。

GSSとGAS

まずはGoogleSpreadsheetでこんな感じの見出しをつけておこう。ただしつけてもつけなくても動くのでどちらでもよい。

GSSの雛形

ここで一応項目の説明をしておく。

ショット№ショット1回毎にインクリメントされる
ESP32起動ごとに初期化され、それ以外はずっとインクリメント。オーバーフローの危険があるので一応毎日再起動するようにプログラムしている
日付作業した日付が入る
時刻プレスした時刻が入るが必ずしも正確に一致はしない
ステータス起動時には1回Startと入り、以後40mmモデルの加工なら40mm、60mmモデルの加工なら60mmと入る
経過秒直前のプレスから何秒経過しているかを小数第2位まで記録する

GAS(GoogleAppScript)の記述

function doGet(e) {   const url = "https://docs.google.com/spreadsheets/d/ここも含めて書き込みたいGSSのシート名入れる/edit#gid=0";   // ↑書き込みたいGSSのフルパスを入れる   const ss = SpreadsheetApp.openByUrl(url);   const sheet = ss.getSheets()[0];   const params = {     // ↓コロン(:)の右のe.parameter.○○文字列とArduinoIDEで書いたプログラム内のパラメータの2つは一致させること     // コロン(:)の左側は変えても良いのかな?     "shotnumber":e.parameter.shotnumber,     "stringDate": e.parameter.stringDate,     "stringTime": e.parameter.stringTime,     "status": e.parameter.status,     "progress": e.parameter.progress   };   // ↓これでシートに1行追加してparamsの内容で左から埋めていってる   // ちなみにシートの途中に変なデータがあったらその下にデータを貼るのでゴミがそこらに散ってないこと   sheet.appendRow(Object.values(params));   return ContentService.createTextOutput('sccess'); }

正直言って内容を完全に理解しきれていないのでほぼ他サイトからのコピペしてるだけだから説明といってもちゃんとできないが一応残しておく。

doGet(e)というのはお決まりの関数宣言なのでこれは変えないほうがいいみたい。eにArduinoIDEで発したhttp.begin()メソッドの内容が入ってくる。

const命令は内容を変えない変数宣言のようで内容を変えたい変数にはvarを使うのかな。

上記プログラムをスプレッドシート画面のメニュー[拡張機能]→[AppScript]と選んで開いた画面で置き換える。

そしたらデプロイして呼べるようになる。

呼べるようになるということはArduinoIDEで編集と書き込みしたESP32からだけでなくブラウザのURLのところにhttps://script.google.com/macros/s/トークン/うんちゃらを入れてもGSSにデータを追加できる。

ハードウェア編

回路図とか準備するの面倒だから写真と言葉のみで残す。

▼適当な箱に入れたESP32をつけたブレッドボードと電源とセンサーコード類。

プレス機にスイッチつけてGSSへデータ送る

▼ESP32の25、26、27ピンに3つのスイッチがつながっている。あとは電源だけ。ちなみにコードはLANケーブル(ツイストペアケーブル)を加工している。複数の信号線を扱うのにはこれが便利だ。

プレス機にスイッチつけてGSSへデータ送る

▼プレス機のピストン最上部に板磁石をつけて横の本来リミットスイッチがつくポールに3箇所リードスイッチをつけてある。

プレス機にスイッチつけてGSSへデータ送る

ESP8266ではだめか?

同じようなボードでESP8266というのがあるが、そちらのほうが安いのでそっちのほうがいいんじゃんって最初思ったが2つの理由で今回は見送った。

1.技適未取得 致命的といえば致命的、些細なことといえば些細なことではあるが一応今回は勤め先の業務の1つとして設置して動作させるのでコンプライアンスは守ろう。個人的なものなら自己責任でどうにかなるけど。

2.どちらかといえばこちらのほうが致命的な理由であるのだがESP8266はマルチスレッド非対応だ。だからスイッチの検出とクラウドサーバーに送信するのをESP32はパラレルにできるがESP8266はシリアルにしかできない。

クラウドサーバーに送信している待ち時間より次のプレスまでのほうが長いというならいいのだが、そうでないならマルチスレッド対応にしないと厳しい。

っていうかキューを上手く使えればできるのかも。

▼いろいろあるから迷ってしまうがものを多くのセラーが扱ってる(楽天と似てる)だけだったりするので安いのを見つけよう。

早くほしいならAmazonか秋月電子。

今回は勤め先の業務だったから一番信頼性の担保できそうな秋月電子で買ったがこれってLEDが基板実装されていないから動いてるんだか動いていなんだかまったく見た目ではわからないのが難点1。

それとプログラムを書き込むときにはいちいちBOOTとENを同時に押してENを先に離すっていう動作をしなければならないのと、書き込み終わったらENを押して再起動させなければならない。これが難点2。

それ以外の点ではスペックはかなり高いのではないかと思う。

ESP32-WROOM-32Eマイコンボード: マイコン関連 秋月電子通商-電子部品・ネット通販

一番早く手に入れられそうだけどやや高いAmazon。

B078WM1YQW
waves NodeMCU-32 開発ボード ESP32 ESP-WROOM-32 WiFi Bluetooth 技適取得済

¥1,280(2022/10/04 08:21時点の価格)
平均評価点:5つ星のうち3.4
>>楽天市場で探す
>>Yahoo!ショッピングで探す

タイトルとURLをコピーしました