「子ども見守りサービス」をDIYで自作する
公開日 更新日 2020/09/16
最近「子ども見守りサービス」が注目されているようです。
どうも私は課金の発生する有料サービスに飛びつくことに抵抗を感じていて、まずは無料で自作できる範囲で試行してみることにしました。
この記事では、とりあえず試行運転を開始した自作「子ども見守りサービス」の設計について簡単にメモを残したいと思います。
Contents
はじめに
ここ最近、緊急地震速報がスマートフォンに通知されたり、子どもが不審者に誘拐されるという類の事件がポツポツ発生したり、子どもがいらっしゃる親の立場からすると、日中の仕事の合間は子どもが心配で心配で不安が増すばかりの世知辛い世の中になっている気がします。
そんなご時勢だからか「子ども見守りサービス」が各社から提供されているところを見ると、やはり私と同じような悩みを持つご家庭での注目度があがっているんではないでしょうか。
しかしながらサービス内容を見ると、GPSで細かな粒度で正確な位置情報を刻一刻と、いわゆる「監視」すると言う内容で、我が家にはちょっとゴージャス過ぎる気がしました。
実際、我が家の子どもは一人で行動することの出来る年齢になっていて、そこまで監視しなくてもいいのではないかと思ってます。
もっとシンプルでやさしく見守れるぐらいの「把握」する程度のレベル感でよいと思ってます。
少し調べてみた感じでは都合のよいサービスはなさそうでしたし、考えれば考えるほど自作したほうが早くないか?と思うようになったため、とりあえず自作することが出来るのかという検証という意味でサービスを自作してみることにしました。
自作「子ども見守りサービス」でできること
「子ども見守りサービス」を自作することでできることについては、作りながら都度やれることを考えていました。
最終的に次のようなことが実現できるようになりました。
- 子どもの現在いる場所をざっくりと把握
行き先を8つ分登録して、その選択肢のうち今どこにいるのかお知らせしてくれる機能を持たせました。
たとえば学校とか習い事などの選択肢で、直近の子どもの行動範囲から、だいたい行き先を8つに分類しました。 - 子どもが入力用端末を手動操作し、自分で行き先を宣言
スイカのICカードリーダーで即座に反応する方法も気になったのですが部品代がとても高いため、考え抜いた挙句に子どもが自分で宣言するタイプになりました。
そのため操作性に気をつけ、入力用端末の設計においては、ボタン数は最低限で操作ミスが無いシンプルな入力方法にしました。 - 入力を忘れないように、気づきやすい工夫を盛り込む
子どもなのでうっかり忘れることが想定されたので、移動時に一番目立つ必要がありました。
そこで入力用端末を電池で稼動するようにして、玄関の出入りで目に付く玄関ドアへマグネットで貼り付けることにしました。 - ランニングコスト不要
とにかく結果が出るまでの投資を抑えたかったので、入力用端末の通信は自宅WiFi限定で接続し、通知先もフリーのチャットサービスであるSlackを採用しました。
これにより運用が始まってからは一切課金が発生しなくなるというのが一般的な見守りサービスとは異なる点です。
常に居場所を把握するのではなく、当日朝に会話したその日のスケジュールにあわせた、行動開始を教えてくれるというサービスになります。 - 原価も抑えてお財布にやさしい
入力用端末を設計するに当たっては、前回がんばったドア開閉イベントをSlackへお知らせするセンサーの知見を活用して、部品で仕入れ電子工作によって組み立てました。
少しばかり部品数が増えてしまいましたが、何とか自力でくみ上げることが出来、そのため端末の初期投資についても想像以上に安くできました。
さらに故障が起きても部品レベルで構造を把握しているので、高い確率で自己解決できる可能性が開けます。
部品調達
要件を達成させるために、ハードウェア面については入力用端末の自作がどうしても必要になります。
その設計において、選んだ部品と選定背景をメモしておきます。
ESP8266
前回つくった「ドア開閉イベントをSlackへお知らせするセンサー」と同様に、メインに使うチップにはWifi接続に必要な部品が組み込まれていて追加部品が少なくて済むESP8266を使います。
どうやら最近ESP8266にはESP32という後継チップがリリースされているようですが、違いはBluetooth機能の追加などで、今回作る端末としては求められていない余剰機能のようです。
価格を見ても、まだ希少価値のためかESP8266に比べて高い値付のようにも見えますので、ESP8266を使います。
プログラムを書き込むためのシリアルコンバーターは前回購入済みで、これはずっと使いまわせる部品なので今回は部品代には不要で流用できたものは安く済みます。
レギュレーターとコンデンサ
電池稼動という要件があり、今回もESP8266の動作に必要となる3.3Vをレギュレーターで出力します。
前回の部品調達時には知識もなく参考サイトをそのまま採用していましたが、今回は少し勉強してみたところレギュレーターにも時代とともに技術力の向上が進んでいるということがわかりました。
レギュレーターの仕様には比較項目が大量にあってどれを見比べてみたらよいのかは残念ながら把握しきれてませんが、いろいろなWEBサイトを眺めていると、ひとつだけ「入出力間電位差」が大事なポイントなんだと読み取りました。
入出力間電位差は、たとえば前回使ったNJU7223F33だと標準0.4Vという値に対して、↓のNJM12888F33だと標準0.1Vなのですが、これは単純に言えば3.3Vを出力したいと思ったら前回使ったNJU7223F33では電池から3.7V以上の入力が必要だったけど、NJM12888F33だと3.4Vまで減っても動作可能になるという違いのようです。
つまり、電池が平均1.2Vまで減った時点で放電完了なんだとすると、3本直列(1.2V * 3本 = 3.6V)だとぎりぎり足りず最後まで使い切れなかったけど、新しい部品だと余裕を持って動作可能になるという違いにつながるようです。
メーカーについても、東芝製や海外メーカー製など選択肢はたくさんあるということも知りましたが、残念ながらどの選択肢が一番かは区別できませんでしたので、とりあえず国産で使ったことのある無難な部品という意味で、↓を選択しました。
- 低飽和レギュレーター 3.3V300mA NJM12888F33(5個入)
- チップ積層セラミックコンデンサー 1μF50V[1608] (20個入)
- チップ積層セラミックコンデンサ 0.1μF50V1005 (100個入)
なお、今回はSOT23と呼ばれる超小型パッケージの部品に挑戦してみました。
YouTubeにも実演する動画もあって、自信が持てたし、
失敗する可能性もあったものの、5個入りだったので1個ぐらい成功すればいいかな、という軽いのりで決めました。
電池ボックス
レギュレーターの仕様通りであれば、3本直列のものがよいかと思います。
ただし、乾電池は2本か4本のパックで販売してるのでどのみち3本を作るには無駄ができます。
また、電池ボックスが4本でもちょっとだけ配線を変えれば3本直列も作れるけど、逆に3本の電池ボックスだと追加が難しいので、失敗しても耐えられそうだったので4本入るものにしました。
ドアに張り付けるパッケージにしたかったので、サイズと重さを考慮して単4の乾電池です。
電池ボックスってあまり選択肢が無い上に、単4だとさらに少なくなりますが、がんばって探すと無事に見つけることが出来ました。
タクトスイッチとスイッチカバー
入力端末にはスイッチをたくさんつけたいと思いましたが、スイッチには色々な種類のものがあり、値段もそれなりに多く時間をかけて選んでみました。
最終的に安い部品でいいんだと言う結論に至り、一般的な工作でよく使われているなタクトスイッチで十分でしたが、その際、タクトスイッチのボタン部分が小さく子供が押しにくそうだったので、スイッチカバーも購入。
スイッチカバーは自分で作ってもよかったのですが、丁度よいサイズのものを発見したので、これにしました。
また、スイッチカバーは裏側に溝があってここに3mm程度の棒を入れるとタクトスイッチまでの高さ調整が出来るようですので、アクリル角棒を使って工作しました。
ケース
スイッチに限らず、この手の電子工作にはケースをこだわりだすとキリがなさそうです。
今回は試作品をすばやく導入することだったので、超すばやく完成させたかったため、デザインにはこだわらずケースは100円ショップで取り揃えました。
購入した部品は基板と単4電池ボックスが収まる程度のプラスチックのケースと、玄関ドアに貼り付けるための強力磁石の2つ。
プラスチックケースは都合のよいサイズが見当たりませんでしたが、小物入れでは無く錠剤入れコーナーを探したら出会うことが出来ました。
プラスチックで出来ていれば同じようなものなので、小物入れにこだわる必要は無かった模様で、ここ非常に重要なところです。
強力磁石は4個入れ立ったらよかったけど、普通の磁石じゃなくて強力磁石ってところが安心感があってよい買い物だと思いました。
何年か経過してもまだ需要があるようならケースは作り変えてもいいかも、という残念な見かけではあります。
74HC595
今回の電子工作では、比較的大目のLEDを光らせて選択状態を表示するというつくりにしたいと思います。
この74HC595というICチップを使うと、大量のLEDをたった3本の信号線で制御できるようになると言う優れものなのですが、回路のオンとオフを繰り返してモールツ信号のように伝えるという仕組みだそうです。
チップ1個あたり8LEDを操作できて、チップを数珠繋ぎにどんどん拡張していくことが出来るので、事実上チップ数x8LEDという計算でどんどん操作できるLEDを増やせるわけです。
今回はこのチップを2個繋いで計16個のLEDを制御したいと思います。
SMDパッケージという、こちらも小さい端子にはんだ付けをしなければなりませんが、気合でがんばって作ります。
LED
安い部品でよいと思ってたので、Amazonで安そうなものを選択。
たぶんLEDの商品仕様とか、色によってもですが電流制御抵抗の抵抗値を細かく分けて回路を作らないといけないんだと思います。
あまり詳しく調べてませんが↓の商品の場合、全部同じ抵抗値として200Ω程度をつなぐと、いい感じの明るさになったので、下調べはこのぐらいにして、先に進めようと思います。
抵抗
最初に買った抵抗がまだまだ残っているので、追加購入は不要でバンバン消費していきます。
汎用基板
汎用基板って両面用と片面用の2つあって今回は片面を使います。
多分どの商品でもよいと思いますが、こちらは日本製でかなりしっかりしたつくりです。
はんだ付けが上手に出来ないし、きれいな基板なのに、どこか申し訳ない気分になるのは仕方ないか。
片面ガラスコンポジット・ユニバーサル基板 Cタイプ めっき仕上げ (72x47mm) 日本製
回路図
完成した回路図が↓こちら。
スイッチやLEDといった部品数に応じてワイヤーが増えていくので当然といえば当然なんですが、見ただけで前回のドアセンサーを超える複雑さになってしまいました。
この回路図に基づいて、実際に基板へのはんだ付けも完了させてますが、一部極小部品にてこづったものの無事完成させることが出来て起動チェックも問題なく完了しました。
スイッチは合計6つ搭載して次の用途になります。
- 電源をオンするためのリセットスイッチ
通常時はスリープしているので立ち上げるためにはリセットすることになりますので、ここでは電源オンするスイッチという意味になります。 - 設定をEEPROMへ書き込みとWiFiで通知するRegistスイッチ
このスイッチを押すことで一連の保存処理を開始します。なお電池節約のため何もせず10秒経過することでもRegistスイッチと同じことをしてます。 - PERSON-A用の選択位置を上下に移動させるためのスイッチ (2個)
- PERSON-B用の選択位置を上下に移動させるためのスイッチ (2個)
LED配置について
LEDが不規則に並んでいるのですが、実際に基板に取り付けながらデザインしたためで、その完成した基板が↓こちら。
組み立てると回路図ではわからない規則的な配置になっているということがわかります。
残念ながらユニバーサル基板のサイズから、たて一列に並べると狭くなってしまうので、最後の2つは横向きに配置することになってしまいました。
回路図のポイント
回路図や回路図に基づいたはんだ付けは完成し、一連の作業での気づきポイントを2点メモ。
- LEDのための電流制御抵抗はまとめて1個
グループAとグループBで同時に点灯するLEDはそれぞれ1個だけだったので、グループごとにまとめて1個にして見ました。
完成したあとに気がついたのですが、ファームアップロード時には74HC595のメモリ状態が不安定になっているみたいでどうしても2個以上点いてしまうようです。
幸い今の回路でLEDは破壊することなく動いているのでそのまま放置しているのですが、もしかしたら抵抗を1個にまとめることってよくなかったかもしれません。 - ESP8266の起動モードの制御とスイッチ動作がうまくかみ合うようにプルアップ・プルダウン抵抗を設置
ESP8266起動時の起動モードを決める上で大事なのはGPIO0,2,15のオン・オフの設定です。
順番に、オンーオンーオフとなっていると通常実行モード、オンーオンーオンだとファームアップロードモードになりますので、GPIO0,2はスイッチを押さないとオンになるようプルアップへ、GPIO15はスイッチを押さないとオフになるようプルダウンへ結線してます。
ファームアップロードをするためにはS2(REGIST)を押しっぱなしでS1(POWER)を押してリセットをするとアップロードの待機モードに入ることが出来るという仕組みです。
この回路で残念だったのは、スイッチのチャタリングを抑止するためのコンデンサーをつけると起動モードが機能しなくなるのでスイッチは直結しており、そのためスイッチを押すときのオンオフがバタバタぶれるあのチャタリングは遅延時間をソフトウェアで入れることで回避しないといけません。
ここはプログラム時に考慮してますが、スイッチを押すときにモッタリ感があって子どもたちからは不評だったりします。
Arduinoプログラム
ドアセンサーに比べるとプログラム行数が増えましたが、やっていることはあまり多くありません。
このプログラムの見所をざっくり言うと、だいたいこのあたりです。
- LED構造体でのモデル設計
LED構造体にスイッチごとのロム保存アドレス、アニメーション用LED配置、一時変数をグループ化して管理。
ただの構造体だしあまり整理されてなくて汚いのですが、ArduinoプログラムってCそのものでこういうことも出来たんだ、という発見があり以降参考にする予定。 - 初期化周り全般
WiFiの無効化による省電力設定やボタン入力割り込み処理など。
ドアセンサーの時もそうだったけど、ESP8266って省電力のためのプログラムに独特な命令を入れる必要があり、setup()関数などは、前回同様の実装になってます。 - メインループでのモードごとの実行制御
状態遷移に照らし合わせて実行モードの切り替えと、それぞれのループ処理
実行モードにはスイッチ入力待ち、スイッチ入力中、WiFi送信の準備、Slackへの通知、通知の成否判定、の5つあってloop()関数にて切り替えるというつくりになってます。 - スイッチ制御
スイッチ入力が必要な実行モードで割り込み設定のオンオフ制御、スイッチ押しっぱなしでの連続入力。
スイッチ周りの処理が後付だったためちょっと汚くなってしまってますが、チャタリングを時間間隔を挟んで回避してるため、少しでもストレスを出さないために連続入力の仕掛けを入れてます。 - アニメーション効果
WiFi経由での登録時の時間のかかる処理でアニメーション効果を追加、LEDを常時点灯ではなく点滅効果を与えて消費電力を削減
アニメーションはLEDを使った電子工作でやってみたかったので実装。子どもたちにもほめられたポイントだったりします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
#include <ESP8266WiFi.h> #include <ESP8266HTTPClient.h> #include <ShiftRegister74HC595.h> #include <EEPROM.h> extern "C" { #include <user_interface.h> } #ifdef DEBUG_ESP_PORT #define DEBUG_PRINTF(...) Serial.printf( __VA_ARGS__ ) #else #define DEBUG_PRINTF(...) #endif // ======================================================================== // Basic configurations // Wifi connection const char* ssid = "<<Your Access Point SSID>>"; const char* password = "<<Your Access Point Password>>"; // Static IPv4 for ESP8266 IPAddress ip (nnn, nnn, nnn, nnn); // IP Address IPAddress subnet (nnn, nnn, nnn, nnn); // Subnet mask IPAddress gateway(nnn, nnn, nnn, nnn); // Gateway IPAddress dns (nnn, nnn, nnn, nnn); // DNS Server // Slack incoming web hook URL const char* slackUrl = "https://hooks.slack.com/services/<<Your Slack Incoming Webhooks URL>>"; const char* slackFingerPrint = "C1 0D 53 49 D2 3E E5 2B A2 61 D5 9E 6F 99 0D 3D FD 8B B2 B3"; const char* slackPayload = "payload={\"text\":\"%s\"}"; const char* slackUserAgent = "IoT_LocationBoard/1.0"; // GPIO# for Regist Button const int btnRegist = 15; // Number of 74HC595 const int numOfShiftRegisters = 2; // GPIO# for 74HC595 const int io595shcp = 14; // CLOCK const int io595stcp = 4; // LATCH const int io595ds = 5; // DATA // Idle time to shutdown const int shutdownIdleMillis = 1000 * 10; // Wait time per loop() const int waitMillis_InputButton1 = 10; const int waitMillis_InputButton2 = 290; const int waitMillis_Anim1 = 10; const int waitMillis_Anim2 = 90; // ======================================================================== // Global variables typedef struct { const word pattern[12]; const unsigned int size; volatile unsigned int index; volatile boolean pressing; const unsigned int button1; const unsigned int button2; const int address; } LED; LED groupA = {{0x0001, 0x0002, 0x0020, 0x0080, 0x0040, 0x0010, 0x0004, 0x0008}, 8, 0, false, 0, 12, 0}; LED groupB = {{0x0100, 0x4000, 0x1000, 0x0200, 0x0400, 0x0800, 0x2000, 0x8000}, 8, 0, false, 2, 13, 1}; LED circleLed = {{0x0001, 0x0002, 0x0020, 0x0080, 0x0040, 0x0010, 0x800, 0x0400, 0x0200, 0x1000, 0x4000, 0x0100}, 12, 0, false, -1, -1, 3}; LED okLed = {{0x0008, 0x0004}, 2, 0, false, -1, -1, 5}; LED ngLed = {{0x2000, 0x8000}, 2, 0, false, -1, -1, 6}; ShiftRegister74HC595 sr (numOfShiftRegisters, io595ds, io595shcp, io595stcp); volatile int shutdownTimer = 0; int notifyCount = 0; enum ExecMode { Booting, InputButtons, PressingButton, StartRegist, PostDatas, PostSuccess, PostFail }; volatile ExecMode execMode ; // ======================================================================== // Main program (setup/loop) void setup() { #ifdef DEBUG_ESP_PORT Serial.begin(115200); #endif wifi_set_sleep_type(LIGHT_SLEEP_T); // Disable network before regist stopWiFi(); clearStatus(); // Read saved status readRom(); // Set GPIO for buttons pinMode(groupA.button1, INPUT); pinMode(groupA.button2, INPUT_PULLUP); pinMode(groupB.button1, INPUT); pinMode(groupB.button2, INPUT_PULLUP); pinMode(btnRegist, INPUT); // attach interrupt for buttons attachInterrupt(digitalPinToInterrupt(groupA.button1), pushButtonA1_EventHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(groupA.button2), pushButtonA2_EventHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(groupB.button1), pushButtonB1_EventHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(groupB.button2), pushButtonB2_EventHandler, CHANGE); attachInterrupt(digitalPinToInterrupt(btnRegist), pushButtonRegist_EventHandler, RISING); execMode = InputButtons; } void loop() { switch(execMode){ default: case InputButtons: writeStatus(true); delay(waitMillis_InputButton1); clearStatus(); delay(waitMillis_InputButton2); shutdownTimer += waitMillis_InputButton1 + waitMillis_InputButton2; if(shutdownTimer >= shutdownIdleMillis){ execMode = StartRegist; } break; case PressingButton: pressAction(); delay(waitMillis_Anim1); clearStatus(); delay(waitMillis_Anim2); break; case StartRegist: // detach interrupt detachInterrupt(digitalPinToInterrupt(groupA.button1)); detachInterrupt(digitalPinToInterrupt(groupA.button2)); detachInterrupt(digitalPinToInterrupt(groupB.button1)); detachInterrupt(digitalPinToInterrupt(groupB.button2)); detachInterrupt(digitalPinToInterrupt(btnRegist)); // Save status if(writeRom()){ // Start network connection startWiFi(); execMode = PostDatas; }else{ eternalSleep(); } break; case PostDatas: circleAnimation(); delay(waitMillis_Anim1); clearStatus(); delay(waitMillis_Anim2); post(); break; case PostSuccess: case PostFail: notifyAnimation(); delay(waitMillis_Anim1); clearStatus(); delay(waitMillis_Anim2); notifyCount += waitMillis_Anim1 + waitMillis_Anim2; if(notifyCount >= 1000){ eternalSleep(); } break; } } // ======================================================================== // Sub functions void pushButtonA1_EventHandler() { DEBUG_PRINTF("[Button] Pushed [A1]\n"); pushButton(&groupA); } void pushButtonA2_EventHandler() { DEBUG_PRINTF("[Button] Pushed [A2]\n"); pushButton(&groupA); } void pushButtonB1_EventHandler() { DEBUG_PRINTF("[Button] Pushed [B1]\n"); pushButton(&groupB); } void pushButtonB2_EventHandler() { DEBUG_PRINTF("[Button] Pushed [B2]\n"); pushButton(&groupB); } void pushButtonRegist_EventHandler() { if(execMode == InputButtons){ DEBUG_PRINTF("[Button] Pushed [Regist]\n"); execMode = StartRegist; } } volatile unsigned long currentMilli; volatile unsigned long previousMilli; const unsigned long chatteringBorderMilli = 200; volatile int pressCounter = 0; void pushButton(LED *led){ if(execMode == InputButtons || execMode == PressingButton){ led->pressing = false; tickButton(led, led->button1, -1); tickButton(led, led->button2, 1); if(led->pressing){ if(execMode == InputButtons){ pressCounter = 0; } execMode = PressingButton; }else{ execMode = InputButtons; } writeStatus(false); shutdownTimer = 0; } } void tickButton(LED *led, int button, int step){ if(button >= 0){ int buttonState = digitalRead(button); if(buttonState == LOW){ led->pressing = true; currentMilli = millis(); if((currentMilli - previousMilli) > chatteringBorderMilli){ DEBUG_PRINTF("Diff milli: %d\n", (currentMilli - previousMilli)); led->index = (led->index + step + led->size) % led->size; previousMilli = currentMilli; } } } } void pressAction(){ pressCounter++; if(groupA.pressing){ if(pressCounter >= 6){ pushButton(&groupA); pressCounter = 6; } } else if(groupB.pressing){ if(pressCounter >= 6){ pushButton(&groupB); pressCounter = 6; } } else { execMode = InputButtons; } } void clearStatus(){ setLed(0); } void writeStatus(boolean isBlink){ word tStatus = 0; if(isBlink || execMode == InputButtons || groupA.pressing){ tStatus |= groupA.pattern[groupA.index]; } if(isBlink || execMode == InputButtons || groupB.pressing){ tStatus |= groupB.pattern[groupB.index]; } setLed(tStatus); } void post() { wl_status_t status = WiFi.status(); if(status != WL_CONNECTED) { if(status != WL_NO_SSID_AVAIL && status != WL_CONNECT_FAILED) { return; } if(status == WL_NO_SSID_AVAIL) { DEBUG_PRINTF("[WiFi] No connection found.\n"); } else if(status == WL_CONNECT_FAILED) { DEBUG_PRINTF("[WiFi] Connection failed.\n"); } eternalSleep(); } HTTPClient http; http.begin(slackUrl, slackFingerPrint); circleAnimation(); http.setUserAgent(slackUserAgent); http.addHeader("Content-Type", "application/x-www-form-urlencoded;"); String body = String("A:") + groupA.index + ", B:" + groupB.index; char slackBuf[128]; sprintf(slackBuf, slackPayload, body.c_str()); int httpCode = http.POST(slackBuf); circleAnimation(); if(httpCode == HTTP_CODE_OK){ DEBUG_PRINTF("[HTTP] POST OK. (%s)\n", http.getString().c_str()); execMode = PostSuccess; }else{ DEBUG_PRINTF("[HTTP] POST failed. (Error: %s)\n", http.errorToString(httpCode).c_str()); execMode = PostFail; } http.end(); circleAnimation(); stopWiFi(); circleAnimation(); } void circleAnimation(){ tickAnimation(&circleLed); } void notifyAnimation(){ if(execMode == PostSuccess){ tickAnimation(&okLed); }else if(execMode == PostFail){ tickAnimation(&ngLed); }else{ // Internal Error return; } } void tickAnimation(LED *led){ word leds = led->pattern[led->index]; setLed(leds); led->index = (led->index + 1) % led->size; } void setLed(word statusWord){ uint8_t ledStatus[] = {statusWord & 0xFF, statusWord >> 8}; sr.setAll(ledStatus); } void readRom(){ EEPROM.begin(4); groupA.index = EEPROM.read(groupA.address) % groupA.size; groupB.index = EEPROM.read(groupB.address) % groupB.size; } boolean writeRom(){ if((EEPROM.read(groupA.address) % groupA.size) != groupA.index | (EEPROM.read(groupB.address) % groupB.size) != groupB.index) { EEPROM.write(groupA.address, groupA.index); EEPROM.write(groupB.address, groupB.index); EEPROM.commit(); return true; } return false; } void stopWiFi(){ WiFi.disconnect(); WiFi.mode(WIFI_OFF); WiFi.forceSleepBegin(); delay(10); } void startWiFi(){ WiFi.forceSleepWake(); WiFi.config(ip, gateway, subnet, dns); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); } void eternalSleep(){ clearStatus(); ESP.deepSleep(0, WAKE_RF_DISABLED); delay(5000); } |
通知されるメッセージについて
このArduinoプログラムでは、電源オン(Rest)した後にPERSON-A用,B用の2つのLEDを見ながら、A,Bそれぞれの上下スイッチで選択位置を選び最後にRegistスイッチを押すことでSlackへ通知メッセージを送信して終了します。
このメッセージの書式はとてもシンプルにこのようなものです。
1 |
A:0, B:0 |
AとBの2人分の状態を通知して、数字部分が現在の選択位置を表しています。
慣れてくればこれだけで十分理解できるのですが、この数字がどう言う意味であるかを翻訳する役目を持ったSlackBotを別途設置したいと思います。
見やすいメッセージに翻訳してくれるBOTを設置
Slackのチャンネルを2つ用意して、1つは入力端末が通知するシンプルな書式でのメッセージにしておき、翻訳BOTがシンプルなメッセージを見たらもう1つのチャンネルへ翻訳後のメッセージを通知するようにします。
これにより翻訳後の見やすいメッセージだけを見ていればよく、シンプルなメッセージがあるチャンネルは普段は見なくてよくなります。
最初から入力端末から出すメッセージをわかりやすいものに刷ればよいのですが、どこからどこへ変わったのかを記憶するための以前の値を覚えておく必要があってちょっと処理が複雑になるし、それに行き先候補はファームに固定値を持たせるのではなく後からカスタマイズできたほうが柔軟性が高そうだったので、BOTが翻訳するという仕組みで行きたいと思います。
SlackへBOTを設置するためにはHubotを導入するのが一番手っ取り早そうです。
最近BOTは注目を集めているので、ちょっと検索するとガイドしているサイトがたくさん見つかるので、導入は非常に簡単に出来ました。
Hubot自身のセットアップや設置するためのサーバーの確保といった構築部分についてはすでに完了しているとして、ここではそのHubotが実行するメッセージ翻訳のためのスクリプトを載せておきます。
といってもとてもシンプルな翻訳BOTなのでおまけみたいなものなんですが・・・。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# Description: # 行き先を連絡 # # Notes: # # Commands: # 現在の場所 (現在の設定をリスト) # myRoom = "<<Your Slack Post Channel ID>>" fs = require('fs') cacheFile = "<<Cache file path>>" module.exports = (robot) -> try fs.accessSync(cacheFile, fs.R_OK | fs.W_OK) contents = fs.readFileSync(cacheFile, "utf8") cache = JSON.parse(contents) catch e cache = "PersonA": loc : [ ":school:School", ":house:House", ":notebook:Lessons", ":books:Library", ":two_men_holding_hands:Friends", ":bicyclist::skin-tone-2:Park", ":shopping_bags:Shopping", ":walking::skin-tone-2:Other" ], cur : 0 "PersonB": loc : [ ":school:School", ":house:House", ":notebook:Lessons", ":books:Library", ":two_women_holding_hands:Friends", ":bicyclist::skin-tone-2:Park", ":shopping_bags:Shopping", ":walking::skin-tone-2:Other" ], cur : 0 statusChange = (name, cur) -> if cache[name]["cur"] != cur prevStr = cache[name]["loc"][cache[name]["cur"]] curStr = cache[name]["loc"][cur] robot.messageRoom myRoom, name + "さんが" + prevStr + "から" + curStr + "へ移動しました" cache[name]["cur"] = cur fs.writeFileSync(cacheFile, JSON.stringify(cache)) robot.hear /^A *: *([0-9]+) *, *B *: *([0-9]+)$/i, (msg) -> curA = msg.match[1] curB = msg.match[2] statusChange("PersonA", curA) statusChange("PersonB", curB) robot.respond /現在の場所/i, (msg) -> for name,map of cache curLoc = map['loc'][map['cur']] msg.send "#{name}さんは現在#{curLoc}にいます" |
翻訳BOTメモ
翻訳BOTは次のようなメッセージに翻訳して通知してくれます。
1 |
PersonAさんがSchoolからHomeへ移動しました |
もちろん人の名前や行き先はプログラム中へ入力することで自由にカスタマイズすることが出来ます。
このスクリプトを設置したBOTを次のような感じで、入力端末が通知するメッセージのためのチャンネルと、翻訳後のメッセージを通知するチャンネルの2つに所属させてあげれば完成です。
- Slackチャンネル1
- 入力端末からのメッセージの通知先
- 翻訳BOTが監視
- Slackチャンネル2
- 翻訳BOTからの翻訳後のメッセージの通知先
- Slackユーザーが選択状態を監視するためのチャンネル
さいごに
前回と比較すると、部品数だけでなく、作業内容やBOTという新たなコンポーネントの追加といった面においても、はるかに凌駕するスケール向上がありました。
さすがに完成させるまで、本当に動くのだろうかと不安になったのですが、完成後の動作しているところを見ると感動ものでした。
入力端末を使うのは子どもたちなのですが、最初は使い方がわからなかったものの興味があったようでがんばって操作方法を覚えてましたが、スイッチカバーの色分けが気に入ったようで、あきらめることも無く、生活の一部に溶け込んでいけたような気がしてます。
日中に通知メッセージを見るたびに、子どもたちの行動を今まで以上の感度で把握できるようになったので安心感が得られている気がしますし、これだけのものをリリースできたってことで自分自身の電子工作の技術力が少しは向上したんだなぁと実感してたりします。
稼働時間について
更新を忘れてましたが、本記事で作った端末は単4のエネループを4本使った回路となっており、通算稼働時間は約4か月でした。
1日に電源を入れる回数が不定期なので、この値はだいたいの平均値と考え、正確な値は何度かサンプリングしないといけないと思われます。
とはいえ、4か月なので単3と単4の容量の違いや、乾電池と充電池の違いを考慮すれば、変な値ではないと思いますので、個人的には特に問題ないものだったと考えています。
変更歴
2018/02/12 : Slackサイトのフィンガープリント(拇印)が2018/02/08付で更新されていたので、ソースコードを修正、すっかり忘れていた通算稼働時間が4か月であることを実況。
レスポンシブ広告
関連記事
-
ArduinoのPinChange割り込みライブラリとタイマーライブラリを使う
前回、素早くマイクミュートやPCスリープが行えるボタンを自作することで、とても晴 …
-
ATmega328へArduinoを書き込む
Arduino UNO等のArduinoボードを購入すると、USB端子に直接接続 …
-
SlackのIncoming Webhooksが失敗した時の対処
我が家のIoTとしてSlackのIncoming Webhooksを活用したサー …
-
ドア開閉イベントをSlackへお知らせするセンサーを作る
最近Raspberry Piを手に入れてから個人的にマイコンが流行っていて、WE …
-
ATmega328の書き込み装置をArduino UNO用シールドとして作成
前回、AVRマイクロコントローラーのATmega328PへのArduinoブート …
-
Arduinoでロータリーエンコーダーの動作確認
ロータリーエンコーダーは、よくボリューム調整のつまみに使われているようなモジュー …
-
素早くマイクミュートとPCスリープが出来るDIYボタンを作る
2020年はCOVID-19の影響で長期にわたる外出自粛が続きました。9月時点で …
-
Attiny13Aの工場出荷時の書き込みエラーを対策
Attiny13Aは工場出荷時に低速の動作周波数になってる模様です。 この時Ar …