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スプレッドシートを使った理由
前述したAmbientについては3年前にラズパイで初めて使ったことがあります。
https://siroitori.hatenablog.com/entry/2018/11/27/155346#env_sensor
なんだか今回と似たようなことをしていますが・・・
このときは子供の機嫌と環境データを比較しようとしました。
そうすると機嫌グラフと環境グラフを同じグラフに表示させることが出来ず(それから進化されてるみたいですので今はわかりません!)、プレーンなデータを受信したほうがグラフの自由度が上がると思ったため、今回はスプレッドシートに連携させました。
Googleスプレッドシートでは頭痛薬のタイミングを一定量の棒グラフで表現してみました。(純粋な棒グラフの意味は成してないのですが、タイミングがわかりやすくなるようにしてみました。)
上記のように、ボタンを押したタイミングで定数(200)をB列に書き込み(左の赤枠)、ここに値がある場合棒グラフ表示(右側の赤枠)しています。
今後
現在1週間、元気に連続稼働中です。
どうやら環境センサは1時間に1回値の更新があることがわかりました。
頭痛持ちの私なのですがなぜかこれを作ってから不思議とまだ頭痛がなくwまだ役には立っていませんがこれからも使っていきたいです。
やはりスマホアプリとは違って、薬を隣に置いておくと薬を取り出すタイミングでボタンが押せるからいいですね。
自分で部品実装した基板がちゃんと動くととてもテンション上がりますね!
投稿者の人気記事
-
siroitori0413
さんが
2021/08/18
に
編集
をしました。
(メッセージ: 初版)
-
siroitori0413
さんが
2021/08/19
に
編集
をしました。
(メッセージ: フローチャートに誤りがあったので修正)
-
siroitori0413
さんが
2021/08/28
に
編集
をしました。
(メッセージ: データの流れの図を追加)
-
siroitori0413
さんが
2021/12/20
に
編集
をしました。
ログインしてコメントを投稿する