これまでのあらすじ:
2016年3月、フェルト生地を手で裁断している際にレーザーカッターがあれば複雑なカットが容易にできるなあと思って、安価になってきたレーザーカッターを購入しようと思ったのがきっかけ。調べていくうちに、合板も切れたほうがいいと思うようになって、CNCルーター(CNCミリング)についても考えるようになった。
Arduinoは以前から使っており、CNCシールドがあると気付いて自作も可能と思うようになった。当初はShapeOkoやX-CARVEを参考にMakerSlide、OpenRail、V-Wheel、2GTタイミングベルトなどで5万円くらいで自作しようと思っていた。AliExpressでも部品が安く買えることが分かって、しばらくは部品探し。探せば探すほど安くて本格的な部品も見つかってくるので、そんなにケチらなくてもいいのではないかと徐々にスペックアップ。最終的には剛性や精度のことも考えてボールスクリューやリニアスライドを使うことになり、予想以上に重厚な3軸CNCマシンをつくることに(約7万円)。
構想から約5週間(制作約3週間)でルーターとレーザーともに使えるようになり、現在はgrbl1.1+Arduino CNCシールドV3.5+bCNCを使用中(Macで)。余っていたBluetoothモジュールをつけてワイヤレス化。bCNCのPendant機能でスマホやタブレット上のブラウザからもワイヤレス操作可能。


CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:

*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。

2017年4月30日日曜日

ESP32:Webサーバから外部ファイルを読み込み

ESP32も少しずつ試してはいるのですが、SPIFFSファイルアップロードの仕方がわからない。ESP8266の場合、HTMLファイルなどをアップロードするためには以前やったように「esp8266fs.jar」を使えばよかったのですが、ESP32には「esp8266fs.jar」のようなものはまだないのでしょうか?
Arduino IDEには、「ライブラリのインポート」では一応「FS」があり、インポートはできるようになっているけれども、ファイルのアップロード自体どうすればいいのか?

インクルードされたライブラリもESP8266のとは違うので、ESP8266と同じ要領ではできなさそう。
ネットで調べてみても、これといったものがないし、フォーラムにもいくつか載っていたけれども、具体的にどうすればいいのかサンプルなどがないのでわかりにくい。
Virtual filesystem compornent(vfs)
partitions
この辺を見ればいいのかもしれないけれども、かなりわかりにくい。SPIFFS用のパーティションのオフセットは0x210000らしい。
おそらく、SPIFFSやFATなどのファイルシステムAPIをパーティション0x210000へflashするとつかえるようになるのかもしれない。Lua-RTOS-ESP32が唯一使えるというような投稿もあるけど、具体的なサンプルなどがないのでわからない。だれか成功例のサンプルをどこかに挙げてもらいたいですね。その他、JavaScriptをベースにするEspruinoやDuktape、あるいはMicroPythonを載せてしまうという手もあるけど。あとは、サンプルのあるSDカードから読み込む方法となるのかも。

ESP8266に関しては、かなりのサンプルが見つかるので扱いやすいけど、ESP32のほうはまだそんなにないというか、仕組みもESP8266よりも複雑みたいで、そのぶん扱いにくいのかも。しかたないのでしばらく待つことにして、かわりに以下の方法で。

他のWebサーバからESP32のRAMへ読み込ませる:
ソース(C言語/Arduino言語)の中にHTML(CSS、JavaScript)を埋め込んで毎回アップロードするのは面倒なので、どうせ読み込んだHTMLデータはString変数へ入れられるので、わざわざROMにアップロードしておかなくてもいいはず。電源を落とせば、RAMの場合消えてしまうけど、毎回電源を入れるたびに、setup()内で外部から読み込ませれば同じことかもしれない。
ということで、ESP32からHTTPリクエストを出して他のサーバに置いてあるHTMLファイルを読み込ませようという実験をしてみました。

まずHTMLファイルをアップロードできる無料Webサーバに、今回の場合data.htmlというファイルをあげておきます。data.htmlには、ESP32(Webサーバ)にリクエストがあった場合のレスポンス用HTMLが書いてあります。ブラウザ上にスイッチのON/OFFなどの画面が出てきて制御できるということになります。

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiClient.h>
#define ledPin 23

const char* ssid = "*****";
const char* password = "*****";

const char* host = "www.abc.com";//無料サーバのホスト
String url ="/esp32/data.html";//読み込ませるデータのurl
String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
String footer = "\r\n\r\n";
String webPage = "";
const uint16_t port = 80;

WiFiServer server(port);

void setup(void){  
    Serial.begin(115200);
    pinMode(ledPin, OUTPUT);
    //webPage += "<!DOCTYPE HTML>\r\n<html>No Contents, yet.</html>\r\n\r\n";

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    //固定IPアドレス
    WiFi.config(IPAddress(192,168,3,20),IPAddress(),IPAddress());
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    
    //DNSサーバ名(esp32.local)
    if (!MDNS.begin("esp32")) {
        Serial.println("Error setting up MDNS responder!");
        while(1) {
            delay(1000);
        }
    }
    Serial.println("mDNS responder started");

    server.begin();
    Serial.println("TCP server started");
    MDNS.addService("http", "tcp", port);
    httpRequest();//ここで外部ファイルをロードしておく
}

void loop(void){
    WiFiClient client = server.available();
    if (!client) {
        return;
    }
    Serial.println("");
    Serial.println("New client");

    while(client.connected() && !client.available()){
        delay(1);
    }

    String req = client.readStringUntil('\r');

    int addr_start = req.indexOf(' ');
    int addr_end = req.indexOf(' ', addr_start + 1);
    if (addr_start == -1 || addr_end == -1) {
        Serial.print("Invalid request: ");
        Serial.println(req);
        return;
    }
    req = req.substring(addr_start + 1, addr_end);
    Serial.print("Request: ");
    Serial.println(req);
    client.flush();

    String s;
    if (req == "/"){//ルートへアクセスした場合はロードしていないHTMLを表示
        IPAddress ip = WiFi.localIP();
        String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]);
        s = header;
        s += "<!DOCTYPE HTML>\r\n<html>Hello from ESP32 at ";
        s += ipStr;
        s += "<div><a href=\"/on\">TEST: ON</a></div>";
        s += "<div><a href=\"/off\">TEST:OFF</a></div>";
        s += "<div><a href=\"/load\">LOAD DATA</a></div></html>";
        s += footer;
    }else if(req == "/load"){//ロードする画面
        httpRequest();//外部サーバへリクエスト
        s = header;
        s += "<!DOCTYPE HTML>\r\n<html>";
        s += "<div><a href=\"/on\">TEST: ON</a></div>";
        s += "<div><a href=\"/off\">TEST:OFF</a></div>";
        s += "<div>HTML Data Loaded</div></html>";
        s += footer;
    }else if(req == "/on"){//ロードされた画面
        s = header;
        s += webPage;
        s += footer;
        digitalWrite(ledPin, HIGH);
    }else if(req == "/off"){//ロードされた画面
        s = header;
        s += webPage;
        s += footer;
        digitalWrite(ledPin, LOW);
    }else{
        s = "HTTP/1.1 404 Not Found\r\n\r\n";
    }
    client.print(s);
}

void httpRequest(){//ロード用のリクエスト
    Serial.print("connecting to ");
    Serial.println(host);
    WiFiClient myclient;

    if (!myclient.connect(host, port)) {
        Serial.println("connection failed");
        delay(5000);
        return;
    }

    myclient.print(String("GET ") + url + " HTTP/1.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Connection: close\r\n\r\n");
    
    delay(100);
    
    boolean start=false;
    while(myclient.available()){
        String line = myclient.readStringUntil('\r');
        if(line.indexOf("<!DOCTYPE html>")>0){
          start=true;
        }
        if(start){
            webPage+=line;
        }
    }
    Serial.println("closing connection");
    myclient.stop();
}

サンプルのつぎはぎなので、もしかすると矛盾があるかもしれないけれども、一応機能しました。
ESP32の電源を入れると、setup()内の最後に書いてあるhttpRequest()によって外部サーバにリクエストを出して、そのURLのHTMLを読み込みます。読み込んだ内容はwebPageという変数に入れてしまいます。これが表示用のHTMLデータとなります。
今回の場合はESP32がWebサーバとして機能しており、静的IPアドレス192.168.3.20に固定され、それ以下のディレクトリにアクセスすると:

/(ルート):もともとソースに書き込まれた単純なHTMLを表示
/load :再度HTMLデータを読み込むとき(表示はもともとのHTML)
/on :ロードしたHTMLによる画面表示、23番ピンに接続したLEDをON
/off :ロードしたHTMLによる画面表示、23番ピンに接続したLEDをOFF

となります。
外部サーバからのレスポンスには「HTTP/1.1 200 OK」などのheaderの情報が含まれるので、
line.indexOf("<!DOCTYPE html>")
をつかってHTMLデータの始まりを確認してから、変数webPageに入れていきます。今回の場合は、CSSやJavaScriptもHTMLファイル内に書き込んであるので、このHTMLファイルだけを読み込めばOKということにしています。

ちょっとだけHTMLを書き換えて確かめたいときは、このやり方のほうが早いのかもしれません。毎回ESP32にアップロードするのは確かに面倒。こうすれば、ESP32にはHTML(CSSやJavaScript)データをアップロードしなくてもすむので楽かなと。
ESP8266の場合も、ファイルアップローダを使うとけっこう時間(5分とか)かかるので、この方法のほうが早そうです。
AliExpress.com Product - Official DOIT ESP32 Development Board WiFi+Bluetooth Ultra-Low Power Consumption Dual Core ESP-32 ESP-32S ESP 32 Similar ESP8266976円(送料無料)最近だとAliExpressなら1000円以下になったみたいです。技適マークつきで、秋月のと同じタイプだと思います。

2017年4月26日水曜日

ESP8266:音声認識Wifiスイッチ:フィードバックで現状表示

前回までは、ブラウザを通してサーバに接続し直すたびに、プログラム自体が初期化されてすべてのスイッチがオフになってしまいましたが、今回の改良で、各ESP8266内部にもスイッチのON/OFF状況を記憶させておく変数を用意したので、現在どのスイッチがON/OFFになっているかが分かるようになりました。これで当初考えていた仕組みがほぼ完成という感じです。

これは、サーバに接続し直したときの制御画面です。一度ブラウザを閉じてしまっても、再接続と同時にフィードバック用のURLへリクエストをだし、そのレスポンスとして現在のON/OFF状態のデータを受け取る仕組みになっています。レスポンスがない場合は、一番下の「トイレ:[非接続]」というような表示になり(トイレ用のESP8266はまだ設置していないため)、それぞれのESP8266が稼働中かどうかもわかります。

それにしても今回のこの単純な仕組みを実現したいがために、Node.js、EJS、Express、Node-RED、PythonのFlaskなども試してみましたが、結局使う必要はなかったという。
しかし、ひとつ気になったのは、以前試したBlynkの応用的な使い方であるBlynk HTTP RESTful APIです。

こちらは、いわゆるスマホで操作するBlynkとは違って、パソコンのブラウザからも制御できるという仕組み。Blynkで発行したTokenを使ってBlynkサーバにリクエストすれば、接続してあるESP8266などをリアルタイムで制御できます。当然、今回のように各端子のON/OFF状況も調べることができます。おそらく登録したプロジェクト(ESP8266などのデバイス一個分)ごとにTokenが発行されるので、ESP8266を3つ使うならば3プロジェクト登録して3つのTokenを使い回せば、それぞれの制御が可能になりそうです。この機能を使えば、従来通りにスマホのBlynkアプリから操作してもいいし、パソコンのブラウザからも操作できるというわけです。
しかしながら、これはBlynkのサービスというよりも、Apiaryというモックサーバを使ってWeb開発などをするサービスでした。最近この手のクラウドコンピューティングのようなサービスが増えてきて、どう使えばいいかまだわからないのですが、ネットで調べる限りでは便利そうでした。そのうち必要があれば使ってみたいと思います。

ということで、話はもどって、

当然ながら、すべてのESP8266が接続されていない場合はこの↑ように[非接続]ばかりになります。ハードのほうがまだ出来上がっていないため、現状ではこんな感じ。

例えば、蛍光灯をONにする場合は、http://192.168.3.12/fluo_onへリクエストを出し、読み込み完了とステータス200が確認できたあとに画面表示内容(ボタンの色など)をアップデートします。この場合はレスポンスとして受け取るデータはなしです。他のボタンや音声認識による操作も同様です。
これとは別に各スイッチのON/OFF状況を知るためには、http://192.168.3.12/fluo_on_checkという別のURLへリクエストを送り、レスポンスとして変数を受け取り画面表示内容に反映させています。

接続時のプログラムの流れ:
・ブラウザでメインサーバへアクセスする
・ブラウザから各ESP8266のフィードバック用URLへリクエストをおくる
・各ESP8266との接続確認をとる
・接続確認とれた場合、レスポンスとしてON/OFF状況の変数を受け取る
・接続確認とれない場合、非接続の変数値に置き換える
・ON/OFF/非接続の状況の変数をブラウザ側のJavaScriptの変数へ入れ直す
・変数に応じて画面表示内容をアップデートする

音声認識による制御プログラムの流れ:
・音声認識を開始する
・音声入力する
・音声入力をテキスト変換する
・変換された入力テキストを正規表現でマッチングさせる
・マッチングの内容に応じて各スイッチのON/OFF制御
・対応するESP8266のON/OFF制御用URLへリクエストを送る
・読み込み完了とステータス200を確認できれば画面表示内容をアップデート
・ESP8266側で端子をON/OFFする
・読み込み完了とステータス200を確認できない場合は、「接続できません」と表示する

ボタン操作による制御プログラムの流れ:
・ON/OFFのボタンを押す
・対応するESP8266のON/OFF制御用URLへリクエストを送る
・読み込み完了とステータス200を確認できれば画面表示内容をアップデート
・ESP8266側で端子をON/OFFする
・非接続中のボタンを押しても反応せず「操作できません」と表示

結局のところ、スイッチのON/OFFの仕組みよりも、ON/OFF状態を画面に反映させるためのプログラムにかなり時間がかかってしまいました。
今回の場合はJavaScriptで非同期通信を行っているため、コマンドを発したタイミングとその処理が終了するタイミングがずれるので、値がnullになってしまったりと、エラーのでる原因を見つけるのに結構手間がかかりました。ようやく非同期通信の仕組みがわかってきたという感じです。

以下がHTML、CSS、JavaScriptのプログラム:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Speech API</title>
</head>
<body id="bg">
    <div class="title" id="status">[音声認識開始]</div>
    <div class="api"><p id="api">COM: 音声入力して下さい</p></div>
    <div class="you"><p id="you">YOU: (認識された音声)</p></div>
    <div class="btn" id="fluo_on" onclick="buttonClick(0)">蛍光灯: ON</div>
    <div class="btn" id="fluo_off" onclick="buttonClick(1)">蛍光灯: OFF</div>
    <div class="btn" id="bed_on" onclick="buttonClick(2)">寝室: ON</div>
    <div class="btn" id="bed_off" onclick="buttonClick(3)">寝室: OFF</div>
    <div class="btn" id="air_on" onclick="buttonClick(4)">エアコン: ON</div>
    <div class="btn" id="air_off" onclick="buttonClick(5)">エアコン: OFF</div>
    <div class="btn" id="wc_on" onclick="buttonClick(6)">トイレ: ON</div>
    <div class="btn" id="wc_off" onclick="buttonClick(7)">トイレ: OFF</div>

<style type="text/css">
    body{
        text-align:center;
        font-family: 'Helvetica',sans-serif;
        color:#fff;
        margin:0px;
        padding:0px;
        background-color:#fff;
    }
    .title{
        width:100%;
        background-color:#fa8;
        margin:0px;
        padding:2% 0% 2% 0;
    }
    .api{
        text-align: left;
        width:96%;
        background-color:#e75;
        padding:2%;
        margin:1% 0 0 0;
    }
    .you{
        text-align: left;
        width:96%;
        background-color:#d66;
        padding:2%;
        margin:1% 0 0 0;
    }
    .btn{
        float:left;
        background-color:#aaa;
        padding:3% 0px;
        margin:1% 1% 0 1%;
        width:48%;
    }
    .btn:hover{
        opacity: 0.8;
        cursor: pointer;
    }

</style>

<script type="text/javascript">
    var flag_speech = 0;
    var youAns=[/^(?!.*(オフ|ない)).*(?=蛍光灯).*(?=(オン|on|音|つけ|付け)).*$/,
                /^(?!.*(オン|ない)).*(?=蛍光灯).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=寝室).*(?=(オン|on|音|つけ|付け)).*$/,
                /^(?!.*(オン|ない)).*(?=寝室).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=エアコン).*(?=(オン|on|音|つけ|付け)).*$/,
                /^(?!.*(オン|ない)).*(?=エアコン).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=トイレ).*(?=(オン|on|音|つけ|付け)).*$/,
                /^(?!.*(オン|ない)).*(?=トイレ).*(?=(オフ|off|切|消)).*$/];

    var apiAns=["蛍光灯をオンにしました",
                "蛍光灯をオフにしました",
                "寝室をオンにしました",
                "寝室をオフにしました",
                "エアコンをオンにしました",
                "エアコンをオフにしました",
                "トイレをオンにしました",
                "トイレをオフにしました"];

    var button_tx=["蛍光灯: ",
                   "蛍光灯: ",
                   "寝室: ",
                   "寝室: ",
                   "エアコン: ",
                   "エアコン: ",
                   "トイレ: ",
                   "トイレ: "];

    var html_id=["fluo_on",
                 "fluo_off",
                 "bed_on",
                 "bed_off",
                 "air_on",
                 "air_off",
                 "wc_on",
                 "wc_off"];

    var ip_address=["http://192.168.3.12/",
                    "http://192.168.3.12/",
                    "http://192.168.3.13/",
                    "http://192.168.3.13/",
                    "http://192.168.3.13/",
                    "http://192.168.3.13/",
                    "http://192.168.3.14/",
                    "http://192.168.3.14/"];

    var url_param = [0,0,0,0];

    var api_tx="COM: 音声入力して下さい";
    var you_tx="YOU: 認識された音声";

    //画面ボタンクリック処理
    function buttonClick(button_id){
        var param_id = parseInt(button_id / 2);
        if(url_param[param_id] >= 0){
            url_param[param_id]=1-(button_id % 2);
            you_tx = "YOU: ボタンで" + apiAns[button_id];
            request(html_id[button_id],button_id);
        }else{
            api_tx = "COM: 非接続中のため操作できません";
            you_tx = "YOU: ボタンで" + apiAns[button_id] + "が、操作不可能です"
            document.getElementById("bg").style.backgroundColor = '#ebf';
            updateHtml();
        };
    };

    //チェックした各種パラメータをセット
    function setParam(){
        for(var i = 0; i < url_param.length; i++){
            checkParam(ip_address[i*2]+html_id[i*2]+"_check",i);
        };
    };

    //非同期リクエストでパラメータを取得
    function checkParam(url,index){
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4 && xhr.status === 200){
                url_param[index] = parseInt(xhr.response);
                updateHtml();
            }else{
                url_param[index] = -1;
                updateHtml();
            };
        };
        xhr.open("GET", url, true);
        xhr.send();
    };

    //ボタン操作や音声認識による制御をリクエスト
    function request(url, index){
        var req_url = ip_address[index] + url;
        if(url_param[parseInt(index/2)] >= 0){
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function(){
                if(xhr.readyState === 4 && xhr.status === 200){
                    api_tx = "COM: " + apiAns[index];
                    updateHtml();
                    document.getElementById("bg").style.backgroundColor = '#fff';
                }else{
                    api_tx = "COM: 接続できません";
                    updateHtml();
                    document.getElementById("bg").style.backgroundColor = '#eef';
                };
            };
            xhr.open("GET", req_url, true);
            xhr.send();
        }else{
            api_tx = "COM: 現在、接続していませんよ";
            updateHtml();
            document.getElementById("bg").style.backgroundColor = '#ebf';
        };
    };

    //HTML画面表示アップデート
    function updateHtml(){
        for(var i=0;i<url_param.length;i++){
            if(url_param[i] == 1 ){
                document.getElementById(html_id[i*2]).innerHTML=button_tx[i*2]+"ON";
                document.getElementById(html_id[i*2+1]).innerHTML=button_tx[i*2+1]+"OFF";
                document.getElementById(html_id[i*2]).style.backgroundColor = '#c2b';
                document.getElementById(html_id[i*2+1]).style.backgroundColor = '#999';
            }else if(url_param[i] == 0){
                document.getElementById(html_id[i*2]).innerHTML=button_tx[i*2]+"ON";
                document.getElementById(html_id[i*2+1]).innerHTML=button_tx[i*2+1]+"OFF";
                document.getElementById(html_id[i*2]).style.backgroundColor = '#999';
                document.getElementById(html_id[i*2+1]).style.backgroundColor = '#c2b';
            }else{
                document.getElementById(html_id[i*2]).innerHTML=button_tx[i*2]+"[非接続]";
                document.getElementById(html_id[i*2+1]).innerHTML=button_tx[i*2+1]+"[非接続]";
                document.getElementById(html_id[i*2]).style.backgroundColor = '#ccc';
                document.getElementById(html_id[i*2+1]).style.backgroundColor = '#ccc';
            };
        };
        document.getElementById("api").innerHTML=api_tx;
        document.getElementById("you").innerHTML=you_tx;
    };

    //ここから音声認識処理
    function vr_function() {
        window.SpeechRecognition = window.SpeechRecognition || webkitSpeechRecognition;
        var rec = new webkitSpeechRecognition();
        rec.lang = 'ja-JP';
        rec.interimResults = false;
        rec.continuous = true;

        rec.onstart = function() {
            document.getElementById('status').innerHTML = "[ 認識中 ]";
        };

        rec.onerror = function() {
            document.getElementById('status').innerHTML = "[ エラー ]";
            if(flag_speech == 0){
                vr_function();
            };
        };

        rec.onsoundend = function() {
            document.getElementById('status').innerHTML = "[ 停止中 ]";
            vr_function();
        };

        rec.onresult = function(event) {
            var results = event.results;
            for (var i = event.resultIndex; i < results.length; i++) {
                if (results[i].isFinal){
                    var youSaid = results[i][0].transcript;
                    for(var j=0;j<youAns.length;j++){
                        if(youSaid.match(youAns[j])){
                            var param_id=parseInt(j/2);
                            if(url_param[param_id] >= 0){
                                url_param[param_id]=1-(j%2);
                            };
                            you_tx = "YOU: " + youSaid;
                            request(html_id[j], j);
                            break;
                        }else{
                            document.getElementById("api").innerHTML ="COM: 音声入力をどうぞ!";
                            document.getElementById("you").innerHTML ="YOU: " + youSaid;
                            document.getElementById("bg").style.backgroundColor = '#fc9';
                        };
                    };
                    vr_function();
                }else{
                    document.getElementById('you').innerHTML = "[解析中]:" + results[i][0].transcript;
                    flag_speech = 1;
                };
            };
        };
        flag_speech = 0;
        document.getElementById('status').innerHTML = "[ 起動中 ]";
        rec.start();
    };

    window.onload = function() {
        setParam();
        vr_function();
    };
</script>

</body>
</html>


尚、Web Speech API(途切れなくする方法)は、こちらを参考にしました。
以下が、ESP8266のプログラム(複数あるうちの一つ):

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>

#define ledPin 2
#define relayPin 16

const char* ssid = "*****";
const char* password = "*****";
const char* reqHost = "192.168.3.13";

String webPage = "";

String fluo_param="0";

ESP8266WebServer server(80);

void setup(void){
  webPage="<html><head><meta charset='utf-8'><title>WIFI SWITCH</title></head><body>";
  webPage+="<div style='text-align:center; font-size:30px;'>";
  webPage+="<a href='/fluo_on'><button>蛍光灯: ON</button></a><br/>";
  webPage+="<a href='/fluo_off'><button>蛍光灯: OFF</button></a><br/>";
  webPage+="<a href='/bed_on'><button>寝室: ON</button></a><br/>";
  webPage+="<a href='/bed_off'><button>寝室: OFF</button></a><br/>";
  webPage+="<a href='/air_on'><button>エアコン: ON</button></a><br/>";
  webPage+="<a href='/air_off'><button>エアコン: OFF</button></a><br/>";
  webPage+="<a href='/wc_on'><button>トイレ: ON</button></a><br/>";
  webPage+="<a href='/wc_off'><button>トイレ: OFF</button></a><br/>";
  webPage+="</div></body></html>";
  
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  
  Serial.begin(115200); 
  delay(500);
  WiFi.begin(ssid, password);
  Serial.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(5000);
    Serial.print(".");
  }
  WiFi.config(IPAddress(192,168,3,12),IPAddress(192,168,3,1),IPAddress(255,255,255,0));

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
    
  server.on("/", [](){
    server.send(200, "text/html", webPage);
  });
  
  server.on("/fluo_on", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, HIGH);
    fluo_param="1";
    delay(1000);
  });
  
  server.on("/fluo_off", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, LOW);
    fluo_param="0";
    delay(1000); 
  });

  server.on("/fluo_on_check", [](){
    server.send(200, "text/html", fluo_param);
    delay(1000);
  });
  
  server.on("/bed_on", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/bed_on");
    delay(1000);
  });
  
  server.on("/bed_off", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/bed_off");
    delay(1000); 
  });

  server.on("/air_on", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/air_on");
    delay(1000);
  });
  
  server.on("/air_off", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/air_off");
    delay(1000); 
  });

  server.on("/wc_on", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/wc_on");
    delay(1000);
  });
  
  server.on("/wc_off", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/wc_off");
    delay(1000); 
  });

  server.begin();
  Serial.println("HTTP server started"); 
}

void http_request(const char* host, String url){
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 5000) {
      Serial.println("Client Timeout!");
      client.stop();
      return;
    }
  }
}

void loop(void){
  server.handleClient();
}

fluo_paramというのが、ESP8266内における蛍光灯のON/OFF状況の変数です。ONのとき"1"、OFFのとき"0"で、フィードバック用URLである/fluo_on_checkにリクエストがくると、そのレスポンスとしてfluo_paramの値("0"か"1")をブラウザへ返します。
その他は前回とほぼ同じで、仮にこのESP8266のIPアドレスにアクセスしてもボタン操作できる画面(音声認識なし)が表示されます。このESP8266から他のESP8266(他のIPアドレス)へもリクエストを送ることができるので、他のESP8266を制御することも可能です。おそらくメインサーバからそれぞれのESP8266を制御することになると思うのですが、基本的にどのESP8266からであっても接続されている全てのESP8266の制御が可能になっています。

追記:
その後、非接続のESP8266に対するボタン操作や音声入力した場合の処理や表示についても追加しておきました。こういったものは、操作するたびに矛盾する処理が見つかるのできりがありません。

関連:
音声認識Wifiスイッチ/ESP8266使用(まとめ)についてはこちら

2017年4月25日火曜日

ESP8266:XMLHttpRequestで改良

ESP8266を使った音声認識Wifiスイッチのさらなる改良の続きです。
前回は、複数あるESP8266の一つをメインサーバとして、そこから他のESP8266へもHTTPリクエストを送る仕組みにしていましたが、今回はAjaxのXMLHttpRequestを使って、それぞれのESP8266へリクエストを送る仕組みに変更しました。
そもそも、XMLHttpRequestは同ドメイン内でしか使えないと思っていたので、前回のようなやり方になってしまったのですが、よく調べてみたらChromeやFireFoxでは異なるドメインへのリクエストも問題ないということでした。結局かなり遠回りしていましたが、クエリもサーバ同士の通信も不要で元の単純なアイデアに戻ってきた感じです。


これは↑、左側のESP8266(server A)がメインサーバとなり、ここに音声認識を含んだJavaScript、ならびにHTML+CSSファイルをアップロードしておき、ブラウザからこのIPアドレスにアクセスします。ブラウザ上では音声認識によるスイッチのON/OFF、そしてボタンでも操作できます。右側のもう一つのESP8266には、speech.html内に記述されたJavaScript(XMLHttpRequest)でON/OFFのリクエストを出します。
要は、画面遷移せずに複数あるESP8266に割り振った静的IPアドレスにリクエストを出しているだけです。一応XMLHttpRequestでリクエストを出したら、onreadystatechangeで反応の確認をとってから、画面内表示をアップデートするようにしています。以下の部分が今回のリクエストの仕方です(ほぼサンプル通りですが)。

    function request(url){
        var index = html_id.indexOf(url);
        var req_url = ip_address[index] + url;
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4 && xhr.status === 200){
                updateHtml();
                console.log(xhr.response);
            }
        }
        xhr.open("GET", req_url, true);
        xhr.send();
    }

それぞれのスイッチをON/OFFするにはそれに対応したURL("/fluo_on"や"/fluo_off"など)へリクエストをすればいいのですが、画面表示上ボタンもあることから、どれがONになっていて、どれがOFFになっているか分かるようにボタンの色を変える(この場合updateHtmlという関数)必要もあります。今回のようなやり方の場合は、onreadystatechangeで確認したのちにupdateHtmlで画面表示内容を変更するので、もしリクエスト先のESP8266から返事がなければボタンを押しても色が変わらないということになります。

これ↑は、前々回クエリをつかってやりとりしていた画面です(ボタンなどに表示画面自体は今回も同じ)。URLのあとに長々とクエリをつけて異なるサーバへ表示用変数を渡す面倒なやり方になっていましたが、今回は画面表示に関してはメインサーバとのやりとりだけなので、メインサーバ内のJavaScriptの変数に記憶させているだけでクエリは不要となりました。



もうひとつの方法として、ESP8266をメインサーバとして使わず、外部にある普通のWebサーバに音声認識も含むJavaScriptならびにHTML+CSSファイルをアップロードしておき、そこをメインサーバにしてしまうというのもあります。
この場合、ESP8266には簡単なボタン操作するだけのHTMLデータ(音声認識なし、画面表示も単純)を書いておき、万が一中央のメインサーバと通信不能になっても、一応それぞれのESP8266へアクセスすればON/OFFだけはできるようにしておきます。
こんな感じ↑の臨時用ボタンくらいは表示できるようにしておく。

ESP8266にはわざわざHTMLファイルをアップロードしなくてもいいので、その分手間が省けそうです。外部サーバをメインサーバとすれば、ファイルが巨大になってもなんとかなりそうです。あるいは、ローカルで使うのであれば、Raspberry Piでサーバを立ち上げていてもいいかもしれません。

あと改良しなければいけない点は、メインサーバと再接続したときに、現状だとすべてがリセット(すべてOFF)になってしまうので、再接続の際にそれぞれのスイッチのON/OFF状態を確認するプログラムが必要という感じです。そのためには、それぞれのESP8266にON/OFF状態の変数を用意しておき、再接続の際に状態チェック用のURLにアクセスして、それぞれのESP8266からそのデータを受け取る仕組みにすればいいかなと考えています。

追記:
音声認識Wifiスイッチ/ESP8266使用(まとめ)についてはこちら

2017年4月23日日曜日

ESP8266:WebサーバからHttpリクエスト

前回までの複数のESP8266を用いた音声認識Wifiスイッチの続きです。
複数の部屋(リビング、寝室、トイレなど)に異なるIPアドレスを割り振ったESP8266があるので、リビングの電源をON/OFFするには、リビングに設置したESP8266のIPアドレスにリクエストしていましたが、リビングから寝室の電源をON/OFFするには、寝室のIPアドレスへ画面遷移してリクエストを送るというややこしい仕組みになってました。


仕組みとしてはこんな感じで、電源をON/OFFさせたい場所のESP8266へ直接リクエストを送って、それぞれのサーバからレスポンスを受け取っていたという感じです。画面遷移しないで、AからBあるいはBからAへリクエストできないものかと、以下のようなシステムに変更してみました。


この場合は、serverAがメインサーバとなり、端末からもこのIPアドレスにアクセスして、Aの電源をON/OFFする場合は従来通りAへリクエストを送り、Bの電源をON/OFFするには、Aにリクエストを送り、Aが内部的にBへHTTPリクエストを送ることになります。
逆にBのIPアドレスへアクセスしても、同様にAに対してリクエストを送ることができるので、AであれBであれ、どちらにアクセスしようと画面遷移せずに両方の電源を制御できます。

今までは、サーバはクライアントからのリクエストを待ち、リクエストがあるときだけレスポンスするのだと思っていたので、サーバから別のURLへHTTPリクエストを送ることはできないと思っていました。しかし、サーバからも普通にHTTPリクエストできるということでした(もっと早く気づけば)。

以下がサンプルコード。今回は音声認識の部分は外しています。単にサーバAを介してサーバBを制御するという仕組みの部分だけです。

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>

#define ledPin 2

const char* ssid = "******";
const char* password = "******";
const char* reqHost = "192.168.3.13";

String webPage = "";

ESP8266WebServer server(80);

void setup(void){
  webPage="<http><head><meta charset='utf-8'><title>WIFI SWITCH</title></head><body>";
  webPage+="<div style='text-align:center; font-size:30px;'>";
  webPage+="<a href='/this_on'><button>THIS: ON</button></a><br/>";
  webPage+="<a href='/this_off'><button>THIS: OFF</button></a><br/>";
  webPage+="<a href='/other_on'><button>OTHER: ON</button></a><br/>";
  webPage+="<a href='/other_off'><button>OTHER: OFF</button></a><br/>";
  webPage+="</div></body></html>";
  
  pinMode(ledPin, OUTPUT);
  
  Serial.begin(115200); 
  delay(500);
  WiFi.begin(ssid, password);
  Serial.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(5000);
    Serial.print(".");
  }
  WiFi.config(IPAddress(192,168,3,12),IPAddress(192,168,3,1),IPAddress(255,255,255,0));

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
    
  server.on("/", [](){
    server.send(200, "text/html", webPage);
  });
  
  server.on("/this_on", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, HIGH);
    delay(1000);
  });
  
  server.on("/this_off", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, LOW);
    delay(1000); 
  });
  
  server.on("/other_on", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/this_on");
    delay(1000);
  });
  
  server.on("/other_off", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/this_off");
    delay(1000); 
  });

  server.begin();
  Serial.println("HTTP server started"); 
}

void http_request(const char* host, String url){
  //host="192.168.3.13";
  //url="/this_on";
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 5000) {
      Serial.println("Client Timeout!");
      client.stop();
      return;
    }
  }
}

void loop(void){
  server.handleClient();
}


ということで、http_request()という関数をつくり、他のIPアドレスのボタンを押したら、レスポンスとしてHTMLデータ(webPage:ボタン画面)を返しつつ、リクエストをもう一方のESP8266へ送るということにしました。setup()内にある以下の部分です。

  server.on("/other_on", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/this_on");
    delay(1000);
  });
  
  server.on("/other_off", [](){
    server.send(200, "text/html", webPage);
    http_request(reqHost,"/this_off");
    delay(1000); 
  });

少しずつ曖昧な部分の理解が深まってきたので、徐々にまともなシステムになりつつあります。
Raspberry Pi上にNode-REDで全体を管理するサーバを立ち上げようと思いましたが、今回のやり方で大丈夫そうなので、Node-REDやMQTTなどは使わずいけそうです。
電気的な制御は単純なON/OFFですが、Wifiが絡んでくると、サーバだとかクライアントだとか、通信の仕組みで手間がかかります。ということで、またもやRaspberry Pi(サーバ利用)は不要となってしまいました。もっと複雑なことをしない限り、しばらくは中枢的なサーバはいらなさそうです。
このシステムは近々完成させて、次は再度IPカメラに戻りたいと思います。

追記:
改良案はこちら(XMLHttpRequestを使ったサンプル)
音声認識Wifiスイッチ/ESP8266使用(まとめ)についてはこちら

2017年4月22日土曜日

久しぶりのRaspberry Pi3: PIXELとかNode-REDとか

最近はESP8266やESP32を扱っていたので、サーバ用Raspberry Piを使ってみようかと。
しばらくアップデートなどしていなかったのですが、どうやら今はPIXELというのに変わっていて、ブラウザもChromiumが標準となっているようだったので、Web Speech APIを使うにはちょうどいいかなと。
ということで、PIXELにアップデートしてみました。
また最初から設定し直しなので、いちいちターミナルで手入力なのかと思いきや、PIXELに変えてみるともはや普通のOSという感じで、ほとんどの設定がGUIで可能でした。
Bluetoothのペアリングなども、Bluetoothアイコンからすぐに設定できるので、ほぼMacと同じような感じの使い勝手で設定などはスムースに進みました。



Raspberry Piはモニタ、マウス、キーボードなどつなげると配線だらけになってしまうので、少し前にBluetoothのタッチパッドつき小型キーボードにしてしまいました。


AliExpress.com Product - Windows PC 59 Keys Ultra Slim Mini Bluetooth Keyboard with Touch Pad Panel RR6V1451円(送料込み)
サイズ、23x15x0.6cm。充電はミニUSBで。
たぶん、これと同じものを、楽天ポイントがあったので楽天で購入。


Raspberry Piとは簡単に接続でき(再接続も一発)、日本語(青Fn+Qで切り替え)も対応しているので問題なく使えます。タッチパッドの部分もスワイプ可能、押すとクリックも可能、タップでクリックも可能。安いわりになかなか高性能です。厚さ6mmなのでかなり薄いし、その分軽いので持ち運びにもいいかもしれません。思った以上につくりは雑ではないです。
キーボードだけのものにしようかと思いましたが、Raspberry Piにはマウスは繋がないで、これひとつで済ませようとタッチパッドつきにしてしまいました(とは言っても、画面自体がタッチパネルだったので、もはやタッチパッドもマウスもどちらも必要なかった)。
AliExpress.com Product - Raspberry Pi 3 B 7inch HDMI LCD 1024 * 600 Resistive Touch Screen LCD Display5823円(送料無料)
同じ大きさで同じ解像度のモニターを使っていますが、最近だとこのタイプがよさそうです。
裏に直接Raspberry Piを差し込めるピンと専用のHDMIプラグもついているようです。
AliExpress.com Product - Elecrow HDMI Connector for 5 inch HDMI Raspberry Pi Screen Dispaly DIY HDMI Connector Kit 113円(送料無料)この180度折り返しのHDMIプラグを探していたのですが、単品だとここで売っているようです。

この折り返しのプラグがないため、現在は以下のようなアダプタなどで接続。
モニターにアクリル板を取り付け、ケーブル類もコネクターを複数つないで、できるだけコンパクトにしようと試みましたが、現状はこんな感じ。これにさらに電源用USB、マウス、キーボード、その他のUSBなどがつながるとケーブル類はごちゃごちゃになってしまいます。できるだけBluetoothなどでつないだほうがすっきりするし、その他のデバイスもUSB接続できます。
AliExpress.com Product - 180/360 Degree Rotating Adapter HDMI Male to Female HDMI Cable Adaptor Connector extender for 1080P HDTV250円(送料無料)
モニターとは、この360度どちらにも折り曲げ可能で、しかも首振りも180度回転可能なHDMIアダプタでなんとかつないでいます。

AliExpress.com Product - LVSUN 5V 3A 9V 12V Universal Qucik Charger 2.0 Portable Travel AC Adapter USB Wall Charger EU UK US Plug for Mobile Phone Tablet1212円(送料込み)
Raspberry Pi3用の電源は2.5A以上が推奨となっているため、以前こんな感じの5V3A出力のUSB電源を購入しました(当時850円くらい)。

実際Raspberry Piに電源を入れてどのくらいの電流が流れているか調べてみると、
何もしていないと1A前後で、ブラウザを使用すると1.2A前後になります。
たぶん、特別なことをしないかぎり2.5Aにはならないと思うので、2.1Aのモバイルバッテリーを試してみると大丈夫でした。以下。


モバイルバッテリーはAliExpressだとなぜか高いので以前Amazonで購入したもの。10000mAhで1.0Aと2.1A出力のもの(現在のタイプは2.4Aに改良されたみたいです)。Raspberry Pi3は消費電力が高いので、持ち運びのバッテリーを用意するのが難しいのですが、この2.4A(2.1Aでもぎりぎり大丈夫)であれば、わざわざAC100Vにつながなくてもよさそうです。

2.1Aでも大丈夫ということなら、以下のAliExpressで売っている安いUSB電源(115円)ではどうか?
こんな感じの小型なタイプなのに、1.0Aと2.1A、合計で3.1A使えるらしい。以下。

AliExpress.com Product - HOT SALE 5V 1.0A 2.1AHome Travel Dual Port AC USB Wall Charger US Plug for iPhones for smart phones ideal for travel115円(送料無料)。
安いので買ってみましたが、Raspberry Pi3で試してみると、

起動中に5V以下になってしまい無理っぽい。これはAliExpressにありがちなスペック詐欺の商品かもしれません。この手の小型タイプは、せいぜい1Aが限度なはず。内部の保護回路など外して無理やり2.1Aとか言っているのかも。まあ、115円だから仕方ないという感じ(というか、そもそもESP8266用にAC100VからDC5Vをとるための降圧回路が欲しかったので分解用に買ったものです)。

ちなみに、このUSBテスターは3.3〜33V、0〜5A。165Wまで計測できるようです。

AliExpress.com Product - USB tester DC Voltmeter ammeter current voltage meters capacity monitor qc2.0/qc3.0 quick charger Battery Power Bank detector442円(送料無料)

USBプラグ/ソケットがついているワニ口クリップをつなげると、以下のような使い方もできるようです。
AliExpress.com Product - USB Crocodile wire Alligator clips Male/female to USB tester Detector voltage meter ammeter capacity power meter monitor, etc 167円(送料込み)

という感じで、あいかわらずAliExpressでつい買ってしまいます。

Raspberry Piに話はもどりますが、PIXELにしてから、Chromiumが使えるのとすでにNode-REDが入っているので便利そうです。


IoTにはよく出てくるNode-Redですが、これをRaspberry Piで立ち上げておけばいろいろできそうです。また覚えなければいけないことが増えますが、音声認識による部屋の電源のON/OFF制御に使えそうです。
ノードと線をつないで、フローチャートのようなシステムを組んでいくタイプで、一見簡単そうに組めそうですが、ある程度全体像が見えて、それぞれのノードの役割も理解していないと難しそうです。
これとは別に一旦システムを組んだものを、Node-REDに置き換えるというのなら、やりやすいのかもしれません。

それから、Raspberry Pi Zero Wですが、日本仕様はもう少しで発売といいながらなかなか発売しない。発売開始しても、おそらくすぐに完売で、しばらくは入手困難になるはず。
ESP32でもいいのですが、同じような値段であればRaspberry Pi Zero Wのほうが様々なインターフェースを持ち合わせているので便利なはず。
ESP32のほうもたまにやってみてはいるのですが、いまだにSPIFFSを使っての外部ファイル読み込みが難しい。どうやらESP8266とは仕組みが違うようで、かなり面倒な感じ。SDカードを使うサンプルはあるので、外部ファイル読み込みなら今のところそれを使うという感じでしょうか。
ESP32に、Micro PythonやJavaScriptで動かすEspduinoやDuktapeをのせてしまったほうがいいのかもしれないけれども、まだ保留中。おそらくNode.jsを使うことになるので、JavaScriptで統一してしまったほうがよさそう。
しかし、Raspberry Pi Zero Wが発売になってしまうと、少し事態は変わって、ちょっとしたものはESP8266でやって、メモリが必要そうなものは、ESP32を使うというよりも、Raspberry Pi Zero Wでやるという感じになるのかもしれません。そうなるとArduino Unoレベルのマイコンは出番がないかもしれません。

2017年4月17日月曜日

ESP8266ファイルシステム:SPIFFSについて

ESP8266にHTMLファイルなどの外部ファイルを添付する際に、SPIFFSを使いますが、いまいちよくわからない部分もあったので、覚書として書いておきます。Arduino IDEでのやり方です。

Toolのインストール(下準備):
基本はこのESP8266のGitに書いてあるので、ここを見ればだいたい分かるはず。
まず、アップロードTool「esp8266fs.jar」をダウンロード(こちらのサイトここから)。
現在のバージョンは0.3.0のようです。
ダウンロードした「ESP8266FS-0.3.0.zip」を解凍。
Macならアプリケーション>Arduinoを右クリックで「パッケージの内容を表示」。
Arduino>Contents>Java>tools>ESP8266FS>tool>esp8266fs.jar
となるような場所にファイルもしくはフォルダごと移動し、Arduino IDEを再起動で準備OK。

外部ファイルのアップロード:
Arduino IDEのメインのソースとなるinoファイルに添付したい外部ファイル(HTMLファイルなど)を取り込みます。
「ファイルを追加...」で任意の場所から外部ファイルを選択(今回はデスクトップ上にあるindex.htmlを選択)。


 外部ファイルを追加したら、「スケッチのフォルダを表示」で中身確認。

こんな感じ↑で、メインのinoファイルと同じディレクトリにdataフォルダが自動的に作成されており、その中に追加した外部ファイル(この場合index.html)が入っていることを確認できます。

つぎに、先ほどのツールを使って外部ファイルだけ(この場合index.htmlファイルだけ)をESP8266へ先にアップロードしておきます。

Arduino IDEのコンソール画面には、こんな感じでアップロード状況が表示されます。
3%くらいずつアップロードしているようで、かなり時間かかります。

この段階では、まだ外部ファイルだけのアップロードなので、メインのinoファイルに書かれたプログラムはアップロードされていないないはず。

最初に外部ファイルだけアップロードするときにこのような画面が出てきて、ファイルsizeなどが表示されています。この場合3052なので、約3kバイトという感じでしょうか。実際のindex.htmlを「情報を見る」で見てみると、8765バイトあるのですが、この差についてはよくわかりません。

メインのプログラムに外部ファイル読み込みコマンドを追加:
外部ファイルを読み込むためのプログラムを追加しておきます。
ESP8266では、Webサーバの表示用HTMLデータを用いることがよくあります。HTMLデータを読み込んだのちにString変数に入れておき、それをクライアント側に送信ということになります。
以下はinoファイルの冒頭部分です。

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#include "FS.h"

#define ledPin 13
#define relayPin 2

MDNSResponder mdns;

const char* ssid = "*****";
const char* password = "*****";
String webPage = "";
ESP8266WebServer server(80);

void setup(void){
  SPIFFS.begin();
  File f = SPIFFS.open("/index.html", "r");
  if (!f) {
    Serial.println("file open failed");
  }else{
    webPage = f.readString();
    f.close();
  }

最初のほうで、
#include "FS.h"
を呼んでおき、
setup()内で、

 SPIFFS.begin();
  File f = SPIFFS.open("/index.html", "r");
  if (!f) {
    Serial.println("file open failed");
  }else{
    webPage = f.readString();
    f.close();
  }

と書いて、index.htmlを読み込みつつ、String変数のwebPageに入れてしまいます。
Arduino IDEでのディレクトリは、data/index.htmlでしたが、ESP8266内では/index.htmlで読み込めます。
あとはこのままメインのプログラムと一緒に「マイコンボードに書き込む」でアップロードすればOK。

外部ファイルの確認や消去方法:
外部ファイルがアップロードされているのかどうかなど確認するには、SPIFFSのGitに詳しく書かれているので参考にするといいと思いますが、実際確認してみるプログラムを書いてみました。
#include "FS.h"

int val;
String filename;

void setup(){
  Serial.begin(115200);
  SPIFFS.begin();
}

void loop(){
  if(Serial.available()>0){
    val=Serial.read();
    if(val=='a'){//ファイル名やサイズの確認
      Dir dir = SPIFFS.openDir("/");
      while (dir.next()) {
        filename=dir.fileName();
        Serial.println("File Name: "+dir.fileName());
        File f = dir.openFile("r");
        Serial.println(f.size());
      }
      Serial.println("---a end");
      Serial.println("");
    }else if(val=='b'){//ファイルが存在するかどうか?
      if(SPIFFS.exists(filename)){
        Serial.println("File exists");
      }else{
        Serial.println("not exists");
      }
      Serial.println("---b end");
      Serial.println("");
    }else if(val=='c'){//ファイル削除
      SPIFFS.remove(filename);
      Serial.println("File removed");
      Serial.println("---c end");
      Serial.println("");
    }
  }
}


外部ファイルをアップロードしたESP8266に、このプログラムをアップロードします(この場合、inoファイルに書かれているプログラムはこれだけなので、もうすでに何らかのプログラムが書き込まれている場合は消えてしまうので要注意です)。
シリアルモニタを開いて、
a を入力でファイル名、サイズ(---a endで終了)
b を入力でそのファイルが存在するかどうか?(---b endで終了)
c を入力でそのファイルを削除(---c endで終了)
という感じです。
特にaはサンプルとほぼ同じですが、 while(dir.next())があるため、複数ファイル(html、css、jsファイルなど)あるならば、順次複数出力するのだと思います。

以下がシリアルモニタの出力結果の画面。


最初に、aでファイル名(ディレクトリも含め)、8765バイト(Macのファイルの「情報を見る」と同じ結果)。
次に、bで/index.htmlファイルが存在するかどうか?File existsで存在確認。
そして、cで/index.htmlファイルを削除。File removedで消去済み。
ファイル削除後、bで/index.htmlファイルの存在の再確認。もちろんnot existsで不在確認。
ちなみに、aを入力すると特に返答なし(ファイルがないため)。

たまに、複数あるESP8266にどの外部ファイルがアップロードされているか(あるいはアップロードされていないか)忘れてしまうときがあります。そのようなときには、内容確認としてこのようなことができれば便利かと。
さらに、ファイルの中身まで確認したいというなら、
File f=SPIFFS.open("/index.html","r");
String contents=f.readString();
Serial.println(contents);
などとすればいいのかもしれません。


2017年4月16日日曜日

IoTその6:音声認識+ESP8266(改良中)

以前から続いている音声認識によるESP8266制御についてです。 いろいろと音声認識のAPIやデバイス、そしてサーバの立ち上げ方や外部クラウドの利用の仕方など試してみましたが、結局のところWeb Speech APIを使い、スマホ、タブレット、ノートパソコンあるいはRaspberry Pi上のブラウザ(Chrome)で、複数あるESP8266をWebサーバとして通信させることにしました。

SnowboyVoice Recognition Module VR3.1も認識力は良かったのですが、事前に録音させた自分の声を元に認識させる仕組みであり、しかもあまり長い言葉をしゃべることができないし、正規表現も使うことができないため、やはりWeb Speech APIになってしまったという感じです。

改良点:
やりたいことは、リビング、寝室、トイレなどに設置したESP8266で電源のスイッチをオンオフすることです。 問題となっていたのは、複数の異なるIPアドレスを与えたWebサーバ同士をどうやってつなげるかということです。今回は、ブラウザからそれぞれのIPアドレスへリクエストを送る際に、今の状況(どれがオンでどれがオフになっているかなど)をアドレスの最後にクエリをつけて変数を渡してあげるという方法でなんとか解決できました。
音声認識のほうについては、前回のようにWeb Speech APIと正規表現によって、かなり柔軟に認識してくれるのでいいのですが、それよりもHTMLとJavaScriptによる変数の受け渡し方に時間がかかりました。
追記:
その後の改良案では、今回の方法(以下の方法)は使わず、直接AjaxのXMLHttpRequestを使う方法で解決できました。

3つのWeb Server間の変数渡し:
蛍光灯のON/OFFはIPアドレス192.168.3.12のESP8266
寝室とエアコンは192.168.3.13のESP8266
トイレは192.168.3.14のESP8266
という感じで、同じ画面でも3つ異なる送信先があります。
以下の状態だと、蛍光灯をONしたので、192.168.3.12にアクセスしているところです。つぎにエアコンをONにする場合は、192.168.3.13/air_on.htmlへ移動するということになります。エアコンONした瞬間、通常なら移動先では今の情報(蛍光灯がONになっているなど)を知らないので初期状態の画面(蛍光灯が画面表示上OFFになってしまう)になってしまいますが、移動の際にクエリを渡すので、移動先でもこの画面を再現できるというわけです。

操作画面:
以下が今回の操作画面です。というか、音声認識なので、あまり画面は重要ではないのですが、一応下半分には、それぞれのオンオフが可能なボタンがついています。
アドレスの.html?以下にクエリとして、各ボタンのオンオフ状態、そして音声のテキスト内容が後続して並んでいます。
 クエリとして日本語文字列はエンコードしないといけないみたいですが、なぜか上のアドレスバーにはそのままエンコードされずに日本語が出ています。その後、HTMLで表示する際にもデコードしなおす必要があるようです。

追加するクエリの仕組み:
クエリ追加の仕組みは簡単で、それぞれのアドレス「〜.html?」以下に

1,0,0,1,0,1,0,1,COMの言葉,YOUの言葉

という感じで「,」で区切った10個の変数値が並んでいます。通常のクエリは、key=valueという組み合わせになり、JavaScript内でも連想配列で[key:value...]となりますが、面倒なのでkey(文字列)だけにしてあります。
本来なら、
〜.html?fluo=1&bed=0&aircon=0&wc=0&com=1234&you=5678
などとなるのかもしれませんが、今回は独自の勝手なやり方で。

ESP8266のほうでは、/fluo_onや/fluo_offのようなアドレスのリクエストがあった場合、対応するPINのオンオフ制御をしつつ、表示用HTML(CSS+JavaScript)が返されます。異なるアドレス(異なるスイッチ)に移動する際に、移動先アドレスのあとに?と上記変数を追加して、JavaScriptのwindow.location.hrefでURL移動しているだけです。
以前は、ESP8266に書き込んだHTMLの一部をArduino言語の変数に置き換えていたのですが、それだとHTMLやJavaScript内にある変数があるたびに、いちいち区切って書き込まなければならないので、今回のようなSPIFFSを使って一気にファイルをアップロードする際には不都合となってしまいます。

画面遷移時にやや問題:
リビング、寝室、トイレと場所が分かれているので、それぞれ異なるIPアドレスへアクセスすることになります。そのためブラウザの画面上では、見た目同じでもIPアドレス192.168.3.12から192.168.3.13へリクエストしたときに画面遷移しなければいけないので、画面の再読み込みが発生して一瞬チラついてしまいます。この辺はAjaxやAngularJSを使うと解決できるのかもしれませんが、Web技術にそれほど詳しいわけでもないので、今回はそこまではできていません。それでも結果的にはESP8266の技術というよりも、利用状況を表示させるHTMLとJavaScriptのほうが難しかったです。

その他のやり方:
当初は、PythonのFlask、あるいはNode.jsなどでサーバを立ち上げておいて、そのサーバが中枢的な役割を果たし、末端にいる複数のESP8266を管理するというイメージもありましたが、そのためにはリアルタイムの送受信であったり、WebSocketを使う方法などいろいろあるようで、そこまで大げさなことをすることもないかなと。

本題である音声認識の仕方や電源のオンオフのさせ方よりも、現在しゃべった言葉やスイッチのオンオフ状況の情報が画面遷移したあとでも保持されて表示されるという部分をどう処理するかで悩みました。 どうやら、同じドメイン内ならAjaxで変化させることができますが、異なるドメイン間になると難しいようで、このへんは割り切るしかありません。pjaxやAngularJsなら可能なのかもしれませんが、そこまで詳しくないので、これもそのうち。 それにしても、今回はかなり迂回しましたが、そのおかげでいろいろ勉強になったというか、たかだかWifiによるワイヤレススイッチなのに、Flask、Node.js、Node-RED、MQTTのようなサーバ関係やAjaxなどのJavaScriptの様々なスキルなどWeb技術ばかりでした。

SPIFFSでアップロード(やり方はページ下の方にあります):
ほぼシステムは出来上がりましたが、ESP8266の設置などはまだです。
今回のソースは以下です。CSSもあるので、かなり長くなりましたが、ESP8266にはSPIFFSを使って別にアップロードしたので大丈夫でした。
今回Speech to Textは使っていますが、Text to Speechは使っていないので音はでません。画面の文字だけです。重くなってしまうかもという懸念とたまに自分でしゃべった音に反応することがあったり、エラーにもなりかねないので外してしまいました。つまらなそうなら、また音声つきに戻そうかとも思っています。


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Speech API</title>
</head>
<body id="bg">
<div class="title" id="status">[音声認識開始]</div>
<div class="api"><p id="api">COM: 音声入力して下さい</p></div>
<div class="you"><p id="you">YOU: (認識された音声)</p></div>
<div class="btn" id="fluo_on" onclick="buttonClick('fluo_on')">蛍光灯: ON</div>
<div class="btn" id="fluo_off" onclick="buttonClick('fluo_off')">蛍光灯: OFF</div>
<div class="btn" id="bed_on" onclick="buttonClick('bed_on')">寝室: ON</div>
<div class="btn" id="bed_off" onclick="buttonClick('bed_off')">寝室: OFF</div>
<div class="btn" id="air_on" onclick="buttonClick('air_on')">エアコン: ON</div>
<div class="btn" id="air_off" onclick="buttonClick('air_off')">エアコン: OFF</div>
<div class="btn" id="wc_on" onclick="buttonClick('wc_on')">トイレ: ON</div>
<div class="btn" id="wc_off" onclick="buttonClick('wc_off')">トイレ: OFF</div>

<style type="text/css">
    body{
        text-align:center;
        font-family: 'Helvetica',sans-serif;
        color:#fff;
        margin:0px;
        padding:0px;
        background-color:#fff;
    }
    .title{
        width:100%;
        background-color:#fa8;
        margin:0px;
        padding:2% 0% 2% 0;
    }
    .api{
        text-align: left;
        width:96%;
        background-color:#e75;
        padding:2%;
        margin:1% 0 0 0;
    }
    .you{
        text-align: left;
        width:96%;
        background-color:#d66;
        padding:2%;
        margin:1% 0 0 0;
    }
    .btn{
        float:left;
        background-color:#aaa;
        padding:3% 0px;
        margin:1% 1% 0 1%;
        width:48%;
    }
    .btn:hover{
        opacity: 0.8;
        cursor: pointer;
    }

</style>
<script type="text/javascript">
    var flag_speech = 0;
    var youAns=[/^(?!.*(オフ|ない)).*(?=蛍光灯).*(?=(オン|on|音|つけて)).*$/,
                /^(?!.*(オン|ない)).*(?=蛍光灯).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=寝室).*(?=(オン|on|音|つけて)).*$/,
                /^(?!.*(オン|ない)).*(?=寝室).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=エアコン).*(?=(オン|on|音|つけて)).*$/,
                /^(?!.*(オン|ない)).*(?=エアコン).*(?=(オフ|off|切|消)).*$/,
                /^(?!.*(オフ|ない)).*(?=トイレ).*(?=(オン|on|音|つけて)).*$/,
                /^(?!.*(オン|ない)).*(?=トイレ).*(?=(オフ|off|切|消)).*$/];

    var apiAns=["はい、蛍光灯をオンにしました",
                "はい、蛍光灯をオフにしました",
                "はい、寝室をオンにしました",
                "はい、寝室をオフにしました",
                "はい、エアコンをオンにしました",
                "はい、エアコンをオフにしました",
                "はい、トイレをオンにしました",
                "はい、トイレをオフにしました"];
    
    var url_to=["fluo_on",
                "fluo_off",
                "bed_on",
                "bed_off",
                "air_on",
                "air_off",
                "wc_on",
                "wc_off"];

    var ipAddress=["http://192.168.3.12/",
                   "http://192.168.3.13/",
                   "http://192.168.3.14/"];

    var url_val=[];

    function setQuery(){
        if(window.location.search.length<1){
            for(var i=0;i<url_to.length;i++){
                if(i%2==0){
                    url_val[i]="0";
                    document.getElementById(url_to[i]).style.backgroundColor = '#aaa';
                }else{
                    url_val[i]="1";
                    document.getElementById(url_to[i]).style.backgroundColor = '#c2b'; 
                };     
            };
            url_val[url_to.length]="COM: 音声入力して下さい";
            url_val[url_to.length+1]="YOU: 認識された音声";
            document.getElementById("api").innerHTML=url_val[url_to.length];
            document.getElementById("you").innerHTML=url_val[url_to.length+1];
        }else{
            var s=window.location.search.slice(1);
            url_val=s.split(",");
            for(var i=0;i<url_to.length;i++){
                if(url_val[i]=="0"){
                    document.getElementById(url_to[i]).style.backgroundColor = '#aaa';
                }else if(url_val[i]=="1"){
                    document.getElementById(url_to[i]).style.backgroundColor = '#c2b';
                };
            };
            document.getElementById("api").innerHTML=decodeURI(url_val[url_to.length]);
            document.getElementById("you").innerHTML=decodeURI(url_val[url_to.length+1]);
        };
    };

    function buttonClick(id){
        var idx=url_to.indexOf(id);
        url_val[idx]="1";
        url_val[url_to.length]="COM: "+apiAns[idx];
        url_val[url_to.length+1]="YOU: ボタンで"+apiAns[idx].slice(3);
        if(idx%2==0){
            url_val[idx+1]="0";
        }else{                 
            url_val[idx-1]="0";
        }
        var ip="";
        if(idx==0||idx==1){
            ip=ipAddress[0];
        }else if(idx<=2 && idx<=5){
            ip=ipAddress[1];
        }else{
            ip=ipAddress[2];
        }
        window.location.href = ip+id+".html?"+url_val;
    };

    function delay_ms( ms ){
        var start = new Date();
        while( new Date() - start < ms );
    };
    
    function vr_function() {
        window.SpeechRecognition = window.SpeechRecognition || webkitSpeechRecognition;
        var rec = new webkitSpeechRecognition();
        rec.lang = 'ja-JP';
        rec.interimResults = false;
        rec.continuous = true;

        rec.onstart = function() {
            document.getElementById('status').innerHTML = "[ 認識中 ]";
        };
        rec.onerror = function() {
            document.getElementById('status').innerHTML = "[ エラー ]";
            if(flag_speech == 0){
                vr_function();
            };
        };
        rec.onsoundend = function() {
            document.getElementById('status').innerHTML = "[ 停止中 ]";
            vr_function();
        };

        rec.onresult = function(event) {
            var results = event.results;
            var youSaid = "";
            var apiSaid = "";
            var apiHtml = document.getElementById("api");
            var youHtml = document.getElementById("you");
            var bgcolor = document.getElementById("bg");
            for (var i = event.resultIndex; i < results.length; i++) {
                if (results[i].isFinal){
                    youSaid = results[i][0].transcript;
                    for(var j=0;j<youAns.length;j++){
                        if(youSaid.match(youAns[j])){
                            apiHtml.innerHTML ="COM:"+apiAns[j];
                            youHtml.innerHTML ="YOU:"+youSaid;
                            if(j%2==0){
                                url_val[j]="1";
                                url_val[j+1]="0";
                            }else{   
                                url_val[j-1]="0";
                                url_val[j]="1";
                            };
                            url_val[youAns.length]=encodeURI("COM: "+apiAns[j]);
                            url_val[youAns.length+1]=encodeURI("YOU: "+youSaid);
                            var ip="";
                            if(j==0||j==1){
                                ip=ipAddress[0];
                            }else if(j>=2 && j<=5){
                                ip=ipAddress[1];
                            }else{
                                ip=ipAddress[2];
                            }
                            window.location.href = ip+url_to[j]+".html?"+url_val;
                        }else{
                            apiHtml.innerHTML ="COM: 音声入力をどうぞ";
                            youHtml.innerHTML ="YOU: "+youSaid;
                            bgcolor.style.backgroundColor = '#fc9';
                        };
                    };
                    vr_function();                       
                }else{
                    document.getElementById('you').innerHTML = "[解析中]:" + results[i][0].transcript;
                    flag_speech = 1;
                };
            };
        };
        flag_speech = 0;
        document.getElementById('status').innerHTML = "[ 起動中 ]";
        rec.start();
    };

    window.onload = function() {
        setQuery();
        vr_function();
    };
</script> 
</body>
</html>


なお、Web Speech APIの途切れない利用法については、以下を参考にいたしました。
http://jellyware.jp/kurage/iot/webspeechapi.html


そして以下がESP8266のソース:

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#include "FS.h"

#define ledPin 13
#define relayPin 2

MDNSResponder mdns;

const char* ssid = "*****";
const char* password = "*****";
String webPage = "";
ESP8266WebServer server(80);

void setup(void){
  SPIFFS.begin();
  File f = SPIFFS.open("/index.html", "r");
  if (!f) {
    Serial.println("file open failed");
  }else{
    webPage = f.readString();
    f.close();
  }

  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  
  Serial.begin(115200); 
  delay(500);
  WiFi.begin(ssid, password);
  Serial.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(5000);
    Serial.print(".");
  }
  WiFi.config(IPAddress(192,168,3,12),IPAddress(192,168,3,1),IPAddress(255,255,255,0));

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
  if (mdns.begin("studio", WiFi.localIP())) {
    Serial.println("MDNS responder started");
  }
    
  server.on("/", [](){
    server.send(200, "text/html", webPage);
  });
  server.on("/index.html", [](){
    server.send(200, "text/html", webPage);
  });
  server.on("/fluo_on.html", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, HIGH);
    digitalWrite(relayPin, HIGH);
    delay(1000);
  });
  server.on("/fluo_off.html", [](){
    server.send(200, "text/html", webPage);
    digitalWrite(ledPin, LOW);
    digitalWrite(relayPin, LOW);
    delay(1000); 
  });

  server.begin();
  Serial.println("HTTP server started"); 
}
 
void loop(void){
  server.handleClient();
}

これは、一台のESP8266のソースで、このほかIPアドレスの異なる(対応するスイッチも異なる)ESP8266へは、多少内容を変更して書き直します。
基本的には、

server.on("/air_on.html,[](){...}

などと対応するスイッチに応じて変更するだけです。ほぼサンプルのままです。


SPIFFSによる外部ファイル(HTMLファイル)の読み込み:
まず、こちらのサイトなどを参考にToolをダウンロードしてインストールしておきます。
Macであれば、アプリケーション>Arduinoを右クリックで「パッケージの内容を表示」。
Contents>Java>tools>ESP8266>tool>esp8266fs.jar
の位置にダウンロード&解凍したesp8266fs.jarを入れておき、Arduino IDEを再起動でOK。

用意したindex.htmlを「ファイルを追加...」でスケッチ内に取り入れます。
ファイル選択画面でデスクトップなどにあるindex.htmlファイルを選択。
取り込んだら、以下で確認します。

そうすると、
このようにメインのソースとなるinoファイルと同じディレクトリにdataフォルダが出来上がり、その中にindex.htmlが取り込まれているのがわかります。
つぎに、


「ESP8266 Sketch Data Upload」を使ってindex.htmlをESP8266のメモリ内にアップロードします。2~3%ずつゆっくりとアップロードしていくので、かなり時間がかかります(3~5分)。
アップロード後、従来通りinoファイルをESP8266にアップロードして終了。

プログラム内でのindex.htmlの読み込み:
index.htmlの内容を読み込むために、setup()内で、

 SPIFFS.begin();
  File f = SPIFFS.open("/index.html", "r");
  if (!f) {
    Serial.println("file open failed");
  }else{
    webPage = f.readString();
    f.close();
  }

を使って読み込んでいます。f.readString()でString変数に代入して終了。
同様のやり方で複数のファイルも読み込むことができるらしいです。
しかし疑問に思ったのは、こうやって別々にアップロードされた外部ファイルは、アップロードするたびに上書きされてしまうのか?それとも残ったままになっているの?この辺はよくわからない。たぶん、上書き保存だと思うのだけれども。

追記:
外部ファイルのアップロード確認については、こちらに書いておきました。
続きの改良案はこちら

2017年4月13日木曜日

AliExpressのトラブル(Open Dispute)

いつも使っているAliExpressですが、今回初めてOpen Disputeをしてみました。Open Disputeというのは、辞書では「紛争」とでてきますが、いわゆるクレームのようなものです。
AliExpressでオーダー履歴を見てみると、今までで約100回ほど買い物をしています。といっても、一回の注文が数百円程度の品物が多いので合計しても大した金額にはなりません。安いものだと60円(送料無料)というものもあり、日本で言えばハガキを送る料金でちょっとした電子パーツなどが手に入るというのは、いまだに不思議です。

まずは注文:
今回始めてクレームをつけることになったのは、注文した品物が期限をすぎても届かなかったということからです。以下に内容を書いておきます。


2017/01/30に上の商品を注文しました。当時はレートの関係で310円、配送方法:普通郵便小包。いままでの経験だと、届くまでには2〜3週間かかるので気長に待ちます。

異常発覚:
その後そろそろ届くかなと、配送状況をチェックしてみると、以下のような表示。
なぜかキャンセル扱いになっています。こんなのは初めてです。もともとChina Post Ordinary Small Packet Plus(普通郵便小包)という一番安い配送方法なので、時間がかかったり、原則的にトラッキング不可です。届いたとしても、受け取り確認なしにポストに投函されるだけという、一番雑な扱いです。
ちなみにもう少しましな配送方法だと、Air MailやRegistered Mail(書き留め扱い)というのがあり、いちおう日本からもトラッキング可能です。受け取る際にもサイン(ハンコ)が必要です。

問い合わせメール:
キャンセルされていたので、早速ショップに「キャンセルしてないのに、なんで、こんなことになってんの?」というような内容のメールを送ってみました。
24時間以内に何らかの返答があるというのが通例です。しかし、いつまでたっても返事がきません。この段階で注文から23日たっています。同時期に頼んだ他の品物は続々と届いています。
まあ、310円だし、しばらく放っておくことに。

通常は注文した際に期限が設定されます。今回の場合は約2ヶ月。その2ヶ月をすぎても届かなければクレーム(Open Dispute)できるということです。逆に言えば、すぐに手に入れたいものであっても、最低2ヶ月待たないとクレームすらつけられないということで、かなり気長に構えていなければいけません。

Open Dispute:
 そしてついに期限を向かえてしまいました。オーダー履歴をチェックすると、この取引自体期限切れになっていて、受け取り待ちリストから外されていました。オーダー全体履歴のほうから、この取引を探し出して、「Open Dispute」ボタンを押します(まだ期限以内なら、このボタンを押しても無効となるようです)。そうするとOpen Disputeの画面を移行します。

まず、Disputeする選択方法があります。なぜキャンセル扱いになったのか理由はわからないので、「Package sent to a wrong address(違うところへ配送)」というのにしました(適当)。
そして、どうしたいかという希望欄があり、今回の場合は、品物を受け取っていないので、「返金(Refund)」ということになり、金額記入欄もあり、310円。

そして、文章による内容記入欄もあります。そこに、以前のメールと似たような内容のことを書いておきました。

「品物をまだ受け取っていない。以下のようなキャンセルのトラッキングになっている。なぜそうなったのかは知らないし、22日にショップに聞いてみたけど返答なし。」というような内容です。
Evidence(証拠)欄もありますが、今回の場合は特になし。もし不良品や配送途中における破損の場合は写真など送ることになると思います。

手続き終了(返金確定):
そしてOpen Dispute後、約3日で以下のようにショップが返金要求を受け入れて無事終了。


My AliExpress>Open Dispute>Dispute Detailに、「Dispute Finished、310円返金」という結果が出ています。
しかし、返金というけど、どうやって返金されるのか?と疑問に思って調べてみると、サイトの一番下のほうにHelpがあり、Return&Refundsを開くと書いてありました。

買い物の流れ:
・品物を注文する(クレジットカード支払い)
・クレジットカード支払い成立(AliExpressへ、ただしショップへはまだ支払われない)
・数週間後、品物が届いて受け取り確認をクリック
・AliExpressからショップへ品物代が支払われる
・取引終了

一旦AliExpressが支払い金額を預かって、その後、手数料など差し引いてショップに支払われるシステムになっているのだと思います(この時間差の分だけAliExpressに集められたお金で資産運用しているのかもしれません)。

ということから、Disputeすると:
・期限までに買い手からの品物受け取り確認がないので、AliExpressはショップに支払わない
・ショップもAliExpressからの支払いがないことを認める
・AliExpressが買い手へクレジットカード会社を通して返金する。
・クレジットカード会社がその返金を買い手の銀行口座へ送金する
という感じでしょうか。

今回はたった310円なので大したこともないのですが、たしかに数万円もする品物の場合だと、きちんと返金されるまでは不安かもしれません。
おそらく、AliExpressの多くのショップはドロップシッピング(無在庫転売)を行っているので、たまに手続き的なミスが発生してしまうのでしょう。しかし、ここまで成長したAliExpressなので、システム自体はけっこうしっかりしていると思います。手続きさえすれば特に問題ないと思います。ただし、手間暇や時間はかかると思います。
今回の場合は、注文してから2ヶ月、Disputeしてから3日で手続きは終了しましたが、さらに銀行へ返金されるまで数週間というところでしょうか。

この売買システムというよりは、品物が粗悪品であったり、スペックが表示内容と微妙に異なるということはいまだによくあります。注文するときは他のショップなどの表示内容と比較しつつ最終的な注文を確定したほうがいいと思います。

2017年4月6日木曜日

音声認識:Voice Recognition Module VR3.1

ここしばらく音声認識が気になっていたので、引き続きいろいろ調べています。
AliExpressで見つけたArduino対応の音声認識モジュールがようやく届いたので試してみることに。

AliExpress.com Product - Free Shipping 5PCS APW7142KI-TRG APW7142KI APW7142 SOP8 in stock new and Original IC 2193円(送料無料)。
安くはないのですが、とんでもなく高いというわけでもないので、試しに購入してみました。これ以外にもいくつか音声認識モジュールはあるのですが、比較的小型(30x48mm)で、そこそこ認識力もありそうだったので。

どうやらELEHOUSEというところがメーカー/販売店らしく、このページから、マニュアルArduino用のライブラリがダウンロードできます。バージョン1や2もあるようで、今回購入したものはバージョン3.1でした。
似たようなモジュールとしてGreentechのVoice Recognition Moduleというのがあります。これはELEHOUSEのバージョン2に相当するモジュールで音声は3グループx5種類、合計15種類まで。基板の色が違うくらいで、構成や価格はほぼ同じです。

5Vでシリアル通信するので、Arduino Unoにはいいかもしれませんが、ESP8266などのような3.3Vのデバイスには、分圧などして電圧を下げないといけなさそうです。
言葉は80個(1.5秒/個)まで記憶できます。ただし一回でロードできるのはそのうち7個までのようですが、応用的な使い方として7個以上を使い分ける方法もあるようです。

グループ1:
0:グループ切り替えコマンド
1:音声1
2:音声2
3:音声3
4:音声4
5:音声5
6:音声6

グループ2:
0:グループ切り替えコマンド
1:音声1
2:音声2
3:音声3
4:音声4
5:音声5
6:音声6

という感じで、それぞれのグループの0番目をグループ切り替えコマンドに割り当てておけば、合計12個使い分けることもできると言う感じです。
グループ1をグループ2〜8へ移行するための分岐点にし、グループ2〜8の音声0をグループ1に戻るコマンドにして、音声1〜6までを通常コマンドに割り当てれば少なくとも42種類使えることになります。さらにツリー状に分岐させていけば80個まで使えるかもしれませんが、そこまで複雑な構造にしてしまうと途中で迷子になってしまいそうです。

ということでライブラリをArduino IDEにインストール。スケッチ>ライブラリをインクルード>ZIP形式のライブラリをインストール...でダウンロードしたzipファイル(解凍せず)を選択。

そして、スケッチ例からvr_sample_trainを選んでArduino Unoにアップロード。このプログラムでまずは音声を登録するようです。5Vなので、今回はArduino Unoで試しています。

配線はVCC、GND、TX、RXの4本だけです。Arduino UnoのほうはSoftwareSerialで通信するようで、サンプルにはTX、RX用にD2、D3のピンを使用することになっていますが、それ以外のピンでも大丈夫そうです。ESP8266の時は、使えるピンに変更したほうがよさそうです。

vr_sample_trainをアップロードしたら、シリアルモニタから音声登録ができます。
まず「settings」を入力すると登録モードに突入します。
sigtrain 0 switch
と入力する。この場合0番目に音声登録、「switch」は任意の名前。
そうすると、すぐに「Speak now」と表示が出るので、そこでマイクに向かって音声録音します。
さらに「Speak again」でもう一回録音し、「Success」が出て以下のようになれば登録完了です。

一個1.5秒しかないので、短い言葉しか入れることができません。例えば、「オン」と「オフ」を録音する場合、「オ」の後にくる「ン」や「フ」が曖昧だと、どちらも「オン」に識別されてしまうこともありました。だめなら、また入力しなおせばいいのですが、基本的にはハッキリしゃべったほうがいいのかもしれません。それから、似ている言葉はあまりつかわないほうがいいのかもしれません。
とはいうものの認識力はそこそこある感じです。

実際録音した音声を確認するには、
load 0 1 2 3 4
などと「load」のあとに録音した番号を連ねて入力します(最大7個まで)。
あとは、マイクに向かってそれぞれの言葉をしゃべると、認識されれば対応した番号が出てきます。
サンプルにあるLEDのオンオフ実験もやってみましたが、大体反応しました。「オン」はすぐに反応しますが、「オフ」は「フ」の部分をハッキリと発音しないと反応しないときもありました。
マイクとの距離は30cmほどで録音し1m離れたところからしゃべっても反応ありました。2m離れると反応しないので、マイクの感度をあげるか、離れたところから音声登録(録音)するといいのかもしれません。
とはいえ、このモジュールは比較的簡単に設定もできるし、Arduinoライブラリなどもあり使いやすいと思います。スタンドアロンのデバイスをすぐにつくることができると思います。


今回のモジュール以外では、Arduino用にuSpeechという音声認識ライブラリがあり、それも試してみましたが、上手くいかないので諦めてしまいました。
そのほか、Snowboyというオープンソースの音声認識システムも試しているところです。


ポイントとしては、長時間無音状態でも待機できるというのが条件です。音声認識させるために、画面をタップしたり、ボタンを押して音声入力するということをせず、気が向いたときに話しかけることで起動するという状態が理想的であり現実的に使える状況だと思います。このSnowboyは、長時間待機でき、インターネット接続なしでも使えるようです。このプログラム(Python)をRaspberry PiなどでRunさせておけばいいのかもしれません。
Amazon Echo/Alexaもgitにありますが、まだ日本語には対応していないようなので保留中です。

まだどの音声認識がいいかいろいろ試している段階です。以前試したWeb Speech APIが今の所個人的には一番便利そうです。もともとデバイス単体で使うというより、ネットワークに複数のデバイスをつなげて信号を送受信させようと思っているので、インターネットに常時接続していなければいけないということは、今の所問題ではないという感じです。Web Speech APIの場合は、Speech to TextとText to Speechなので、いろいろ応用できそうです。

Google Speech API(月間60分まで無料、61分〜100万分まで$0.006/15秒)
Amazon Echo Alexa(Git hub)
Web Speech API(MDN)
Watson Speech to Text(月間1000分まで無料、以降$0.02/分)
Julius
Python Speech Recognition3.6.3(python2.6以上)
Snowboy

Matrix Voice(WIFI/BT/MCU:$65)
EasyVR(4860円)
Voice Recognition V3(2193円)
Speech Recognition LD3320(1606円)

関連:
音声認識Wifiスイッチ/ESP8266使用(まとめ)についてはこちら

人気の投稿