編集履歴一覧に戻る
RYUJIのアイコン画像

RYUJI が 2021年05月04日18時35分38秒 に編集

初版

タイトルの変更

+

【Wekiの気持ち】植木の環境状態を測定して植木とのコミュニケーションを図る

タグの変更

+

obnizIoTコンテスト

+

obniz

+

GoogleAppsScript

+

LINE

+

Machinist

+

GoogleSheets

+

obnizcloud

+

JavaScript

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

本文の変更

+

# システム概要 ![キャプションを入力できます](https://camo.elchika.com/ac685322e894e82c754e9349c0ee1513c6186775/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f61323238393435372d633330352d346463632d623565612d646134613330623332373639/) **IoTとは何か?に一石を投じる問題作!** あなたの育てる植木さんは日々様々な環境下で過ごしています。このIoTシステムは人間の代わりに植木さんをお世話することはできません。しかし、植木さんの気持ちをLINEを通して知ることができます。植木さんにどんどん話しかけてください。植木さんの気持ちを知り、植木さんに愛情あるお世話をするのはあなた自身です! 植木さんとLINEを使ってコミュニケーションを図り、植木のお世話をするための補助となる「植木さんの気持ち」を知ることができるシステムです。 LINEで色々と話しかけると、植木さんの環境状況をリアルタイムにセンシングしてその環境状況に応じた植木さんの気持ちが返信されます。 例として、以下のようなLINE投稿に対する動作と返信が行われます。 | LINEへの投稿 | 実行されるobnizセンシング | 植木さんからのLINE返信 | |---|---|---| | 喉が乾いてない? | 土壌水分センサーを使って水分量を計測 | のどが渇いたよ!/潤ってるよ! | | 暑くない? | 温度センサーを使って温度を計測 | 暑いよ!/寒いよ! | | 暗くない? | 照度センサーを使って明るさを計測 | 明るいよ!/暗いよ! | | 調子はどう? | 上記全部センサー+Co2センサーを使って光合成条件を計測 | 調子がいいよ!/悪いよ! | ※実際には上記のような2択の回答ではなく、計測数値のしきい値によって回答内容をいろいろと段階分け設定できるようになっています。 その他、人間からのLINE投稿が無い時でも定期間隔で自動計測するようになっており、その計測データをMachinistに蓄積してグラフ化していますので、LINEにてグラフを要求する投稿を行うと計測データ履歴グラフを見ることもできます。 | LINEへの投稿 | 実行されるobnizCloud側処理 | 植木さんからのLINE返信 | |---|---|---| | 暑さの履歴を見せて | 温度計測グラフのリンクを生成して返信作成 | グラフにしてみたよ!(+Machinistグラフ表示リンク)| ■ WekiさんとLINEで会話(暑さ、明るさ、水分) ![キャプションを入力できます](https://camo.elchika.com/14a21c7e65f4ea1d98b9f34d6c6499a1860a7441/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f32623130366463622d663130642d343536332d616666302d656633663837313164376161/) ■ Wekiさんと会話(調子を聞くと全センサー稼働:調子=水+光+Co2=光合成!) ![キャプションを入力できます](https://camo.elchika.com/3ee75811a3e31985ca54d4542d69fcb7e6f5b7cf/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f62386464316666332d393437312d346462622d393366392d333832373561393631343232/) ■ 定期測定を自動実行しており、グラフを参照可能 ![キャプションを入力できます](https://camo.elchika.com/925b1b3ddce3894035553873e6d716a7958d3db0/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f33343831343064382d613231652d343634632d393636332d663162346364343534393431/) ![キャプションを入力できます](https://camo.elchika.com/1f0c34adde3e7e31bc18bd3fef8c5cd063209299/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f33393863306335622d656262632d343065302d613737652d356137376562306239386230/) # 大まかな仕組み 細かい工夫はいろいろあるのですが、一番の肝である投稿メッセージに対するセンシングまでの仕組みについては以下のようなプログラムとなっています。これはobniz Cloud上のプログラムで処理しています。 ・LINEメッセージのテキストを取得 ・メッセージテキストの中に、センシング対象となるキーワードが含まれているか?を判断  ⇒これは事前にGoogleスプレッドシートに各種センサー起動対象のキーワードを羅列しておき、その文字列が含まれるかをチェックしています(例えば、「乾く」というキーワードが含まれていれば、水分量を測る等) ・センシング対象が決まったらセンサー値を測定 ・センサー値と「返信メッセージ閾値」を比べて、該当する返信メッセージを送信  ⇒これも事前にGoogleスプレッドシートに各センサー閾値とその時の応答メッセージを羅列しておきます。(例えば、温度が28度以上ならば「すごい暑いよ!」と返す等) また、対象センサーの起動キーワード以外にも「全体的な調子」に関するキーワード一覧を設定しておき、これにヒットした場合は全センサーを稼働して全体的な調子をポイント計算し応答メッセージを決定します。全体的な調子の計算は、光合成の環境が揃っているか?を各センサーの値からポイント付けして計算しています。(光と水と二酸化炭素が多いと光合成もバッチリできるので植木さんは調子がいいはず!温度の影響がよくわからいけど、一応快適温度のほうがポイント高くしてあります) # デモ動画 @[youtube](https://youtu.be/zYJ6u5x1fz0) # システム機器構成 | 機器名 | 機器概要 | 価格 | 購入先リンク | |:---:|:---:|:---:|:---:| | obniz Board 1Y | obniz本体 |ー | 本コンテスト参加条件としてご提供いただいたものです | SEN0114 | 水分センサー | ¥955 | [Amazonで購入しました](https://www.amazon.co.jp/gp/product/B08HLVLVDX/) | LM35DZ | 温度センサー | ¥190 | [Amazonで購入しました](https://www.amazon.co.jp/gp/product/B01HNVCJC0/) | MH-Z19B | Co2センサー | ¥3,799 | [Amazonで購入しました](https://www.amazon.co.jp/gp/product/B07B3VGGNL/) | | TEMT6000 | 光センサー | ¥750 | [Amazonで購入しました](https://www.amazon.co.jp/gp/product/B0863Q4VKG/) ※上記価格は筆者が購入した時点での価格であり、現在の販売価格は異なる場合があります。 # エッジ側機器配線図 Co2センサー(MH-Z19B)だけは暖気がいるため常時通電としています。(obniz Board 1Yだとそれができた・・・) ![キャプションを入力できます](https://camo.elchika.com/1060a6536a2eeaf286c012dbcae3740a6b1817d9/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f36633739623337382d363134662d343264372d623337382d363866353037613835333164/) ![キャプションを入力できます](https://camo.elchika.com/6b99118de92eae60670fc12739f09a4d9b210f0b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f31313231363561392d326539642d343966642d626436322d323261666239356566393061/) # システムフロー ![キャプションを入力できます](https://camo.elchika.com/fdbfde101c6c692e81e207ff28f7d9ae9a11df8d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f39643636643235322d663636332d343765372d613763622d346362373465393334376366/) ■ 通常のメッセージ送受信プロセスフロー | No. | プログラム処理概要 | 実行プラットフォーム | |:---:|---|---| | ① | 利用者がLINEチャネルへメッセージ投稿 | LINEチャネル | | ② | Webフック設定にてobniz CloudへメッセージをPost | LINEチャネル | | ③ | Googleスプレッドシート から設定情報をGet | obniz Cloud / GoogleAppsScript | | ④ | 各種センサーにて環境データをセンシング | obniz Cloud / obniz Board 1Y | | ⑤ | 環境測定結果から返信メッセージを生成し送信 | obniz Cloud | | ⑥ | LINEチャネルへ返信メッセージを投稿 | GoogleAppsScript | | ⑦ | LINEチャネルで返信メッセージを受信 | LINEチャネル | ■ 定期データ測定プロセスフロー | No. | プログラム処理概要 | 実行プラットフォーム | |:---:|---|---| | ⑧ | 定期時間毎に測定指示メッセージをPost | GoogleAppsScript | | ④ | 各種センサーにて環境データをセンシング | obniz Cloud / obniz Board 1Y | | ⑤ | 環境測定結果データを生成し送信 | obniz Cloud | | ⑨ | 環境測定結果データを保存 | GoogleAppsScript / Machinist | # obniz Cloud上の測定アプリケーション本体の公開設定 LINEからのメッセージを受け取り、各種センサーにより測定実行して返信メッセージまで作成するプログラムの本体はobniz Cloudに上に作成します。 このプログラムは、フロー図の②や⑧にあるように外部からのメッセージを受け取って起動するように設定しますので、obniz Cloud上で「ブラウザアプリ (HTML/JavaScript)」として作成後、アプリ設定画面にて「Webhookで実行」にチェックをつけてください。 ![キャプションを入力できます](https://camo.elchika.com/800b77efac9aae1c4a5a08275b79c07bfe5d41b0/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f62666631326362662d306136362d343035612d386661382d323831663865366263333464/) その後、利用するobniz Boardへのインストールを実施し、デバイス管理画面から「Webhook URL (GET/POST)」にて公開URLを確認してください。(LINEやGAS側で設定利用します) ![キャプションを入力できます](https://camo.elchika.com/8348a24cd0656dd1f4894c8a8df9242b7f4146d5/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f37313532353936612d316433372d346433622d393536612d613137323439323162643338/) # LINEチャネルからobniz CloudアプリケーションへのWebhook設定 今回のシステムは幾つかのクラウドサービス間の連携実装により実現していますが、これらは基本的にはプログラム内でHTTP-POST/GETを記述して連携させています。ただし、フロー図の②にあたる部分の「LINE投稿からobnizアプリケーションへのPOST」についてだけはLINE上のWebhook設定にて実現しています。 [LINE Developersサイト](https://developers.line.biz/ja/)にてbot用のチャネルを作成し、そのチャネルの Messaging API 設定画面にて「Webhookの利用」をONにし、「Webhook URL」にobniz Cloud側のアプリケーション公開URLを設定してください。 この設定にて、このチャネルにメッセージ投稿があるとobniz CloudアプリケーションにメッセージJSONを含む内容がPOSTされるようになります。 ![キャプションを入力できます](https://camo.elchika.com/3a6a3716486eeb663aee185a861d5b56c3f8775a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f34343132396332312d376132632d346634332d616233612d393638386130623961633866/) # GoogleスプレッドシートによるConfig情報の外出し obniz Cloud上のプログラムでは各種センサーの測定値を見て応答メッセージを決定しますが、その閾値ごとのメッセージをGoogleスプレッドシートに登録しておきます。 最初はプログラム上にハードコーディングしていましたが、閾値の調整や応答メッセージのバリエーション追加変更などをプログラム変更ではなく、設定ファイルとして外出ししておくべきと思いこのような仕込みを追加しました。 設定してある各種センサー毎の閾値と対応するメッセージは下記のとおりですが、もっと細かい閾値に段階分けしても良いですし、応答メッセージをお好みで、システムを運用しながら変更できます。 ![キャプションを入力できます](https://camo.elchika.com/6e40555e8dc98720a60cc3000d698859ea241d02/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f33373235666664662d613737612d343536342d383035632d303063323161376435316532/) # 定期測定実行の設定(GoogleAppsScript) グラフ作成用に自動的にデータ採取のメッセージをGoogleAppsScript側から送信しています。(フロー図の⑧) これはGoogleAppsScriptの「トリガー設定」にて設定しています。 ![キャプションを入力できます](https://camo.elchika.com/3535c0fa2924e5ca13e31db0117c2d95934e2fab/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f64653435316664392d336435352d346135392d613534342d346538363037393638646537/) なお、メッセージを受け取る側のobniz Cloud上のアプリケーションは同じですので、受信したメッセージがLINEからの投稿なのか、定期測定実行の指示なのかはメッセージJSONの中身(typeのところ)で判別しています。 ```html:LINEからの投稿メッセージJSON { "events": [ { "message": { "type": "text", "text": "LINEで入力したテキスト文字列" } } ] } ``` ※実際のLINEからのメッセージJSONには他のパラメータも含まれますが、見ているのはこれだけです。メッセージ全体の仕様については[Line Developesのドキュメント](https://developers.line.biz/ja/reference/messaging-api/#text-message)を参照ください。 ```html:GoogleAppsScriptからの定期測定指示メッセージJSON { "events": [ { "message": { "type": "sensor", "text": "get sensor data" } } ] } ``` # 各種プログラムソース ```html:StartMeasure(obnizCloud上の測定・応答プログラム) <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" /> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@3.x/obniz.js" crossorigin="anonymous" ></script> </head> <body> <div id="obniz-debug"></div> <script> const res_url = 'GoogleAppsScirpt上の(Postfromobniz)プログラムの公開URL'; const config_url = 'GoogleAppsScirpt上の(GetWekiConfig)プログラムの公開URL'; const humidity_graph = 'Machinist上の水分センサーグラフ公開URL'; const temp_graph = 'Machinist上の温度センサーグラフ公開URL'; const light_graph = 'Machinist上の光センサーグラフ公開URL'; const co2_graph ='Machinist上のCo2センサーグラフ公開URL'; const condition_graph = 'Machinist上のコンディション(光合成)グラフ公開URL'; const graph_msg = 'グラフにしてみたよ!\n'; var thirsty_words = []; var thermo_words = []; var light_words = []; var condition_words = []; var graph_words =[]; var co2_msg = []; var thirsty_msg = []; var thermo_msg = []; var light_msg = []; var condition_msg = []; var random_msg = []; var stamp_msg = []; var msgs = []; var condition_point = 0; var isWords = false; var isGraph = false; var obniz = new Obniz("OBNIZ_ID_HERE"); // called on online obniz.onconnect = async function () { if (Obniz.App.isCloudRunning()) { // リクエストbodyを取得 const req = Obniz.App.req(); // メッセージタイプ取得 const input_type = req.body.events[0].message.type; // LINEからのメッセージ取得 var input_msg = req.body.events[0].message.text; // 各種メッセージデータをgoogleスプレッドシートから取得する try { var httpObj = new XMLHttpRequest(); httpObj.open('GET', config_url, true); httpObj.onload = function() { var config_data = JSON.parse(this.responseText); thirsty_words = config_data.thirsty_words; thermo_words = config_data.thermo_words; light_words = config_data.light_words; condition_words = config_data.condition_words; graph_words = config_data.graph_words; co2_msg = config_data.co2_msg; thirsty_msg = config_data.thirsty_msg; thermo_msg = config_data.thermo_msg; light_msg = config_data.light_msg; condition_msg = config_data.condition_msg; random_msg = config_data.random_msg; stamp_msg = config_data.stamp_msg; } httpObj.send(); await obniz.wait(1000); } catch(e) { console.error(e); }; await obniz.wait(6000); // CLS obniz.display.clear(); // 定期実行ならば各種センサーデータ取得して返却 if(input_type === 'sensor') { var data = {}; // Co2センサー var uart = obniz.getFreeUart(); uart.start({tx:11, rx:10, baud:9600}); uart.onreceive = function(res_data, text) { if (res_data[0] == 0xff) { var co2 = res_data[2] * 256 + res_data[3]; obniz.display.print('co2: ' + co2); data.co2 = co2; for(var j=0; j<co2_msg.length; j++) { if(co2 < co2_msg[j].threshold) { condition_point += co2_msg[j].point; break; } } } } uart.send([0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]); await obniz.wait(1000); // 土壌センサー var sensor = obniz.wired('SEN0114', {vcc:0, gnd:1, output:2}); var humidity = await sensor.getHumidityWait(); obniz.display.print('Humidity: ' + humidity); data.humidity = humidity; for(var j=0; j<thirsty_msg.length; j++) { if(humidity < thirsty_msg[j].threshold) { condition_point += thirsty_msg[j].point; break; } } // 温度センサー var tempsens = obniz.wired('LM35DZ', {gnd:5, output:4, vcc:3}); var temp = await tempsens.getWait(); obniz.display.print('Thermo: ' + temp); data.temp = temp; for(var j=0; j<thermo_msg.length; j++) { if(temp<thermo_msg[j].threshold) { condition_point += thermo_msg[j].point; break; } } // 照度センサー obniz.io6.output(true); obniz.io7.output(false); var voltage = await obniz.ad8.getWait(); obniz.io6.output(false); var lux = voltage * 200; // (x / 10000.0) * 2000000.0; obniz.display.print('Light: ' + lux); data.lux = lux; for(var j=0; j<light_msg.length; j++) { if(lux<light_msg[j].threshold) { condition_point += light_msg[j].point; break; } } // 調子 => 光×Co2×水=光合成 data.condition = condition_point; // データ送信 var postdata = { 'type': 'sensor', 'data': data } postMessages(res_url, postdata); } else { // LINEからのメッセージならば、内容に応じた返信作成 var i; // グラフ要求か確認 i = graph_words.length; while (i--) { if(input_msg.indexOf(graph_words[i]) !== -1) { isGraph = true; break; } } // 調子はどうか? => Co2 i = condition_words.length; while (i--) { if(input_msg.indexOf(condition_words[i]) !== -1) { isWords = true; if(isGraph) { // グラフ要求の場合は計測しない msgs.push({'type':'text', 'text': graph_msg + condition_graph}); } else { var uart = obniz.getFreeUart(); uart.start({tx:11, rx:10, baud:9600}); uart.onreceive = function(res_data, text) { if (res_data[0] == 0xff) { var co2 = res_data[2] * 256 + res_data[3]; obniz.display.print('co2: ' + co2); for(var j=0; j<co2_msg.length; j++) { if(co2 < co2_msg[j].threshold) { condition_point += co2_msg[j].point; break; } } } } uart.send([0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]); input_msg = 'condition'; // 他センサーを全部hitさせる await obniz.wait(1000); } break; } } // 喉が乾いてるか? => 土壌センサー i = thirsty_words.length; while (i--) { if(input_msg.indexOf(thirsty_words[i]) !== -1) { isWords = true; if(isGraph) { // グラフ要求の場合は計測しない msgs.push({'type':'text', 'text': graph_msg + humidity_graph}); } else { var sensor = obniz.wired('SEN0114', {vcc:0, gnd:1, output:2}); var humidity = await sensor.getHumidityWait(); obniz.display.print('Humidity: ' + humidity); for(var j=0; j<thirsty_msg.length; j++) { if(humidity < thirsty_msg[j].threshold) { msgs.push({'type':'text', 'text':thirsty_msg[j].msg}); condition_point += thirsty_msg[j].point; break; } } } break; } } // 暑くないか?寒くないか? => 温度センサー i = thermo_words.length; while (i--) { if(input_msg.indexOf(thermo_words[i]) !== -1) { isWords = true; if(isGraph) { // グラフ要求の場合は計測しない msgs.push({'type':'text', 'text': graph_msg + temp_graph}); } else { var tempsens = obniz.wired('LM35DZ', {gnd:5, output:4, vcc:3}); var temp = await tempsens.getWait(); obniz.display.print('Thermo: ' + temp); for(var j=0; j<thermo_msg.length; j++) { if(temp<thermo_msg[j].threshold) { msgs.push({'type':'text', 'text':thermo_msg[j].msg}); condition_point += thermo_msg[j].point; break; } } } break; } } // 暗くないか? => 照度センサー i = light_words.length; while (i--) { if(input_msg.indexOf(light_words[i]) !== -1) { isWords = true; if(isGraph) { // グラフ要求の場合は計測しない msgs.push({'type':'text', 'text': graph_msg + light_graph}); } else { obniz.io6.output(true); obniz.io7.output(false); var voltage = await obniz.ad8.getWait(); obniz.io6.output(false); var lux = voltage * 200; // (x / 10000.0) * 2000000.0; obniz.display.print('Light: ' + lux); for(var j=0; j<light_msg.length; j++) { if(lux<light_msg[j].threshold) { msgs.push({'type':'text', 'text':light_msg[j].msg}); condition_point += light_msg[j].point; break; } } } break; } } // 調子はどうか? => 光×Co2×水=光合成 if(input_msg === 'condition') { for(var j=0; j<condition_msg.length; j++) { if(condition_point <= condition_msg[j].threshold) { msgs.push({'type':'text', 'text':condition_msg[j].msg}); break; } } } // どれにも引っかからないときはランダムなテキストとスタンプを返す if(isWords === false) { var r = Math.floor(Math.random() * (random_msg.length)); msgs.push({'type':'text', 'text':random_msg[r]}); r = Math.floor(Math.random() * (parseInt(stamp_msg[2]) - parseInt(stamp_msg[1]) + 1) + parseInt(stamp_msg[1])); msgs.push({'type':'sticker', 'packageId':String(stamp_msg[0]), 'stickerId':String(r)}); } // メッセージ送信 var postdata = { 'type': 'message', 'messages': msgs } postMessages(res_url, postdata); } } else { Obniz.App.done({ status: 'success', text: `nothing action` }); } } // GASにメッセージ or データを送る async function postMessages(url, postdata) { try { fetch( url, { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postdata) } ); await obniz.wait(1000); } catch(e) { console.error(e); Obniz.App.done({ status: 'error', text: e.status }); } Obniz.App.done({ status: 'success', text: `Webhook called` }); } // called on offline obniz.onclose = async function () {} </script> </body> </html> ``` ```html:getWekiConfig(GoogleAppsScript上のGoogleスプレッドシートデータ取得プログラム) function doGet(e) { var thirsty_words = []; var thermo_words= []; var light_words = []; var condition_words = []; var graph_words = []; var co2_msg = []; var thirsty_msg = []; var thermo_msg = []; var light_msg = []; var condition_msg = []; var random_msg = []; var stamp_msg = []; const colname = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; const sheet = SpreadsheetApp.getActiveSheet(); const range = sheet.getRange(1, 1, 1, colname.length); const data_types = range.getValues(); var i = 0; var data_type = data_types[0][i]; while(data_type !== "") { switch(data_type) { case "thirsty_words": thirsty_words = getKeyData(sheet, colname[i]); break; case "thermo_words": thermo_words = getKeyData(sheet, colname[i]); break; case "light_words": light_words = getKeyData(sheet, colname[i]); break; case "condition_words": condition_words = getKeyData(sheet, colname[i]); break; case "graph_words": graph_words = getKeyData(sheet, colname[i]); break; case "co2_msg": co2_msg = getKeyAndThreshold(sheet, colname[i], colname[i+1], colname[i+2]); break; case "thirsty_msg": thirsty_msg = getKeyAndThreshold(sheet, colname[i], colname[i+1], colname[i+2]); break; case "thermo_msg": thermo_msg = getKeyAndThreshold(sheet, colname[i], colname[i+1], colname[i+2]); break; case "light_msg": light_msg = getKeyAndThreshold(sheet, colname[i], colname[i+1], colname[i+2]); break; case "condition_msg": condition_msg = getKeyAndThreshold(sheet, colname[i], colname[i+1], colname[i+2]); break; case "random_msg": random_msg = getKeyData(sheet, colname[i]); break; case "stamp_msg": stamp_msg = getKeyData(sheet, colname[i]); break; case "threshold": break; case "point": break; } i++; data_type = data_types[0][i]; } var response = { "thirsty_words": thirsty_words, "thermo_words": thermo_words, "light_words": light_words, "condition_words": condition_words, "graph_words": graph_words, "co2_msg": co2_msg, "thirsty_msg": thirsty_msg, "thermo_msg": thermo_msg, "light_msg": light_msg, "condition_msg": condition_msg, "random_msg": random_msg, "stamp_msg": stamp_msg }; Logger.log(response); return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON) } function getKeyData(sheet, colname) { var ret = []; const rangevals = sheet.getRange(colname + ":" + colname).getValues(); const lastRow = rangevals.filter(String).length; for(i=1; i<lastRow; i++) { ret.push(rangevals[i][0]); } return ret; } function getKeyAndThreshold(sheet, k_colname, t_colname, p_colname) { var ret = []; const rangevals_k = sheet.getRange(k_colname + ":" + k_colname).getValues(); const lastRow = rangevals_k.filter(String).length; const rangevals_t = sheet.getRange(t_colname + "1:" + t_colname + lastRow).getValues(); const rangevals_p = sheet.getRange(p_colname + "1:" + p_colname + lastRow).getValues(); var key; var threshold; var point; for(i=1; i<lastRow; i++) { key = rangevals_k[i][0]; threshold = rangevals_t[i][0]; point = rangevals_p[i][0]; ret.push({"msg":key, "threshold":threshold, "point":point}); } return ret; } ``` ```html:Postfromobniz(GoogleAppsScript上のデータ登録・LINE返信プログラム) function doPost(d) { const url_push = "https://api.line.me/v2/bot/message/push"; const channel_id = "LINEのbot用チャネルのチャネルID"; const access_token = "LINEのbot用チャネルのアクセストークン"; const url_machinist = "https://gw.machinist.iij.jp/endpoint"; const access_token_machinist = "Machinist REST-API用のアクセストークン"; var params = JSON.parse(d.postData.getDataAsString()); console.log(params); // LINEに返信 if(params.type === "message") { console.log(params.messages); var data = { "to": channel_id, "messages": params.messages }; var options = { "method": "post", "contentType": "application/json", "headers": { "Authorization": "Bearer " + access_token }, "payload": JSON.stringify(data), "muteHttpExceptions": true }; var response = UrlFetchApp.fetch(url_push, options); Logger.log(response); // Machinistに登録 } else if(params.type === "sensor") { var data = { "agent": "Weki Agent", "metrics": [ { "name": "humidity", "namespace": "Environment Sensor", "data_point": { "value": parseInt(params.data.humidity) } }, { "name": "temperature", "namespace": "Environment Sensor", "data_point": { "value": parseInt(params.data.temp) } }, { "name": "light", "namespace": "Environment Sensor", "data_point": { "value": parseInt(params.data.lux) } }, { "name": "co2", "namespace": "Environment Sensor", "data_point": { "value": parseInt(params.data.co2) } }, { "name": "condition", "namespace": "Environment Sensor", "data_point": { "value": parseInt(params.data.condition) } } ] }; var options = { "method": "post", "contentType": "application/json", "headers": { "Authorization": "Bearer " + access_token_machinist }, "payload": JSON.stringify(data), "muteHttpExceptions": true }; var response = UrlFetchApp.fetch(url_machinist, options); Logger.log(response); } } ``` ```html:cron_measure(GoogleAppsScript上の定期測定指示プログラム) function myFunction() { // 定期計測実行エージェント const obniz_url = "obniz Cloud上のStartMeasureアプリケーションの公開URL"; // obnizに測定コマンド送付 var data = { "events": [ { "message": { "type": "sensor", "text": "get sensor data" } } ] }; var options = { "method": "post", "contentType": "application/json", "muteHttpExceptions" : true, "payload": JSON.stringify(data) }; try { var response = UrlFetchApp.fetch(obniz_url, options); console.log(response); } catch(e) { console.log("Error:") console.log(e) } } ``` # 今後の課題 現在は各種センサー値の閾値単位ごとに1つのメッセージが固定になってます(明るさが888lux~999luxだったら”あいうえお”のような設定) 実際に使ってみると(あたりまえですが)測定値は俊敏に大きく変化するものではなく緩やかに変化しますので短時間に同じ質問をすると同じ回答ばかりでいまいち「おもしろさ」が足りないです。 ですので、値が同じ範囲内でも複数の候補からランダムに応答メッセージが返却されるように広げようと思います。 もっと言うと、対話応答を機械学習のサービスに繋いでもっと自然に会話できるようになれば完璧でしょうか。。。 # おまけ LINEへの投稿メッセージテキストを見て、Googleスプレッドシートに記載の各種センサー起動ワードにヒットする単語が含まれていると該当センサーで測定して結果のテキストを返しますが、どのセンサー起動ワードにもヒットしない場合には何もしないというのではおもしろくないので、この場合は適当な応答メッセージを返してあげる処理を入れています。かつ、せっかくLINEでコミュニケーションをとっていますので、単なるテキスト文字列ではなくランダムにスタンプを返すようにしました。 利用するスタンプIDの範囲をGoogleスプレッドシート上に設定しておくだけです。 [LINEで送信可能なスタンプリスト](https://developers.line.biz/ja/docs/messaging-api/sticker-list/#sticker-definitions) ![キャプションを入力できます](https://camo.elchika.com/885b419803d166b5c88f80bfd17fb4725ede0eae/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663338303863652d313163612d343231322d623133622d3563336436616637346266322f63376265643633322d383137312d343832332d396634642d303266303537643536663234/) # 参考情報 ■ MH-Z19Bのセンシングは常時通電の状態がいけないのか、引数にvcc,gndがないとうまくないのか不明なのですが、obniz公式のパーツリストにあるサンプルプログラム通りにデータ取得できなかったためUART直接操作で計測するようにしました。その際に以下のサイトの情報を参考にさせていただきました。 [arohajiroさんの投稿-【obniz】MH-Z19B CO2センサーモジュールを使って二酸化炭素濃度を計測する](https://crieit.net/posts/obniz-MH-Z19B-CO2) ■ SEN0114 [obniz公式パーツリスト](https://obniz.com/ja/sdk/parts/SEN0114/README.md) 上記パーツリストに表記されている接続図では、SEN0114側の端子が左からVCC, GND, SIGとなっていますが、私の購入した機器では左からGND, VCC, SIGになっています。(上記公式パーツリストの機器写真でもワイヤカラーを見る限りそうなっていると思います) 互換品等の物によって違うのかな?もしくはプラマイの方向は関係ない?(よくわかりませんが注意必要) ■ LM35DZ [obniz公式パーツリスト](https://obniz.com/ja/sdk/parts/LM35DZ/README.md) 今回の話とは直接関係ないですが、この形状のものって配線時にデータシートの図に対して現物は上下がひっくり返したり前がどっちか?みたいに混乱しやすくて、私も今回の試作中に向きが混乱して誤って逆さに配線してしまい、高熱状態になってブレッドボードがちょっと溶けてしまった。。。。。素人にはややこしい形状です。(まぁ、ずぶの素人なので仕方なし) ■ TEMT6000 lux値への変換式については以下のサイトを参考にさせていただきました。 [Converting TEMT6000 value to lux](https://forum.arduino.cc/t/converting-temt6000-value-to-lux/180676)