HakoHiroのアイコン画像
HakoHiro 2024年06月09日作成 (2024年06月11日更新) © MIT
製作品 製作品 閲覧数 120
HakoHiro 2024年06月09日作成 (2024年06月11日更新) © MIT 製作品 製作品 閲覧数 120

光ファイバー秒針ネットワーク時計withデータロガーその1 Googlesheets編

概要

ネットワーク時計を作ります。無垢の木材にLED8×8ドットマトリクス4連を嵌め込み、外周に光ファイバーの秒針を埋め込みます。光ファイバーはLED8×8ドットマトリクス1個に60本取り付けます。時刻はNTP(Network Time Protocol)で時刻同期します。
時間表示だけでは物足りないので、温湿度センサーBME680を取り付け、温湿度、気圧、ガスを検出しそれを表示します。
更に、データーロガーとして、ネットワークを経由してGoogleSheetsにデーターを書き込みます。

構成

物理系H/W

背面ーファイバー取付前
上面ーファイバー取付後

  • 筐体
    葉書大のアフリカ産『ウエンジ』の無垢材を使用。
    比重が0.8〜1.0と非常に重く、堅い木材です。
    木材通販のマルトクショップのサンプル木材を購入
    それを彫り込んで、LED4連モジュールを嵌め込みました。
    また、後述する秒針用光ファイバーの穴を60個キリ加工しました。
  • 光ファイバー
    三菱レイヨン 光ファイバー 『エスカ』φ2mm
    ファイバー切断は超音波カッターを使用⇒切断面が滑らかで光ムラが出ません。
  • ファイバー固定板
    シナ合板をレーザーカッターで2mmの穴を8×8個開けてファイバーを通します。
  • ファイバーとLEDの接合
    当初接着を検討していましたが、ファイバー固定板とLEDユニットをインシュロックで機械的に固定するだけで充分な光量が得られました。
    LED出力レベル0〜15の最低レベル0で充分視認できます。また、隣接ドットLEDの光の回り込みも無く、インシュロック固定で充分でした。

電気系H/W

  • ESP32 DevkitC
    秋月電子通商 1,600円
  • マトリックスLED 8*8ドット4連モジュール
    確かAliExpressで購入したはず。いくらだったかはもう忘れました。安かったのだけ憶えています。
    LED自体は 秋月電子通商にて200〜300円程度/1個
    LEDマトリックスドライバ MAX7219を使用したユニットです。
    MAX7219は1個で8x8ドットのLEDマトリクスを制御できます。
  • 上記マトリックスLED4連モジュールの基板を分割し1個使いします。
    ファイバーを1個のLED毎に取り付ける
  • BME680使用 温湿度・気圧・ガスセンサーモジュールキット
    秋月電子通商 1,320円
    『 ボッシュ(BOSCH)製総合環境センサBME680を使用したセンサモジュールです。圧力、湿度、温度に加えガス(有機溶剤、アルコール等)の検出が可能です。基板上にI2CレベルコンバータICを実装しているので、多くのマイコン、マイコンボードに接続することが可能です。』(秋月電子通商HomePageから引用)

配線

  • 4連マトリックスLED:SPI接続 CLK=GPIO16, DATA=GPIO23, CS=GPIO5
  • 秒針用LED:    SPI接続 CLK=GPIO26, DATA=GPIO25, CS=GPIO27
    MAX7219ライブラリーを駆使すれば、カスケード接続で5連を1つのSPIで制御できるかもしれませんが、ライブラリーを読み解くよりも配線で解決したほうが簡単なので、SPI2系統にしました。
  • BME680:      I2C接続 SDA=21, SCL=22

Soft系

  • 開発PC: Mac mini(intel) OS:Sonoma 14.5
  • 環境:VSCode + PlatformIO
    今回は特に『RandomNerdTutorials.com』の記事が参考になりました。英文ではありますが、内容もわかり易く、ソースも提供されていて、ライブラリーの説明も豊富でIoT関係の方は必読です。
    使用したライブラリーは以下の通りです。

PlatformIO.ini=環境設定ファイル

[env:esp32dev] platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 upload_speed = 115200 lib_deps = adafruit/Adafruit BME680 Library@^2.0.4 mobizt/ESP-Google-Sheet-Client@^1.4.4 majicdesigns/MD_MAX72XX@^3.5.1 majicdesigns/MD_Parola@^3.7.3

MD_Parola, MD_MAX72XX

LEDマトリックスのコントローラー MAX7219用のライブラリーです。
MD_MAX72XX はハードよりで、MD_Parolaは更に抽象化を極めたライブラリーです。
今回は 秒針の制御にMD_MAX72XXを使い、4連LEDの動的スクロール表示にMD_Parolaを使用しています。
ただ、MD_MAX72XXでも、スクロール表示等かなりな所まで使えます。MD_MAX72XXのみでも今回の表示程度でしたら対応可能と思います。

Adafruit BME680

BME680
IoTでよく使われているBME680用のライブラリーです。日本語の解説記事も多いですが、参考として『RandomNerdTutorials.com』の『ESP32: BME680 Environmental Sensor using Arduino IDE (Gas, Pressure, Humidity, Temperature)』の記事がおすすめです。
https://randomnerdtutorials.com/esp32-bme680-sensor-arduino/

ESP-Google-Sheet-Client

データロガーとして、色々な実現手段が考えられるのですが、
無料、PCを占拠したくない、PC停止中もロギング出来るなどの条件を考慮して GoogleSheetsにWeb経由で書き込みすることにしました。Google Apps Scriptを使わないで、表題のライブラリーを使用します。これに関しても『RandomNerdTutorials.com』の『ESP32 Datalogging to Google Sheets (using Google Service Account)』の記事を参考にしました。
https://randomnerdtutorials.com/esp32-datalogging-google-sheets/
前準備から始まってGoogle Service Accountの設定・・・・と詳細に説明されています。英文ではありますが、記載通りに進めてすんなり行きました。ただし、日本版のGoogleSheetのシート名は 『シート1』となっていますが、それを『sheet1』に変更するか、プログラム中で『sheet1』を『シート1』に変更するかのどちらかをしなければなりません。

動作状況

LEDの表示状況はタイトルの動画を御覧ください。(サイズ制限のため、画像が荒いですが)
Googlesheetへの2分間隔でのロギング結果を下記に乗せます。
30秒間隔でも動作OKです。本当は10分間隔ぐらいでロギングしたいのですが、2回目はエラーになってしまいます。このライブラリーの設定の理解度が不足しています。接続再設定をすればよいのかな?
ロギング結果
以下は、別プログラムのESP32上のWebServerにロギングファイルを残し、それをPCからアクセスした状況です。
一見良さそうですが、サーバープログラム、データ管理で手間がかかり、PCまたはスマホを専有してしまいます。当然対応は可能なのですが、自由度がどんどん失われていきます。
GoogleSheetの場合は、データさえ取り込めればシート側で好きなように処理でき、自由度が段違いです。
当方は北海道在住で、建物の密閉度が高いため換気のタイミングを図るためガスセンサーのデータを取って判断していこうと思っています。そのためにも長期間PC無しでロギングしたいのですが、その用途にはGooglesheetを使ったこの方式がBestだと思っています。(コロナのときにやっておけばよかった)
別プログラムのサーバーロギング

プログラム

整理が不十分ですが、ソースコードを以下に記載します。

ネットワーク時計Withデータロガー

// DevKitWatchGoogle-2 // google sheet access timerからmain-loop内に移動 // LED表示も実行 // 6/8 動作OK /* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp32-datalogging-google-sheets/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Adapted from the examples of the Library Google Sheet Client Library for Arduino devices: https://github.com/mobizt/ESP-Google-Sheet-Client */ #include <Arduino.h> #include <WiFi.h> #include <Adafruit_BME680.h> #include <ESP_Google_Sheet_Client.h> #include <MD_MAX72xx.h> #include <MD_Parola.h> #include <time.h> #include <SPI.h> #include <Ticker.h> // #include <GS_SDHelper.h> // For SD/SD_MMC mounting helper #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CLK_PIN 18 // or SCK = 18 #define DATA_PIN 23 // or MOSI = 23 #define CS_PIN 5 // or SS = 5 MD_Parola P = MD_Parola(HARDWARE_TYPE,DATA_PIN,CLK_PIN, CS_PIN, MAX_DEVICES); MD_MAX72XX dot = MD_MAX72XX(HARDWARE_TYPE, 25, 26, 27, 1); //秒表示用 Blue unit Adafruit_BME680 bme; // wifiの設定 const char* ssid = "自分のssid"; const char* password = "自分のpassword"; #define JST 3600*9 const int oneSecond = 1000; // Timer variables unsigned long lastTime = 0; // unsigned long timerDelay = 30000; unsigned long timerDelay = 120000; // ------------------- Google access config ------ // Google Project ID #define PROJECT_ID "GoogleSheetに合わせる" // Service Account's client email #define CLIENT_EMAIL "GoogleSheetに合わせる" // Service Account's private key const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----\nM・・GoogleSheetに合わせる・・・・・\n-----END PRIVATE KEY-----\n"; // The ID of the spreadsheet where you'll publish the data const char spreadsheetId[] = "GoogleSheetに合わせる"; Ticker tkSec; Ticker tkMin; static uint16_t y = 0; static uint8_t x = 0; int seccount = 0; String week[] = {"Sun","Mon","Teu","Wed","Thu","Fri","Sat"}; char ledDate[10]; // mmddFri\n char ledTime[9]; // mm:ss\n char ledBME[50]; char ledMsg[80] = ""; unsigned long epochTime; //------------ function prototype ----------------- void dotSecSet(); void minuteDisplay(); void initWiFi_NTPTime(); void initBME680(); void waitZeroSec(); void getBME680(); void setupGoogleSheet(); unsigned long getTime(); void tokenStatusCallback(TokenInfo info); // Token Callback function //---------------------------- setup ------------------------------- void setup(){ Serial.begin(115200); delay(500); initWiFi_NTPTime(); initBME680(); setupGoogleSheet(); // 4unit 8*8LED表示設定 P.begin(); P.setIntensity(0); //照度最低(0〜15) P.displayText( "Waiting 0sec", PA_LEFT, // 目的の場所 (PA_LEFT, PA_CENTER, PA_RIGHT) 100, // 指定ms毎にスクロールする 50000, // 目的の場所に来てから指定msだけ立ち止まる PA_SCROLL_LEFT, // この効果を使って来る PA_SCROLL_DOWN // この効果を使って帰る ); P.displayAnimate(); // 秒針 表示設定 dot.begin(); dot.control(MD_MAX72XX::INTENSITY,0); //照度1(0〜15) waitZeroSec(); //00秒になるまで待つ dot.clear(); dot.setPoint(0,0,true); tkSec.attach_ms(oneSecond,dotSecSet); //timer割込みー>秒針表示 tkMin.attach_ms(oneSecond*60,minuteDisplay); //timer割込みー>分毎の表示 } //--------------------- loop ------------------------------------ void loop(){ P.displayAnimate(); // Call ready() repeatedly in loop for authentication checking and processing bool ready = GSheet.ready(); if (ready && millis() - lastTime > timerDelay){ lastTime = millis(); FirebaseJson response; Serial.println("\nAppend spreadsheet values..."); Serial.println("----------------------------"); FirebaseJson valueRange; getBME680(); // Get timestamp epochTime = getTime(); valueRange.add("majorDimension", "COLUMNS"); valueRange.set("values/[0]/[0]", epochTime); valueRange.set("values/[1]/[0]", (int)bme.temperature); valueRange.set("values/[2]/[0]", (int)bme.humidity); valueRange.set("values/[3]/[0]", (int)(bme.pressure/100.0)); valueRange.set("values/[4]/[0]", (int)(bme.gas_resistance/1000.0)); // For Google Sheet API ref doc, go to https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append // Append values to the spreadsheet // bool success = GSheet.values.append(&response /* returned response */, spreadsheetId /* spreadsheet Id to append */, "シート1!A1" /* range to append */, &valueRange /* data range to append */); bool success = GSheet.values.append(&response /* returned response */, spreadsheetId /* spreadsheet Id to append */, "Sheet1!A1" /* range to append */, &valueRange /* data range to append */); if (success){ response.toString(Serial, true); valueRange.clear(); } else{ Serial.println(GSheet.errorReason()); } Serial.println(); Serial.println(ESP.getFreeHeap()); } } void setupGoogleSheet(){ GSheet.printf("ESP Google Sheet Client v%s\n\n", ESP_GOOGLE_SHEET_CLIENT_VERSION); // Set the callback for Google API access token generation status (for debug only) GSheet.setTokenCallback(tokenStatusCallback); // Set the seconds to refresh the auth token before expire (60 to 3540, default is 300 seconds) GSheet.setPrerefreshSeconds(30 * 60); // Begin the access token generation for Google API authentication GSheet.begin(CLIENT_EMAIL, PROJECT_ID, PRIVATE_KEY); } // Function that gets current epoch time unsigned long getTime() { time_t now; struct tm timeinfo; if (!getLocalTime(&timeinfo)) { //Serial.println("Failed to obtain time"); return(0); } time(&now); return now; } void tokenStatusCallback(TokenInfo info){ if (info.status == token_status_error){ GSheet.printf("Token info: type = %s, status = %s\n", GSheet.getTokenType(info).c_str(), GSheet.getTokenStatus(info).c_str()); GSheet.printf("Token error: %s\n", GSheet.getTokenError(info).c_str()); } else{ GSheet.printf("Token info: type = %s, status = %s\n", GSheet.getTokenType(info).c_str(), GSheet.getTokenStatus(info).c_str()); } } void initWiFi_NTPTime(){ // WiFiを通して NTPサーバーと同期 WiFi.mode(WIFI_STA); if (String(WiFi.SSID()) != String(ssid)) { WiFi.begin(ssid, password); } while (WiFi.status() != WL_CONNECTED) { delay(500); } configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp"); } void initBME680(){ //+++++++++++++++ BME680 setup; Serial.println(F("BME680 test")); Wire.begin(21,22); //I2C SDA=21 SCL=22 if (!bme.begin()) { Serial.println("Could not find a valid BME680 sensor, check wiring!"); while (1); } // Set up oversampling and filter initialization bme.setTemperatureOversampling(BME680_OS_8X); bme.setHumidityOversampling(BME680_OS_2X); bme.setPressureOversampling(BME680_OS_4X); bme.setIIRFilterSize(BME680_FILTER_SIZE_3); bme.setGasHeater(320, 150); // 320*C for 150 ms } void waitZeroSec(){ struct tm localTime; if (!getLocalTime(&localTime)) { Serial.println("Failed to get the time"); } while(localTime.tm_sec != 0){ delay(100); getLocalTime(&localTime); } } void dotSecSet(){ // Serial.println(seccount); // uuuu seccount 使わないように(割込み考慮) seccount ++; if (seccount >=60){ seccount = 0; dot.clear(); x=0;y=0; dot.setPoint(0,0,true); } else { x ++; if (x==8){ x=0; y++; } dot.setPoint(x,y,true); } } void minuteDisplay(){ struct tm t; x=0;y=0; dot.clear(); dot.setPoint(0,0,true); getBME680(); getLocalTime(&t); sprintf(ledDate,"%02d/%02d %s",t.tm_mon+1,t.tm_mday,week[t.tm_wday]); sprintf(ledTime,"%02d:%02d",t.tm_hour,t.tm_min); sprintf(ledMsg,"%s %s %s ",ledDate,ledBME,ledTime); Serial.println(ledMsg); P.displayReset(); P.setTextBuffer(ledMsg); } void getBME680(){ if (! bme.performReading()) { Serial.println("Failed read BME680"); return; } sprintf(ledBME,"%02.0fC %04ldhPa %02.0f%% %.0fkOhms ",bme.temperature,bme.pressure/100,bme.humidity,bme.gas_resistance/1000.0); }

感想

  • 光ファイバー
    φ2は流石に硬い。単品LEDを埋め込んだほうが良かったかなと思いながらファイバーを取付けていました。
    でも裏面のファイバーの色合いも結構行けるので、筐体の後ろカバーを取付けていません。
  • マトリックスLEDとライブラリー
    非常に高機能なので、ライブラリーの理解の学習コストが高い。
    でも、見場の良いものが簡単に出来る。
  • GoogleSheets
    無料でここまで出来、発展性もあり、特にこういう低速、長期間のデータロガー用途に最適と思います。
    このライブラリーを充分使いこなすだけで、かなりのことが出来、更にGoogle Apps Scriptを使えば自由度は格段に上がると思います。IFTTTが有料になったのでGooglesheetの価値が更に上がりますね。

今後の課題

  • LED表示フォント
    日本人なら曜日は”月火水木金・・・”としたい。
    温度=℃、kOhms=kΩ としたい。ツールはあるので作って組み込むだけなのだが・・・
  • ネットワーク環境がない場合の対応
    ネットワークが無いと何も出来ない時計になってしまったので、デモなどで外に持ち出すと動作できない。アクセスポイントモードでESP32を立ち上げ、スマホなどから時刻合わせを出来るようにしたい。また、ロギング動作も上記のサーバーモードでスマホから可能とする。
  • ESP-Nowによる他の部屋のモニタリング
    いっぱい使っていないESP32があるので、各部屋に設置してモニタリングとデータロギングをしたい。今のネットワーク時計がセンターマシンとなる。冬の北海道は換気のモニタリングと水道凍結対策のモニタリングが重要なので、どちらも臨界値を超えたらアラームを出すようにしたい。

最後に

2024年4月29日に、NT函館に出展しました。ものづくりのメンバーが集まって、elchika投稿者の参加もあり、楽しい大会でした。結構来場者も多かったです。そして、夜は五稜郭で満開の桜の下、ジンギスカン打ち上げでした。また来年も参加したいと思います。

HakoHiroのアイコン画像
函館在住のOldエンジニアです。マイコンは Z80ワンボードの頃から使っています。なにせあの頃は、ハンドアセンブルでプログラム作成していました。(ニーモニックを手書きで書き、それを表を見ながら機械語コードに落とします)おかげで リターンコード(RTN=0xC9)はいまだに覚えています。RTNまで来るとサブルーチンが一段落でホッとするので・・・ 現在は、電子工作の他に錫で色々作って楽しんでいます。
ログインしてコメントを投稿する