前回に続いて,スマートフォンから水やりできるシステムを Raspberry Pi を使って作る方法を紹介します.
はじめに
ハードに引き続きソフトも本格的に作ってみました.次の動画を再生すると動いている様子が見れます.
主な機能は次の通りです.
- 蛇口の開閉
- タップで蛇口を開閉できます.
- 水流量確認
- 流れている水の量をリアルタイムに確認できます.
- 予約実行
- 指定した時間に自動的に水やりができます.
- ログ
- 水やりの記録を確認できます.蛇口の開閉だけでなく,「水やり量は約 1.23L でした。」のような表示もします.
- 自然な UI
- 最近のモダンな WEB UI 相当の自然な動作を行います.
構成
UI を Angular 7.1 と Bootstrap で構築し,API サーバを兼ねた Flask で配信しています.
実は,まとまった量の Javascript を書くのは久しぶりで,Angular というか TypeScript を書くのは初めてという状況だったのでいろいろと不安だったのですが,下記の書籍を7章まで目を通すだけでサクサク作れました.この手のソフト関係の入門本はアタリにあたった記憶がここ何年もなかったのですが,これは相当良かったです.
単なる解説にとどまらず,考え方や思想がが織り込まれている上に,ハマりがちな落とし穴にも手当されていて本当に助けられました.
ちょっと脱線しますが,作る過程で Javascript ベースの UI 構築の生産性が 10年前と比べて圧倒的に向上していることを見せつけられてかなりびっくりしました.抽象化を正しく行うことの重要さを実感します.
Bootstrap と Flask は WEB 上のリソースを参照するだけでなんとかなりました.
コード
ソースコード全体は github にあげてあります.
工夫したポイント
ソフトとしては割とシンプルですが,工夫したポイントを簡単に紹介します.
Server-Sent Events の活用
Server-Sent Events と Angular のサービスを組み合わせて,サーバ上のデータが更新された場合にリアルタイムに UI に反映するようにしました.
サービス側のコードはこんな感じです.EventSource を使って,サーバ側のイベントを監視します.
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 |
import { Subject } from 'rxjs'; import { Injectable } from '@angular/core'; import { Inject } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class PushService { private dataSource = new Subject<string>(); public dataSource$ = this.dataSource.asObservable(); private eventSource; constructor( @Inject('ApiEndpoint') private readonly API_URL: string, ) { let self = this; this.eventSource = new EventSource(`${this.API_URL}/event`); this.eventSource.addEventListener('message', function (e) { self.notify(e.data); }); } private notify(message) { this.dataSource.next(message); } } |
参照する側では次のようにして,pushService のデータソースを subscribe してイベントを受け取ります.
1 2 3 4 5 6 7 8 9 10 11 |
import { Subscription } from 'rxjs'; import { Component, OnInit } from '@angular/core'; import { PushService } from '../service/push.service'; (中略) ngOnInit() { this.subscription = this.pushService.dataSource$.subscribe( msg => { if (msg == "log") this.updateLog(); } ); } |
Flask サーバ側は次のようになります.
リクエスト毎に生成される generator 内でカウンタ(last_count)を持っておき,グローバルのカウンタ(event_count)と1秒ごとに比較を行い,不一致の場合に通知を行います.もう少しリアルタイム性を持たせるなら,セマフォとか使うと良いかもしれません.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
event_count = { EVENT_TYPE_LOG: 0, EVENT_TYPE_SCHEDULE: 0, } @rasp_water.route('/api/event', methods=['GET']) def api_event(): def event_stream(): last_count = event_count.copy() while True: time.sleep(1) for method in last_count: if (last_count[method] != event_count[method]): yield "data: {}\n\n".format(method) last_count[method] = event_count[method] res = Response(event_stream(), mimetype='text/event-stream') res.headers.add('Access-Control-Allow-Origin', '*') res.headers.add('Cache-Control', 'no-cache') return res |
SQLite の活用
Flask でのデータの保持にはメモリ上の SQLite を使いました.Raspberry Pi がリセットするとデータが消えてしまいますが,手軽さを優先しました.
Flash はマルチスレッドなので,そのままだと SQLite は使えませんが,次のように check_same_thread=False
オプションをつけると,複数スレッドからアクセスできるようになります.ただし,更新する際は排他処理が必要です.
1 |
sqlite = sqlite3.connect(':memory:', check_same_thread=False) |
また,SQLite から SELECT した結果を JSON にして返したいので,次のようにして SELECT の結果が dict になるようにしました.
1 |
sqlite.row_factory = lambda c, r: dict(zip([col[0] for col in c.description], r)) |
cron の活用
予約実行は Linux に標準で用意されている cron を使いました.python-crontab というライブラリを使うと簡単にユーザの crontab を読み書きでき,今回の目的くらいであれば十分な感じです.
まとめ
最近の UI フレームワークを活用すると,電子工作で作ったハードにモダンな UI を手軽に追加できるのでお勧めです.お試しあれ.
コメント
[…] ソフト編に続く. […]