ArduinoでFFTを使う

マイクから拾った音の音階を調べるプログラムのテストです。Arduinoだけを使ってFFTする方法を調べてみたのですが、情報が古く、日本語では欲しい情報になかなかたどり着けなかったので上手く行ったやり方を紹介します。

FFTとは

一定期間の音信号を解析して、どの周波数成分がどの程度含まれているか求める手法です。信号の数を2のべき乗にすることによって高速に計算することが出来ます。

あんま上手く説明できません。とりあえず、鳴っている音の周波数が何なのかわかる手法です。

使うもの

  • ESP8266(手持ちのArduino nanoでもやってみましたが、メモリが足りずスケッチが書き込めませんでした。他のArduinoは要検証です。)。以下の2個セットのやつが値段的にも最安でWiFiも使えてメモリも多いのでよく買ってます。
  • マイクモジュール

やり方

ここのやり方に沿って進めていくだけですが、かいつまんで解説していきます。
https://www.norwegiancreations.com/2017/08/what-is-fft-and-how-can-you-implement-it-on-an-arduino/

ライブラリをインストール

Arduino IDEを開き、スケッチ→ライブラリをインクルード→ライブラリを管理…の順にクリックします。

スケッチ→ライブラリをインクルード→ライブラリを管理… の順にクリック

検索バーに「fft」を入力し、「arduinoFFT」をインストールします。

arduinoFFTをインストール

これだけで完了です。

プログラム書き込み

下にサンプルプログラムを置いておきます。上記サイトに掲載されているプログラムほぼそのままです。一部ESP8266向けに変更しています。オリジナルよりもサンプル数とサンプリング周期を上げています。

FFTの特徴として、サンプリング周波数の半分の周波数までしか解析することが出来ません。サンプリング周波数が8000Hzなら、4000Hz以上の音は検出できません。

また、サンプル数を多くすると、結果の分解能が上がる代わりに測定にかかる時間が増えます。サンプル数は2のn乗にしなければならないと決まっていますので、欲しい分解能に応じて決める必要があります。

分解能はサンプリング周波数[Hz]÷サンプル数になります。つまり下のサンプルプログラムでは8000÷256=32[Hz]刻みの結果しか得られないことになります。判別する周波数の精度を上げたい場合にはサンプル数(=サンプリング時間)をあげる必要があります。

下記のプログラムでは最も成分の多い周波数(ピーク周波数)を出力するようにしています。

#include "arduinoFFT.h"
 
#define SAMPLES 256             //Must be a power of 2
#define SAMPLING_FREQUENCY 8000 //Hz, must be less than 10000 due to ADC
 
arduinoFFT FFT = arduinoFFT();
 
unsigned int sampling_period_us;
unsigned long microseconds;
 
double vReal[SAMPLES];
double vImag[SAMPLES];
 
void setup() {
    Serial.begin(115200);
 
    sampling_period_us = round(1000000*(1.0/SAMPLING_FREQUENCY));
}
 
void loop() {
   
    /*SAMPLING*/
    for(int i=0; i<SAMPLES; i++)
    {
        microseconds = micros();    //Overflows after around 70 minutes!
     
        vReal[i] = analogRead(A0);  //ESP8266の場合は「A0」。普通のArduinoは「0」。
        vImag[i] = 0;
     
        while(micros() < (microseconds + sampling_period_us)){
        }
    }
 
    /*FFT*/
    FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
    FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
    double peak = FFT.MajorPeak(vReal, SAMPLES, SAMPLING_FREQUENCY);
 
    /*PRINT RESULTS*/
    Serial.println(peak);     //Print out what frequency is the most dominant.
 
    for(int i=2; i<(SAMPLES/2); i++)
    {
        /*View all these three lines in serial terminal to see which frequencies has which amplitudes*/
         
        //Serial.print((i * 1.0 * SAMPLING_FREQUENCY) / SAMPLES, 1);
        //Serial.print(" ");
        //Serial.println(vReal[i], 1);    //View only this line in serial plotter to visualize the bins
    }
 
    delay(1);  //Repeat the process every second OR:
    //while(1);       //ESPだと無限ループがエラーになるので注意
    
}

Arduinoとマイクを接続

わざわざ書くまでもありませんが、Arduinoとマイクを接続します。3.3VとGNDとA0をマイクモジュールにつなぐだけです。

マイクモジュールを使えば簡単!

テスト

プログラムを書き込んだらシリアルプロッタを開いて口笛を吹いてみます。

口笛が下手

口笛の音階があっているかはさておき、音の高さを判別することができました!

追記:電源を安定化すれば精度が上がる

上記の方法で問題なくFFTはできるのですが、たまに誤差が多い結果が出ることもありました。安いマイクなのでしょうがないのかな?と思っていましたが、ふと思いついて電源を見直したところ精度がめっちゃ向上したので報告します。

どうやらArduinoの3.3Vピンから電源を取るとノイズが多いようで、マイクの出力がノイズを含んでいました。

マイクの電源を別に用意したところ、ノイズがなくなりFFTの出力もキレイになりました。

マイク用電源を別に追加

電池を別に用意するのが面倒な場合は、電源安定回路やレギュレータを使うのもありです。esp8266のVinピンから取っても大丈夫でした。(VinはUSBの5Vと繋がっている)

今回は以上です。

【キルミーベイベー】あぎりさんの部屋にある装置をつくってみた

はじめに

キルミーベイベー第6話にて…

やすな「からくりっぽいのとかは無いんですか?」
あぎりさん「ありますよ~」
「この置物を動かすと~
置物(ヤッベー)
「クーラーがつきます♪」

あぎりさんの部屋にあった置物のイメージ

何この装置…欲しい…
ということで今回はこれを作っていこうと思います。

基本構想

  • エアコンは赤外線を使ってつける
  • 赤外線送信機と置物は分けて作る
  • 赤外線送信機と置物はWiFi通信でつなぐ
  • 置物を動かしたらWiFiで送信機に指令を送る→送信機がエアコンをつける

赤外線送信機と置物は一体で作ることもできますが(そっちのほうが安いし簡単)、どうしても赤外線がエアコンに届くように配置を工夫する必要がありますし、置物の正面に立つと使えなかったりするのでここはWiFiを使っています。
それにLEDむき出しだとなんか凄さが半減して嫌じゃないですか(笑)

作りたい装置のイメージ

必要なもの

  • 適当な置物
  • 回転センサ(10kΩ可変抵抗器、10kでなくても可)
  • 10kΩ抵抗器(アナログ入力が最大1Vの場合のみ必要。可変抵抗の2倍)
  • ESP8266モジュール(NodeMCU ESP8266 Board)×2
  • 赤外線LED
  • トランジスタ(2CS1815)
  • 200Ω抵抗器(あったほうがいいけどなくても可)

まず置物側ですが、置物本体に回転によって抵抗値が変わる可変抵抗器を使いたいと思います。可変抵抗器は別名ボリュームともいい、昔のテレビやラジオのダイヤルやアンプの音量調節など、ダイヤル状のものにはまず使われているであろう基本的な部品です。これを使って置物が一定以上回ったらWiFi信号を送るデバイスを作ろうと思います。
可変抵抗と普通の抵抗は10kΩと書きましたが別に1kΩでもいいです。

次に送信機側ですが、赤外線LEDの他に抵抗とトランジスタを使って赤外線が最大限届くようにします。

必要なものリスト(AMAZON)

使う部品リストを載せておきます。ただ、抵抗やトランジスタはAmazonだとバラ売りされていないので、お近くの電子部品の販売店に行かれる方が絶対良いです。東急ハンズとかでも売ってます。逆にESP8266モジュールはネットで買った方が安くておすすめです。

置物側の設計

置物を回転させたらWiFiを通じてリクエストを送信する装置を作ります。

必要なものは、ESP8266と回転によって抵抗が変化する可変抵抗、確認用のLEDです。

回転を検知する可変抵抗

可変抵抗とESP8266モジュールのつなぎ方

可変抵抗の仕組みは簡単です。両端に電圧をかけると、(抵抗が一様ならば)その間で電圧は一様に降下します。可変抵抗を回転させるとこの抵抗のどこを触るかが変えられるので、電圧をESP8266で読み取るだけで回転量が分かります。

可変抵抗の仕組み

アナログ入力の上限が1VのESP8266を使う場合

僕のESP8266モジュールはアナログ入力に0~3.3Vが使えるのですが、アナログ入力が0~1VのESP8266もあります。そういう場合は次のように20kΩの抵抗を入れることによって出力の範囲を変えることが出来ます。

出力の範囲を変える方法

組み立て

100均の木箱のフタを利用しました
見た目がスッキリするように
適当な置物をくっつけて完成!

Arduinoソースコード(置物側)

回転を検知→HTTP GETリクエストを送信します。

/* 
 *  あぎり「この置物を動かすと~
 *  クーラーが付きます♪
 *  (具体的な動作:可変抵抗の値に応じてHTTPリクエストを送る)
 */
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266HTTPClient.h>
#include <mDNSResolver.h>       //https://github.com/madpilot/mDNSResolver

#define WIFI_SSID "********"
#define WIFI_PWD "****"
#define TARGET_HOSTNAME "ir-sender.local"
String host_ip = "";


// 前回送ったコマンドを記憶
#define STATE_OFF 0
#define STATE_ON 1
int state = STATE_OFF;

// ONまたはOFFのしきい値
// 置物を回したときのセンサ値を見て、いい感じの値に設定
#define ON_THRESH 220
#define OFF_THRESH 240

// 赤外線LEDの+側
#define LED_PIN 14

// 名前解決してくれる(ir-sender.local → 192.168.0.22)
WiFiUDP udp;
mDNSResolver::Resolver resolver(udp);

String ip_to_string(IPAddress ip){
  return 
  String(ip[0]) + String(".") +
  String(ip[1]) + String(".") +
  String(ip[2]) + String(".") +
  String(ip[3]);
}

String getPageSource(String host) {
  HTTPClient http;
  
  http.setTimeout(500);
  http.begin(host);
  
  int httpCode = http.GET();
  
  String result = "";

  if (httpCode < 0) {
    result = http.errorToString(httpCode);
  } else if (http.getSize() < 0) {
    result =  "size is invalid";
  } else {
    result = http.getString();
  }

  http.end();
  return result;
}

void wake_wifi(){
  // 原因未解明!
  // WiFi接続が確認できるまで待つ。これがないとGETが失敗することがある。
  while(WiFi.status() != WL_CONNECTED){
    delay(100);
    Serial.print(".");
  }
}

String get_ip(char* host){
  IPAddress ipaddr = resolver.search(host);
  return ip_to_string(ipaddr);
}

void setup() {
  Serial.begin(115200);
  delay(200);
  
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  
  wake_wifi();
  
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  
  // DNSの名前解決は最初の一回だけ行う
  host_ip = get_ip(TARGET_HOSTNAME);

  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  
  int read_val = analogRead(A0);
  //Serial.print("analog read = ");
  //Serial.println(read_val);

  if(state == STATE_OFF && read_val < ON_THRESH){
    // OFF→ONになったとき、GETリクエストを送る
    digitalWrite(LED_PIN, HIGH);

    wake_wifi();  // WiFiが使えるようになるまで待つ必要あり?未解明
    String get_result = getPageSource(String("http://") + host_ip + String("/on"));
    //Serial.println(get_result);
    
    digitalWrite(LED_PIN, LOW);

    state = STATE_ON;
  }else if(state == STATE_ON && read_val > OFF_THRESH){
    // ON→OFFになったとき、GETリクエストを送る
    digitalWrite(LED_PIN, HIGH);

    wake_wifi();  // WiFiが使えるようになるまで待つ必要あり?未解明

    
    String get_result = getPageSource(String("http://") + host_ip + String("/off"));
    //Serial.println(get_result);

    digitalWrite(LED_PIN, LOW);

    state = STATE_OFF;
  }

  delay(10);
}

赤外線LED側の設計

WiFiリクエストを受け取って、エアコン用の赤外線を出す装置を作ります。必要なものはESP8266と、赤外線LED、電流増幅用のトランジスタと抵抗です。

ソースコード(赤外線LED側)

WiFiリクエストを受け取って何かをするのはWebServer.hを使います。まずは→ライブラリ管理→でESP8266WebServerと検索してライブラリをダウンロードします。

赤外線の送信は「」というライブラリを使いました。こちらはTOSHIBA製や日立製など主要なエアコン製品の赤外線信号を提供してくれているので、好きなコマンドを簡単に送ることが出来ます。

自分が持っているリモコンの信号を覚えさせてそれを再生する方法もありますが、全部のボタンを覚えさせるのが手間なのと、たまに送信ミスすることがあったのでこちらを使うことにしました。

/* IRremoteESP8266: IRsendDemo - demonstrates sending IR codes with IRsend.
 *
 * Version 1.1 January, 2019
 * Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009,
 * Copyright 2009 Ken Shirriff, http://arcfn.com
 *
 * An IR LED circuit *MUST* be connected to the ESP8266 on a pin
 * as specified by kIrLed below.
 *
 * TL;DR: The IR LED needs to be driven by a transistor for a good result.
 *
 * Suggested circuit:
 *     https://github.com/markszabo/IRremoteESP8266/wiki#ir-sending
 *
 * Common mistakes & tips:
 *   * Don't just connect the IR LED directly to the pin, it won't
 *     have enough current to drive the IR LED effectively.
 *   * Make sure you have the IR LED polarity correct.
 *     See: https://learn.sparkfun.com/tutorials/polarity/diode-and-led-polarity
 *   * Typical digital camera/phones can be used to see if the IR LED is flashed.
 *     Replace the IR LED with a normal LED if you don't have a digital camera
 *     when debugging.
 *   * Avoid using the following pins unless you really know what you are doing:
 *     * Pin 0/D3: Can interfere with the boot/program mode & support circuits.
 *     * Pin 1/TX/TXD0: Any serial transmissions from the ESP8266 will interfere.
 *     * Pin 3/RX/RXD0: Any serial transmissions to the ESP8266 will interfere.
 *   * ESP-01 modules are tricky. We suggest you use a module with more GPIOs
 *     for your first time. e.g. ESP-12 etc.
 */

#ifndef UNIT_TEST
#include <Arduino.h>
#endif
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Toshiba.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#define WIFI_SSID "Buffalo-G-4310"
#define WIFI_PWD "buf50317"
#define DNS_NAME "ir-sender"

#define HTML_HEADER "<!doctype html>"\
  "<html><head><meta charset=\"UTF-8\"/>"\
  "<meta name=\"viewport\" content=\"width=device-width\"/>"\
  "</head><body>"
#define HTML_FOOTER "</body></html>"

ESP8266WebServer server(80);

const uint16_t kIrLed = 4;  // ESP8266 GPIO pin to use. Recommended: 4 (D2).
IRToshibaAC ac(kIrLed);  // Set the GPIO to be used for sending messages.

void printState() {
  // Display the settings.
  Serial.println("Toshiba A/C remote is in the following state:");
  Serial.printf("  %s\n", ac.toString().c_str());
  // Display the encoded IR sequence.
  unsigned char* ir_code = ac.getRaw();
  Serial.print("IR Code: 0x");
  for (uint8_t i = 0; i < kToshibaACStateLength; i++)
    Serial.printf("%02X", ir_code[i]);
  Serial.println();
}

void handle_root(){
  String str = HTML_HEADER
    "<h1>I'm IR sender!</h1>"
    "<h2>Usage</h2>"
    "<h3>/</h3>"
    "このページです。<br>"
    "<h3>/set?temp=22&fan=0&mode=a</h3>"
    "デフォルトのパラメータを設定します。<br>"
    "<h3>/state</h3>"
    "デフォルトのパラメータを表示します。<br>"
    "<h3>/on?temp=22&fan=0&mode=a または /on</h3>"
    "エアコンONを送信します。クエリをつけるとそれをデフォルトのパラメータに設定して起動します。<br>"
    "クエリをつけないと以前に設定したパラメータを使います。<br>"
    "<h3>/off</h3>"
    "エアコンOFFを送信します。<br>"
    HTML_FOOTER;

  server.send(200, "text/html", str);
}

void handle_set(){
  for (int i = 0; i < server.args(); i++) {

    if(server.argName(i) == "temp"){
      ac.setTemp(server.arg(i).toInt());
    }else if(server.argName(i) == "fan"){
      ac.setFan(server.arg(i).toInt());
    }else if(server.argName(i) == "mode"){
      if(server.arg(i) == "a") ac.setMode(kToshibaAcAuto);
      if(server.arg(i) == "h") ac.setMode(kToshibaAcHeat);
      if(server.arg(i) == "c") ac.setMode(kToshibaAcCool);
      if(server.arg(i) == "d") ac.setMode(kToshibaAcDry);
    }
  
  }
}

void handle_on(){
  if(server.args() > 0)
    handle_set();
  
  ac.on();
  ac.send();

  String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
  server.send(200, "text/html", str);
}

void handle_off(){
  ac.off();
  ac.send();

  String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
  server.send(200, "text/html", str);
}

void setup() {

  ac.begin();
  Serial.begin(115200);
  delay(200);
  
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  
  // Wait until WiFi is connected
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin(DNS_NAME)) {
    Serial.println("MDNS responder started");
  }

  // Setup WebServer Handlers
  server.on("/", handle_root);
  server.on("/on", handle_on);
  server.on("/off", handle_off);
  server.on("/set", handle_set);

  server.on("/state", [](){
    String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
    server.send(200, "text/html", str);
  });

  server.begin();
}

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

回路図

よくあるLEDを光らせる回路と同じです。僕が買った赤外線LEDは3.3Vでは光が弱すぎてエアコンまで届かなかったので、5VをトランジスタでON/OFFするようにしています。

赤外線LEDをトランジスタでON/OFFするだけ
基盤もなしでそのまま配線しちゃいます

また、このままだと5VがLEDに直接かかっているので、電流が流れすぎてLEDを壊す可能性がありますが、たくさん電流を流したほうが赤外線がよく届くのであえて抵抗を入れていません。

今のところ問題なく動いていますし、LEDが光るのは一瞬なので大丈夫だとは思いますが、最初は抵抗をはさみつつテストする方が良いでしょう。

適当な箱に入れて目立たなくする

置物側を作るときに使った箱のもう半分を利用して適当に作ってみました。木の箱に入れるだけでちゃんとしてる感が出ますね!

LEDやトランジスタにジャンパワイヤを切ってハンダ付け。テープで巻いときゃなんとかなる。
木箱からのぞくLED…

完成!

置物を目立つところに、LEDの箱を目立たないところに置いておきましょう。置物を動かすと…クーラーが付きます♪

以上です。

吹き消しキャンドルを作る

はじめに

前回部屋の明かりをIoT化してWiFiから消したり点けたりできるようになりました。でも消そうと思うたびにスマホを取り出してブラウザで操作しないといけないので、ちょっとスマートじゃないなということで息を吹きかけると消える装置を作ってみました。

手元にあったLEDキャンドルをリメイクしてみたかったっていうのもある。

つくりたいもの

  • 息を検出する
  • 検出したらWiFiを介してHTTPのGETリクエストを送る(またはLEDを消す)

必要なもの

  • ESP8266モジュール
  • または適当なArduino(LEDを消すだけ)
  • コンデンサマイク(※アンプなしのもの。)
  • 抵抗(2kΩ✕1、200Ω✕2)
  • コンデンサ(0.01μF✕1)
  • LED✕1
  • ブレッドボード、ジャンパワイヤー

↓いつもの安くておすすめなやつです

ブレッドボードを組み立てる

必要なものを揃えたらブレッドボードにさしていきましょう。回路図はこんな感じです。

吹き消しキャンドル回路図
ブレッドボード配置図。できるだけ回路図と同じになるように表示しています。

最低限の部品しか使っていませんがこれで動きます。たぶん詳しい方からすればミスが多いと思いますので、ご遠慮無くご指摘ください。簡単にポイントを説明します。

ポイント① マイク部分

この部分はコンデンサマイクの基本的な回路です。データシートにも書いてあります。抵抗R3とコンデンサC1の値はデータシートとかなり違いますが、これしか手元になかったので使っています。一応問題なく動いているのでOKとします。
コンデンサマイクは極性があるのでプラスとマイナスを間違えると動きません。逆に、コンデンサC1は極性があるとだめなのでセラミックコンデンサを使います。

ポイント② バイアス回路

この部分はバイアス回路です。コンデンサマイクからの信号に一定電圧をプラスして信号を見やすくします。

僕が使っているESP8266モジュールはアナログ入力が0~3.3Vなので、その半分くらいの1.65Vが足されるようにしました。

アナログ入力が0~1Vのesp8266 もある(むしろこっちが一般的?)ので、その場合はバイアスが0.5VくらいになるようにR1とR2の値を決めてください。

ポイント③ LED

動作確認用にLEDを付けています。普通はLEDに電流が流れすぎないように数百Ωの抵抗を入れたほうが絶対いいですが、これでも動くのでテストだけなら抵抗無しでやってます。長時間使うとあんま良くないと思います。

【注意!】esp8266に電源を差し込む前に、回路が間違っていないか確認しましょう。特にesp8266の3.3VとGNDがショートしてるとヤバイです。過大な電流が流れてesp8266を壊します。僕はこれで2つ壊しました。テスター等で必ず確認してください。

ESP8266にプログラムを書き込む

Arduino IDEを使ってesp8266にプログラムを書き込みます。コードの全体像はこちらです。

/*
 * 吹き消すと火が消えるライト
 * 息はコンデンサマイクで検知。
 */

#define A1 0.9999  // 定常値を求めるLPFの係数。長期的なセンサ値の平均を求める。[]

// 検出判定のパラメータ。TIME_DURATION[s]間にCOUNT_MAX回しきい値を超えたら反応する。
#define TIME_DURATION 500  // [ms]
#define COUNT_MAX 5  // []
#define V_DIFF 2.0  // 閾値

#define LED_PIN 14


float v = 0;  // センサ値
float v_stable = 0;  // センサ値の定常値
unsigned long last_detected_time = millis();  // 最後にセンサ値が閾値を超えた時間
int detect_count = 0;  // 検出回数

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

  // 最初のanalogReadは値が信用できないので、何回か読む。
  for(int i=0; i<50; i++){
    v_stable = analogRead(A0);
    delay(10);
  }

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
}

void loop() {
  v = analogRead(A0);  // センサ値
  v_stable = A1*v_stable + (1-A1)*v;  // センサ値にローパスフィルタをかけたもの。急には変化しない。

  // 閾値を超えたらカウントを増やす
  if( fabs(v - v_stable) > V_DIFF ){
    last_detected_time = millis();
    detect_count++;
  }

  // タイムアウト
  if( millis() - last_detected_time > TIME_DURATION )
    detect_count = 0;

  // カウントが溜まったらLEDを消灯
  if( detect_count > COUNT_MAX ){
    digitalWrite(LED_PIN, LOW);
    delay(1000);
    digitalWrite(LED_PIN, HIGH);
    detect_count = 0;
  }
  
  Serial.println(v);
  delay(10);
}

息の検出アルゴリズム

loop()関数内で息の検出をしています。コンデンサマイクはかなり小さい信号しか出すことが出来ないので、通常はアンプ回路で信号を増幅しますが、息くらいの大きい振動ではアンプ無しでも検出できるくらいに信号が変化します。今回のプログラムではこの信号の変化をトリガーにしてアクションを起こします。

息以外の音でも反応してしまいそうですが、意外とそんなことはありませんでした。

書き込み

僕と同じモジュールを使っている方は以下の設定で大丈夫です。

ESP8266モジュールの書き込み設定

完成!

コンデンサマイクに直接息を吹きかけてみましょう。LEDが消えます。

写真では分かりづらいですが、LEDが消えました!

もし消えなかった方はプログラム中の閾値やMAX_COUNTを減らすなどして調整してみてください。

以上です。

TinkerBoardと外付けHDDでRAIDのデータサーバを作る

はじめに

こんにちは。今回はTinkerBoardと外付けHDDで家庭内データサーバを組もうとした話をご紹介します。

RaspberryPiでもできる?

要るもの

いよいよ設定!

HDDをフォーマット

コマンドライン上でフォーマットする方法もあるけれど、今回はみんな大好きgpartedを使いました。GUIで直感的に操作できるし、確定を押すまで全ての処理は行われないので最終確認してから実行できるので僕はいつもgparted使ってます。入っていない人はsudo apt install gpartedすれば入ると思われます。

sudo gparted
gpartedの操作画面

まずは右上の/dev/xxxと表示されているプルダウンからフォーマットするデバイスを選択します。買ったばかりの状態のHDDでは注意マークが出ているかと思います。そういう場合は「デバイス」→「パーティションテーブルの作成」を行ってください。続いて「パーティション」→「New」を選択します。

パーティションの設定

パーティションの設定はほとんどデフォルトでいいと思います。参考にしたサイトでも紹介されている通り、「新しいサイズ」の項目は最大サイズよりも数百MB少なくしました。今回は全く同じHDDを購入し、最大サイズも全く同じでしたが念の為です。ラベル名は好きな名前をつけてください。その名前でフォルダ名が作られ自動でマウントされるので、WD01、WD02などわかり易い名前がいいでしょう。
操作が終わったらチェックマークを押して確定しましょう。処理が始まります。

HDD2台をRAID構成にする

複数台のHDDを使うことで、例えどれかが壊れてデータが失われても復旧できるようにデータに冗長性をもたせる技術をRAIDといいます。今回は2台のHDDに全く同じデータを書き込む「RAID1」を構成します。参考サイトのmdadmコマンドを一部変更して、次のようにしました。

sudo apt install mdadm
sudo mdadm --create /dev/md0 -v --raid-devices=2 --level=raid1 --chunk=128 /dev/sd[ab]

変更点は–raid-devicesの数を2にしたこと、–levelをraid1にしたこと、最後のデバイス名を自分の環境に合わせたことです。進捗は

watch -n 1 -d cat /proc/mdstat

とすることで確かめられます。RAID構築に一日以上かかるみたいです。ひえぇぇ。気長に待ちましょう。

できたデバイスをフォーマットしてラベル付け

2TBのディスクのフォーマットが完了するまでに2日以上かかってしまいました。完了したら最後の手順です。gpartedを使い、最初にHDDをフォーマットした時と同じ手順でデバイス/dev/md0をext4等の形式でフォーマットします。
そのままだとマウントしたときにやたら長いランダム文字列のフォルダができてしまうので、ラベル名を変えて置きます。gpartedでデバイスをアンマウントして、「パーティション」→「Label File System」です。

これで2つのHDDをRAID構成で使うことができます。お疲れ様でした。

参考にしたサイト

Raspberry PiにUSBメモリ4本でRAID10を組む

ownCloud入れた(初期設定エラー)

今日は自前で作るクラウド、ownCloudをTinkerBoardに導入してみました。1TBの外付けハードディスクにデータを保存すれば容量無制限のDropBox的なやつが作れちゃいます。というわけでインストールではまった所を挙げておきます。

管理者アカウント作るところで「 このユーザ名は既に使用されています」と出る

owncloudフォルダコピー、mysqlデータベース作成、サーバ設定後、ブラウザからアクセスして初期設定画面が出るとこまではOKでしたが、各項目を正しく入力しても「このユーザ名は既に使用されています」と出て進めなくなりました。ちなみにデータベースにはSQLiteではなくMySQLを使用していましたが、原因はその前に何回か初期設定に失敗したことでデータベースにユーザだけ存在してしまったことでした。次のコマンドでデータベースを削除したら行けました。

sudo mysql -u root
mysql>drop database cp932db;
mysql>exit;

あとはデータベースの作成からやり直せばOKです。
参考サイト

外部ストレージにデータを置きたい

外付けHDDにデータを置きたかったので、owncloudフォルダをHDDに移して、パーミッションを変更しました。ところがnginxでドキュメントルートをowncloudフォルダがあるパスにしても、ブラウザでアクセスすると「File not found.」と表示されてしまいました。パスが/media/ppdr/xxxとかになると思いますが/mediaも/media/ppdrも/media/ppdr/xxxも所有者をwww-data(nginxの場合)にしないといけないみたいです。

sudo chown www-data:www-data /media
sudo chown www-data:www-data /media/ppdr
sudo chown www-data:www-data /media/ppdr/xxx

結構適当にやってしまったので間違っているかもしれません。ご存知の方がいらっしゃいましたらご教示ください。

【ROS】ros::Time::now()ができない話

現象

次のようなノードを書いたときにros::Time::now().toSec()の値が0になってしまって困りました。

int main(){
ros::init(“test”);

ros::Time t_start = ros::Time::now();
ROS_INFO(“%lf”, t_start.toSec());

return 0;
}

ちなみにこの現象はrosparam set use_sim_time trueをしている時だけで、しかもプログラムの後半で使ったときは普通にコンピュータ時間が出てきました。うーん、謎。

解決

原因はよくわからなかったけど、解決策としてはROS_INFOの前に

ros::Duration delay(1.0);
delay.sleep();

のように待ち時間を設けるか、

while( t_start.toSec() <= 0 ) t_start = ros::Time::now();

のように値が得られるまで待つかすることでちゃんと値が出てきました。
おそらくsim_timeを使う(=bagファイルの時間を使う)ときはクロックが遅くなる?のかな。ノードが起動してあまりにも早くros::Time::now()を呼びすぎると失敗するみたいです。

WordPressを使おうと思っても80番ポートだけ外部からアクセスできない問題

症状

Nginxで80番でWordPress、8080番で適当なhtmlファイルにアクセスするように設定したら、

  • LAN内からローカルIPアドレス「192.168.0.5」→アクセスできる
  • LAN内から80番以外のポート「192.168.0.5:8080」→アクセスできる
  • LAN外からグローバルIPアドレスまたはドメイン名「ppdr.softether.net」→アクセスできない
  • LAN外から80番以外のポート「ppdr.softether.net:8080」→アクセスできる

チェックしたこと

ファイアウォールを確認

ファイアウォールが有効になっていて80番ポートをブロックしていないか確かめた。でもファイアウォール自体入っていなかった。

nginxが正しく80番ポートを受けているか

nginxが正しく機能しているか確認した。ポートを待ち受けているプロセスを見るには、

$ sudo netstat -ltupn
稼働中のインターネット接続 (サーバのみ)
Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態 PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1188/nginx: master
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1188/nginx: master

いた。そもそもLAN内からはアクセスできるから待ち受けてはいる。

DDNSが正しく機能しているか

DNSのチェックサービスを利用してDNSと自宅のサーバーのグローバルIPが正しく結びついているか確認した。大丈夫だった。

ブラウザのキャッシュをクリア

LAN外からのアクセスにはスマホを使っていた。同じURLで何度か試しているので前回読み込んだ結果がキャッシュに保存されているのかもしれない。chromeの「︙」から「履歴」→「閲覧データを削除…」→「キャッシュされた画像とファイル」にチェックを入れてデータを消去。

これをやることで表示のされ方が変わった。もう一歩。

WordPressの設定

これ重要。

WordPressの初期設定はローカルでの接続で行っていたので、サイトのURLが「http://192.168.0.5」になっていた。WordPressの「設定」→「一般」から「 WordPress アドレス (URL) 」と「サイトアドレス(URL)」を「http://ppdr.softether.net」に変えた。

これで正しく表示できるようになった!!
よかったよかった。

RaspberryPi 3BからTinker Boardにお引越し

こんにちは。パパドラゴンです。数年前からラズパイでサーバーを動かしています。もちろんこのページがあるサーバーも自作です。しかし最近音声認識とかOpenCVを使ったり、他のIoT機器と連携したりするとなるとちょっとCPUやら通信速度やらの性能不足が気になってきました。最近流行っているスマートホーム的な使い方を考えてみても、音声認識や画像認識、センサ値の取得などは小型のArduinoやラズパイに任せ、メインのコンピュータはそれらのデータを集めて他の機器に指示を出すような構成にしてみようかと思います。

そのため、ネットワーク通信の性能と低消費電力を基準に他のシングルボードコンピュータを調べてみたところ、Tinker Boardというのが良さそうだったので買ってきました。

Tinker Board

ASUS製のシングルボードコンピュータです。ラズパイと比較して、CPUの計算パワー、ネットワーク通信速度がかなり高いです。その代り価格もラズパイの2倍くらいしますが、この価格でこのスペックならばコストパフォーマンスはとても良いと言い切っていいでしょう。



型番にSがついているものはeMMC(内蔵フラッシュメモリ)があるタイプですが、僕の使い方では高速起動とかは必要ないかなと思ったので通常版を買いました。
電源はラズパイよりもさらに多い3.0Aが必要ですので、こちらも専用品を買ったほうが良いと思います。SDカードについては何でもいいですが、現在のラズパイで使っていてまだ一度も壊れていないという理由でSUMSUNG製を選びました。

OSインストール後やったこと

SDカードにTinker OSを焼いて起動した後の設定の備忘録です。

SSH関連のインストール

sudo apt-get install avahi-daemon xrdp xorgxrdp

ユーザー名「linaro」の変更

sambaのインストール

sudo apt-get install samba

geditのインストールとrootで編集する際のエラーの対策

sudo apt-get install gedit
visudoして最後の行にDefaults env_keep=”DISPLAY XAUTHORITY” を追加
このコマンド実行するとエラー無くなる(?)
xhost si:localuser:root
ターミナルを開くたびに実行しないといけないので、~/.bashrcに書いておいた

softetherのインストール

これが大変だった。まずはよくある手順通りにsoftetherのサイトからファイルをダウンロードして展開、makeした後/usr/localに移動。
起動スクリプトを/etc/init.d/vpnserverに書いて、systemctl start vpnserver.service。ところがクライアント側のWindowsのsoftether管理マネージャからログインができない。

原因はよくわからなかった。でもnetstat -ltupnと打ってみて443番や992番ポートをvpnserverが使っていなかった。sudo systemctl restart vpnserver.serviceすると治った。よくわからない。後は手順通りにWindows側から設定。

nginxのインストールとletsencrypt

nginxはsudo apt install nginx
let’s encryptはgit clone

(追加予定…)

ラズパイにOpenCV入れた(ビルドエラー解決)

こんにちは、パパドラゴンです。大晦日に何やってんだって感じですが、今ラズパイ3にOpenCVを入れました。ビルドエラーに嵌ってしまい、3日も無駄にしてしまいましたが、なんとか年越し前に完了しました。

環境

raspbian: 8.0
OpenCV: 3.1.0
Python: 2.7.9

方法

こちらのサイトに書いてあるとおりにコマンドを実行しました。

Raspberry Pi講座 Python3 + OpenCV3(環境構築編)
https://sites.google.com/site/memorandumjavaandalgorithm/raspberry-pi-jiang-zuo-opencv3

失敗① ビルドでerror終了する

通常のapt-get upgradeではなく、sudo apt-get dist-upgradeすると成功しました。

失敗② ビルドで失敗する その2

makeするところでmake -j4 ではなくsudo makeだけで実行すると成功しました。

失敗③ make後、installすると、pkgconfig is not a directoryというerrorが出る

音声解析ライブラリJuliusをインストールしたときに作られたファイルが既にあったのが原因でした。makeファイルの中でpkgconfigで検索かけて保存先を別の名前に変更するか、元々あったファイルをリネームするととりあえず解決しました。

これにてインストール完了。やったね!

【ROS】DS4コントローラを振動させる

やりたいこと

ROSを使うとプレステのコントローラ「DualShock4」を使ってロボットの操作が簡単にできます。しかし、コントローラの操作をロボットに伝える事例はたくさんあっても、コントローラを振動させる(フォースフィードバック)やり方でハマったのでご紹介します。

王道は「ds4drv」と「joy_node」を使う。でも…

DS4コントローラとROSの連携で最も一般的なのはコントローラ用のドライバ「ds4drv」とROSのjoyパッケージのノードである「joy_node」を使うやり方です。(こちらのサイトを参考にしました)
ROSでPS4のコントローラ(DualShock4)を使う方法

ところが、振動機能を試してみると失敗します。
以下は失敗事例です。
振動のテストにはfftestコマンドを使います。まずはds4drvを起動して、コントローラとペアリング

$ sudo ds4drv 
[info][controller 1] Created devices /dev/input/js0 (joystick) /dev/input/event11 (evdev) 
[info][bluetooth] Scanning for devices
[info][bluetooth] Found device DC:0C:2D:5C:86:2B
[info][controller 1] Connected to Bluetooth Controller (DC:0C:2D:5C:86:2B)
[info][bluetooth] Scanning for devices
[info][controller 1] Battery: 37%
[warning][controller 1] Signal strength is low (31 reports/s)

一番上の行にあるevdevのイベント番号を見ます。今回は/dev/input/event11となっています。続いてfftestを実行します。

$ fftest /dev/input/event11
Force feedback test program.
HOLD FIRMLY YOUR WHEEL OR JOYSTICK TO PREVENT DAMAGES

Device /dev/input/event11 opened
…
(中略)
…

Uploading effect #0 (Periodic sinusoidal) ... Error:: Function not implemented
Uploading effect #1 (Constant) ... Error: Function not implemented
Uploading effect #2 (Spring) ... Error: Function not implemented
Uploading effect #3 (Damper) ... Error: Function not implemented
Uploading effect #4 (Strong rumble, with heavy motor) ... Error: Function not implemented
Uploading effect #5 (Weak rumble, with light motor) ... Error: Function not implemented
Enter effect number, -1 to exit

Uploading effectで始まる行ですべてError: Function not implementedが出てしまっています。この状態で0~5の数字キーを押しても全く振動してくれません。

確認すべきこと

カーネルのバージョン

まず第一に、linuxカーネルのバージョンを確認しましょう。linuxカーネルとはOSの中核を担うプログラムのことです。ds4の振動機能はlinuxではカーネル4.10以上でしかサポートされていないので、それ未満の場合は更新する必要があります。

$ uname -r 
4.9.0-43-generic

linuxカーネルはapt-get upgrade等では自動更新されないので、手動で更新する必要があります。といってもapt-getを使って簡単に更新できます。(※バージョンによっては更新後にエラーが起こりLinuxの起動に問題が出ることもあるので注意が必要ですが、最悪CUIでログインできれば元のバージョンに戻すとは可能です。)
カーネルのアップデートは以下コマンドでできます。

sudo apt-get install linux-generic-hwe-16.04

ds4drvを使わない

前述の通りds4drvは振動機能に対応していません。Linuxカーネル4.10以降ではds4drvを使わなくてもBluetoothでPCとペアリングが可能です。ペアリングの方法は、ds4drvを使う場合と同じくPSボタンとshareボタンを5秒くらい長押しし、ds4の青いランプがチカチカッと光るようになったら、PCのBluetoothの設定から新規デバイス追加を行います。

よくあるBluetoothペアリング

ペアリングが完了したら何もしなくてもds4drvで接続したのと同じ状態になっています。ROSを使う際もそのままrosrun joy joy_nodeを実行すればジョイスティックの値を見ることが可能です。


この状態でもう一度fftestによる振動テストを実行してみましょう。

$ fftest /dev/input/event19
Force feedback test program.
HOLD FIRMLY YOUR WHEEL OR JOYSTICK TO PREVENT DAMAGES

Device /dev/input/event19 opened
…
(中略)
…

Setting master gain to 75% ... OK
Uploading effect #0 (Periodic sinusoidal) ... OK (id 0)
Uploading effect #1 (Constant) ... Error: Invalid argument
Uploading effect #2 (Spring) ... Error: Invalid argument
Uploading effect #3 (Damper) ... Error: Invalid argument
Uploading effect #4 (Strong rumble, with heavy motor) ... OK (id 1)
Uploading effect #5 (Weak rumble, with light motor) ... OK (id 2)
Enter effect number, -1 to exit
0
Now Playing: Sine vibration
Enter effect number, -1 to exit

OKとなっている、#0、#4、#5が使えます。数字を入力するとコントローラが振動します。やったね!。

pythonプログラムから振動

kharlashkin氏のpythonプログラムです。いろんな強さ&時間で振動させることができます。python2系で動作します。python3系で動かすときはself.ff_joy = open(file, "r+")の部分をself.ff_joy = open(file, "r+b")に変えるといいみたいです。他のプログラムやROSから使うときはpythonで振動させられるのは便利ですね。

import fcntl, struct, array, time

EVIOCRMFF = 0x40044581
EVIOCSFF = 0x40304580
LOG_CLASS_ON = False
TIME_DELTA = 250

class Vibrate:

    def __init__(self, file):
        self.ff_joy = open(file, "r+")

    def close(self):
        self.ff_joy.close()

    def new_effect(self, strong, weak, length):
        effect = struct.pack('HhHHHHHxHH', 0x50, -1, 0, 0, 0, length, 0, int(strong * 0xFFFF), int(weak * 0xFFFF))
        a = array.array('h', effect)
        fcntl.ioctl(self.ff_joy, EVIOCSFF, a, True)
        return a[1]
        id = a[1]
        return (ev_play, ev_stop)

    def play_efect(self, id):
        if type(id) == tuple or type(id) == list:
            ev_play = ''
            for i in id:
                ev_play = ev_play + struct.pack('LLHHi', 0, 0, 0x15, i, 1)
        else:
            ev_play = struct.pack('LLHHi', 0, 0, 0x15, id, 1)
        self.ff_joy.write(ev_play)
        self.ff_joy.flush()

    def stop_effect(self, id):
        if type(id) == tuple or type(id) == list:
            ev_stop = ''
            for i in id:
                ev_stop = ev_stop + struct.pack('LLHHi', 0, 0, 0x15, i, 0)
        else:
            ev_stop = struct.pack('LLHHi', 0, 0, 0x15, id, 0)
        self.ff_joy.write(ev_stop)
        self.ff_joy.flush()

    def forget_effect(self, id):
        if type(id) == tuple or type(id) == list:
            for i in id:
                fcntl.ioctl(self.ff_joy, EVIOCRMFF, i)
        else:
            fcntl.ioctl(self.ff_joy, EVIOCRMFF, id)

f = Vibrate("/dev/input/event14")
p = f.new_effect(1.0, 1.0, TIME_DELTA )
f.play_efect((p))
time.sleep(TIME_DELTA / 1000.0)
f.stop_effect((p))
f.forget_effect((p))

https://github.com/chrippa/ds4drv/issues/91から引用

今回は以上です。今後はこれを使って危険をユーザに知らせるアラートシステムを作っていきたいと思っています。振動させるROSノードが完成したらそちらも公開しようと思います。論文で大変なので時間があればですが…。

参考