これまでのあらすじ:
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年3月27日月曜日

IoTその5:音声認識でESP8266をWifi制御

いくつかのスイッチにIPアドレスを与えてスマホやパソコンのブラウザからオンオフできるようにはしてみましたが、次はそれを音声(アマゾンエコーのように?)でオンオフできないかとネットを探していました。
Google Cloud Speech APIがかなりすごいので、そのまま使えないかと思いましたが、月間60分までは無料、それ以上は15秒ごとに0.006ドルらしい(しかも月間100万分までの制限つき)。もし、ずっとつなぎっぱなしなら、1分で0.024ドル、1時間で1.44ドル、1日で34.56ドル、1ヶ月で1036.8ドルということになってしまいます。ということで、他の方法で。

Siri+Raspberry Pi+Arduinoでやっている例もありましたが、今回は単純に:
Web Speech API2012年のはここ
・ESP8266
でやってみました。

ネット上にあるWeb Speech APIのサンプルを試してみたりしましたが、まあまあの認識力があり、それなりには使えそうです。基本的にはChromeかFireFoxを使用しなければいけないようですが、MacBookからのChrome、AndroidからのChromeでも認識できました(FireFoxはまだ合成音声/発話のみらしいです)。

*デモページ(このブログ内)はこちらへ(日本語仕様)




自動タイムアウトという問題点:
Web Speech APIは、無言のままでいると約5〜6秒でタイムアウトしてしまい、その後はいくら話しかけても反応しなくなります。
大抵のサンプルは、ボタンをクリックし、タイムアウトする前に話しかけるという手順になっています。しかし、音声認識させるためにボタン操作するのであれば、電源用ボタンを画面上につくっておいて、それをクリックしたほうが早いということになってしまい二度手間です。
理想的には、音声が聞こえるまで長時間待機していられればいいのですが、この自動的にタイムアウトしてしまう機能がなんとかならないかと検索してみると

recognition.onend = function(event) {
    recognition.start();
}

このように認識コマンドが終了したら、またスタートさせる(ある意味無限ループ)と可能と書いてありました。プログラム的にはあまりよくなさそうですが、たしかに、これを試してみると、10秒経っても、あるいは数分経っても入力待機しており、その後話しかけても反応しました。
これとは別に、アマゾンエコーのようにずっと待機させておくには、同時にブラウザでそのアドレスのサイトをずっと立ち上げておかなければいけないという問題もあります。

バックグラウントでも動く:
MacBookでプログラムを書いたページ(Chrome上で)をバックグラウンドで立ち上げていても反応するので(その分メモリや電力は消費され続けるけれども)、気が向いたときに話しかけても機能しました。


例えば上画像のように、このブログを書いている最中に別のタブにサイトを立ち上げておいても機能し続けます。Speech Recognitionタブの赤丸が消えると、音声認識は停止してしまいますが、ずっとつきっぱなしです。画面を最小化して隠してしまい、他のアプリケーションを立ち上げて作業していても機能しました。
ということで、実際使うかどうかは後回しにして、できるところまでやってみることに。APIの説明を見ると、文法の解析だったりとかけっこう複雑なこともできるのかもしれませんが、とりあえずこちらが用意したコマンド(言葉)を認識したら、オンオフするという程度のものをつくろうと思います。

手順としては:
ESP8266に、Web Speech API+Javascript+HTMLを書き込んで、パソコンやスマホからESP8266のIPアドレスにアクセスして、音声認識でスイッチをオンオフするという感じです。
今回は、照明器具の電源のオンオフとエアコンのオンオフ(赤外線通信)をしようと思います。
以下がESP8266に書き込んだプログラムです(その後改良したので、まだ挙動が変かも)。

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

#define relayPin 5
#define IRPin  4
#define ledPin  15

#define duty_high 8
#define duty_low 16

MDNSResponder mdns;
const char* ssid        = "*****";
const char* password    = "*****";
ESP8266WebServer server(80);

unsigned long data_on[243] = {3400, 1400, 600, 250, 550, 250, 550, 1050, 550, 250, 550, 
1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550, 1050, 550, 250, 550, 300, 500, 250, 
550, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 300, 500, 300, 500, 
350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 
450, 350, 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 
350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 1150, 450, 1150, 450, 1150, 450, 
1150, 450, 1200, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 
450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 1200, 450, 350, 450,
350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 
450, 350, 450, 400, 400, 1200, 400, 400, 450, 350, 450, 350, 450, 350, 450, 350, 450, 
350, 450, 350, 450, 350, 450, 350, 450, 400, 400, 400, 400, 400, 400, 400, 400, 400, 
400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 350, 450, 350, 450, 
350, 450, 350, 450, 350, 450, 350, 450, 400, 400, 400, 400, 400, 400, 400, 400, 400, 
400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 1150, 450, 1150, 450, 
350, 450, 1200, 400, 400, 400, 400, 400, 1200, 400, 400, 400};

unsigned long data_off[99] = {3400, 1450, 550, 250, 550, 250, 500, 1100, 550, 250, 550, 
1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550, 1050, 550, 300, 500, 250, 550, 300, 
500, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 350, 450, 300, 500, 
350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 
450, 350, 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 
350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 
450, 350, 450};

String webPage="";

void handleRoot(){
  Serial.println("Access");
  contents();
  server.send(200, "text/html", webPage);
}

void lamp_on(){
  digitalWrite(relayPin,HIGH);
  contents();
  server.send(200, "text/html",webPage);
}

void lamp_off(){
  digitalWrite(relayPin,LOW);
  contents();
  server.send(200, "text/html",webPage);
}

void air_on() {
  int dataSize = sizeof(data_on) / sizeof(data_on[0]);
  for (int i = 0; i < dataSize; i++) {
    unsigned long duration = data_on[i];
    unsigned long start_time = micros();
    while (start_time + duration > micros()){
      digitalWrite(IRPin, 1-(i&1));
      delayMicroseconds(duty_high);
      digitalWrite(IRPin, 0);
      delayMicroseconds(duty_low);
    }
  }
  contents();
  server.send(200, "text/html",webPage);
}

void air_off() {
  int dataSize = sizeof(data_off) / sizeof(data_off[0]);
  for (int i = 0; i < dataSize; i++) {
    unsigned long duration = data_off[i];
    unsigned long start_time = micros();
    while (start_time + duration > micros()){
      digitalWrite(IRPin, 1-(i&1));
      delayMicroseconds(duty_high);
      digitalWrite(IRPin, 0);
      delayMicroseconds(duty_low);
    }
  }
  contents();
  server.send(200, "text/html",webPage);
}

void setup() {
  webPage="";  
  pinMode(IRPin, OUTPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  digitalWrite(relayPin, LOW);

  Serial.begin(115200);
  WiFi.begin(ssid, password);
  WiFi.mode(WIFI_STA);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  WiFi.config(IPAddress(192,168,3,10),IPAddress(),IPAddress());
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  if (MDNS.begin("mirror")) {
    Serial.println("MDNS responder started");
  }
  server.on("/", handleRoot);
  server.on("/index.html", handleRoot);
  server.on("/lamp_on", lamp_on);
  server.on("/lamp_off", lamp_off);
  server.on("/air_on", air_on);
  server.on("/air_off", air_off);
  server.begin();
  MDNS.addService("http", "tcp", 80);    
}

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

void contents(){
webPage="<!DOCTYPE html><html><head><meta charset='UTF-8'/>\
<title> Speech Recognition</title><script type='text/javascript'>\
var rec=new webkitSpeechRecognition();\
rec.continuous = true;\
rec.interimResults = false;\
rec.lang = 'ja-JP';\
rec.start();\
var apiSpeech=new SpeechSynthesisUtterance();\
apiSpeech.lang = 'ja-JP';\
apiSpeech.rate=1.2;\
apiSpeech.text='こんにちは';\
var mode=0;\
rec.onresult = function (e) {\
for (var i = e.resultIndex; i < e.results.length; ++i) {\
if (e.results[i].isFinal) {\
var youSaid= e.results[i][0].transcript;\
var apiSaid='';\
var apiHtml = document.getElementById('api');\
var youHtml = document.getElementById('you');\
if(mode==0){\
if(youSaid=='こんにちは'){mode=1;\
apiSaid=youSaid+'、どうぞ';\
}else{apiSaid=youSaid+'?';}}else{\
if(youSaid=='ライトスイッチオン'){mode=0;\
apiSaid='OK、'+youSaid;\
window.location.href = 'lamp_on';\
}else if(youSaid=='ライトスイッチオフ'){mode=0;\
apiSaid='OK、'+youSaid;\
window.location.href = 'lamp_off';\
}else if(youSaid=='エアコンスイッチオン'){mode=0;\
apiSaid='OK、'+youSaid;\
window.location.href = 'air_on';\
}else if(youSaid=='エアコンスイッチオフ'){mode=0;\
apiSaid='OK、'+youSaid;\
window.location.href = 'air_off';\
}else{apiSaid='もう一度';}}\
apiSpeech.text=apiSaid;\
speechSynthesis.speak(apiSpeech);\
apiHtml.innerHTML ='API:  '+apiSaid;\
youHtml.innerHTML='あなた:  '+youSaid;}}};\
var count = 0;\
var countup = function(){console.log(count++);};\
apiSpeech.onstart=function(){rec.stop();};\
apiSpeech.onend=function(){\
setTimeout(countup, 3000);\
rec.start();};\
rec.onend = function(){\
setTimeout(countup, 5000);\
rec.start();}\
</script>\
</head><body>\
<div style='font-size:30px; text-align:center;'>\
<p> Speech Recognition</p>\
<p id='api'>「こんにちは」で開始</p>\
<p id='you'>.....</p>\
</div><div style='font-size:18px; text-align:center;'>\
<p>MENU</p>\
<p>「こんにちは」:操作開始</p>\
<p>「ライトスイッチオン」</p>\
<p>「ライトスイッチオフ」</p>\
<p>「エアコンスイッチオン」</p>\
<p>「エアコンスイッチオフ」</p>\
</div></body></html>";}

最初はHTMLやJavascriptを外部読み込みさせようと思いましたが、読み込みの際にエラーが起きてしまったので、そのまま全部一つのプログラムに書き込んでしまいました。

HTML、Javascriptの部分でエラー:
後半のJavascriptの部分でもなぜかエラーがでてしまい、試行錯誤していると、どうやらこのHTMLやJavascriptが、改行なしにブラウザに渡されてしまうと、ブラウザ側が読み込めなくなるようでした。
たとえば、

apiSpeech.onstart=function(){rec.stop();};


この一行の最後の部分↑ですが、通常は波括弧「}」のあとにはセミコロンをつけないで書いていますが、あえて「};」というようにセミコロンを付け足してあります。これがないと、なぜかブラウザ上で上記のブログラムが読み込めなくなってしまって機能しませんでした。どうやらプログラム上の分節が上手くできないようで、セミコロンを何箇所かにつけたという感じです。これで一応機能するようになりました。

いろいろスペースあけたり改行したりしなかったり、エスケープシーケンス記号をつかったり試していたので、Javascriptの部分は読みにくくなっています。
以下にさらに改良した内容としてJavascriptの部分だけ書いておきます。

基本的には、最初に「こんにちは」というと、「こんにちは、操作メニューをどうぞ」と言われて操作メニューモードに入るという感じです。
そして、「エアコンスイッチオン」などというと、「OK、エアコンスイッチオン」と返答して、赤外線信号のページへ移行し信号を発するという仕組みです(まだ4つのメニューしかありません)。
認識されない場合は、「にちわ?」などと認識した言葉に?マークをつけて返事してきたり、「もう一度、操作メニューを」などと言ってきます。以下だけのHTML+Javascriptだけでも、音声認識だけなら確かめることができると思います。

追記/正規表現:
その後、音声入力の部分で正規表現を使えばいいということが分かったので、さらに改良中(というか、正規表現も勉強中)。いまのところ(以下のコードも)、こちらが求めている言葉通りでなければ反応しない仕様になっており、そのキーワードの前後に余計な言葉があっても反応しないということになってしまいます。正規表現を使えば、キーワードが含まれていればOKにできたり(あるいは除外することもでき)、余計な言葉(助詞なども)が前後に入っていても大丈夫のようです。
例えば、「ライトスイッチオン」や「エアコンスイッチオン」の場合なら、「ライト」「エアコン」「オン」「オフ」が重要ワードとなり、「オフ」や「(オンに)しない」などという言葉が入っていれば、反応しないようにもできます。「スイッチ」は共通なので、今回の場合なくてもいいかもしれません。

youSaid.match(/^(?!.*(オフ|ない)).*(?=ライト).*(?=(オン|on)).*$/)

たぶん、「ライト・オン」の場合はこんな感じでしょうか?
まだ、この条件に合わせた正規表現については確実ではないのですが、以前よりは確かに受け入れる言葉の幅が広がり、いままでは、「エアコンスイッチオン」と言わないとダメでしたが、「エアコンのスイッチをオンにして下さい」でも反応するようになり、「エアコンをオンにしないで」と言えば、重要ワード「エアコン」と「オン」が含まれていても「〜ない」が含まれているので、ダメという判定を出せるようになりました(まだ改良中なので、このサンプルはなしです)。


*これまでのデモページ(このブログ内)であれば、こちらへ(ブラウザはChromeで)

<!DOCTYPE html><html>
<head><meta charset="UTF-8"/>
<title> Speech Recognition</title>
<script type="text/javascript">
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition
var rec=new SpeechRecognition();
rec.continuous = true;
rec.interimResults = false;
rec.lang = "ja-JP";
//'en-US','en-GB','de-DE','fr-FR','it-IT','cmn-Hans-CN','ko-KR' 
rec.start();

var apiSpeech=new SpeechSynthesisUtterance();
apiSpeech.lang = "ja-JP";
apiSpeech.rate=1.2;
apiSpeech.volume=0.8;
apiSpeech.text="こんにちは";

var mode=0;
var count = 0;
var countup = function(){
  console.log(count++);
}

rec.onresult = function (e) {
  var youSaid= e.results[0][0].transcript;
  var apiSaid="";
  var apiHtml = document.getElementById("api");
  var youHtml = document.getElementById("you");
  if(mode==0){
    if(youSaid=="こんにちは"){
      mode=1;
      apiSaid=youSaid+"、操作メニューをどうぞ";
    }else{
      apiSaid=youSaid+"?";
    }
  }else{
    mode=0;
    if(youSaid=="ライトスイッチオン"){
      apiSaid="OK、"+youSaid;
      window.location.href = "lamp_on";
    }else if(youSaid=="ライトスイッチオフ"){
      apiSaid="OK、"+youSaid;
      window.location.href = "laml_off";
    }else if(youSaid=="エアコンスイッチオン"){
      apiSaid="OK、"+youSaid;
      window.location.href = "air_on";
    }else if(youSaid=="エアコンスイッチオフ"){
      apiSaid="OK、"+youSaid;
      window.location.href = "air_off";
    }else if(youSaid=="こんにちは"){
      apiSaid="操作メニューをどうぞ";
      mode=1;
    }else{
      apiSaid="もう一度、操作メニューを";
      mode=1;
    }
  }
  apiSpeech.text=apiSaid;
  speechSynthesis.speak(apiSpeech);
  apiHtml.innerHTML ="API:  "+apiSaid;
  youHtml.innerHTML="あなた:  "+youSaid;
  setTimeout(countup, 2000);
  count=0;
}

apiSpeech.onstart=function(){
  rec.stop();
}
apiSpeech.onend=function(){
  setTimeout(countup, 3000);
  count=0;
  rec.start();
}
rec.onend = function(){
  setTimeout(countup, 6000);
  count=0;
  rec.start();
}
</script>
</head><body>
<div style="font-size:30px; text-align:center;">
<p> Speech Recognition</p>
<p id="api">「こんにちは」で開始して下さい</p>
<p id="you">ここに認識された言葉が出ます</p>
</div>
<div style="font-size:18px; text-align:center;">
<br/>
<p>操作メニュー:</p>
<p>「ライトスイッチオン」</p>
<p>「ライトスイッチオフ」</p>
<p>「エアコンスイッチオン」</p>
<p>「エアコンスイッチオフ」</p>
</div>
</body></html>

実は、先ほどの無限ループのイベントハンドラが上手くいかず、たまにAPI自身がしゃべったことに反応して、さらに音声入力されてしまうということがあります。多少ディレイをつけてみたのですが、たまに変になります(まだ慣れていないJavascriptの問題かもしれません)。
しかし認識力は高く、ほぼ一発で反応してくれます。感度のいいマイクを用意すれば、数m離れた場所から話しかけても大丈夫かもしれません。期待以上に簡単にできたので、さらに完成度をあげて他のことにも応用できるようになればいいと思います。

これを生活のなかで実用化するには、Raspberry Piで常時このサイトを立ち上げておき、感度のいいマイクをつけておけばいいのかもしれません。Raspberry Pi用のマイクがないので、まだ確かめてはいませんが、Raspberry Piをステーションにして、複数のESP8266がぶらさがっているような仕組みにしてもいいのかもしれません。Raspberry PiならJuliusという音声認識もあるようなので、Raspberry Pi用のUSBマイクが手に入ったら試してみようと思います。
AliExpress.com Product - 2015 New Mini USB 2.0 Microphone MIC Audio Adapter Driver Free For MSN PC Notebook
157円(送料無料)。とりあえずこんなものでもいいのでUSBマイクを買っておこうかと。
AliExpress.com Product - 7.1 Channel 3D External USB Audio Sound Card Mic Adapter 3.5mm Jack Stereo Headset For Win XP / 7 8 Android Linux for Mac OS
133円(送料無料)。あるいは、マイクを差し込めるソケットつきの音声入力カードなら、こんな感じ。なぜか安い。

AliExpress.com Product - Free Shipping 5PCS APW7142KI-TRG APW7142KI APW7142 SOP8 in stock new and Original IC 2186円(送料無料)。Arduinoなどに直接音声認識させるならこのような音声認識モジュールがいいかと。マニュアルはこちら。実際、使用してみたときの内容はこちら


AliExpress.com Product - LD3320 ASR Voice Recognition Professional SP Voice Recognition Voice Module
1680円(送料無料)。こちらは少し安いモジュールですが、ネットを検索しても使い方が見つけられず。マニュアルは一応ここのはずなんだけど、なぜかない。基板は違うけどこれもLD3320

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

0 件のコメント:

コメントを投稿