WiFi通信を使用してブラウザベースで遠隔操作、リアルタイムデータ通信を行う方法をコピペ用サンプルプログラムを使って紹介します。
今回は「M5StickC Plus」を使ってますが他のマイコンボードでも基本は同じで、サーバー機能を利用して「JavaScript」の「fetch」を使うことでデータの送受信を行います。
1.動作原理
・概要
・サーバーとは
2.動作紹介
3.用意するもの
4.配線図
5.サンプルプログラム(コピペ)
6.操作画面(ブラウザページ)プログラム(抜粋)
7.ブラウザページの動作紹介
・html(ボタン、データ表示)
・CSS(ページの装飾)
・JavaScript(ボタン操作イベント、fetch 非同期通信)
8.おまけ:Webカメラで遠隔監視モニター
9.まとめ
1.動作原理
・概要
ブラウザベースのリモートコントローラの動作原理について紹介します。
以前に以下のリンクで「簡易IoT」の紹介をしました。
この時もマイコンボード本体をサーバーに設定してブラウザボタンのリンク機能を利用することで遠隔操作を実現しましたが、リンクを送信する度にページが更新されてしまいます。
本体のデータを表示する時もブラウザページを1秒ごとに再読み込みすることで表示をしていました。
シンプルなページであれば小規模な構成で実現できるため、これはこれで便利です。
しかし、容量の大きなページの場合は表示が完了する前にまた再読み込みということになり実用的ではありません。
特にカメラの動画をモニター表示する場合には、頻繁にページが更新されるとまともに映像が見られません。
これからサーバー機能を利用した遠隔操作の方法を紹介していきますが、単純な操作をするだけなのに回りくどい処理を行っているように思えるかもしれません。
今回はあくまで「サーバー機能を利用」した遠隔操作です。
これは本来のサーバーの利用目的とは違うかもしれませんが、使えるものは使っちゃいましょう♪
ただ自分で作っていても回りくどいと思うことが何度もありました(汗)。
まず先にサーバーの動作について理解しておくと「サーバー利用してるからしょうがないか」と思えると思うので、先にサーバーの動作について簡単に紹介しておきます。
・サーバーとは
プログラムをやっていると「サーバー」等のように、いろんな用語が出てきます。
これらを理解するには、まずその語源を知ることが手っ取り早いと思います。
「サーバー」とは英語で「server」と書きます。
でも、このまま翻訳しても「サーバ」と出てくるのがほとんどだと思います。
調べてみると「server」は「serve(仕える、尽くす)」+「er(~する人)」を組み合わせたもので「仕える者、尽くす者」といった意味のようです。
ここから「server」とはそれ単体で何かをするものでなく、求められたときに何かをしてくれるものだと想像されます。
実際にその通りで「サーバー」の動作は下画像のようになります。
ここで「クライアント」という言葉が出てきます。
「クライアント」とは英語で「client」と書かれ「顧客、取引先、来訪者」と訳されます。
「サーバー」から見た「クライアント」とは、「サーバー」に対して何かを要求する私たちが操作するパソコンやスマホ、タブレット等のことを言います。
「サーバー」は「クライアント」の「要求」に対して忠実に「応答」を返します。
「要求」と「応答」を英訳すると
・応答:response(レスポンス)
となります。ということで、
「サーバー」とは「インターネット」経由で「クライアント」の「リクエスト」に対して忠実に「レスポンス」を返す「仕組み」の事を言います。
「リクエスト」に対して忠実に「レスポンス」を返しますが、逆に言えば「リクエスト」がなければ基本的に何もしません。
「サーバー」が「リクエスト」無しに自分から勝手に「レスポンス」としてデータを送信することはありません。
実際にWebページで何かを検索することに例えると、何も検索してないのに勝手にページが表示されたら困りますね。
この「仕組み」はセキュリティーを考えると非常に良くできた「仕組み」です。
今回はこの「サーバー」の「仕組み」を利用します。
ですので「サーバー」機能を持たせた「マイコンボード」本体の状態を知るためには「リクエスト」を送って「レスポンス」として応答を返してもらう必要があります。
本体のセンサが何かを検知したとしても自分からは知らせてくれません。
この辺りが回りくどく感じるかもしれませんが「サーバー」を利用するということはそういうことです。
この「仕組み」を使う利点もたくさんあって「サーバー」との通信は「WiFi通信」で行い、データの表示や操作はスマホやパソコンの「ブラウザ」を使用することができます。
「WiFi通信」も「ブラウザ」も身の回りに身近にあります。
スマホでもパソコンでも機種やメーカーが違ってもほぼ同じように動作します。
同じ「WiFi通信」ネットワーク内であればどんなに離れていても操作可能で、ラジコンやチルトパン付きの監視カメラもつくれますし、侵入検知や温度等のデータ収集もできます。
特別な端末やアプリを使用せずに手軽に実行できる遠隔操作の環境としては最も良い手段と思いますので便利に使っていきましょう。
2.動作紹介
今回準備したサンプルプログラムは以下のような動作になります。
プログラムを書き込むと「M5StickC Plus」に「IPアドレス」が表示されます。
これを同じネットワーク上に接続しているスマホやパソコンのブラウザを立ち上げてアドレスバーに入力してください。
ブラウザページのデータは「M5StickC Plus」内に文字列で埋め込んであるので「IPアドレス」を入力すると「サーバー」機能でブラウザページのデータが送信され下画像(スマホ画面)のように表示されます。
「M5StickC Plus」本体の液晶画面に表示された「IPアドレス」の「192.168.0.31(お使いの環境によって変わります)」をスマホのアドレスバーに入力すると上画像のような画面が表示されます。
スマホ画面の「ボタン0」を押すと「M5StickC Plus」本体のLED赤が点灯し、本体液晶画面の「LED_RED:」が1から0に変わります。
スマホ画面では「ボタン0」の色が緑色に変わります。
「M5StickC Plus」本体正面のボタンを押すと本体のLED赤が点灯し、本体液晶画面の「LED_RED:」が1から0に変わります。
スマホ画面では「ボタン0」の色が緑色に変わります。
ボリュームを回すと本体液晶画面の「ADC:」表示のアナログ入力電圧が変化し、スマホ画面ではアナログ入力電圧の表示が変化します。
次にページが更新されていないことを確認します。
更新されていなければカメラの映像がリアルタイムで表示されるはずです。
おまけとして「M5CAMERA」の映像もブラウザページに表示できるようにしてあります。
「Arduino IDE」のスケッチ例でネットワークカメラに設定し、スマホ画面のテキストボックスに「http://カメラのIPアドレス/stream」と入力し「エンター」を押すと映像が表示されます。
以下が「M5CAMERA」の画像です。
液晶表示器SSD1306を使用してSSIDとIPアドレスが確認できるようにしたものです。
「SSD1306 液晶表示器OLED」については以下のリンクで詳しく紹介しています。
カメラのストリーム画像表示アドレスを入力すると「M5CAMERA」の映像がブラウザ画面内に表示されます。
カメラの映像は「M5StickC Plus」を経由せずにスマホに直接表示されてます。
スマホ側をカメラで写すと上画像のような不思議な映像がブラウザ上に表示されます。
ブラウザの「ボタン0」を押している間、本体LEDは点灯し続け映像は途切れず表示されています。
「M5StickC Plus」本体のボタンを操作しても同じ状態です。
今回のサンプルプログラムでは本体ボタンの代わりに外部センサ等の状態を検知したり、本体LEDの代わりに外付けのリレーを動作させて遠隔操作スイッチにすることも想定して、入出力端子も連動して使用できるようにしてあります。
動作確認のために外部センサ代わりにボタンスイッチを、外部リレーの代わりに抵抗内蔵LEDを使用して動作確認を行います。
追加ボタンの「デュアルボタンユニット」を接続するのにちょうどいいものがありました。
GROVE用 4ピン変換コネクタです。
「スイッチサイエンス」で購入できます。
オスピン付のGROVEコネクタ配線でも代用できます。
「M5StickC Plus」の「5V」と「GND」端子に合わせて差し込むと「G26」「G36/G25」が「Groveコネクタ」で使用できます。
ここに「デュアルボタンユニット」を接続します。
抵抗内蔵Lの青色LEDは上画像のように「3.3V」に「+」を「G0」に「ー」を接続します。
上画像のように赤色ボタンを押すと本体LED赤と連動して外付けLED青が点灯します。
スマホ画面では「ボタン0」が緑色になるのが確認できると思います。
「M5StickC Plus」の本体ボタンを押すと、本体LED赤と連動して、追加した抵抗内蔵LED青も点灯します。
スマホ画面では「ボタン0」が緑色になるのが確認できると思います。
全体の構成は上画像のようになります。
基本的な動作だけなら「M5StickC Plus」と「ボリューム」だけあれば大丈夫です。
ブラウザであればほとんどのブラウザで動作確認できます。
下画像は「Microsoft Edge」で表示したものです。
パソコンのブラウザの場合下画像右側のように「開発ツール」(Ctrl+Shift+i か右クリックで選択して開きます)のコンソールでログ情報が確認できます。
1秒ごとに本体情報(info)やアナログ入力値(v_in)、LEDの状態(LED_state)が「M5StickC Plus」から送信されて表示されます。
3.用意するもの
用意するものは、スマホやパソコンと以下の部品です。
動作確認だけなら「マイコンボード(M5StickC Plus)」と「ボリューム」だけあれば出来ます。
スイッチは以下のボタンユニットにGrove変換コネクタの組み合わせで使用しています。
ほとんどがAmazonで購入できますが、抵抗内蔵LEDは「秋月電子通商」の方が必要な数量、色だけ購入できて送料(600円)を考えても別で購入するのがお得だと思います。
品名 | 型式等 | 購入先 | 価格 |
---|---|---|---|
抵抗内蔵LED | 抵抗内蔵5mmLED 5V 青:[ I-06247 ] | 秋月電子通商 | 1パック(10個)200円 1個25円 |
「M5StickC Plus」や「ボリューム」「スイッチ」「LED」については以下のリンクで詳しく紹介しています。
4.配線図
各部品を接続した配線図は下画像のようになります。
基本的な動作の確認は「M5StickC Plus」と「ボリュームユニット」だけ接続すれば大丈夫です。
5.サンプルプログラム(コピペ)
サンプルプログラムは以下になります。「コピペ」して書き込んでください。
(出来るだけ行数が少なくなるように書いてあるので見にくい部分があるかもしれません。)
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでコピーできます。
#include <M5StickCPlus.h>
#include <WiFi.h> //WiFi接続用
#include <WebServer.h> //サーバー設定用
// サーバー設定ポート80で接続
WebServer server(80);
// 端子割り付け
#define IN0 26 //入力端子
#define IN1 36
#define OUT0 0 //出力端子
#define ADC0 33 //アナログ入力端子
// 変数設定
float ad_val; //アナログ入力値格納用
float v_in = 0; //アナログ入力電圧換算値
int btn_pw = 0; //電源ボタン状態取得
float battery; //バッテリー残量表示用
int btn0_sig; //ブラウザボタン0信号
// htmlブラウザ画面データ(文字列)----------------------------------------
// 「"」は「\"」に置き換え、htmlの改行は「\n」、コードの改行は「\」
String html = "\
<!DOCTYPE html><html lang=\"jp\"><head>\n\
<meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\
<title>REMOTE-CONTROLLER</title>\n\
<!-- CSS処理(ブラウザページ装飾)------------------------ -->\n\
<style>\n\
body{font-family: sans-serif; background-color: #22578b; max-width: 480px; margin: 0 auto; align-items: center;}\n\
h1 {color:#ffffff; text-align: center; font-size: 28px; margin: 10px auto;}\n\
div {display: flex; flex-direction: row; justify-content: center; margin-top: 10px;}\n\
p {margin: 0px;}\n\
td {padding: 0px 15px; width: 110px; color:#ffffff; text-align: center; font-size: 18px; width: auto;}\n\
.btn {height: 70px; width: 100px; color: #555555; background-color: #dddde9; font-size: 18px; font-weight: bold; border-radius: 7%; margin: 0 10px; -webkit-appearance: none;}\n\
.btn_on {background-color: springgreen;}\n\
</style></head>\n\
<!-- html処理(ブラウザ表示)----------------------------- -->\n\
<body>\n\
<h1>REMOTE-CONTROLLER</h1>\n\
<div>\n\
<!-- ボタンが複数の場合は以下をコピペで増やす。idのbtn番号変更 -->\n\
<button class=\"btn\" id=\"btn0\">ボタン0</button>\n\
</div>\n\
<div> <table border = \"1\">\n\
<!-- 表示内容が複数の場合は以下をコピペで増やす。idはJSONのkeyに変更 -->\n\
<tr><td><p>アナログ<br>入力電圧</p></td> <td><span id=\"v_in\">0</span><span>V</span></td></tr>\n\
</table> </div>\n\
<!-- 以下はおまけ、M5CAMERAの「IPアドレス:81/stream」の動画表示 -->\n\
<div>\n\
<form id=\"id_form1\" onsubmit=\"return startStream()\">\n\
<input id=\"input_IP\" type=\"text\" placeholder=\"カメラIPアドレス\" />\n\
</form>\n\
</div>\n\
<hr width='90%' />\n\
<div id=\"output\"></div>\n\
<!-- JavaScript処理--------------------------------------- -->\n\
<script type=\"text/javascript\">\n\
let btn = [];\n\
const btnOn = (i) => { //ボタンON時処理(ボタン番号「i」ごとに分岐、複数可)\n\
btn[i].classList.add('btn_on');\n\
switch (i) {\n\
case 0: getBtnOn(i); break;\n\
}\n\
}\n\
const btnOff = (i) => { //ボタンOFF時処理(ボタン番号「i」ごとに分岐、複数可)\n\
btn[i].classList.remove('btn_on');\n\
switch (i) {\n\
case 0: getBtnOff(i); break;\n\
}\n\
}\n\
for (let i = 0; i < 1; i++) { //ブラウザボタン状態取得(イベント処理、複数可)※i=btn番号\n\
btn[i] = document.getElementById('btn' + i);\n\
btn[i].addEventListener('touchstart', (e) => {e.preventDefault(); btnOn(i);} );\n\
btn[i].addEventListener('mousedown', () => {btnOn(i);} );\n\
btn[i].addEventListener('touchend', () => {btnOff(i);} );\n\
btn[i].addEventListener('mouseup', () => {btnOff(i);} );\n\
}\n\
let get_data;\n\
async function getData() { //マイコンボード側JSONデータ取得(インターバル)\n\
await fetch(\"/get/data\")\n\
.then((response) => {if (response.ok) {return response.json();} else {throw new Error();} })\n\
.then((json) => {\n\
console.log(json);\n\
get_data = json;\n\
let el;\n\
//以下に取得したデータごとに処理したい内容を記入\n\
el = document.querySelector('#v_in'); //アナログ電圧表示要素取得(idはJSONのキー)\n\
el.textContent = get_data.v_in; //アナログ電圧表示更新\n\
if (get_data.LED_state == 0) {btn[0].classList.add('btn_on');} //LEDがONならブラウザボタン緑\n\
else {btn[0].classList.remove('btn_on');} //LEDがOFFならブラウザボタン白\n\
})\n\
.catch((error) => console.log(error));\n\
}\n\
setInterval(getData, 1000); //インターバル設定(1秒ごとに本体データ取得)\n\
\n\
async function getBtnOn(i) { //ブラウザボタンON時処理(複数可)\n\
let link;\n\
switch (i) {\n\
case 0: link = \"/get/btn0_on\"; break; //ブラウザボタン番号(btn i)ごとに処理を分岐\n\
}\n\
await fetch(link)\n\
.then((response) => { if (response.ok) {return response.text();} else {throw new Error();} })\n\
.then((text) => { console.log(text) })\n\
.catch((error) => console.log(error));\n\
}\n\
async function getBtnOff(i) { //ブラウザボタンOFF時処理(複数可)\n\
let link;\n\
switch (i) {\n\
case 0: link = \"/get/btn0_off\"; break; //ブラウザボタン番号(btn i)ごとに処理を分岐\n\
}\n\
await fetch(link)\n\
.then((response) => { if (response.ok) {return response.text();} else {throw new Error();} })\n\
.then((text) => { console.log(text) })\n\
.catch((error) => console.log(error));\n\
}\n\
// M5CAMERA動画取得\n\
const startStream = () => {\n\
let target = document.getElementById(\"output\");\n\
let stream = document.forms.id_form1.input_IP.value;\n\
console.log(\"Start Stream:\" + stream);\n\
stream = \"<img src= '\"+ stream + \"' width='90%' height='90%'>\";\n\
target.innerHTML = stream;\n\
return false;\n\
}\n\
</script></body></html>\n";
// サーバーリクエスト時の処理関数 -------------------------------------------
// ルートアクセス時の応答関数
void handleRoot() {
// htmlData(); //htmlデータ更新
server.send(200, "text/html", html); //レスポンス200を返しhtml送信
}
// エラー(Webページが見つからない)時の応答関数
void handleNotFound() {
server.send(404, "text/plain", "404 Not Found!"); //エラー情報送信(text)
}
// ブラウザONボタン処理
void btn0On() {
btn0_sig = 1; //ブラウザボタン信号を1(ON)にする
server.send(200, "text/plain", "ボタン0 ON"); //レスポンス200を返し情報送信(text)
}
// ブラウザOFFボタン処理
void btn0Off() {
btn0_sig = 0; //ブラウザボタン信号を0(OFF)にする
server.send(200, "text/plain", "ボタン0 OFF"); //レスポンス200を返し情報送信(text)
}
// ブラウザへデータ送信(JSONファイル)-------------------------------------
void getData() {
String data= "";
//JSONフォーマット({ "key(項目)" : "value(値)" ~ ,"key(項目)" : "value(値)"})
data += "{\"info\":\""; data += "M5StackC Plus"; //本体情報
data += "\",\"v_in\":\""; data += v_in; //アナログ入力電圧
data += "\",\"LED_state\":\""; data += digitalRead(10); //本体LEDの状態
data += "\"}";
server.send(200, "text/plain", data); //JSONデータ送信実行
}
// WiFi接続処理 -----------------------------------------------------------
const char ssid[] = "自宅のWiFi接続先を記入"; //接続先SSID
const char pass[] = "接続先のパスワードを記入"; //接続先パスワード
void WiFiLocal() {
WiFi.begin(ssid, pass); //WiFiローカル接続開始
// WiFi接続完了待ち
while (WiFi.status() != WL_CONNECTED) { //接続完了するまで繰り返す
delay(500); //0.5秒待機
Serial.print("."); //「.」シリアル出力
}
// 接続情報シリアル出力
Serial.printf("\nSSID:%s\n", ssid); //接続先SSID表示
Serial.printf("PASS:%s\n", pass); //接続パスワード表示
Serial.print("IP address: ");
Serial.println(WiFi.localIP()); //IPアドレス(配列)
//ブザー処理
M5.Beep.beep(); //ブザー初期値(周波数:4000, 発音時間:100ms)で鳴らす
}
// 初期設定 ----------------------------------------------
void setup() {
M5.begin(); //(LCD有効, POWER有効, Serial有効)
Serial.begin(9600); //標準のシリアル通信設定
WiFiLocal(); //WiFi接続処理呼出し
// サーバー設定
server.on("/", handleRoot); //ルートアクセス時の応答関数
server.onNotFound(handleNotFound); //Webページが見つからない時の応答関数
server.on("/get/btn0_on", btn0On); //ボタン0オン受信処理
server.on("/get/btn0_off", btn0Off); //ボタン0オフ受信処理
server.on("/get/data", getData); //ブラウザへのデータ送信処理
server.begin(); //Webサーバー開始
// 入出力ピン設定
// 入力設定
pinMode(IN0, INPUT_PULLUP); //入力設定(プルアップ)
pinMode(IN1, INPUT_PULLUP);
// 出力設定
pinMode(OUT0, OUTPUT); //本体LED赤と連動
pinMode(10, OUTPUT); //本体LED赤
pinMode(2, OUTPUT); //本体ブザー
digitalWrite(OUT0, HIGH); //OUT0初期値OFF(HIGH)
digitalWrite(10, HIGH); //本体LED初期値OFF(HIGH)
// アナログ入力設定
pinMode(ADC0, ANALOG); //アナログ入力
// G36とG25は同時使用不可。使っていない方は以下のようにフローティング入力にする
gpio_pulldown_dis(GPIO_NUM_25); //G25をフローティング入力に設定
gpio_pullup_dis(GPIO_NUM_25);
// 液晶表示初期設定
M5.Lcd.fillScreen(BLACK); //背景色
M5.Lcd.setRotation(1); //画面向き設定(USB位置基準 0:下/ 1:右/ 2:上/ 3:左)
M5.Lcd.setTextSize(1); //文字サイズ(整数倍率)
M5.Lcd.setTextFont(4); //フォント 1(8px), 2(16px), 4(26px), 6(36px数字-.:apm), 7(7セグ-.:), 8(75px数字-.:)
}
// メイン -----------------------------------------
void loop() {
M5.update(); //本体ボタン状態更新(ブザーも更新)
server.handleClient(); //クライアント(ブラウザ)からのアクセス確認
// 本体ボタン、外部ボタン、ブラウザボタン入力処理
//ボタンA または 外部スイッチ赤 または ブラウザボタン0 が押されているなら
if (M5.BtnA.isPressed() || digitalRead(IN0) == 0 || btn0_sig == 1) {
digitalWrite(10, LOW); //本体LED赤点灯
digitalWrite(OUT0, LOW); //OUT0出力(本体LED赤連動)
} else { //そうでなければ
digitalWrite(10, HIGH); //本体LED赤消灯
digitalWrite(OUT0, HIGH); //OUT0出力OFF(HIGH)
}
// アナログ入力処理
ad_val = analogRead(ADC0); //アナログ入力値を取得
v_in = ad_val * (3.3 / 4095); //3.3Vへ換算
// バッテリー残量(MAX約4.2V、限界電圧3V)パーセント換算表示
battery = (M5.Axp.GetBatVoltage() - 3) * 90; //バッテリー残量取得、換算
if (battery > 100) { battery = 100; } //換算値が100以上なら100にする
// LCD表示処理
M5.Lcd.setTextFont(4); //フォント変更
M5.Lcd.setCursor(5, 5); M5.Lcd.setTextColor(CYAN, BLACK); //SSID表示
M5.Lcd.print("SSID : "); M5.Lcd.print(ssid);
M5.Lcd.setCursor(5, 30); M5.Lcd.setTextColor(ORANGE, BLACK); //IPアドレス表示
M5.Lcd.print("IP : "); M5.Lcd.print(WiFi.localIP());
M5.Lcd.setCursor(5, 55); M5.Lcd.setTextColor(RED, BLACK); //本体LEDの状態表示
M5.Lcd.printf("LED_RED : %d", digitalRead(10));
M5.Lcd.setCursor(5, 80); M5.Lcd.setTextColor(WHITE, BLACK); //アナログ入力値表示
M5.Lcd.printf("ADC : %01.2fv ( %04.0f )", v_in, ad_val);
M5.Lcd.setTextFont(2); //フォント変更
M5.Lcd.setCursor(200, 118); M5.Lcd.setTextColor(DARKGREY, BLACK); //バッテリー残量表示
M5.Lcd.printf("%.0f%% ", battery);
delay(100); //遅延時間(ms)
}
上コードハイライト部のサーバー設定プログラムについては以下のリンクで詳しく紹介しています。
6.操作画面(ブラウザページ)プログラム(抜粋)
サンプルプログラムではブラウザページのデータは文字列としているため色分けがされず見にくいので、ブラウザページのデータだけ以下に抜粋しました。
このデータは「メモ帳」に「コピペ」で貼り付けて、拡張子「.txt」を「.html」に書き換えて保存して開くことで単体でもサンプルプログラムと同じページが表示されます。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでコピーできます。
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>REMOTE-CONTROLLER</title>
<!-- CSS処理(ブラウザページ装飾)------------------------ -->
<style>
body{font-family: sans-serif; background-color: #22578b; max-width: 480px; margin: 0 auto; align-items: center;}
h1 {color:#ffffff; text-align: center; font-size: 28px; margin: 10px auto;}
div {display: flex; flex-direction: row; justify-content: center; margin-top: 10px;}
p {margin: 0px;}
td {padding: 0px 15px; width: 110px; color:#ffffff; text-align: center; font-size: 18px; width: auto;}
.btn {height: 70px; width: 100px; color: #555555; background-color: #dddde9; font-size: 18px; font-weight: bold; border-radius: 7%; margin: 0 10px; -webkit-appearance: none;}
.btn_on {background-color: springgreen;}
</style>
</head>
<!-- html処理(ブラウザ表示)----------------------------- -->
<body>
<h1>REMOTE-CONTROLLER</h1>
<div>
<!-- ボタンが複数の場合は以下をコピペで増やす。idのbtn番号変更 -->
<button class="btn" id="btn0">ボタン0</button>
</div>
<div> <table border = "1">
<!-- 表示内容が複数の場合は以下をコピペで増やす。idはJSONのkeyに変更 -->
<tr><td><p>アナログ<br>入力電圧</p></td> <td><span id="v_in">0</span><span>V</span></td></tr>
</table> </div>
<!-- 以下はおまけ、M5CAMERAの「IPアドレス:81/stream」の動画表示 -->
<div>
<form id="id_form1" onsubmit="return startStream()">
<input id="input_IP" type="text" placeholder="カメラIPアドレス" />
</form>
</div>
<hr width='90%' />
<div id="output"></div>
<!-- JavaScript処理--------------------------------------- -->
<script type="text/javascript">
let btn = [];
const btnOn = (i) => { //ボタンON時処理(ボタン番号「i」ごとに分岐、複数可)
btn[i].classList.add('btn_on');
switch (i) {
case 0: getBtnOn(i); break;
}
}
const btnOff = (i) => { //ボタンOFF時処理(ボタン番号「i」ごとに分岐、複数可)
btn[i].classList.remove('btn_on');
switch (i) {
case 0: getBtnOff(i); break;
}
}
for (let i = 0; i < 1; i++) { //ブラウザボタン状態取得(イベント処理、複数可)※i=btn番号
btn[i] = document.getElementById('btn' + i);
btn[i].addEventListener('touchstart', (e) => {e.preventDefault(); btnOn(i);} );
btn[i].addEventListener('mousedown', () => {btnOn(i);} );
btn[i].addEventListener('touchend', () => {btnOff(i);} );
btn[i].addEventListener('mouseup', () => {btnOff(i);} );
}
let get_data;
async function getData() { //マイコンボード側JSONデータ取得(インターバル)
await fetch("/get/data")
.then((response) => {if (response.ok) {return response.json();} else {throw new Error();} })
.then((json) => {
console.log(json);
get_data = json;
let el;
//以下に取得したデータごとに処理したい内容を記入
el = document.querySelector('#v_in'); //アナログ電圧表示要素取得(idはJSONのキー)
el.textContent = get_data.v_in; //アナログ電圧表示更新
if (get_data.LED_state == 0) {btn[0].classList.add('btn_on');} //LEDがONならブラウザボタン緑
else {btn[0].classList.remove('btn_on');} //LEDがOFFならブラウザボタン白
})
.catch((error) => console.log(error));
}
setInterval(getData, 1000); //インターバル設定(1秒ごとに本体データ取得)
async function getBtnOn(i) { //ブラウザボタンON時処理(複数可)
let link;
switch (i) {
case 0: link = "/get/btn0_on"; break; //ブラウザボタン番号(btn i)ごとに処理を分岐
}
await fetch(link)
.then((response) => { if (response.ok) {return response.text();} else {throw new Error();} })
.then((text) => { console.log(text) })
.catch((error) => console.log(error));
}
async function getBtnOff(i) { //ブラウザボタンOFF時処理(複数可)
let link;
switch (i) {
case 0: link = "/get/btn0_off"; break; //ブラウザボタン番号(btn i)ごとに処理を分岐
}
await fetch(link)
.then((response) => { if (response.ok) {return response.text();} else {throw new Error();} })
.then((text) => { console.log(text) })
.catch((error) => console.log(error));
}
// M5CAMERA動画取得
const startStream = () => {
let target = document.getElementById("output");
let stream = document.forms.id_form1.input_IP.value;
console.log("Start Stream:" + stream);
stream = "<img src= '"+ stream + "' width='90%' height='90%'>";
target.innerHTML = stream;
return false;
}
</script>
</body>
</html>
7.ブラウザページの動作紹介
ブラウザページのデータをhtml、css、JavaScriptに分けてそれぞれの動作を紹介します。
(見やすくなるように改行して整列しなおしてます。)
・html(ボタン、データ表示)
「html」はブラウザページのベースとなる表示を行うプログラムでタグというものを使用します。
サンプルページから「html」部を以下に抜粋して紹介します。
<h1>REMOTE-CONTROLLER</h1>
<div>
<!-- ボタンが複数の場合は以下をコピペで増やす。idのbtn番号変更 -->
<button class="btn" id="btn0">ボタン0</button>
<button class="btn" id="btn1">ボタン1</button>
</div>
<div>
<table border = "1">
<!-- 表示内容が複数の場合は以下をコピペで増やす。idはJSONのkeyに変更 -->
<tr><td><p>アナログ<br>入力電圧</p></td> <td><span id="v_in">0</span><span>V</span></td></tr>
<tr><td><p> 追加項目 </p></td> <td><span id=" 追加データID ">(更新データ)</span><span> 単位 </span></td></tr>
</table>
</div>
3~5行目で「button」タグを使用してボタンを配置しています。
サンプルプログラムでは4行目の「ボタン0」だけですが、ボタンを追加する場合は5行目のように「コピペ」して追加してください。
追加したボタンには「id=”btn1″」のように「id」の「btn」番を連番で増やして変更してください。
9~11行目で「table」タグを使用してデータ表示を行うテーブル(表)を配置しています。
サンプルプログラムでは10行目の「アナログ入力電圧」だけですが、表示するデータを追加する場合は11行目のように「コピペ」して追加してください。
追加したテーブルでは(更新データ)部の「id」を、受信する「JSON」データの「key」に設定してください。(JavaScriptの「マイコンボードからのデータ取得」で詳しく紹介します。)
・CSS(ページの装飾)
「css」は「html」で書かれたブラウザページの文字色やフォント、ボタン色、背景等の装飾や配置の指定を行うプログラムです。
サンプルページから「css」部を以下に抜粋して紹介します。
<style>
body{font-family: sans-serif; background-color: #22578b; max-width: 480px; margin: 0 auto; align-items: center;}
h1 {color:#ffffff; text-align: center; font-size: 28px; margin: 10px auto;}
div {display: flex; flex-direction: row; justify-content: center; margin-top: 10px;}
p {margin: 0px;}
td {padding: 0px 15px; width: 110px; color:#ffffff; text-align: center; font-size: 18px; width: auto;}
.btn {height: 70px; width: 100px; color: #555555; background-color: #dddde9; font-size: 18px; font-weight: bold; border-radius: 7%; margin: 0 10px; -webkit-appearance: none;}
.btn_on {background-color: springgreen;}
</style>
「body」や「h1」「div」はこれらのhtmlタグへの指定を指し、これらのタグで囲まれたテキストに対して行う装飾を指定します。
「.btn」や「.btn_on」「.format」はhtmlのタグに設定した「クラス(class=” “)」を指し、この「クラス」を設定したタグに囲まれたテキストに対して行う装飾を指定します。
「div」等のタグはページ内で同じものが複数使用されますが、これらの一部のグループにだけに装飾をしたい場合は「クラス」を設定します。
その中でも特定の部分にだけ装飾をしたい場合はタグに「id」を設定します。
「id」の指定方法は以下のように「#」を付けて指定します。
#id名 { 装飾内容:数値等; }
「div」に指定している「display: flex;」は「Flexbox」の指定で、指定したタグを1つのボックスとして扱い、配置を指定できます。
今回はシンプルなページなので使うまでも無いのですが、複雑なページやスマホ用配置の「レスポンシブ」対応をする場合にはとても便利です。
「Flexbox」の理解には下画像の遊んで学べる「FLEXBOX FROGGY」をおすすめします。
カエルを蓮の葉に配置することでクリアしていくゲームです。
以下のリンクからブラウザ上で遊べるので確認してみましょう。
・JavaScript(ボタン操作イベント、fetch 非同期通信)
「JavaScript」はブラウザページに動きを持たせるためのプログラムです。
サンプルページから各動作ごとに「JavaScript」部を以下に抜粋して紹介します。
ボタン操作イベント(マウスクリック、タッチ)
「html」で記述した「ボタン(button)」には「JavaScript」で「イベント」を設定しています。
Webページでは「何かが起こった時に、何かを実行する」という動作をしたい時があります。
これを実現するために「html」で記述した要素(ボタンクやテキストボックス)にイベントを指定します。
let btn = [];
const btnOn = (i) => { //ボタンON時処理(ボタン番号「i」ごとに分岐、複数可)
btn[i].classList.add('btn_on');
switch (i) {
case 0: getBtnOn(i); break;
case 1: getBtnOn(i); break; //ボタンを増やした場合は追加(違う処理も指定可)
}
}
const btnOff = (i) => { //ボタンOFF時処理(ボタン番号「i」ごとに分岐、複数可)
btn[i].classList.remove('btn_on');
switch (i) {
case 0: getBtnOff(i); break;
case 1: getBtnOff(i); break; //ボタンを増やした場合は追加(違う処理も指定可)
}
}
for (let i = 0; i < 1; i++) { //ブラウザボタン状態取得(イベント処理、複数可)※i=btn番号
//ボタンを2個にした場合は i < 2 に変更する
btn[i] = document.getElementById('btn' + i);
btn[i].addEventListener('touchstart', (e) => {e.preventDefault(); btnOn(i);} );
btn[i].addEventListener('mousedown', () => {btnOn(i);} );
btn[i].addEventListener('touchend', () => {btnOff(i);} );
btn[i].addEventListener('mouseup', () => {btnOff(i);} );
}
16~22行目でボタンにイベントを設定しています。
2行目、9行目からがイベント発生時に実行する関数(イベントハンドラー)です。
イベントの指定は以下のように行います。
変数(配列).addEventListener(‘イベント‘, () => {実行する関数名(関数に渡したい値);} );
まず変数や配列にボタン(button)要素を取得します。
要素の取得には「document.getElementById」でボタンの「id名 」を指定します。
要素を取得した変数(配列)に対して「.addEventListener」を指定して「イベント」を設定していきます。
「イベント」はたくさんの種類がありますが今回使用したものは以下のようになります。
・touchstart:タッチした時
・mousedown:マウスでクリックした時
・touchend:タッチした指を離した時
・mouseup:マウスのクリックボタンを離した時
「イベント」が発生した時に実行する関数をアロー関数(関数の書き方)で以下のように指定しています。
・btnOn(i):ボタンをタッチまたはクリックしたときに実行する関数
・btnOff(i):ボタン上でタッチした指またはクリックしたボタンを離した時に実行する関数
※(i)でボタン番号を実行する関数に渡しています。
これによって実行する関数側で(i)を使った処理や実行内容の分岐ができます。
マイコンボードからのデータ取得(fetch 非同期通信、インターバル動作)
マイコンボードのデータはJavaScriptの「インターバル」を使用して1秒ごとに「getData()」関数を実行し「fetch(非同期通信)」によってデータを「JSONファイル」形式で取得しています。
2行目の「async function getData()」内の「fetch」を使用してデータの送受信を行います。
「async」は英単語「asyncronous」が由来で「非同期」という意味です。
「async」を付けることで「非同期」で処理を行うことができます。
「getData()」内では3行目の「 await fetch(“/get/data“) 」でサーバーへ「リクエスト」を送ります。
「await」とは英単語で「待つ」という意味で「非同期通信」中に実行完了を待ちたい処理に書きます。
「fetch」の前につけているのでデータを送信して他の処理を実行中でもデータを受信した時にこの部分の処理が実行されます。
let get_data;
async function getData() { //マイコンボード側JSONデータ取得(インターバル)
await fetch("/get/data")
.then((response) => {if (response.ok) {return response.json();} else {throw new Error();} })
.then((json) => {
console.log(json);
get_data = json;
let el;
//以下に取得したデータごとに処理したい内容を記入
el = document.querySelector('#v_in'); //アナログ電圧表示要素取得(idはJSONのキー)
el.textContent = get_data.v_in; //アナログ電圧表示更新
if (get_data.LED_state == 0) {btn[0].classList.add('btn_on');} //LEDがONならブラウザボタン緑
else {btn[0].classList.remove('btn_on');} //LEDがOFFならブラウザボタン白
if (get_data.追加データid == 0) { btn[1].classList.add('btn_on'); } //追加データONならブラウザボタン緑
else { btn[1].classList.remove('btn_on'); } //追加データOFFならブラウザボタン白
})
.catch((error) => console.log(error));
}
setInterval(getData, 1000); //インターバル設定(1秒ごとに本体データ取得)
サーバー側(M5StickC Plus)では以下の処理(サンプルプログラムより抜粋)が実行され「クライアント」へ「レスポンス」として「JSONファイル」を返します。
追加で送信したいデータがあればサンプルプログラムの「getData関数」に以下のコードの7行目のように追加します。
「JSONファイル」は以下のように「data」変数に文字列としてJSONフォーマットで準備します。
void getData() {
String data= "";
// JSONファイルフォーマット({ "key(項目)" : "value(値)" ~ ,"key(項目)" : "value(値)"})
data += "{\"info\":\""; data += "M5StackC Plus"; //本体情報
data += "\",\"v_in\":\""; data += v_in; //アナログ入力電圧
data += "\",\"LED_state\":\""; data += digitalRead(10); //本体LEDの状態
data += "\",\"追加データkey\":\""; data += 追加データvalue; //追加データ
data += "\"}";
server.send(200, "text/plain", data); //JSONデータ送信実行
}
// サーバー設定
server.on("/get/data", getData); //「/get/data」へのリクエストがあったら「getData」を実行
データの取得は「setInterval」を使用して1秒間隔で行います。
「setInterval」の使い方は以下になります
JSONファイルについて
取得するデータのJSONファイルについて紹介します。
「JSON」とは「Java Script Object Notation」の頭文字をとったもので、JavaScriptのオブジェクトの書き方を元にしたデータの定義方法です。
{ “key“: “value“, “key“: “value” }
{ }波括弧で括った中に、” “で囲んで名前の「key」を指定し : で区切って値の「value」を書きます。
複数のデータを書く時は「,」カンマで区切って書いていきます。
「key」とはデータにつける名前で、値の「value」を呼び出すためのキーワードとなります。
「value」とは「key」に関連図けられたデータのことです。
JSONを使用すると、データの名前「key」を指定することで値「value」を呼び出すことができます。
サンプルプログラムで使用したJSONファイルは以下のように設定しています。
※以下のように改行して書くと見やすくなります。
“info“: “M5StackC Plus”,
“v_in“: v_in,
“LED_state“: “digitalRead(10)” ,
“追加取得データid“:”追加取得データ“
}
※取得するデータを増やしたい時は「,」で区切って同じように書いていきます。
「key」の部分は「追加取得データid」でなくてもいいですが、わかりやすいように同じにしてます。
この場合、「key」の “info” を指定することで「value」の “M5StackC Plus” という文字列を得ることができます。
「v_in」を指定すると変数「v_in」に格納された値を得ることができます。
ボタン操作時動作
ブラウザボタンのON/OFFでも「fetch」を使用して「リクエスト」を送ります。
ボタンONの場合について紹介しますがボタンOFFでも同じような動作です。
async function getBtnOn(i) { //ブラウザボタンON時処理(複数可)
let link;
switch (i) {
case 0: link = "/get/btn0_on"; break; //ブラウザボタン番号(btn i)ごとに処理を分岐
case 1: link = "/get/btn1_on"; break; //ボタンを追加した場合はcase1を追加(btn番号は連番)
}
await fetch(link)
.then((response) => { if (response.ok) {return response.text();} else {throw new Error();} })
.then((text) => { console.log(text) })
.catch((error) => console.log(error));
}
7行目で「fetch」を実行して「リクエスト」を送信します。
サーバー側(M5StickC Plus)でブラウザのボタン0が押されたことが確認されたら「レスポンス」としてtextデータ(ボタン0 ON)を返します。
ブラウザの開発ツール(ctr + shift + i で表示)のコンソールで「ボタン0 ON/ボタン0 OFF」が確認できます。(9行目の「console.log(text)」による)
サーバー側(M5StickC Plus)では以下の処理が実行されます。
ブラウザから「/get/btn0_on」にリクエストがあると以下コードの13行目で「btn0()関数」が呼び出されます。
「btn0()関数」では「btn0_sig」変数を1にして「クライアント」へ「レスポンス」として「textデータ」(ボタン0 ON)を返します。
// ブラウザONボタン処理
void btn0On() {
btn0_sig = 1; //ブラウザボタン信号を1(ON)にする
server.send(200, "text/plain", "ボタン0 ON"); //レスポンス200を返し情報送信(text)
}
// ボタンを追加した場合は以下の処理を追加(btn1の番号は連番)
int btn1_sig; //ボタンを追加した場合はbtn1_sig変数も追加
void btn1On() {
btn1_sig = 1; //ブラウザボタン信号を1(ON)にする
server.send(200, "text/plain", "ボタン1 ON"); //レスポンス200を返し情報送信(text)
}
// サーバー設定
server.on("/get/btn0_on", btn0On); //「/get/data0_on」へのリクエストがあったら「btn0On()」を実行
server.on("/get/btn1_on", btn1On); //ボタンを追加した場合はbtn1(番号は連番)にして追加
4行目の「server.send」で送信するデータはファイル形式ごとに「MIMEタイプ」というものを指定して送信します。
「MIMEタイプ」には以下表のようなものがあります。
ファイル形式 | 一般的な拡張子 | MIMEタイプ |
---|---|---|
テキスト | .txt | text/plain |
HTML文書 | .htm .html | text/html |
XML文書 | .xml | text/xml |
JavaScript | .js | text/javascript |
VBScript | .vbs | text/vbscript |
CSS | .css | text/css |
GIF画像 | .gif | image/gif |
JPEG画像 | .jpg .jpeg | image/jpeg |
PNG画像 | .png | image/png |
CGIスクリプト | .cgi | application/x-httpd-cgi |
Word文書 | .doc | application/msword |
PDF文書 | application/pdf |
8.おまけ:Webカメラで遠隔監視モニター
「M5CAMERA」の映像は「JavaScript」を使用してブラウザページに埋め込んでいます。
カメラのstream画像のアドレスを取得するために「html」では「form」タグを使用しています。
「input」タグでテキストボックスを作成して、そこにアドレスを入力してエンターを押すと「JavaScript」の「startStream()」が実行されます。
<div>
<form id="id_form1" onsubmit="return startStream()" action="">
<input class="input1" id="input_IP" type="text" value="" placeholder="カメラIPアドレス" />
</form>
</div>
<hr width='90%' />
<div id="output"></div>
以下コードの「startStream()」では「html」で「id」を「output」に指定した「div」タグの要素を変数「target」に取得しています。
変数「stream」に「form」のテキストボックスに入力したstream画像のアドレスを取得して、このアドレスをリンク先とした「img」タグを使用した「html」で上書きします。
最後にstream画像を表示したい「div」タグの要素を取得した変数「target」に「innerHTML」を使用して変数「stream」の「html」データを書き出すことで表示しています。
const startStream = () => {
let target = document.getElementById("output");
let stream = document.forms.id_form1.input_IP.value;
console.log("Start Stream:" + stream);
stream = "<img src= '"+ stream + "' width='90%' height='90%'>";
target.innerHTML = stream;
return false;
}
9.まとめ
WiFi通信を使用したブラウザベースの遠隔操作方法を紹介しました。
「サーバー」機能を利用しているのでWiFi環境があればスマホでもパソコンでもブラウザで表示させて操作できる遠隔操作モニタを作ることができます。(アクセスポイントに設定しても可)
モニタするブラウザのページ作成は「html」でベースを作り「css」で装飾や配置を行います。
配置の指定には「Flexbox」を使いましょう。
作ったページに「JavaScript」を使用して動きをつけていきます。
「サーバー」とは「インターネット」経由で「クライアント(スマホやパソコン等)」からの「リクエスト」に対して「レスポンス」を返す「仕組み」です。
「仕組み」なので「サーバー」は特別なものではなく、この「仕組み」を持ったパソコンです。
小規模であればマイコンボードにもこの「仕組み」を実装することができます。
「リクエスト」はサーバーへアドレスを送信するため「html」だけではリンク機能を使用することになります。
しかし、リンクを送信するとブラウザの表示は更新されてしまい、操作を頻繁に行いたい場合は毎回ページが更新されてしまうため実用的ではありません。
これを解決するために「JavaScript」の「fetch」を使用しました。
「fetch」を使用することで「リクエスト」としてリンク先のアドレスのみを送信できます。
データの送受信は「非同期通信」で行っているため処理が中断されることはありません。
「サーバー」側で処理が終わって「レスポンス」が帰ってきた時点で受信データを使用した処理を行うことができます。
今回はサーバー機能を利用して遠隔操作を実現しました。
サーバー本来の使い方では無いと思うので高速な操作には向きませんが、「IoT」でデータ収集や稼働監視、モニターカメラの操作をするのにはちょうど良いと思います。
ここまで長々と書きましたが私自身「html」や「JavaScript」を使いこなしているわけではありません。
ほぼ「コピペ」で作ってるのでググれない状況でプログラムを書いてと言われると困ってしまいます(汗)
今回の構成もググってあちこちから集めたプログラムに追加したり、余分なものを削除して作ってるので、消しちゃいけないものも消してるかもしれませんw(動いてるので良しとしてます)
動くことは間違いないので参考として紹介しました。
これを作ることが目的ではなくWiFi通信でブラウザで操作できるラジコンが作りたかっただけなのです。
遠回りしたようですが勉強になりました。
今後はこれを使っていろいろ作ってまた紹介したいと思います。
「CORE2」については以下のリンクで詳しく紹介しています。
コメント