われわれ丑之日プロジェクトが2021年9月26日に予選が開催された「Digital Hack Day 2021」に参加して24時間で開発し、73組の中から決勝まで勝ち残った「けんこちゃん」のガジェット側の組み込み系について書かせていただきます。
アプリ側のバックエンドについては「ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)」をご覧ください。
DigitalHackDayについて
「Digital Hack Day 2021」については有名なHackathonですので、詳細の説明は省かせていただきます。今回のDigital Hack Dayのテーマは日本のデジタル化でした。最優秀賞に選出されるための審査基準は、「課題解決」「Hack」「Fun」の3つでした。(詳細はHackDayのホームページをご覧ください)
われわれ丑之日プロジェクトは参加した3人で脳みそを絞り、日本人の健康問題にメスを入れることを決心しました。題して「けんこちゃん」。審査基準へのアンサーは以下の通りです。
課題解決: 塩分の取りすぎ
Hack: 調味料置き場
Fun: ツンデレ
意味がわからないと思います。百聞は一見に如かず、実際の決勝での発表をご覧ください。
けんこちゃんについて
ソフトウェアもハードウェアも3人で限られた時間で開発しました。
システム
まずは簡単にシステムの概要をご覧ください。
使用したモノ
マイコン: Wio LTE JP Version - 4G, Cat.1
ロードセル: ARCELI HX711 1KG
LCD: Grove - 16x2 LCD (White on Blue)
3Dプリンタ: Creality Ender 3 Pro
フィラメント: Pxmalion Wood 1.75mm
構成図
Wio LTEで計測した調味料の重さをGASにHTTP POST、その結果をSpread Sheetsに保存した後にその日の調味料の摂取量をLINE Messaging APIを使用してユーザーに通知、さらにはAngularからGASにGETリクエストすることで様々なデータをレスポンシブに表示することができる、というものです。
シーケンス図
我々がプログラムを書いたのは以下の3箇所です。
・ガジェット: Arduino
・アプリ バックエンド: Google Apps Script
・アプリ フロントエンド: TypeScript
本項ではガジェット側の組み込み系、次項でアプリ側のバックエンドのプログラムについて駄文を弄させていただきます。
ガジェットの制御
今回はDigital Hack Dayの協賛企業の中から、SORACOMさんのGrove IoT スターターキット for SORACOM(Wio LTE JP Version)を使用させていただくことで、調味料の重さを計測してGASにデータをPOSTするまでの一連の動作をたった1枚のボードで完結させることができました。
Wio LTEの仕様の詳細は主にメーカーWebサイト(Seeed社)を確認しながら開発を進めました。が、利点が先ほど述べた「ボード1枚で簡潔」だとすれば、難点は「ドキュメントの少なさ」です。苦戦しまくりました。
僕たちがガジェット側で実現したかったのは、
- 調味料の重さをLCDに常時リアルタイムに表示
- 調味料の重さの変化を検知
- 変化した重さをGASにHTTP POST
この3つです。
先にコードの全体をご覧にたりたい場合はGitHubに公開しているのでそちらをご覧ください。
大事なことをお伝えします。Hackathonの限られた時間で未熟者が作ったモノなので、変数名が適当だったり、余計な文が入っていたりします。宗教上の理由でこれらを受け付けない方はそっとブラウザバックしてください…。その他の方はコメントでご指摘ください。まじで助かります。
調味料の重さをLCDに常時リアルタイムに表示
#include <WioLTEforArduino.h>
#define PRESSURE_SENSOR_1_CLK (WioLTE::D38)
#define PRESSURE_SENSOR_1_DAT (WioLTE::D39)
#define PRESSURE_SENSOR_2_CLK (WioLTE::D20)
#define PRESSURE_SENSOR_2_DAT (WioLTE::D19)
#include <Wire.h>
#include "rgb_lcd.h"
// Initialize variables of the total taken weights
long pre_initial_weight_1 = 0;
long pre_initial_weight_2 = 0;
char pin_num_1 = 20;
char pin_num_2 = 38;
long weight_coefficient = 1;
String seasoning_1 = " salt ";
String seasoning_2 = " suger ";
WioLTE Wio;
rgb_lcd lcd;
void setup() {
SerialUSB.println("");
SerialUSB.println("--- START ---------------------------------------------------");
SerialUSB.println("### I/O Initialize.");
Wio.Init();
SerialUSB.println("### Power supply ON.");
Wio.PowerSupplyGrove(true);
delay(500);
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
delay(2000);
// set sensor pins as INPUT
pinMode(PRESSURE_SENSOR_1_CLK, OUTPUT);
pinMode(PRESSURE_SENSOR_1_DAT, INPUT);
pinMode(PRESSURE_SENSOR_2_CLK, OUTPUT);
pinMode(PRESSURE_SENSOR_2_DAT, INPUT);
SerialUSB.println("### Setup completed.");
delay(800);
// Initialize weight sensors
pre_initial_weight_1 = GetWeights(pin_num_1);
pre_initial_weight_2 = GetWeights(pin_num_2);
}
void loop() {
long weight_1;
long weight_2;
weight_1 = GetActualWeight(pin_num_1);
weight_2 = GetActualWeight(pin_num_2);
DisplayWeights(weight_1, weight_2);
}
long GetWeights(char pin_num){
long measured_weight = 0;
if (pin_num == 20) {
// Read and save analog values from pressure sensors
for (char i = 0; i < 24; i++) {
digitalWrite(PRESSURE_SENSOR_1_CLK, 1);
delayMicroseconds(1);
digitalWrite(PRESSURE_SENSOR_1_CLK, 0);
delayMicroseconds(1);
measured_weight = (measured_weight << 1) | (digitalRead(PRESSURE_SENSOR_1_DAT));
}
} else {
// Read and save analog values from pressure sensors
for (char i = 0; i < 24; i++) {
digitalWrite(PRESSURE_SENSOR_2_CLK, 1);
delayMicroseconds(1);
digitalWrite(PRESSURE_SENSOR_2_CLK, 0);
delayMicroseconds(1);
measured_weight = (measured_weight << 1) | (digitalRead(PRESSURE_SENSOR_2_DAT));
}
}
measured_weight = measured_weight * (-1);
measured_weight = measured_weight ^ 0x800000;
return measured_weight;
}
long GetActualWeight(char pin_num){
long recorded_weight = 0;
long weight = 0;
if (pin_num == 20){
recorded_weight = GetWeights(pin_num_1);
weight = ((recorded_weight - pre_initial_weight_1) / 1000) *weight_coefficient;
} else {
recorded_weight = GetWeights(pin_num_2);
weight = ((recorded_weight - pre_initial_weight_2) / 1000) *weight_coefficient;
}
return weight;
}
void DisplayWeights(long weight_1, long weight_2){
char weight_1_digit = GetDigit(weight_1);
char weight_2_digit = GetDigit(weight_2);
// Print a message to the LCD.
lcd.clear();
lcd.print(seasoning_1 + seasoning_2);
lcd.setCursor(0, 1);
for (char i = 0; i < 4 - weight_1_digit; i++){
lcd.print(" ");
}
lcd.print(weight_1);
lcd.print(" g ");
for (char i = 0; i < 4 - weight_2_digit; i++){
lcd.print(" ");
}
lcd.print(weight_2);
lcd.print(" g");
}
char GetDigit(long num){
if (num == 0){
return 1;
}else if (num < 0){
return 4;
} else {
return log10(num)+1;
}
}
Wio LTEはArduino IDEで編集できます。ガジェット遊びの経験者であれば気軽にとっつけると思います。
今回はロードセルを2つ使用して2つの調味料の重さを扱えるようにしました。
WioLTEforArduino.h
はWio LTEのArduino IDE用ライブラリです。リファレンスを参考に実装しました。
Wio LTEのポート番号に気をつけて、重さの計測に使用するロードセルのピン番号を定義していきます。
Wire.h
を用いてI2C通信することで、計算結果をLCDに送信します。
rgb_lcd.h
を用いてLCDを制御します。
pre_initial_weight_1
とpre_initial_weight_2
には、ロードセルに何も乗っていない状態での計測値を覚えてもらい、あとから調味料が乗せられた時の計測値からこれらの値を引くことで、調味料の重さを計算します。
GetWeights()
では、ロードセルで計測された重さをそのまま取得します。
ロードセルから値を取得するためにOUTPUTのピンを指定しているのは、ロードセルにクロックを入力するとWio LTEにシリアル24ビットでデータが返ってくるためです。このデータは重さの値ではないため、HX711のデータシートを参考にGetWeight()
で重さの値に計算し直します。
GetActualWeight()
では、ロードセルの上に乗っているモノの実際の重さを計算して取得します。
DisplayWeights()
では、GetActualWeight()
で取得した値をLCDに表示します。
Wio LTEでは、
Wio.PowerSupplyGrove(true);
で明示的に電源供給を開始しなければGrove製品に通電しません(恥ずかしながらこれに長いこと気づけず、めちゃくちゃ苦労しました…)。
lcd.begin(16, 2);
でLCDモジュールの文字の表示範囲を16文字、2行と指定します。これを行わないと1行表示となり、2行目の表示ができません。
lcd.clear();
でそれまでLCDに表示されていた文字を消してから
lcd.print();
で1行目の文字列を表示、
lcd.setCursor(0, 1);
でカーソルを2行目に移動させてから
lcd.print();
で2行目の文字列を表示させます。
調味料の重さをLCDに常時リアルタイムに表示
以下のフローチャートを作成し、コードを実装しました。
けんこちゃんを起動する際にはロードセルの上に何も乗せず、pre_initial_weights
を計測します。
起動してから最初に20g以上を計測したときに、その日の最初の調味料の重さを計測してアプリ側にPOSTします。ロードセルが20g以下を計測したときに調味料が使用されたことを感知し、次にロードセルが20g以上を感知したときの値をアプリ側にPOSTすることでアプリ側で値の差を計算し、その日の調味料の使用量を算出するという仕組みです。
変化した重さをGASにHTTP POST
全体のコードは、かなり長くなってしまうのでGitHubを参照していただければ幸いです。
#define APN "apn"
#define USERNAME "username"
#define PASSWORD "passward"
Wio LTEに挿入するSIMカードの契約情報などを参照して、LTE通信に必要な各情報を設定します。
Wio.PowerSupplyLTE(true);
で、Wio LTE上のLTEモジュールの電源供給を開始し、
if (!Wio.TurnOnOrReset()) {
SerialUSB.println("### Could NOT Turn ON, ERROR! ###");
return;
}
で、LTEモジュールの電源がOFFであればONに、ONであれば再起動します。
if (!Wio.Activate(APN, USERNAME, PASSWORD)) {
SerialUSB.println("### Could NOT Activate, ERROR! ###");
return;
}
で、冒頭で指定したAPN、ユーザー名、パスワードを使用してLTEデータ通信を有効にします。
void PostData(long weight_1, long weight_2){
// generate a json presenting data
time_data = millis();
const int capacity_1 = JSON_OBJECT_SIZE(3);
StaticJsonDocument<capacity_1> json_request;
json_request["timestamp"] = time_data;
json_request["salt"] = weight_1;
json_request["suger"] = weight_2;
char buffer[255];
serializeJson(json_request, buffer, sizeof(buffer));
int status;
SerialUSB.println("### Post.");
SerialUSB.print("Post:");
SerialUSB.print(buffer);
SerialUSB.println("");
if (!Wio.HttpPost(WEBHOOK_URL, buffer, &status)) {
SerialUSB.println("###Webhook ERROR! ###");
SerialUSB.println("### Wait.");
delay(INTERVAL);
}
SerialUSB.print("Status:");
SerialUSB.println(status);
}
PostData()
では、計測した調味料の重さをアプリ側にHTTP POSTする処理を行います。
アプリ側での利便性を考え、JSON形式でPOSTすることにしました。
最後に、GAS側でのバックエンド開発が完了したら、WEBHOOK_URL
にアプリのURLを代入します。
#define WEBHOOK_URL "https://script.google.com/macros/s/****..."
これでガジェット側のプログラムは完成です!
苦しんだこと
今回の開発では山ほど苦しみました…。
センサーの選択ミス
まず、予選当時は感圧センサーで調味料の重さを計測しようとしてしまいました。感圧センサーでは上に何かしらの物が乗っていることは検知できてもその重さを正確に計測することは非常に難しく、「決勝に進めたらロードセルに換装しよう」と予選の開発中からチームメンバーで話していました。
LCDの選択ミス
こちらも予選当時はGrove - LCD RGB Backlightを使用しようと試みたのですが、Wio LTEのI2Cポートでは3.3Vの電圧しか出力できず、5Vの入力電圧を必要とするRGB BacklightのLCDはWio LTEから直接扱うことができませんでした。決勝の実装時には「シールドを用意する」などの案も出たのですが、Wio LTEがボード1枚で完結できるという利点を活かせないという理由で、White on BlueのLCDを新たに買い足しました。
関連リンク:
ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み編)
ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)
#丑之日プロジェクト
私たち丑之日プロジェクトはAnii、Mark、Taroの3人で3Dプリンターを鉄の棒をノコギリで切るところから自作したりする様子をYouTubeに投稿しているモノづくり集団です!ぜひチャンネル登録して、役には立たないがなんだか楽しいものをたくさん発明する僕らを応援してください!よろしくお願いします!!