ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み)

われわれ[丑之日プロジェクト](https://www.youtube.com/watch?v=zIvZpHonD8g&list=PLD6iesuOnD4N5p1YKejy6whGA2BegcaRB)が2021年9月26日に予選が開催された「Digital Hack Day 2021」に参加して24時間で開発し、73組の中から決勝まで勝ち残った「けんこちゃん」の**ガジェット側の組み込み系**について書かせていただきます。
**アプリ側のバックエンド**については「[ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)](https://qiita.com/YSSkywalker/items/b21e14fbb1a96d4163e7)」をご覧ください。

# DigitalHackDayについて
「Digital Hack Day 2021」については有名なHackathonですので、詳細の説明は省かせていただきます。今回のDigital Hack Dayのテーマは**日本のデジタル化**でした。最優秀賞に選出されるための審査基準は、「課題解決」「Hack」「Fun」の３つでした。(詳細は[HackDayのホームページ](https://hackday.yahoo.co.jp/#:~:text=%E8%A9%B3%E7%B4%B0%E3%82%92%E8%A6%8B%E3%82%8B%EF%BC%9E-,%E6%9C%80%E5%84%AA%E7%A7%80%E8%B3%9E,-%E8%B3%9E%E9%87%91%20300%E4%B8%87)をご覧ください)
われわれ丑之日プロジェクトは参加した３人で脳みそを絞り、**日本人の健康問題**にメスを入れることを決心しました。題して「けんこちゃん」。審査基準へのアンサーは以下の通りです。

課題解決: **塩分の取りすぎ**
Hack: **調味料置き場**
Fun: **ツンデレ**

意味がわからないと思います。百聞は一見に如かず、実際の決勝での発表をご覧ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/w_Fb7wVX0bA?start=749" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# けんこちゃんについて
ソフトウェアもハードウェアも３人で限られた時間で開発しました。
## システム
まずは簡単にシステムの概要をご覧ください。
### 使用したモノ
マイコン: [Wio LTE JP Version - 4G, Cat.1](https://www.seeedstudio.com/Wio-LTE-JP-Version-4G-Cat-1-p-3018.html)
ロードセル: [ARCELI HX711 1KG](https://www.amazon.co.jp/gp/product/B07MY35F92/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)
LCD: [Grove - 16x2 LCD (White on Blue)](https://www.seeedstudio.com/Grove-16x2-LCD-White-on-Blue.html)
3Dプリンタ: [Creality Ender 3 Pro](https://www.amazon.co.jp/gp/product/B07GZ9DNDX/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
フィラメント: [Pxmalion Wood 1.75mm](https://www.amazon.co.jp/gp/product/B07DMJGNF4/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
### 構成図
![システム構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/55449864-61d1-6e14-64d5-921980ee28b3.png)
Wio LTEで計測した調味料の重さをGASにHTTP POST、その結果をSpread Sheetsに保存した後にその日の調味料の摂取量をLINE Messaging APIを使用してユーザーに通知、さらにはAngularからGASにGETリクエストすることで様々なデータをレスポンシブに表示することができる、というものです。
### シーケンス図
以下のようなシーケンスを考案しました。
![sequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/df5bc5af-5b89-43cf-3546-af74ec1e45cf.png)

我々がプログラムを書いたのは以下の３箇所です。
・ガジェット:　Arduino
・アプリ バックエンド:　Google Apps Script
・アプリ フロントエンド:　TypeScript

本項では**ガジェット側の組み込み系**、次項で[**アプリ側のバックエンド**](https://qiita.com/YSSkywalker/items/b21e14fbb1a96d4163e7)のプログラムについて駄文を弄させていただきます。
## ガジェットの制御
今回はDigital Hack Dayの協賛企業の中から、SORACOMさんの[Grove IoT スターターキット for SORACOM(Wio LTE JP Version)](https://soracom.jp/store/5228/)を使用させていただくことで、**調味料の重さを計測**して**GASにデータをPOSTする**までの一連の動作を**たった１枚のボードで完結させる**ことができました。
Wio LTEの仕様の詳細は主に[メーカーWebサイト（Seeed社）](https://wiki.seeedstudio.com/Wio_LTE_Cat.1/)を確認しながら開発を進めました。が、利点が先ほど述べた「ボード１枚で簡潔」だとすれば、難点は「ドキュメントの少なさ」です。苦戦しまくりました。
僕たちがガジェット側で実現したかったのは、

1. 調味料の重さをLCDに常時リアルタイムに表示
2. 調味料の重さの変化を検知
3. 変化した重さをGASにHTTP POST  

この３つです。
先にコードの全体をご覧にたりたい場合は[GitHub](https://github.com/UshinohiProject/DigitalHackDay2021-WioLTE/blob/master/DigitalHackDay2021-WioLTE.ino)に公開しているのでそちらをご覧ください。

大事なことをお伝えします。Hackathonの限られた時間で未熟者が作ったモノなので、変数名が適当だったり、余計な文が入っていたりします。宗教上の理由でこれらを受け付けない方はそっとブラウザバックしてください…。その他の方はコメントでご指摘ください。まじで助かります。

### 調味料の重さをLCDに常時リアルタイムに表示

```C#:DigitalHackDay2021-WioLTE.ino
#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用ライブラリです。[リファレンス](https://seeedjp.github.io/Wiki/Wio_LTE_for_Arduino/Reference-ja.html#turnonorreset)を参考に実装しました。
Wio LTEのポート番号に気をつけて、重さの計測に使用するロードセルのピン番号を定義していきます。
`Wire.h`を用いてI2C通信することで、計算結果をLCDに送信します。
`rgb_lcd.h`を用いてLCDを制御します。

`pre_initial_weight_1`と`pre_initial_weight_2`には、ロードセルに何も乗っていない状態での計測値を覚えてもらい、あとから調味料が乗せられた時の計測値からこれらの値を引くことで、調味料の重さを計算します。

`GetWeights()`では、ロードセルで計測された重さをそのまま取得します。
ロードセルから値を取得するためにOUTPUTのピンを指定しているのは、ロードセルにクロックを入力するとWio LTEにシリアル２４ビットでデータが返ってくるためです。このデータは重さの値ではないため、HX711の[データシート](chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/viewer.html?pdfurl=https%3A%2F%2Fcdn.sparkfun.com%2Fdatasheets%2FSensors%2FForceFlex%2Fhx711_english.pdf&clen=164280&chunk=true)を参考に`GetWeight()`で重さの値に計算し直します。

`GetActualWeight()`では、ロードセルの上に乗っているモノの実際の重さを計算して取得します。
`DisplayWeights()`では、`GetActualWeight()`で取得した値をLCDに表示します。

Wio LTEでは、

```c#:DigitalHackDay2021-WioLTE
Wio.PowerSupplyGrove(true);
```
で明示的に電源供給を開始しなければGrove製品に通電しません(恥ずかしながらこれに長いこと気づけず、めちゃくちゃ苦労しました…)。

```c#:DigitalHackDay2021-WioLTE
lcd.begin(16, 2);
```
でLCDモジュールの文字の表示範囲を16文字、2行と指定します。これを行わないと1行表示となり、2行目の表示ができません。

```c#:DigitalHackDay2021-WioLTE
lcd.clear();
```
でそれまでLCDに表示されていた文字を消してから

```c#:DigitalHackDay2021-WioLTE
lcd.print();
```
で1行目の文字列を表示、

```c#:DigitalHackDay2021-WioLTE
lcd.setCursor(0, 1);
```
でカーソルを2行目に移動させてから

```c#:DigitalHackDay2021-WioLTE
lcd.print();
```
で2行目の文字列を表示させます。

### 調味料の重さをLCDに常時リアルタイムに表示
以下のフローチャートを作成し、コードを実装しました。
![Kenkoフローチャート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268123/92351edf-a2ea-ef55-c062-0cfcea6a3170.png)
けんこちゃんを起動する際にはロードセルの上に何も乗せず、`pre_initial_weights`を計測します。
起動してから最初に20g以上を計測したときに、その日の最初の調味料の重さを計測してアプリ側にPOSTします。ロードセルが20g以下を計測したときに調味料が使用されたことを感知し、次にロードセルが20g以上を感知したときの値をアプリ側にPOSTすることでアプリ側で値の差を計算し、その日の調味料の使用量を算出するという仕組みです。

### 変化した重さをGASにHTTP POST
全体のコードは、かなり長くなってしまうので[GitHub](https://github.com/UshinohiProject/DigitalHackDay2021-WioLTE/blob/master/DigitalHackDay2021-WioLTE.ino)を参照していただければ幸いです。

```c#:DigitalHackDay2021-WioLTE
#define APN               "apn"
#define USERNAME          "username"
#define PASSWORD          "passward"
```
Wio LTEに挿入するSIMカードの契約情報などを参照して、LTE通信に必要な各情報を設定します。

```c#:DigitalHackDay2021-WioLTE
Wio.PowerSupplyLTE(true);
```
で、Wio LTE上のLTEモジュールの電源供給を開始し、

```c#:DigitalHackDay2021-WioLTE
if (!Wio.TurnOnOrReset()) {
  SerialUSB.println("### Could NOT Turn ON, ERROR! ###");
  return;
}
```
で、LTEモジュールの電源がOFFであればONに、ONであれば再起動します。

```c#:DigitalHackDay2021-WioLTE
if (!Wio.Activate(APN, USERNAME, PASSWORD)) {
  SerialUSB.println("### Could NOT Activate, ERROR! ###");
  return;
}
```
で、冒頭で指定したAPN、ユーザー名、パスワードを使用してLTEデータ通信を有効にします。

```c#:DigitalHackDay2021-WioLTE
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側でのバックエンド開発](https://qiita.com/YSSkywalker/items/b21e14fbb1a96d4163e7)が完了したら、`WEBHOOK_URL`にアプリのURLを代入します。

```c#:DigitalHackDay2021-WioLTE
#define WEBHOOK_URL       "https://script.google.com/macros/s/****..."
```

これでガジェット側のプログラムは完成です！
# 苦しんだこと
今回の開発では山ほど苦しみました…。
## センサーの選択ミス
まず、予選当時は[**感圧センサー**](https://wiki.seeedstudio.com/Grove-Round_Force_Sensor_FSR402/)で調味料の重さを計測しようとしてしまいました。感圧センサーでは上に何かしらの物が乗っていることは検知できてもその重さを正確に計測することは非常に難しく、「決勝に進めたらロードセルに換装しよう」と予選の開発中からチームメンバーで話していました。
## LCDの選択ミス
こちらも予選当時は[Grove - LCD RGB Backlight](https://wiki.seeedstudio.com/Grove-LCD_RGB_Backlight/)を使用しようと試みたのですが、Wio LTEのI2Cポートでは3.3Vの電圧しか出力できず、５Vの入力電圧を必要とするRGB BacklightのLCDはWio LTEから直接扱うことができませんでした。決勝の実装時には「シールドを用意する」などの案も出たのですが、Wio LTEがボード１枚で完結できるという利点を活かせないという理由で、White on BlueのLCDを新たに買い足しました。

関連リンク:
[ツンデレ「けんこちゃん」が僕を健康管理してくれる話(組み込み編)](https://qiita.com/YSSkywalker/items/3489e0185cb649b8cb88)
[ツンデレ「けんこちゃん」が僕を健康管理してくれる話(バックエンド編)](https://qiita.com/YSSkywalker/items/b21e14fbb1a96d4163e7)

#丑之日プロジェクト
私たち[丑之日プロジェクト]()はAnii、Mark、Taroの３人で**3Dプリンター**を**鉄の棒をノコギリで切るところから自作**したりする様子をYouTubeに投稿しているモノづくり集団です！ぜひ[**チャンネル登録**](http://www.youtube.com/channel/UCvTSx-2FzWu-x_wDwKjXN5A?sub_confirmation=1)して、役には立たないがなんだか楽しいものをたくさん発明する僕らを応援してください！よろしくお願いします！！
<iframe width="560" height="315" src="https://www.youtube.com/embed/KzjsY0QX_j8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>