siroitori0413のアイコン画像

ALGYAN 6周年基板で環境データと服薬記録

siroitori0413 2021年08月18日に作成  (2021年08月28日に更新)

ALGYAN 6周年基板で環境データと服薬記録

ALGYAN基板にはじめての部品実装

先日参加させていただいたALGYANのイベントで基板をいただきました。
しかもこれにはんだづけをするもくもく会が催されるとのことで、早速部品セットを購入し、もくもく会当日はじめての部品実装をしました。

電子工作歴の短い私、雑なはんだづけでしたしわからないところは適当にやってみたのですがしっかり動くものが作れてとても感動しました。

この基板を使ってIoT作品を作ってみました。

環境&服薬記録を作る

私は頭痛持ちなので頭痛薬を飲んだ記録をつけています。いや、つけようとしています。
スマホアプリで記録しているのですがこれがアプリを起動して入力するのがとても面倒で「あとで」と思って付け忘れることが多いのです。
なのでワンタッチでいけるものを作ろうと思いました。

材料

  • 部品実装済みのALGYAN 6周年基板(ESP32)
  • オムロン環境センサ(ブロードキャストモードで動作させます)

概要

頭痛薬を飲んだ記録をスイッチをポチるだけでできるようにするとともに、
オムロンの環境センサから受信したデータ(気温、湿度、光、UV、気圧、騒音)も記録します。
この環境データは服薬に関わらず常に記録します。
また、記録先はGoogleスプレッドシートにしてGoogle Apps Script(GAS)で処理します。

↓こちらの黄色で囲んだスイッチを利用
スイッチ

データの流れ

ESP32プログラム

ESP32で動作するArduinoプログラムです。

EnvBleScanner.ino

#include <Wire.h> #include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <EEPROM.h> //https://github.com/espressif/arduino-esp32/tree/master/libraries/EEPROM #include "BLEDevice.h" #define PIN_BUZZAR 14 #define PIN_USR_SW 35 #define SW_ANTI_CHATTERING_CNT 5 static String omronSensorAdress = "XX:XX:XX:XX:XX:XX"; //TODO OMRON環境センサーのアドレス const char* ssid = "XXXXXXXXXXXX"; //TODO SSIDを設定 const char* password = "XXXXXXXXXXXXXX"; //TODO パスワードを設定 const char* host = "script.google.com"; char sw_json[100]; // postするjson // google scriptのscriptID const char *GScriptId = "XXXXXXXXXXXX"; //TODO GASのScriptIdを設定 const String sw_published_url = String("/macros/s/") + GScriptId+ "/exec"; const String published_url = String("https://") + host + sw_published_url; String responseString; // Http Getで戻った値 DynamicJsonDocument json_response(255); int isSwitchOn = 0; // スイッチ押下状態 BLEScan* pBLEScan; int readId = 0; // 再起動後にも受け渡されるパラメータ値 #define EEPROM_SIZE 2 // 環境センサ取得値 long seq; float temp; float humid; long light; long uv; float press; float noise; long accelX; long accelY; long accelZ; long batt; // -----=====<<<<<[[[[[ Prototype ]]]]]>>>>>=====----- void pSensorThread(void *arg); static void setBuzzer(int onoff); long getParamFromAdvertisedData(std::string advertisedDataString, int pos, int length, String paramName) { // 38桁の16進表記文字列から対象のパラメータ位置で抜き出して10進数に変換して戻す std::string substrData = ""; String manufData = ""; Serial.print(paramName); Serial.print(": [hex] "); substrData = advertisedDataString.substr(pos, length); // 指定位置から指定桁数抜き出す Serial.print(substrData.c_str()); if (length > 2){ // 2桁以上(今回は4桁しかない)のときは反対にする 例)7d08→087d for(int i=length-2; i>=0; i=i-2){ manufData.concat(substrData.substr(i, 2).c_str()); } }else{ manufData = substrData.c_str(); } // 16進数→10進数変換 long ret = strtol(manufData.c_str(), NULL, 16); Serial.print(" [dec] "); Serial.println(ret); return ret; } // 環境値をGASへ送信 boolean sendGasEnv(){ //WiFi接続 connectWifi(); Serial.println("EnvValue GAS Sending..."); //make JSON sprintf(sw_json, "{\"id\": \"%s\" , \"temp\": \"%5.2f\" , \"humid\": \"%5.2f\" , \"light\": \"%ld\" , \"uv\": \"%ld\" , \"press\": \"%5.2f\" , \"noise\": \"%5.2f\"}", "env", temp, humid, light, uv, press, noise); Serial.println(published_url); boolean ret = postRequest(published_url ,sw_json); // 後処理 WiFi.disconnect(true); return ret; } //WiFi接続 void connectWifi(){ WiFi.begin(ssid, password); // Wi-Fi接続 while (WiFi.status() != WL_CONNECTED) { // Wi-Fi AP接続待ち delay(500); Serial.print("."); } Serial.print("WiFi connected\r\nIP address: "); Serial.println(WiFi.localIP()); } // GASへ送信(SW ON時に送信) static boolean sendGasSwMessage(){ //WiFi接続 connectWifi(); Serial.println("SwStatus GAS Sending..."); //make JSON sprintf(sw_json, "{\"id\": \"%s\"}", "sw"); Serial.println(published_url); boolean ret = postRequest(published_url ,sw_json); // 後処理 WiFi.disconnect(true); return ret; } // [[[[[ ブザーの設定関数 ]]]]] static void setBuzzer(int onoff) { if (onoff != 0 ) { digitalWrite(PIN_BUZZAR, HIGH); } else { digitalWrite(PIN_BUZZAR, LOW); } } static void repeatBuzzer(int repeattime, int delaytime){ for (int i = 0; i<= repeattime-1; i++){ setBuzzer(1); delay(delaytime); setBuzzer(0); delay(delaytime); } } void setup() { // put your setup code here, to run once: Serial.begin(115200); delay(1000); Serial.println("Starting..."); //Init EEPROM EEPROM.begin(EEPROM_SIZE); // 再起動前に書き込まれた値を取得しておく int address = 0; readId = EEPROM.read(address); //EEPROM.get(address,readId); Serial.print("EEPROM Read Id = "); Serial.println(readId); // Setup Pins pinMode(PIN_USR_SW, INPUT); pinMode(PIN_BUZZAR, OUTPUT); digitalWrite(PIN_BUZZAR, LOW); // 起動音 ピ repeatBuzzer(1, 100); // BLE 初期化 Serial.println("Step 0"); Serial.println("Starting Arduino BLE Client application..."); BLEDevice::init(""); // 追加 pBLEScan = BLEDevice::getScan(); pBLEScan->setActiveScan(false); // パッシブスキャンに設定 // 入力デバイス用タスクを起動 Serial.println("Starting Sensor Task"); xTaskCreate(pSensorThread, "SensorTask", 8192, NULL, 0, NULL); Serial.println("Done!!!!!!"); } // [[[[[ センサー用のタスク関数 ]]]]] void pSensorThread(void *arg) { while (1) { readInputDevices(); delay(10); } } // [[[[[ 入力デバイスのメイン処理 ]]]]] void readInputDevices() { static int buz_onoff_before = false; static int sw_raw_before = -1; static int sw_same_count = SW_ANTI_CHATTERING_CNT; int sw_raw; static int sw_value; // スイッチの取得処理 sw_raw = digitalRead(PIN_USR_SW); // チャタリング除去処理 if (sw_raw == sw_raw_before) { if ( sw_same_count == 0) sw_value = sw_raw; else sw_same_count--; } else sw_same_count = SW_ANTI_CHATTERING_CNT; sw_raw_before = sw_raw; // スイッチが押されていたらブザーを鳴らす処理 if (sw_value){ if (buz_onoff_before == false){ Serial.println("sw_raw = 1"); // ボタン押下認識のブザー1回 setBuzzer(true); isSwitchOn = true; } buz_onoff_before = true; } else { if (buz_onoff_before == true) setBuzzer(false); buz_onoff_before = false; } } void loop() { // put your main code here, to run repeatedly: Serial.println("loop()"); // BLEで環境データを取得 if (getBleEnvDatas()){ // 新しいデータが取得された // BLE終了処理(これをやらないとHTTPS通信によるGAS書き込みできない) pBLEScan->stop(); btStop(); BLEDevice::deinit(""); // GAS書き込みに行く sendGasEnv(); // 現在のシーケンス値を書き込み→リブート後にパラメータを受けわたす int address = 0; EEPROM.write(address, seq); //EEPROM.put(address, seq); EEPROM.commit(); // ESP32リブート ESP.restart(); } if (isSwitchOn){ Serial.println("SW押下状態"); // BLE終了処理(これをやらないとHTTPS通信によるGAS書き込みできない) pBLEScan->stop(); btStop(); BLEDevice::deinit(""); // スイッチ押下されていたらGAS送信 if (sendGasSwMessage()){ Serial.println("SW操作、GAS送信成功"); // 送信成功ブザー ピーー repeatBuzzer(1, 2000); }else{ Serial.println("SW操作、GAS送信失敗"); // 失敗時は2回鳴らす repeatBuzzer(2, 100); } isSwitchOn = false; // ESP32リブート ESP.restart(); } delay(1000); // Delay a second between loops. } boolean getBleEnvDatas(){ bool found = false; BLEScanResults foundDevices = pBLEScan->start(3); int count = foundDevices.getCount(); for (int i = 0; i < count; i++) { BLEAdvertisedDevice advertisedDevice = foundDevices.getDevice(i); if (advertisedDevice.haveManufacturerData()) { Serial.println("BLE Advertised Device found: "); std::string advertisedDataString = advertisedDevice.toString().c_str(); Serial.println(advertisedDataString.c_str()); // このなかの文字列 manufacturer data:d502のあとがデータになる String deviceAddress = advertisedDevice.getAddress().toString().c_str(); Serial.println(deviceAddress.c_str()); //ペリフェラルをアドレスで判断 if(deviceAddress.equalsIgnoreCase(omronSensorAdress)){ int manufPos = advertisedDataString.find("manufacturer data: d502"); Serial.print("manufPos: "); Serial.println(manufPos); seq = getParamFromAdvertisedData(advertisedDataString, manufPos+23, 2, "seq"); if (readId != seq){ // 前回取得したシーケンスと違う場合にのみ取得 temp = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2, 4, "temp"); temp = temp / 100 ; humid = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4, 4, "humid"); humid = humid / 100 ; light = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4, 4, "light"); uv = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4, 4, "uv"); press = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4, 4, "press"); noise = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4, 4, "noise"); noise = noise / 100 ; accelX = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4, 4, "accelX"); accelY = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4, 4, "accelY"); accelZ = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4, 4, "accelZ"); batt = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4+4, 2, "batt"); return true; } } } } return false; } boolean getRequest(String url){ HTTPClient http; boolean isSuccess = false; // configure traged server and url http.begin(url); //HTTP Serial.print("[HTTP GET] begin...\n"); // start connection and send HTTP header int httpCode = http.GET(); // httpCode will be negative on error if (httpCode > 0) { // HTTP header has been send and Server response header has been handled Serial.printf("[HTTP GET] Return... code: %d\n", httpCode); // file found at server if (httpCode == HTTP_CODE_OK) { // 200 Serial.println("[HTTP GET] Success!!"); responseString = http.getString(); Serial.println(responseString); isSuccess = true; } } else { Serial.printf("[HTTP GET] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); } http.end(); return isSuccess; } boolean postRequest(String url, String json){ //HTTPClient code start HTTPClient http; boolean isSuccess = false; Serial.println(json); Serial.print("[HTTP POST] begin...\n"); if (!http.begin(url)){ Serial.print("[HTTP POST] http.begin() failed \n"); return false; } // Locationをとるためにこれを書かないといけない const char* headerNames[] = { "Location"}; http.collectHeaders(headerNames, sizeof(headerNames)/sizeof(headerNames[0])); Serial.print("[HTTP POST] ...\n"); // start connection and send HTTP header int httpCode = http.POST(json); // httpCode will be negative on error if (httpCode > 0) { // HTTP header has been send and Server response header has been handled Serial.printf("[HTTP POST] Return... code: %d\n", httpCode); // file found at server if (httpCode == HTTP_CODE_OK) { // 200 Serial.println("[HTTP] Success!!"); String payload = http.getString(); Serial.println(payload); isSuccess = true; }else if (httpCode == HTTP_CODE_FOUND) { // 302 … ページからreturnが戻った場合はリダイレクトとなりこのエラーコードとなる String payload = http.getString(); Serial.println(payload); // ヘッダのLocation(リダイレクト先URL)を取り出す Serial.println("Location"); Serial.println(http.header("Location")); // リダイレクト先にGetリクエスト isSuccess = getRequest(http.header("Location")); } } else { Serial.printf("[HTTP POST] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); } http.end(); return isSuccess; }

フローチャート

プログラムは1秒間隔でBLEデバイスを探しに行きます。
そこで環境センサをみつけたら環境データを読み込みます。(ブロードキャストモードでのやり方です)

環境データは時々しか更新されないのでSEQの値を読んでみて確認し、もしSEQが変わっていたら新しいデータになったと判断して、BLEを切断後Wi-Fiに接続してスプレッドシートのGASへPOSTします。
GASへのPOSTはリダイレクト方式になっているのでPOSTしたあとLocationの値でGETします。
そしてその後リブート。

スイッチを押したタイミングでも同じようにGASへPOSTしてリブートします。

電子音は以下のタイミングで鳴らしています

  • 起動時/再起動時:ピ
  • POST送信成功(服薬スイッチ押下タイミング):ピー
  • POST送信失敗:ピピ

はまったところ

BLEとHTTPS通信が同時にできない件にかなり時間を使ってしまいました。

今回使用していないのですがデータ可視化サービスのAmbientではBLEとPOSTが同時に使える情報があり自分でも動かして動作することを確かめたので、GASにPOSTできないのは自分の書き方が悪いと思ってすごく悩みました。
結局ネットで情報を調べていくとESP32ではHTTPS通信ができないという結論に至りました。(AmbientではHTTP通信だったのでうまくできていたようです)

GASプログラム

スプレッドシートのGoogle Apps Scriptで作ったプログラムです。

function doPost(e) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  var params = JSON.parse(e.postData.getDataAsString());
  var id = params.id;

  // データをシートに追加
  sheet.insertRows(2,1);
  if (id == "sw"){
    // スイッチ操作記録の時
    sheet.getRange(2, 1).setValue(new Date());       // 受信日時を記録
    sheet.getRange(2, 2).setValue("200");            // ONのフラグとしてグラフ上見た目にわかりやすく200とする

    // 環境値は前回の値をそのままセットする
    sheet.getRange(2, 3).setValue(sheet.getRange(3, 3).getValue());       
    sheet.getRange(2, 4).setValue(sheet.getRange(3, 4).getValue());      
    sheet.getRange(2, 5).setValue(sheet.getRange(3, 5).getValue());      
    sheet.getRange(2, 6).setValue(sheet.getRange(3, 6).getValue());       
    sheet.getRange(2, 7).setValue(sheet.getRange(3, 7).getValue());    
    sheet.getRange(2, 8).setValue(sheet.getRange(3, 8).getValue());

  }else{
    // 環境記録
    var temp = params.temp;
    var humid = params.humid;
    var light = params.light;
    var uv = params.uv;
    var press = params.press;
    var noise = params.noise;
    sheet.getRange(2, 1).setValue(new Date());       // 環境記録日時を記録
    sheet.getRange(2, 3).setValue(temp);       
    sheet.getRange(2, 4).setValue(humid);      
    sheet.getRange(2, 5).setValue(light);      
    sheet.getRange(2, 6).setValue(uv);       
    sheet.getRange(2, 7).setValue(press);    
    sheet.getRange(2, 8).setValue(noise);    
  }

  return;
}

データ受信時スプレッドシートの行を1行増やしてデータをセットしています。

あとはGoogleスプレッドシートの機能でグラフを作成すると、グラフが自動更新されていきます。

Googleスプレッドシート

Googleスプレッドシートを使った理由

前述したAmbientについては3年前にラズパイで初めて使ったことがあります。
https://siroitori.hatenablog.com/entry/2018/11/27/155346#env_sensor

なんだか今回と似たようなことをしていますが・・・
このときは子供の機嫌と環境データを比較しようとしました。

そうすると機嫌グラフと環境グラフを同じグラフに表示させることが出来ず(それから進化されてるみたいですので今はわかりません!)、プレーンなデータを受信したほうがグラフの自由度が上がると思ったため、今回はスプレッドシートに連携させました。

Googleスプレッドシートでは頭痛薬のタイミングを一定量の棒グラフで表現してみました。(純粋な棒グラフの意味は成してないのですが、タイミングがわかりやすくなるようにしてみました。)

キャプションを入力できます
上記のように、ボタンを押したタイミングで定数(200)をB列に書き込み(左の赤枠)、ここに値がある場合棒グラフ表示(右側の赤枠)しています。

今後

現在1週間、元気に連続稼働中です。
どうやら環境センサは1時間に1回値の更新があることがわかりました。

頭痛持ちの私なのですがなぜかこれを作ってから不思議とまだ頭痛がなくwまだ役には立っていませんがこれからも使っていきたいです。
やはりスマホアプリとは違って、薬を隣に置いておくと薬を取り出すタイミングでボタンが押せるからいいですね。

自分で部品実装した基板がちゃんと動くととてもテンション上がりますね!

siroitori0413のアイコン画像
https://siroitori.hatenablog.com/
  • siroitori0413 さんが 2021/08/18 に 編集 をしました。 (メッセージ: 初版)
  • siroitori0413 さんが 2021/08/19 に 編集 をしました。 (メッセージ: フローチャートに誤りがあったので修正)
  • siroitori0413 さんが 2021/08/28 に 編集 をしました。 (メッセージ: データの流れの図を追加)
ログインしてコメントを投稿する

投稿者の人気記事