はじめに
本システムはSORACOM Summer Challenge 2020にて優勝しました!
https://www.lp.soracom.jp/summer-challenge-2020
ありがとうございます!!
オンラインハッカソンSORACOM Summer Challenge 2020に参加し、本システムを製作しました。
コンセプトは見守る側と見守られる側の双方にとってお互いにとって便利であり、安価で製作できる身近な見守りシステムです。
近年少子高齢化の加速とともに高齢者の孤独死などが社会問題化し、センシングによるIoTを活用した見守りシステムの重要性が高まっています。
しかし私が高齢者側の立場であればカメラや人感センサー等で生活を見守られるのはなんだか私生活を監視されているような気分になるのではと考えてしまいます。
そこで日常生活の中で必要不可欠な動作をトリガーとして自然な形でデータを取得するのがベストだと考えました。
本システムでは冷蔵庫の扉の開閉を記録しサーバー上にアップロードすることで生活状況を記録し、扉の開けっ放し防止のブザーやいざというときの緊急通報機能なども実装し見守られる側にも便利なデバイスとなることを目標としました。
タイトルのシステム名を何度も呼ぶのは長いので「みまもりくん」と名付けます。
製作過程
プロトタイピング製作
LTE-M Button Plusは単4電池2本で動作するほど小型でありながらeSIM+LTE-MによりWi-FI環境がなくてもデータの送信をどこでも行うことができるデバイスです。
インターネット環境がない住宅においても電源供給さえ行えば安定して必要最小限のデータの送信ができることから見守りシステムにピッタリだと考えました。
内蔵ボタンだけでなく接点入力も持つため、ここではマイコンと連動させて使用します。
ボタンの押下パターンに応じて三種類の状態を持つことができるので以下のように割り当てました。
| 接点入力 | マイコン側の状態 | 
|---|---|
| シングルクリック(0.1〜1.2秒のクリック動作) | 扉の開閉を確認 | 
| ダブルクリック(2秒以内にシングルクリックを2回) | 熱中症危険環境を確認 | 
| ロングクリック(1.2秒以上のクリック動作) | 緊急ボタン押下を確認 | 
| フォトカプラによりマイコン側から接点入力を制御することでクリックパターンを送信し、SORACOM LTE-M Button Plusからデータを送信します。 | 
マイコンはブートローダー書き込みを行ったAVRマイコン ATMEGA328Pを使うことで最小構成のArduino Uno互換機としてArduino IDE上で開発を行いました。

これにより小型化と部品代の大幅な節約を可能にします。
ブレッドボード上でハードウェアをあらかじめ確定させて、その後ソフト面で機能を追加しながら開発を進めていく方針としました。
材料費
配線材などはカウントできないので省くこととします☆(・ω<)
| 品物 | 個数 | 金額 | 
|---|---|---|
| -秋月電子通商- | - | --- | 
| 5V出力昇圧DCDCコンバーター | 1 | 300円 | 
| フォトカプラ TLP621-1 | 1 | 40円 | 
| ドアセンサースイッチ | 1 | 250円 | 
| ゲームスイッチ | 1 | 150円 | 
| 2.1mm標準DCジャック | 1 | 60円 | 
| 3.5mmステレオミニジャックMJ-073H | 1 | 60円 | 
| 3.5mmステレオミニジャックMJ-074N | 1 | 65円 | 
| 3.5mmステレオミニジャックMJ-355W | 1 | 55円 | 
| ATmega328P | 1 | 210円 | 
| ICソケット28P スリム300milタイプ | 1 | 20円 | 
| 3.5mmステレオミニプラグ L型 | 1 | 75円 | 
| SHT31使用 高精度温湿度センサ | 1 | 950円 | 
| 5mm赤外線LED OSI5LA5113A グレー | 1 | 10円 | 
| カーボン抵抗1/4W 220Ω | 1 | 1円 | 
| カーボン抵抗1/4W 10kΩ | 1 | 1円 | 
| 分割ロングピンソケット(メス) | 1 | 80円 | 
| 圧電スピーカー(SPT08) | 1 | 50円 | 
| 絶縁ラジアルリード型積層セラミックコンデンサー 22pF50V | 2 | 20円 | 
| 絶縁ラジアルリード型積層セラミックコンデンサー 0.1uF50V | 1 | 10円 | 
| タクトスイッチ | 1 | 10円 | 
| -AliExpress- | - | --- | 
| 0.96 インチOLEDディスプレイ(128x64) | 1 | 191円 | 
| -SORACOM IoTストア- | - | --- | 
| SORACOM LTE-M Button Plus | 1 | 7380円 | 
| 合計 | - | 9988円 | 
このような形でブレッドボード上に組んだ回路動作を確認し、試作が完了しました。

しかしこれを冷蔵庫の扉に貼り付けるのは大変不格好であり連続稼働は行えないのでケースに組み込みすることとしました。これにより1万円超えちゃうのは暖かい目で見てください(うるうる)
ケースへの組み込み
タカチ電機工業のワンタッチ開閉式プラスチックケースSW-120を採用しました。

持ち運ばないのでSORACOM LTE-M Button Plusをケースに内蔵して、電源は電源アダプターから外部供給し電池切れとなることなく連続稼働を可能とします。この空きスペースに入力端子と基板を組み込みます。

ぎりぎりですがなんとかケースのふたが閉まる程度に組み込むことができました。

裏にゴムクッションをを取り付け、小型ネオジム磁石を内蔵することで冷蔵庫の側面に貼り付けることができます。

どこでもつながるLTE-M Buttonの良さを活かすためにも万が一不注意でOFFになることなどないように電源ボタンなどは取り付けず電源を刺すだけで動く設計としました。
また扉の開閉を検知するリードスイッチや緊急ボタンの接点入力端子および赤外線信号を送信する出力端子にはいずれもオーディオに使われる3.5mmステレオジャックを採用しました。
この理由としてはオーディオ用途して100円ショップなどでも3mの延長ケーブルというのは身近に手に入るので、これを利用することでデバイスの設置場所(コンセント)からエアコンや冷蔵庫が遠かったとしても容易に延長ケーブルを利用できます
システム構成

システム構成は上図の通りです。
SORACOM Lagoonでは得られたデータの可視化及び扉の開閉回数の監視を常時行います。

(クリックで拡大します)
データの回数や頻度を数値やグラフとして可視化できると一目で生活状況がわかることができ、これをほとんどコードを書くことなくかっこいいダッシュボードとできるSORACOM Lagoonすごいです!
アラート機能を利用して過去24時間以内で扉の開閉回数が1回以上あるか監視しており、0回だった場合はLINE及びメールにアラート通知が発火します。

扉の開閉通知と異なり熱中症危険時や緊急ボタンが押されたときは素早いアラート通知が求められるのでSORACOM Funkを通じてAWS LambdaからIFTTTを経由してLINE Notifyへの通知及びGoogleスプレッドシートへの蓄積を行います。
LINE Notify

みまもりくん用のグループを作っておき、そこに通知が流れるようにIFTTT側で設定します。
しかし扉開閉時と違いアラート通知は万が一見落とすようなことがあってはならないのでスタンプ代わりに画像を送信するようにしました。

あらかじめサーバー上に置いておいた2つのイラストをValue(ClickType)に応じて対応づけるだけのシンプルな実装ですが一目で状態が確認できます。

Google スプレッドシート
LINE Notifyへの通知と同様にして同じイベント名でWebhookを利用し、扉の開閉データと緊急ボタンまたは熱中症危険アラートが発火したタイミングをGoogle スプレッドシートに記録しています。
SORACOM Lagoonはデータの可視化を目的とするのに対して、こちらにはデータの蓄積を目的しています。

これらの連携機能はAWS Lambdaから一度IFTTTを通していることにより、スマートフォンなど手元の端末から容易に両者システムのON,OFFを行うことができます。扉開閉通知と熱中症危険時や緊急ボタン押下時などのアラートはEvent Nameを分けているため、例として毎回扉が開かれるたびにLINEに通知が来ることがうるさく感じたときには扉開閉通知のみをOFFにすることができます。

AWS Lambdaでのプログラムは以下の通りです。
const https = require('https');
const url = require('url');
exports.handler = function(event, context, callback) {
  console.log('processing event: %j', event)
  var key = "xxxxxxxxxxxxxxxxxxxxxx" // My IFTTT KEY
  var Event_alert = "Mimamori_Kun_Alert"      // IFTTT Event Name for Alert
  var Event_door = "Mimamori_Kun_door"      // IFTTT Event Name for Door Notify
  var clickType = event.clickTypeName
  var is_alert = "No"
  var values = []
  var data = {}
  if (clickType == "DOUBLE"){
    values[1] = "WBGT値が31℃を超えて熱中症危険です。" 
    values[2] = 2
    is_alert = "Yes"
  } else if (clickType  =="LONG"){
    values[1] = "緊急ボタンが押されました。"  
    values[2] = 3
    is_alert = "Yes"
  } else if (clickType  =="SINGLE"){
    values[1] = "冷蔵庫の扉が開かれました!"  
    values[2] = 1
    is_alert = "No"
  }
  if (is_alert == "Yes"){
    var webhookUrl = "https://maker.ifttt.com/trigger/"+Event_alert+"/with/key/"+key
  } else {
    var webhookUrl = "https://maker.ifttt.com/trigger/"+Event_door+"/with/key/"+key
  }
  webhookOptions = url.parse(webhookUrl)
  webhookOptions.headers = {'Content-TYpe':'application/json'}
  
  var data = { 'value1': values[1],'value2': values[2]}
  var body = JSON.stringify(data)
  console.log("sending: "+body+ " to " + webhookUrl)
  webhookOptions.headers['Content-Length'] = Buffer.byteLength(body)
  var req  = https.request (webhookOptions, function(res){
    if (res.statusCode === 200)
    {
      console.log('webhook succeeded')
      callback(null, true)
    }
    else {
      callback("webhook failed with "+res.statusCode)
    }
    return res;
  })
  req.write(body)
  req.end()
}
マイコン側のプログラムは以下の通りです。
# include <SPI.h>
# include <Wire.h>
# include <Adafruit_GFX.h>
# include <Adafruit_SSD1306.h>
# include "AE_SHT31.h"
# include <PanasonicHeatpumpIR.h>
# include <Fonts/FreeSans18pt7b.h>
//ライブラリの入手先
//arduino-heatpumpir  https://github.com/ToniA/arduino-heatpumpir
//SHT31 http://akizukidenshi.com/download/AE_SHT31.zip
//Adafruit_SSD1306 https://github.com/adafruit/Adafruit_SSD1306
//Adafruit-GFX-Library https://github.com/adafruit/Adafruit-GFX-Library
//宣言
void door_buzzer();
void air_cool_on(int target_temp);
void air_cool_off();
void get_env_info();
void wbgt_check();
void emergency();
void heatstroke_alert();
void soracom_send_single();
void soracom_send_double();
void soracom_send_long();
void OLED_display();
//ピン設定
const int buzzer_pin = 9;
const int emergency_button = 10;
const int soracom_signal_pin = 8;
const int door_sensor_pin = 12;
AE_SHT31 SHT31 = AE_SHT31(0x45);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
IRSenderPWM irSender(3);
PanasonicDKEHeatpumpIR *heatpumpIR;
void setup() {
  // シリアル通信を9600bpsに設定
  Serial.begin(9600);
  // シリアルに文字を出力
  Serial.println("SHT31 Test!!");
  // SHT31をソフトリセット
  SHT31.SoftReset();
  // 内蔵ヒーター 0:OFF 1:ON
  SHT31.Heater(0);
  pinMode(emergency_button,INPUT_PULLUP);
  pinMode(door_sensor_pin,INPUT_PULLUP);
  pinMode(soracom_signal_pin, OUTPUT);
  
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(SSD1306_WHITE);
  display.clearDisplay();
  tone(buzzer_pin,523,300);
  delay(300);
  
}
//変数
float temp,humi,wbgt;
unsigned long open_start_time;
unsigned long last_heatalert_time = 0;
unsigned long day_elapsed_time = 0;
boolean door_recorded = false;
int door_elapsed_time = 0;
int b4_elapsed_time = 100;
int buzzer_status = 0;
int ac_status = 0;
int door_count = 0;
# define BUZZER_LEN 200 //開閉中のブザーの長さ
//http://javl.github.io/image2cpp/を用いて生成
// '秒'のグラフィック表示, 29x29px
const unsigned char byo_Bitmap [] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0xf0, 0x18, 0x00, 
  0x07, 0xc0, 0x18, 0x00, 0x1f, 0x00, 0x18, 0x00, 0x03, 0x01, 0x18, 0x00, 0x03, 0x01, 0xd9, 0x00, 
  0x03, 0x01, 0x98, 0x80, 0x03, 0x31, 0x18, 0xc0, 0x3f, 0xf3, 0x18, 0x60, 0x03, 0x03, 0x18, 0x60, 
  0x03, 0x02, 0x18, 0x70, 0x03, 0x06, 0x18, 0x20, 0x07, 0xc4, 0x18, 0x00, 0x07, 0x64, 0x18, 0x80, 
  0x07, 0x30, 0x19, 0xc0, 0x0b, 0x30, 0x19, 0x80, 0x0b, 0x10, 0x7b, 0x00, 0x13, 0x00, 0x26, 0x00, 
  0x13, 0x00, 0x0e, 0x00, 0x23, 0x00, 0x1c, 0x00, 0x03, 0x00, 0x38, 0x00, 0x03, 0x00, 0x60, 0x00, 
  0x03, 0x00, 0xc0, 0x00, 0x03, 0x03, 0x00, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00
};
// '通報完了画面', 128x64px
const unsigned char emergency_Bitmap [] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x03, 0xf8, 0x00, 0x3f, 0xfc, 0xff, 0xc0, 0x00, 0x00, 0x18, 0xff, 0xc0, 0x00, 0x00, 
  0x00, 0x00, 0x1e, 0x00, 0x00, 0x1f, 0xfc, 0x02, 0xf0, 0x00, 0x00, 0x0c, 0x31, 0x80, 0x00, 0x00, 
  0x00, 0x00, 0xe0, 0x3f, 0x80, 0x1f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x04, 0x1b, 0x00, 0x00, 0x00, 
  0x00, 0x03, 0xc1, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x06, 0xff, 0xc0, 0x00, 0x00, 
  0x00, 0x07, 0xc7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x0f, 0xcc, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x1f, 0xd8, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x00, 
  0x00, 0x1f, 0xf0, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x30, 0x00, 0x1c, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x3e, 0x20, 0x0c, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x20, 0x40, 0x1f, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0xff, 0xe0, 0x00, 0x00, 
  0x00, 0x40, 0x40, 0xec, 0x03, 0x80, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x40, 0x87, 0xc0, 0x7d, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0xc0, 0xfc, 0x07, 0xb0, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x84, 0x60, 0x00, 0x00, 
  0x00, 0x80, 0xe0, 0x78, 0x30, 0x60, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0e, 0x85, 0xc0, 0x00, 0x00, 
  0x00, 0x80, 0x87, 0x20, 0x70, 0x60, 0x3f, 0xff, 0x83, 0xff, 0xc0, 0x1b, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x80, 0xff, 0x38, 0x60, 0x70, 0x7f, 0xff, 0xc0, 0x0e, 0xc0, 0x11, 0xff, 0xe0, 0x00, 0x00, 
  0x01, 0x80, 0x7e, 0x1c, 0xc0, 0x30, 0x7f, 0xff, 0xf0, 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x01, 0x80, 0x7c, 0x0c, 0x00, 0x70, 0x7f, 0xff, 0xf8, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x01, 0x80, 0x78, 0x00, 0x30, 0xf0, 0x7f, 0xff, 0xfe, 0x00, 0x30, 0x02, 0x00, 0x00, 0x00, 0x00, 
  0x01, 0x00, 0x78, 0x18, 0x30, 0xf0, 0x7f, 0xff, 0xff, 0x00, 0x18, 0x02, 0x1f, 0xe0, 0x00, 0x00, 
  0x01, 0x3f, 0xf0, 0x18, 0x01, 0xf0, 0x3f, 0x01, 0xff, 0xc0, 0x08, 0x02, 0x18, 0x60, 0x0c, 0x00, 
  0x01, 0x3f, 0x30, 0x00, 0xc1, 0xf0, 0x3f, 0x00, 0x3f, 0xf0, 0x0c, 0x1f, 0xd8, 0x60, 0x0c, 0x00, 
  0x01, 0x3f, 0x38, 0x00, 0x01, 0xe0, 0x3f, 0x00, 0x3f, 0xf8, 0x04, 0x02, 0x18, 0x67, 0xff, 0xf8, 
  0x03, 0x3f, 0x38, 0x00, 0x01, 0xe0, 0x3f, 0x00, 0x0f, 0xfe, 0x06, 0x02, 0x1b, 0xc6, 0x00, 0x18, 
  0x02, 0x3f, 0x18, 0x00, 0x01, 0xe0, 0x1f, 0x00, 0x03, 0xff, 0x02, 0x1f, 0xf8, 0x06, 0x00, 0x18, 
  0x02, 0x3f, 0x1c, 0x00, 0x30, 0xf0, 0x1f, 0x00, 0x00, 0x7f, 0xc2, 0x08, 0x9f, 0xc6, 0x00, 0x18, 
  0x02, 0x3f, 0xcc, 0x03, 0xf0, 0xf0, 0x1f, 0x00, 0x00, 0x01, 0xe0, 0x0c, 0x9c, 0x60, 0xff, 0xc0, 
  0x02, 0x3f, 0xc6, 0x01, 0xe0, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x19, 0x04, 0x9c, 0x40, 0x00, 0x00, 
  0x02, 0x3f, 0xc2, 0x00, 0x00, 0x18, 0x1f, 0x00, 0x00, 0x00, 0x7f, 0x1f, 0xfa, 0x40, 0x00, 0x00, 
  0x02, 0x3f, 0xc0, 0x00, 0x00, 0x08, 0x0f, 0x00, 0x00, 0x00, 0x3f, 0x02, 0x1a, 0xc7, 0xff, 0xf8, 
  0x02, 0x7f, 0xe0, 0x00, 0x01, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x02, 0x1b, 0x80, 0x22, 0x00, 
  0x02, 0x7f, 0xe0, 0xf8, 0x0b, 0x03, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xd9, 0x80, 0x22, 0x00, 
  0x02, 0x7f, 0xe0, 0x00, 0x02, 0x01, 0x07, 0xff, 0xff, 0xff, 0xff, 0x82, 0x19, 0x80, 0x22, 0x00, 
  0x00, 0x7f, 0xe0, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1b, 0xc0, 0x62, 0x08, 
  0x02, 0x7f, 0xe0, 0x40, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1e, 0x60, 0xc2, 0x08, 
  0x02, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x01, 0x7f, 0x82, 0x1c, 0x21, 0x82, 0x08, 
  0x02, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x07, 0x03, 0xf8, 
  0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x00, 0x00, 0x00, 0x40, 0x00, 0x04, 0x00, 0x00, 
  0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 
  0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 
  0x06, 0x00, 0x01, 0x78, 0x00, 0x00, 0x03, 0xc0, 0x1f, 0xff, 0x81, 0xc0, 0x00, 0x03, 0xff, 0xf0, 
  0x02, 0x00, 0x01, 0x78, 0x00, 0x00, 0x03, 0xc0, 0x7f, 0x1f, 0x81, 0xc0, 0x00, 0x00, 0x00, 0x30, 
  0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x7f, 0x1f, 0x81, 0xe0, 0x00, 0x00, 0x00, 0x60, 
  0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0xc0, 0xff, 0xff, 0xc1, 0xe0, 0x00, 0x00, 0x00, 0xc0, 
  0x02, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xd0, 0x3c, 0x07, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x80, 
  0x02, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 
  0x02, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x0c, 0x00, 
  0x02, 0x3f, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x0c, 0x00, 
  0x02, 0x7f, 0x80, 0x40, 0x07, 0x80, 0x00, 0x00, 0x0f, 0xff, 0x80, 0x40, 0x00, 0x00, 0x0c, 0x00, 
  0x01, 0xff, 0xc0, 0x40, 0x3f, 0xc0, 0x00, 0x00, 0x3f, 0x01, 0x87, 0xc0, 0x00, 0x00, 0x0c, 0x00, 
  0x01, 0xff, 0xef, 0xf8, 0x3f, 0xe0, 0x01, 0xff, 0xe0, 0x00, 0xfe, 0xc0, 0x00, 0x00, 0x0c, 0x00, 
  0x01, 0xe3, 0xe1, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xe0, 0x01, 0x80, 0xc0, 0x00, 0x00, 0x0c, 0x00, 
  0x01, 0xe1, 0xe0, 0x30, 0x78, 0xf8, 0x00, 0x00, 0x60, 0x00, 0x03, 0x80, 0x00, 0x00, 0x0c, 0x00, 
  0x01, 0xe1, 0xe0, 0x18, 0x78, 0x78, 0x00, 0x00, 0x60, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x0c, 0x00, 
  0x00, 0xe1, 0xe7, 0xfe, 0x70, 0x78, 0xff, 0xf3, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x0c, 0x00, 
  0x00, 0xf1, 0xe0, 0x00, 0x78, 0x78, 0x7f, 0xff, 0xfb, 0xff, 0x80, 0x00, 0x00, 0x00, 0x7c, 0x00, 
  0x00, 0x7f, 0xe0, 0x00, 0x38, 0xf8, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x7f, 0xc0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x1f, 0x80, 0x00, 0x1f, 0xf0, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x0f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
void loop() {
  if (digitalRead(emergency_button) == LOW){ //緊急ボタンを押された
  emergency();
  }
  if(digitalRead(door_sensor_pin) == HIGH){ //冷蔵庫の扉が開かれた
    if(door_recorded == false){
      open_start_time = millis(); //冷蔵庫の扉が開かれた時の時刻を記録
      door_recorded = true;
      soracom_send_single(); 
    }else{
      door_elapsed_time = (millis() - open_start_time)/1000;
      if(door_elapsed_time != b4_elapsed_time){      
        display.clearDisplay();
        display.setTextSize(2);
        display.setCursor(0,57);
        display.setFont(&FreeSans18pt7b);
        display.println(door_elapsed_time);
        display.drawBitmap(98, 34, byo_Bitmap, 29, 29, WHITE);//漢字で'秒'を表示
        display.display();
        door_buzzer();
        b4_elapsed_time = door_elapsed_time;
      }
    }
  }else{ //冷蔵庫の扉が開かれていない場合
    if(door_recorded == true){
      if(buzzer_status >= 8){
        noTone(buzzer_pin);
      }
      door_count++;
      door_recorded = false;
      b4_elapsed_time = 100;
      buzzer_status = 0;
    }
    OLED_display();
  }
}
void door_buzzer(){ //扉の開閉時間に応じてドレミファソラシドでお知らせ
  if(door_elapsed_time >= 25 && buzzer_status == 0){ 
    tone(buzzer_pin,262,BUZZER_LEN); // ド
    buzzer_status = 1;
  }
  if(door_elapsed_time >= 30 && buzzer_status == 1){
    tone(buzzer_pin,294,BUZZER_LEN); // レ
    buzzer_status = 2;
  }
  if(door_elapsed_time >= 35 && buzzer_status == 2){
    tone(buzzer_pin,330,BUZZER_LEN); // ミ
    buzzer_status = 3;
  }
  if(door_elapsed_time >= 40 && buzzer_status == 3){
    tone(buzzer_pin,349,BUZZER_LEN); // ファ
    buzzer_status = 4;
  }
  if(door_elapsed_time >= 45 && buzzer_status == 4){
    tone(buzzer_pin,392,BUZZER_LEN); // ソ
    buzzer_status = 5;
  }
  if(door_elapsed_time >= 50 && buzzer_status == 5){
    tone(buzzer_pin,440,BUZZER_LEN); // ラ
    buzzer_status = 6;
  }
  if(door_elapsed_time >= 55 && buzzer_status == 6){
    tone(buzzer_pin,494,BUZZER_LEN); // シ
    buzzer_status = 7;
  }
  if(door_elapsed_time >= 60 && buzzer_status == 7){
    tone(buzzer_pin,523); // ド
    buzzer_status = 8;
  }
  if(door_elapsed_time >= 3600 && buzzer_status == 8){ //1時間も開けっ放しなのは明らかに緊急事態
    emergency();
    buzzer_status = 9;
  }
}
void air_cool_on(int target_temp){ //エアコンON(温度変更)赤外線信号を送信
  heatpumpIR = new PanasonicDKEHeatpumpIR();
  heatpumpIR->send(irSender, POWER_ON, MODE_COOL, FAN_AUTO, target_temp, VDIR_AUTO, HDIR_AUTO);
  delay(600);
  heatpumpIR->send(irSender, POWER_ON, MODE_COOL, FAN_AUTO, target_temp, VDIR_AUTO, HDIR_AUTO);
}
void air_cool_off(){ //エアコンOFF赤外線信号を送信
  heatpumpIR = new PanasonicDKEHeatpumpIR();
  heatpumpIR->send(irSender, POWER_OFF, MODE_COOL, FAN_AUTO, 26, VDIR_AUTO, HDIR_AUTO);
  
}
void get_env_info() { //温湿度および推定WBGT値を取得
  SHT31.GetTempHum();
  
  temp = SHT31.Temperature();
  humi = SHT31.Humidity();
  wbgt_check();
}
void wbgt_check(){ //WBGT値を推測
  wbgt = 0.735*temp+0.0374*humi+0.00292*temp*humi-4.064;
  if(((millis() - last_heatalert_time)/1000) >= 1800 || last_heatalert_time==0){ //前回発動から30分が経過して再確認
    if(wbgt>=31){ //WBGT値が危険基準値(31℃以上)に達したらエアコン24℃
      heatstroke_alert(); 
      air_cool_on(24);
    }else if(wbgt>=28){ //WBGT値が厳重警戒値(28℃以上)に達したらエアコン26℃
      if(ac_status != 1){
        air_cool_on(26);
        ac_status = 1;
      }
    }else{
      if(ac_status != 0){
        ac_status = 0;
      }
    }
  }
}
void OLED_display(){ //OLEDディスプレイへの表示を行う
  get_env_info();
  if(((millis()-day_elapsed_time)/1000)>=86400){//24時間経過したら開閉回数リセット
    door_count = 0; 
    day_elapsed_time = millis();
  }
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(2);
  display.setFont();
  display.print(F("  "));  display.print(temp); display.println(F(" C"));
  display.print(F("  "));  display.print(humi); display.println(F(" %")); 
  display.print(F("WBGT:")); display.println(wbgt); 
  display.print(F("Count: ")); display.println(door_count); 
  display.display();
}
void emergency(){ //緊急ボタン押下時に接点入力へデータ送信
  display.clearDisplay();
  tone(buzzer_pin,1046,6000);
  display.drawBitmap(0, 0, emergency_Bitmap, 128, 64, WHITE);
  display.display();
  soracom_send_long();
}
void heatstroke_alert(){ //熱中症危険時に接点入力へデータ送信
  tone(buzzer_pin,900,3000);
  soracom_send_double();
  last_heatalert_time = millis();
}
void soracom_send_single(){ //シングルクリック<1> 扉開閉時
  digitalWrite(soracom_signal_pin, HIGH);
  delay(500);
  digitalWrite(soracom_signal_pin, LOW);
}
void soracom_send_double(){ //ダブルクリック<2> 熱中症危険時
  digitalWrite(soracom_signal_pin, HIGH);
  delay(500);
  digitalWrite(soracom_signal_pin, LOW);
  delay(500);
  digitalWrite(soracom_signal_pin, HIGH);
  delay(500);
  digitalWrite(soracom_signal_pin, LOW);
}
void soracom_send_long(){ //長押し入力<3> 緊急ボタン押下時
  digitalWrite(soracom_signal_pin, HIGH);
  delay(1500);
  digitalWrite(soracom_signal_pin, LOW);
  delay(4000);
}
https://github.com/reo-g/Mimamori-Kun でも公開しています。
見守られる側に便利な機能
エアコン自動ON機能
熱中症の危険度を判断するための指標としてWBGT値(暑さ指数)が用いられます。
環境省 熱中症予防情報サイトに記載の通りWBGT値が28℃(WBGTの単位は℃)を超えると熱中症患者は著しく増加する傾向があり、
31℃を超えると危険基準となり高齢者では安静状態でも熱中症が発生する可能性が高いといえます。
そこでSHT31から得られる温湿度情報からWBGT値を推定し、それを元にしてエアコンを自動でONにすることで室温の上昇を抑え熱中症を防ぐことを目的としました。
これは体温調節機能が低下している高齢者だけではなく十分に発達していない幼児,子供への対策としても有効だと考えています。
WBGT値は本来黒球温度,湿球温度,乾球温度の3要素が算出に必要なのですがそれはあまりにハードルが高いので小野雅司,登内道彦(2014).通常観測気象要素を用いたWBGT(湿球黒球温度)の推定 日本生気象学会雑誌 50巻4号 を参考にし、ある程度高精度で全国共通のWBGT推定式として用いることが以下の式を利用した。
WBGT = 0.735*Temp+0.0374*Humi+0.00292*Temp*Humi+7.619*SR-4.557*SR^2-0.0572*WS-4.064;
Temp:乾球温度(℃),Humi:相対湿度(%),SR:全天日射量(kW/m^2),WS:風速(m/s)
今回は想定される使用場所が室内であることから日射量と風速を0とした値を推定WBGT値として用いることにしました。
この推定WBGT値が28℃を超えるとエアコンを冷房26℃に,31℃を超えるとアラートをSORACOMサーバーに送信し通知すると同時に冷房24℃にセットする赤外線信号をエアコンに送信します。
冷蔵庫開けっ放しお知らせ機能
冷蔵庫をうっかり開けっ放しにしてしまうと庫内温度の上昇により食材が危なくなると同時に電力消費が増える要因となってしまいます。
私事ではありますがワイン10本が常に占拠されている僕の冷蔵庫では夏場の庫内温度上昇はなんとしても避けなければならないので扉が閉まりきってない状況をなんとしても回避しなければなりません
そこでリードスイッチで監視している扉の開閉時間が25秒を超えると5秒ごとにドレミファソラシドの音階でお知らせするとともに、1分以上の開放を検知すると連続してブザーを鳴らし開けっ放しをお知らせします。
OLEDディスプレイに経過時間を表示し、音階でお知らせすることで家庭での節電意識の向上にもつながるかもしれません(?)
25秒からは5秒ごとにドレミファソレシドが流れて1分以上の開放を検知すると閉め忘れと判断してずっと鳴ります pic.twitter.com/yo2dI39vO6
— れおまる (@_reo_g) August 14, 2020
緊急通報機能
接点入力としてスイッチを押すと前述の通りアラート通知がすぐさまIFTTTにプッシュされ、LINE Notifyなどで通知を飛ばすことができます。
ゲームスイッチという軽い力で押し心地のいいスイッチを使っているので誰でも簡単に押せちゃいます。
メッセージはプログラムでいつでも変更できることから緊急通報としてではなくスマホなど持っていない子供が家に帰ったときのお知らせに使うなど他の用途も考えられるのではないでしょうか。
緊急ボタンを押すとLINE Notifyからの通知で助けを呼べます pic.twitter.com/MSOewhIwFM
— れおまる (@_reo_g) August 14, 2020
開発の中で詰まった点
フォント変更
display.setTextSize(2); //文字サイズを2倍にする
これはこれで味がありますが秒の表示に対して文字の粗さが際立ってしまいますね

そこでAdafruit GFX Graphics LibraryでFreeSans18pt7bをインポートして使うことにしました。
https://learn.adafruit.com/adafruit-gfx-graphics-library/using-fonts
これにより数字の解像度が上がって数字が鮮明に確認できて自然ですね。

画像表示
緊急ボタンを押してもブザーの音がなるだけではなんだか味気ないので画像を表示させることで助けがくるんだという安心感をもたらします。
いらすとやのこちらのイラストがかわいい上にやる気が感じられるのでこれを使わせていただくことにします。

これを元に128x64でそれっぽい画像をまず作り、http://javl.github.io/image2cpp/でOLEDディスプレイ出力用に変換します。

これをAdafruit SSD1306 LibraryのdrawBitmap関数で描画させ、かわいいイラストが表示できました。
  // '通報完了画面', 128x64px
  const unsigned char emergency_Bitmap [] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x03, 0xf8, 0x00, 0x3f, 0xfc, 0xff, 0xc0, 0x00, 0x00, 0x18, 0xff, 0xc0, 0x00, 0x00, 
 (略)
  }
  void emergency(){ //緊急ボタン押下時に接点入力へデータ送信
    display.clearDisplay();
    tone(buzzer_pin,1046,6000);
    display.drawBitmap(0, 0, emergency_Bitmap, 128, 64, WHITE); //イラストを描画
    display.display();
    soracom_send_long();
  }
扉開閉時の漢字の"秒"も同様に画像を変換することで表示しています。
SORACOM Orbit
扉の開閉回数を表示させたり、過去24時間の開閉回数を元にアラートを発火させるためにはclickTypeを区別する必要があるので、
SORACOM Lagoon単体では行うことができません。
そこでSORACOM OrbitによってLTE-M ButtonからのJSONを加工してclickTypeごとにフィールドの追加を行い、それをSoracom Harvestを通じてSORACOM Lagoonで可視化します。
締め切り前日の深夜にWebAssemblyの環境構築を始めるギリギリのタイミングでしたが使ってみるととても便利なサービスだと感じました。
環境構築およびWASM モジュールの開発,SORACOMプラットフォームへのデプロイはSORACOM Orbit開発者ガイドが大変わかりやすく、特にorbit-development-environment.zip中のサンプルコードを参考にしました。
https://dev.soracom.io/jp/orbit/what-is-orbit/
https://dev.soracom.io/jp/orbit/development/
# include <cmath>
# include <cstdint>
# include <cstdlib>
# include <string>
# include <emscripten.h>
# include "soracom/orbit.h"
# include "nlohmann/json.hpp"
using nlohmann::json;
int32_t uplink_body();
extern "C" {
EMSCRIPTEN_KEEPALIVE
int32_t uplink() {
    soracom_log("hello, orbit!\n");
    return uplink_body();
}
}
int32_t uplink_body() {
    const char* buf = NULL;
    size_t siz = 0;
    int32_t err = soracom_get_input_buffer_as_string(&buf, &siz);
    if (err < 0) {
        return err;
    }
    soracom_log("received data: %s\n", buf);
    json j = json::parse(buf);
    soracom_release_input_buffer(buf);
    j["singleClick"] = 0;
    j["doubleClick"] = 0;
    j["longClick"] = 0;
    if(j["clickType"]==1){
        j["singleClick"] = 1;
    }else if(j["clickType"]==2){
        j["doubleClick"] = 1;
    }else if(j["clickType"]==3){
        j["longClick"] = 1;
    }
    std::string output = j.dump();
    soracom_set_json_output(output.c_str(), output.size());
    return 0;
}
これにより作成したWASMモジュールをSORACOMプラットフォームにデプロイさせ、設定を行うと以下のようにフィールドが追加され成功していることがわかります!
'0','1'の値をClicktypeに対応したフィールドに格納しているので、単純にこのsum()を取ることで過去24時間にどのプッシュパターンが何回得られたのかSORACOM Lagoon上で可視化することができるようになりました。
さいごに
ギリギリとなってしまいましたが最低限実装したかった機能は全て何とか間に合ったので目的としていた見守りシステムの製作はひとまず完成となりました!
しかしプリント基板発注や3Dプリンターによるケースの製作などによってさらなる小型化を行い、学習リモコン機能の追加などさらなる発展も考えられるので量産という野望を持ちながら今後とも開発していけたらいいなと思います。
高校時代から家電と連動した見守りシステムというのは継続して考えていたアイデアだったのですが、当時は技適取得された比較的安価な3G/4G通信モジュールは存在しておらず、Wi-Fiに接続できるデバイスを作ったところでおばあちゃんの家にはインターネット環境がないため設置できないという問題がずっと残ってしまい、実用的なシステムというのは電子工作の範囲ではなかなか手の届かない領域でした。
しかしLTE-M Button PlusやGPSマルチユニットを筆頭として個人製作においてもハードとソフトの両面が手の届く範囲となり、IoTテクノロジーの民主化が進みゆく進化ですね!!
AWSもSORACOMのサービスも触れたことなかった初心者だったのですがSORACOM Summer Challenge 2020を通じてさらにIoTへの興味とものづくりの面白さを再確認できました。それと同時にアイデアを具現化するためのプラットフォームとしてドキュメントの丁寧さや欲しい機能がすでにあることなど"SORACOM"の強さを感じました。
コロナの影響で大学施設への立ち入りが制限され当初のアイデア案の実現が困難となり、おまけにテスト日程がずれたことで7日間での参加となりバタバタ忙しい開発となりましたが、質問への迅速かつ丁寧な対応など手厚いサポートをしてくださったSORACOMメンターの方々に感謝でいっぱいです。本当にありがとうございました!
参考文献
- 公式ガイドブック SORACOM プラットフォーム
- 公式ワークブック SORACOM 実装ガイド
- SORACOM LTE-M Button for Enterprise で 休校中の自宅学習時間を記録する

