編集履歴一覧に戻る
toolwareのアイコン画像

toolware が 2021年02月28日13時55分44秒 に編集

HX711モジュールの接続方法を修正

本文の変更

## コーヒースケールって何だろう 今回作ったものはこちらです。 ![M5Stackで作るIoTコーヒースケール](https://camo.elchika.com/4fdf56e4fce8fe5a9540897c073319ece75f409d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64393666613037372d343035382d346332342d393832612d3031663266626632313539302f66376264376333312d383263372d346562382d616131322d636638313862306365303139/) 最近、コーヒースケール(ドリップスケール)というものを知りました。ストップウォッチが付いた「はかり」みたいなものです。 コーヒーをドリップしながら、注いだお湯の量と経過時間を表示していきます。 [ハリオのドリップスケール](https://www.amazon.co.jp/dp/B00TF7AJ64/) お湯の注ぎ方にも「レシピ」があるようです。45秒間隔でまずxx%、次にxx%・・・と5回に分けて注ぐ、みたいな。 これって、時計付きのはかりだから、**前に作った「切れ味測定器」と同じ?** @[youtube](https://youtu.be/uHjjgxWUXwg) 市販品を見てみると、トータルの重さしか測定していません。ドリッパーとカップの両方で測定すれば、注いだお湯の量とできたコーヒーの量、そしてドリッパーを通過する速度がわかるのに。また、データを保存できるものもありますが、高価です。 ## 家にあったパーツで作ってみた 3Dプリンタを作ったときのアルミフレームと、切れ味測定器を作ったときに購入したロードセル、あとはM5StackとHX711のモジュール2枚とロードセルを組み合わせて、プロトタイプを作ってみました。アクリルの部材は切れ味測定器用にカットしたものを流用。 ## 材料 - アルミフレーム - HX711モジュール (x2) - ロードセル (x2)(5kg用) - M5Stack - プロトタイピング基板 - その他、ビスやアクリル版など ## 接続方法 2枚のHX711モジュールにロードセルを接続し、モジュールをM5Stackに接続します。モジュールの裏面のパターンをカットして、80サンプル/秒で測定しています。

-

- HX711モジュール1とM5Stackの接続: DAT-2x CLK-2x - HX711モジュールその2とM5Stackの接続:DAT- CLK-2

+

- HX711モジュール1とM5Stackの接続: DAT-22 CLK-21 - HX711モジュールその2とM5Stackの接続:DAT-36 CLK-26

両方のモジュールのVCCとGNDも、それぞれM5Stackの3.3Vか5V、GNDに接続します。 モジュールとロードセルは、赤、黒、白、緑の4線を指定された場所に接続します。 ![HX711モジュールをM5Stackプロトタイピング基板に](https://camo.elchika.com/eb33d3f134a4f3c4e138c44ee5bd4f5fbe486042/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64393666613037372d343035382d346332342d393832612d3031663266626632313539302f31633165626265632d366261612d343138352d386366342d353535663336663166666231/) ![アルミフレームにドリッパー用ロードセルを取り付け](https://camo.elchika.com/1101e348ee0df197b9e393d778a04a72ce90a16a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64393666613037372d343035382d346332342d393832612d3031663266626632313539302f31326163326631662d323162302d346139632d616237392d623433356135313030303430/) ![カップを載せる台のロードセル](https://camo.elchika.com/e968c9c2f954fbdb7f1a00fe1894f2d34f5242c4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64393666613037372d343035382d346332342d393832612d3031663266626632313539302f64613864633166342d393935352d343132332d386161632d313665336231663737353430/) ## プログラム 以下、ちょっと長いですがプログラムです。HX711のライブラリはいろいろありますが、コメントに記載したものを使用しました。WiFiに接続するためのSSIDとパスワードは各自設定してください。 ```C++:コーヒースケール(プロトタイプ) #include <Arduino.h> #include <HX711.h> //https://github.com/bogde #include <M5Stack.h> #include <WiFiClientSecure.h> #include "time.h" //ネットワーク、Google Spreadsheet関連 const char* M5ID = "CoffeeScale_"; //M5識別用ID、ファイル名にいれる const char* ssid = "xxxxxxxxxx"; // your network SSID (name of wifi network) const char* password = "xxxxxxxxxx"; // your network password const char* host = "script.google.com"; String exec_url = "https://script.google.com/macros/s/xxxxxxxxxx/exec";//postするためのurl WiFiClientSecure client; boolean WiFiOn; // WiFi接続したらtrue String filename, sheetname;// ファイル名、シート名 //****** 時計用 ****** const char* ntpServer = "ntp.jst.mfeed.ad.jp"; const long gmtOffset_sec = 9 * 3600; // 時刻取得用、 const int daylightOffset_sec = 0; struct tm timeinfo; //サウンド関連 #define DAC_PIN 25 uint32_t toneEndMillis;//なっている音を止めるmillis uint8_t tone_volume = 10; //min 1, max 255 uint8_t toneNum, toneStep;//曲番号、音符番号 boolean toneOff = true;//音が出ているとfalse //音名と1/2波長(マイクロ秒) const int16_t C_1 =1902, Cis_1 =1796, D_1 =1695, Dis_1 =1600, E_1 =1510, F_1 =1425, Fis_1 =1345, G_1 =1270, Gis_1 =1198, A_1 =1131, Ais_1 =1068, H_1 =1008, C_2 =951; typedef struct { uint16_t tone; uint16_t duration; } toneData; const toneData myTone[4][2] = { {{C_1, 50},{G_1, 100}}, //計測開始 {{E_1, 50},{A_1,70}}, //レシピ次ステップへ {{G_1, 100},{C_2,200}}, //注湯終了 {{G_1, 150},{C_1, 150}} //測定終了 }; //タイマー関連 #define TIMER0 0 hw_timer_t * samplingTimer = NULL; volatile int t0flag; //****** HX711 pins etc ****** //Dripper const int DATDrp = 22; const int CLKDrp = 21; //Cup const int DATCup = 36; const int CLKCup = 26; HX711 scaleDrp, scaleCup;//HX711 for dripper and cup double weightDrp, weightCup;//計測したドリッパーとカップの重量 double weightDrpAvg = 0, weightCupAvg = 0;//表示するドリッパーとカップの重量 double weightDrpPrev=0, weightCupPrev=0;//前回のドリッパーとカップの重量 double weightDrpDisp=0, weightCupDisp=0;//表示用のドリッパーとカップの重量 double weightDrpMax=0, weightCupMax=0;//ドリッパーとカップの最大値 double weightPrev = 0; //抽出速度計算用 double dripVelocity; //抽出速度 double weightDrpData[300], weightCupData[300];//データ保存用配列 int targetData[300];//表示されていたターゲット重量保存用配列 long dataSavedMillis = 0;//前回保存の時刻 int16_t secCounter = 0;//データ記録のインデックス 経過秒数をカウント int target = 100; //抽出するコーヒーの量の初期値 int recipe[] = {18, 40, 60, 80, 100}; //抽出のレシピ 100分率積算で指定 現状固定 int recipeSteps = sizeof(recipe)/sizeof(int);//レシピの総ステップ数 int recipeCount = 0; //レシピの何ステップ目にいるか int minute = 0, sec = 0; //経過時間表示用の分と秒 long startMillis = 0; //計測スタート時点のミリ秒 long soundMillis; //音を出し始めたミリ秒 bool measuring = false; //計測中かどうか bool targetReached = false; //注湯量がターゲット到達 bool measureEnded = false; //ドリッパー外して計測終了 bool targetSet = false; //目標重量設定済かどうか int gap;//レシピの重量と現在の重量の差 void LcdInit(){ M5.Lcd.clear(BLACK); M5.Lcd.setTextSize(2); M5.Lcd.setTextColor(WHITE, BLACK); M5.Lcd.setCursor(0, 0); } void connectingWiFi(){ int n_trial, max_trial = 10; //WiFi接続試行回数とその上限 WiFi.mode(WIFI_STA); WiFi.disconnect(); WiFi.begin(ssid, password); LcdInit(); M5.Lcd.print("Connecting to WiFi"); // attempt to connect to Wifi network: n_trial = 0; while (WiFi.status() != WL_CONNECTED && n_trial < max_trial) { M5.Lcd.print("."); // wait 1 second for re-trying n_trial++; delay(1000); } LcdInit(); if (WiFi.status() == WL_CONNECTED) {WiFiOn = true; M5.Lcd.print("Connected to "); M5.Lcd.println(ssid); M5.Lcd.print("IP: "); M5.Lcd.println(WiFi.localIP()); } else {WiFiOn = false; M5.Lcd.print("WiFi connection failed"); } delay(500); } void IRAM_ATTR onTimer0() { // タイマ割込関数 if(t0flag == 1){ dacWrite(DAC_PIN, tone_volume); t0flag = 0;} else{ dacWrite(DAC_PIN, 0); t0flag = 1; } } //音を出す void toneStart(int16_t t_length, int16_t t_duration){ timerAlarmWrite(samplingTimer, t_length, true); // 周期 マイクロ秒単位 timerAlarmEnable(samplingTimer); // タイマを動かして音を出す toneEndMillis = millis()+t_duration;//グローバル変数に音を止めるmillisをセット toneOff = false;//音がなっている } //ドリッパーとカップをnumMeasure回計測して平均する void measureWeight(int numMeasure){ weightDrp = 0; weightCup = 0; for (int i=0; i<numMeasure; i++){ weightDrp += scaleDrp.get_units(); weightCup += scaleCup.get_units(); } weightDrp /= numMeasure; weightCup /= numMeasure; } //データをpostする String postValues(String values_to_post) { M5.Lcd.println("postValues");delay(1000); if (client.connect(host, 443)){ M5.Lcd.println("Posting data..."); client.println("POST " + exec_url + " HTTP/1.1"); client.println("HOST: " + (String)host); client.println("Connection: close"); client.println("Content-Type: text/plain"); client.print("Content-Length: "); client.println(values_to_post.length()); client.println(); client.println(values_to_post); delay(100); while (client.available()) { char c = client.read(); M5.Lcd.print(c); } client.stop(); return "post end"; } else { return "ERROR"; } } //グラフ描画 void drawGraph(){ int i;//ループ用 int XindexStep, YindexStep;//X軸、Y軸の数字の見出し間隔 double xStep, yStep, maxY=0; LcdInit(); for (i=0;i<secCounter;i++){//secCounterは最終保存インデックス+1、すなわちデータの個数になっている if (weightDrpData[i]+weightCupData[i] > maxY){maxY = weightDrpData[i] + weightCupData[i];}//重量データの最大値を求める if (targetData[i] > maxY){maxY = targetData[i];}//目標データが最大値になる場合もある 途中でやめた場合など } //プロットの範囲は(10,3)から(316,210)とする。円全体がはみ出さないように。 xStep = 306/(double)secCounter; yStep = 207/(double)maxY; Serial.print("xStep=");Serial.println(xStep); if (secCounter > 200){XindexStep = 100;}//X軸の見出しを100毎に else if (secCounter > 50){XindexStep = 10;}//10毎に else XindexStep = 5;//5毎に if (maxY > 200){YindexStep = 100;} else if (maxY > 50){YindexStep = 10;} else YindexStep = 5; //軸と見出し M5.Lcd.drawLine(10,0,10,210,TFT_WHITE);//Y軸 M5.Lcd.drawLine(10,210,319,210,TFT_WHITE);//x軸 M5.Lcd.setTextSize(1); M5.Lcd.setTextColor(TFT_WHITE,TFT_BLACK); M5.Lcd.setCursor(8,213); M5.Lcd.print("0"); for (i=1;i<=20;i++){ //見出しの数は最大20個 if (i * XindexStep * xStep +8 < M5.Lcd.width()){ M5.Lcd.setCursor(i * XindexStep * xStep + 8 ,213); M5.Lcd.print(i*XindexStep); } if (208 - i * YindexStep * yStep < M5.Lcd.height()){ M5.Lcd.setCursor(0, 208 - i * YindexStep * yStep);//文字は左上基準なので208とした if (i*YindexStep < 100){M5.Lcd.print(" ");}//2桁の場合の位置合わせ M5.Lcd.print(i*YindexStep); } } //データプロット for (i=0;i<secCounter;i++){ M5.Lcd.fillCircle(10 + i * xStep, 210 - weightCupData[i] * yStep, 2, TFT_WHITE); M5.Lcd.fillCircle(10 + i * xStep, 210 - weightDrpData[i] * yStep, 2, TFT_GREEN); M5.Lcd.fillCircle(10 + i * xStep, 210 - targetData[i] * yStep, 2, TFT_YELLOW); M5.Lcd.fillCircle(10 + i * xStep, 210 - (weightDrpData[i] + weightCupData[i]) * yStep, 2, TFT_RED); } } //データ保存か破棄か void savingData(){ boolean saveOrDiscard = false; M5.Lcd.setCursor(38,220); M5.Lcd.setTextSize(2); M5.Lcd.setTextColor(TFT_WHITE); M5.Lcd.print("Save Reset "); M5.Lcd.setCursor(30,100); while(saveOrDiscard == false){ M5.update(); if (M5.BtnA.wasReleased()|| M5.BtnA.wasPressed()){ M5.Lcd.print("save"); saveOrDiscard = true; } if ((M5.BtnB.wasReleased()|| M5.BtnB.wasPressed()) && (saveOrDiscard == false)){ LcdInit(); M5.Lcd.print("Resetting"); delay(500); M5.Power.reset(); } } LcdInit(); M5.Lcd.setTextSize(2); M5.Lcd.println("saving data"); filename = M5ID; // ファイル名はM5IDで始める filename += String(timeinfo.tm_year-100,DEC);//1900年から数えるので調整 if(timeinfo.tm_mon<9){filename += "0";}//0からはじまるので調整 filename += String(timeinfo.tm_mon+1,DEC);//0からはじまるので調整 if(timeinfo.tm_mday<10){filename += "0";} filename += String(timeinfo.tm_mday,DEC); filename += ".csv"; M5.Lcd.println("Filename =");M5.Lcd.println(filename); sheetname =""; // シート名初期化 以下、時分秒からシート名作成 if(timeinfo.tm_hour<10){sheetname = "0";} sheetname += String(timeinfo.tm_hour,DEC); if(timeinfo.tm_min<10){sheetname += "0";} sheetname += String(timeinfo.tm_min,DEC); if(timeinfo.tm_sec<10){sheetname += "0";} sheetname += String(timeinfo.tm_sec,DEC); M5.Lcd.print("Sheetname =");M5.Lcd.println(sheetname); String values = filename; values += ","; values += sheetname; values += ",5,";//データは5列 values += "Second, Dripper, Cup, Target, Total";//1行目は見出し for (int i=0;i<secCounter;i++){//以下、データを格納 values += ","; values += i; values += ","; values += weightDrpData[i]; values += ","; values += weightCupData[i]; values += ","; values += targetData[i]; values += ","; values += weightDrpData[i]+weightCupData[i]; } String response = postValues(values); M5.Lcd.print(response); M5.Lcd.setCursor(30,210);//以下ボタンが押されるまで待ちリセット M5.Lcd.setTextSize(3); M5.Lcd.print(" Reset "); M5.Lcd.setCursor(30,50); M5.Lcd.setTextSize(4); while(1){ M5.update(); if (M5.BtnB.wasReleased()|| M5.BtnB.wasPressed()){ LcdInit(); delay(100); M5.Lcd.print("Resetting"); delay(500); M5.Power.reset(); } } } void setup() { M5.begin(); Serial.begin(115200); connectingWiFi(); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); M5.Lcd.println("configTime OK."); getLocalTime(&timeinfo); //時刻取得 delay(1000); M5.Lcd.clear(BLACK); //注湯する重量の設定 M5.Lcd.setTextSize(3); M5.Lcd.setTextColor(WHITE, BLACK); M5.Lcd.setCursor(0, 10); M5.Lcd.print("Coffee Scale 01\n"); M5.Lcd.setCursor(0, 50); M5.Lcd.print("Set Coffee Grams.\n"); M5.Lcd.setCursor(30,210); M5.Lcd.print("+100 +10 SET"); M5.Lcd.setTextSize(2); M5.Lcd.setCursor(190,75); M5.Lcd.print("(max 500g)"); M5.Lcd.setTextColor(TFT_RED, BLACK); while(targetSet == false){ M5.update(); if (M5.BtnA.wasReleased()){ if (target <= 400){target += 100;}//最大値は500g 超えるとゼロに戻る else {target = 0;}} if (M5.BtnB.wasReleased()){ if (target <= 490){target += 10;}//調整は10g単位 else {target = 0;}} if (M5.BtnC.wasReleased() || M5.BtnC.wasPressed()){targetSet = true;}//セット完了 M5.Lcd.setCursor(50,100); M5.Lcd.setTextSize(5); M5.Lcd.print(target);M5.Lcd.print("g "); } //タイマー関連 samplingTimer = timerBegin(TIMER0, 80, true); // 分周比80、1μ秒のタイマを作る timerAttachInterrupt(samplingTimer, &onTimer0, true); // タイマ割込みハンドラを指定 scaleDrp.begin(DATDrp, CLKDrp);//スケール初期化 scaleCup.begin(DATCup, CLKCup); scaleDrp.set_scale(447.5f); // this value is obtained by calibrating the scale with known weights; see the README for details scaleCup.set_scale(446.0f); // ここの値は実際に計測してみて決定 delay(10); scaleDrp.tare();// スケールをゼロにリセット scaleCup.tare(); delay(500); M5.Lcd.clear(BLACK); } void loop() { measureWeight(5);//5回計測して平均 if ((weightDrp>weightDrpPrev+500)||(weightDrp<weightDrpPrev-500)){weightDrp=weightDrpPrev;}//稀に大きく外れた値が測定される if ((weightCup>weightCupPrev+500)||(weightCup<weightCupPrev-500)){weightCup=weightCupPrev;}//500g以上変動した値は捨てる if (weightDrp > weightDrpMax){weightDrpMax = weightDrp;}//最大値更新 if (weightCup > weightCupMax){weightCupMax = weightCup;} weightDrpAvg = (weightDrpPrev + weightDrp)/2;//表示は前回測定と今回測定の平均 weightCupAvg = (weightCupPrev + weightCup)/2;//初回はPrevが0だが気にしないことにする weightDrpDisp = (float)((int)((weightDrpAvg+0.25) * 2 )) / 2;//表示は0.5g単位とする weightCupDisp = (float)((int)((weightCupAvg+0.25) * 2)) / 2; Serial.print(weightDrp); Serial.print(","); Serial.println(weightCup); //ドリッパーかカップが最大値より50g以上軽くなったら終了 if ( ((weightDrp < weightDrpMax - 50)||(weightCup < weightCupMax - 50)) && measuring == true && measureEnded == false) { measureEnded = true; measuring = false; toneNum = 3; toneStep = 0;//4曲目の最初の音から toneStart(myTone[toneNum][toneStep].tone,myTone[toneNum][toneStep].duration);//計測終了 } if (toneOff == true && measureEnded == true){//終了の音が終わっていることを確認 drawGraph();//グラフ表示 savingData();//終了処理へ M5.Lcd.print("syuuryou"); while(1); } weightDrpPrev = weightDrp;//次回の平均算出用に今回の値を保存 weightCupPrev = weightCup; if (weightDrp > 1 && measuring==false && measureEnded == false){//1g増えたら計測開始 measuring = true; startMillis = millis(); toneNum = 0; toneStep = 0;//0曲目の最初の音から toneStart(myTone[toneNum][toneStep].tone,myTone[toneNum][toneStep].duration);//計測開始音をならす } if (measuring){ //表示する分と秒の値 minute = (millis() - startMillis) / 60000; sec = (millis() - startMillis - minute*60*1000 ) / 1000; } //注湯量がレシピ指定の量を超えたら、レシピを1ステップ進める。ステップが最後までいったらそこでストップ。 if ((weightCupAvg + weightDrpAvg > target * recipe[recipeCount]/100) & (recipeCount < recipeSteps-1)) {recipeCount++; toneNum = 1; toneStep = 0;//2曲目の最初の音から toneStart(myTone[toneNum][toneStep].tone,myTone[toneNum][toneStep].duration);//レシピのステップが進んだ音 } //注湯量がレシピ指定の最後に到達 if ((weightCupAvg + weightDrpAvg > target * recipe[recipeSteps-1]/100) && targetReached == false) {targetReached = true; // 音を出し続けないための処理 toneNum = 2; toneStep = 0;//3曲目の最初の音から toneStart(myTone[toneNum][toneStep].tone,myTone[toneNum][toneStep].duration);//レシピの最後に到達した音 } M5.Lcd.setTextSize(3); M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK); M5.Lcd.setCursor(0,0); M5.Lcd.printf("%02d:%02d ",minute, sec);//分と秒を表示 M5.Lcd.setCursor(30,100); M5.Lcd.printf("Total %4.1f g ",weightDrpDisp + weightCupDisp);//トータルの注湯量 0.5g単位 M5.Lcd.setCursor(30,130); M5.Lcd.printf("Dripper %4.1f g ",weightDrpDisp);//ドリッパーにある量 0.5g単位 M5.Lcd.setCursor(30,160); M5.Lcd.printf("Cup %4.1f g ",weightCupDisp);//カップに落ちた量 0.5g単位 M5.Lcd.setCursor(30,190); if (measuring == true && measureEnded == false && secCounter>2){ M5.Lcd.printf("Speed %4.1f g/s ",weightCupData[secCounter - 1 ] - weightCupData[secCounter - 2]);//抽出スピード ドリップ中のみ表示 } else{ M5.Lcd.printf("Speed ---- "); } M5.Lcd.setCursor(30,30); M5.Lcd.setTextSize(5); M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK); M5.Lcd.print(target * recipe[recipeCount] / 100);//レシピのターゲット量を表示 gap = (target * recipe[recipeCount] / 100) - (weightDrpDisp + weightCupDisp);//ターゲットまであと何グラムか M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); M5.Lcd.setCursor(155,30); //桁を合わせてgapを表示 if (0<= gap && gap<10){M5.Lcd.print(" ");}//gapは1桁 if (-9 <= gap && gap<100){M5.Lcd.print(" ");}//1-2桁 if (-99 <= gap ){M5.Lcd.print(" ");}//1-3桁 -100以下は4桁 M5.Lcd.print(gap); M5.Lcd.setTextSize(2); M5.Lcd.setTextColor(TFT_WHITE, TFT_OLIVE); M5.Lcd.setCursor(30,75); M5.Lcd.print("Target"); M5.Lcd.setCursor(200,75); M5.Lcd.print("To Go"); //前回データ保存から1秒以上経っていたらデータを保存 if ((measuring) && (millis() > dataSavedMillis + 1000)){ weightDrpData[secCounter] = weightDrp; weightCupData[secCounter] = weightCup; targetData[secCounter] = target * recipe[recipeCount] / 100; secCounter++; dataSavedMillis = millis(); if (secCounter>=300){measureEnded = true;measuring = false;}//5分経過で計測終了する } weightPrev = weightCupAvg;// if(toneOff==false){//音が出ている if (millis()>toneEndMillis){//次の音に進む toneStep++; if(toneStep>1){//曲の最後 timerAlarmDisable(samplingTimer); // タイマを止めて音を消す toneOff = true; } else { toneStart(myTone[toneNum][toneStep].tone, myTone[toneNum][toneStep].duration); } } } } ``` ## Google Spreadsheetに保存 測定データをGoogle Spreadsheetに保存するためには、データを受け取るGoogle側の設定が必要となります。過去に作成した動画をご参照ください。 @[youtube](https://www.youtube.com/watch?v=PejQ2qN7qaA) もちろん、データ保存を行わなくても、ドリップスケールとしての使用は可能です。 ## ちゃんと動きました コーヒーをドリップしてみた動画がこちら。 @[youtube](https://www.youtube.com/watch?v=vRqEPNeKozk) こんな機能があります。 - 抽出するお湯の量は500gまで設定可能 - ドリッパーにお湯を注ぐと、音が出て時計が動き始める - ドリッパーとカップのそれぞれの重量を測定して、表示・記録 - レシピで設定したお湯の量を表示し、注湯すると、あと何グラム注げばよいかをカウントダウン - カップの重量の増え方から、流速も計算して表示 - 設定湯量になると音が出て知らせます - 抽出終了して、ドリッパーかカップを外すと、測定データをグラフ表示 - Saveボタンを押すと、Google Spreadsheetに自動保存 - 保存時のファイル名、シート名は、それぞれ年月日、時分秒を使用するので、あとでデータの整理が楽 まずは動きました。レシピはプログラム中に記述してある1種類だし、UIもいまいちですが、少しずつ改良していきましょう。