初心者のためのIoT&電子工作入門 ①準備編

はじめに

最近スマート家電やホームコントロールという言葉もすっかり身近になってきました。声をかけるだけでテレビやエアコンがオンする環境はもう当たり前になってきています。

しかし、家電コントロールのシステムや規格はまだまだ統一されておらず、家電自体にネットワーク機能がついたものを購入するか、テレビを付けたりなどのリモコン(赤外線)操作を行うデバイスを購入するしかないのが現状です。しかも、そのスマートリモコンシステムが1万円くらいしたりするので、便利かもしれないけど購入は迷う値段です。

家の中のすべての電化製品をコントロールするためには、自分でモータやセンサを購入し、回路を作成し、基盤にはんだ付けし、プログラムを書き込むという作業が必要になってきます。本記事は「好きな家電をコントロールしてみたい」「電子工作でなにか役に立つものを作りたい」という方に向けて、やるべきことを解説していきます。

対象とする人

  • 家庭内IoTシステムを安く、簡単に作りたい方
  • 無線IoTデバイスの自作に挑戦してみたい方
  • RaspberryPiやArduinoを買ってみたは良いが、使い道に迷っている方
  • 実際に使えるプログラミングの勉強を(少しだけ)したい方

家庭内IoTシステムの完成形

現在私が家で稼働させているシステムの構成図です。

本記事で解説するシステムの完成形

本記事では①~④を中心に解説していきます。最終的には声やスマホ、時刻やセンサによってテレビやエアコンを点けたり、ライトを操作することができることを目指します。

①~④は一応優先順位です。それぞれの役割と、作るのに必要な知識・技術を簡単に解説しておきます。

①無線マイコン(ESP8266)

  • 役割:
    WiFiによる通信で動作命令を受け取ったり、一定時間経過など何らかのタイミングによって、出力ピンから電圧を出力しモータやLEDを動作させます。
  • 必要事項:
    Arduinoプログラミング(C++)、HTTP通信、電子回路(モータ、LED、抵抗、トランジスタ等)

IoTのキモとなる、プログラミングとハードウェアを結びつけるマイコン(小型コンピュータ)です。本記事ではESP8266というモジュールを使います。このモジュールは最近では有名なArduinoにWiFi通信モジュールがついたものです。かなり安価(1個700円程度)で購入でき、一つの機能に対して一つのモジュールを使うのが基本です。

②サーバ(RaspberryPi等)

  • 役割:
    マイコンではできないような、Webから天気予報の取得や画像・音声の解析を行い、必要に応じてマイコンに動作命令を送ります。また、家の外からスマホでデータを送る際、通信を受け取るための玄関的な役割(=サーバ)を担います。
  • 必要事項:
    Pythonプログラミング、Linuxの知識、ネットワーク(サーバ)の知識

マイコンでできないような画像・音声解析や、複雑なネットワーク機能を担当します。ちゃんとしたコンピュータであり、できることは無限ですが、LinuxというOSを使っているため初心者には多くの知識が必要となります。本記事ではサーバの構築など一部だけ紹介します。

③ルータ

  • 役割:
    WiFiや有線LANで繋がれた機器同士の通信経路を作ります。家の中だけで使っている場合はSSIDとパスワードを各機器に登録するだけで接続できますが、サーバで家の外からの通信を受ける場合には少し設定が必要になってきます。
  • 必要事項:
    ネットワークの知識

少し設定すれば終わりですが、家の外からの通信を受けるときは適切に設定する必要があります。

④スマートスピーカー(Google Home)

  • 役割:
    音声を検出し、予め登録した言葉を検出したら何か処理をさせます。
  • 必要事項:
    特になし

Google Homeやアレクサ等最近流行りのスマートスピーカーですが、対応機器が少ないため持て余している方もいるかと思います。本記事では自作したIoTデバイスをスマートスピーカーで操作できるようにします。

上の図では家庭内だけで通信が完結しているように書きましたが、Google Homeを使うにはIFTTTというサービスを利用するのが一番簡単です。そのためには家の外からの通信を受け入れる必要があり、②のサーバが必要になってきます(IFTTTが外部にあるため)。

できるようになること

この記事を順番に見ていくとできるようになることを段階的にまとめてみます。

1.①無線マイコンのみ

無線マイコンのみでもTVやエアコンを付けることはできます。問題はどうやって動作命令を伝えるかですが、簡単にやるならスマホでchromeなどを開いて特定のURLを打ち込むことでマイコンを動作させることができます。

また、無線マイコンにセンサを付け、無線マイコン同士で通信させることも可能です。

無線マイコンのみでできること

2.①無線マイコンと②RaspberryPi(サーバなし)

RaspberryPiを導入することで、Webから天気予報や現在の気温等を取得することができます。よって人が入力しなくても無線マイコンを動作させることが可能です。

また、RaspberryPiに音声解析ソフトをインストールすればスマートスピーカーがなくても音声で家電を操作することができます。ですが、認識精度はやはりGoogle Homeが圧倒的に高精度ですので、ここで終わるのはもったいない気がします。

無線マイコンとRaspberryPi(サーバなし)でできること

3.①無線マイコンと②サーバと③ルータを設定

RaspberryPiをサーバ化すると、今まではできなかった家の外からの通信を受けることができます。これにより外出先からスマホでエアコンを付けることが可能になります。

また、2つのサービスを結びつける「IFTTT」を使うことで、さらに動作命令を送る幅が広がります。

無線マイコン+サーバでできること

4.①無線マイコン+②サーバ+③ルータ+④スマートスピーカー

最後にスマートスピーカーを追加すれば、好きな家電を声で操作できるようになります。IFTTTを使えばスマートスピーカーの利用は簡単なので、次の最終形にするまでに時間はほとんどかからないと思います。

無線マイコン+サーバ+スマートスピーカー

今回は最終的な全体システムの説明と、それぞれの構成がどのような役割を持つのかを解説しました。次回からはESP8266を使って動作命令を受け取る→LED、モータを動かす部分を解説していこうと思います。

つたない文章でしたが、次回もよろしくお願いします。

【Arduino】加速度センサ MPU9250の使い方&測定レンジ設定

こんにちは!今回は加速度+ジャイロ+方位が取れる高精度センサであるMPU-9250の使い方をご紹介します。データ取得や測定レンジ変更など基本的な使い方と使用レビューをしようと思います。

なおセンサの詳しい仕様はこちらのデータシートを参照すれば分かります。この記事はデータシートを見るのが面倒な時のための備忘録的記事です。

この記事は自分で色々試行錯誤した結果でもあるので、間違っている箇所があればぜひご指摘ください。

目次

  • 用意するもの
  • 配線時の注意点
  • データの取得
  • 測定レンジ、内部LPFの設定

用意するもの

  • MPU-9250
    今回使用するセンサです
  • Arduino nano
    たまたま手元にあったnanoを使いましたが、Arduino Unoやラズパイなど、I2CまたはSPI通信が使えるなら何でもOKだと思います。今回はArduino系でI2Cを使う想定でサンプルコードをご紹介します。

配線時の注意

配線はデータシートの通りですが、必要最小限でこれだけになります。ポイントは左下と右下のピンで、左下はアドレス選択ピンで3.3VかGNDどちらに接続してもいいですが通信する時のアドレスが異なります。

左下ピンスレーブアドレス
LOW(0V)0x68
HIGH(3.3V)0x69

右下はI2Cの場合3.3Vに接続します。

最小構成での配線

データの取得

I2C通信するライブラリはArduinoならばWire.hが使えます。MPU-9250のデータ読み込みはデバイスアドレスを指定して、メモリアドレスを書き込んだあと、メモリを読み出すことで取得します。

加速度、ジャイロの値を取得するサンプルコードをご紹介します。データは16ビットで、上位8ビットと下位8ビットに分かれているので、データの読み込みを2回行い結合する関数i2c_read2を用意しています。この関数に読みたいメモリアドレスを入力するだけで、データを取得することが出来ます。

#include <Wire.h>

#define DEVICE_ADDRESS 0x69
#define MPU_WAKE_UP1 0x6B
#define MPU_WAKE_UP2 0x37
#define X_a 0x3B
#define Y_a 0x3D
#define Z_a 0x3F
#define X_g 0x43
#define Y_g 0x45
#define Z_g 0x47

#define ACC_RESOLUTION 32768.0
#define GYR_RESOLUTION 32768.0
#define ACC_RANGE 2.0//[m/s/s]
#define GYR_RANGE 250//[deg/s]

//メモリアドレスから2バイト読み込んで整数に直します。データはビッグエンディアンです。
short i2c_read2(char memory_address){
  //先頭アドレスの読み込み。16bitのshort型に格納。
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write(memory_address);
  Wire.endTransmission(false);

  Wire.requestFrom(DEVICE_ADDRESS,1);
  short d1 = Wire.read();
  Wire.endTransmission(true);

  //次のアドレスの読み込み。16bitのshort型に格納。
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write(memory_address + 0x1);
  Wire.endTransmission(false);

  Wire.requestFrom(DEVICE_ADDRESS,1);
  short d2 = Wire.read();
  Wire.endTransmission(true);

  //一つ目のデータを8ビットシフト。
  d1 <<=8;

  //2つのデータを結合して返す。
  return d1 | d2;
}

void setup() {
  Wire.begin();
  Serial.begin(9600);
}



void loop() {
  float acc_x = i2c_read2(X_a)/ACC_RESOLUTION * ACC_RANGE;
  float acc_y = i2c_read2(Y_a)/ACC_RESOLUTION * ACC_RANGE;
  float gyro  = i2c_read2(Z_g)/GYR_RESOLUTION * GYR_RANGE;

  Serial.println(acc_x);

  delay(10);
}

測定レンジと内部LPFの設定

このセンサは測定レンジが加速度は±2G~±16G、ジャイロは±250deg/s~±2000deg/sまで設定可能です。設定は特定のメモリアドレスにデータを書き込むことで行います。

メモリアドレスに値を書き込む関数i2c_writeを用意しました。

//メモリアドレスに値を書き込む
void i2c_write(char address, char value){
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write(address);
  Wire.write(value);
  Wire.endTransmission(true);
}

これを使って、次のような感じに設定を行います。メモリアドレス26~29が設定値を書き込むアドレスです。

i2c_write(26, 0b00000110);//角加速度LPF設定(Bandwidth:5Hz Delay:33.48ms)
i2c_write(27, 0b00000000);//角加速度レンジ設定([0b000??000] 00:250dps 01:500dps 10:1000dps 11:2000dps)
i2c_write(28, 0b00000000);//加速度レンジ設定([0b000??000] 00:2g 01:4g 10:8g 11:16g)
i2c_write(29, 0b00000110);//加速度LPF設定(Bandwidth:5Hz Delay:66.96ms)

結果を見てみる

X軸とY軸の加速度をプロットしてみました。縦軸が加速度[G]で横軸が時刻です。いい感じに取れてると思います!

加速度をプロットしたグラフ。縦軸が加速度[G]、横軸が時間。青がX軸、赤がY軸。

この例ではLPF設定をカットオフ周波数5Hzに設定したので、かなり強いフィルターがかかっています。それでも手で振ったくらいの加速度は問題なく表示されます。電動歯ブラシを当ててみたところ、殆ど値が変化しませんでした。ここまで強いフィルターが必要な状況はそう無いと思いますが、必要に応じて調整するのがいいと思います。

今回は以上です。

ESP8266で指パッチン検出器を作る②

前回、FFTと相関係数で指パッチンを検出する仕組みを考えました。ところが、実際使ってみると何もしていないのに作動してしまうことがたまにあり、完璧ではありませんでした。

そこで今回は、誤作動しないような判定をSVM(サポートベクターマシン)で実現した話をご紹介します。

SVM(サポートベクターマシン)とは

SVMはデータの集合に対して、データを2つに分類するような境界線(面)を引く手法です。例によって、説明は適当です。詳しく知りたい方は「SVM」で検索すれば簡単に見つかると思います。

今回はこれを使って指パッチンデータとそれ以外のデータを分離したいと思います。分離が出来たら、得られた境界面を使って新たに得られたデータが指パッチンかそれ以外のどちらのグループに所属するか判定します。

SVMは学習データから分離面を求める時こそ多少計算を必要とするものの、一度分離面が得られたら新たなデータを判定するときは単純な計算ですむという利点があります。したがって学習だけパソコンで行えば、判定するときはESP8266でも十分です。

SVMを使って分離面を引く

前回は指パッチンの正解データだけを使いましたが、今回は指パッチンでないデータも必要です。前回同様にマイク入力をサンプリング周波数8000Hz、サンプル数1024のFFTにかけたデータ(512次元)を扱います。指パッチンしたときに出力されたデータと、それ以外の例えば手を叩いたときのデータをそれぞれ100データくらい用意しました。

早速SVMを使って分離面を引きたいと思います。SVMによる学習はpythonのライブラリであるscikit-learnを使いました。

環境を整えるのも面倒なので、Google Colaboratoryを使います。Google ColaboratoryはGoogleが提供しているWeb上でコーディングできるpython環境で、必要なパッケージも全て最初から入っているのでめちゃめちゃ簡単に開発を始められます。初めて使いましたが簡単すぎて驚きました。

scikit-learnでどうやってSVMをするかは下記のサイトがとても参考になりました。

https://data-science.gr.jp/implementation/iml_sklearn_svm.html

(ほとんど上記サイトのままですが、一応僕が使ったコードは後でgitか何かで公開する予定です。)

プログラムを走らせるとサポートベクトルとモデルの精度などの情報が出力されます。

得られた分離面を使ってデータを判定する

得られた分離面を使って、指パッチン判定を行っていきます。分離面といっても上記サイトの例ではRBFカーネルを用いた非線形分離を行っていますので、得られるのは面の式ではなく何個かのサポートベクトルになりますが、この辺の詳しい説明は割愛させていただきます(正直に言うと自分がまだ十分に理解していないからです笑)

とにかく、このサポートベクトルを使って新しいデータを判定するには、次の式に代入すれば良いとのことです。

サポートベクター分類の場合,以下の式を解くことで分類が出力される.上のパラメーターにおいて .support_vectors_ は以下の \(x_i\),.dual_coef_ は \(y_i\alpha_i\),.intercept_ は \(\rho\) である.

$$\begin{eqnarray}\operatorname{sgn}(\sum_{i=1}^{n}y_i\alpha_iK(x_i,x)+\rho)\tag{1}\end{eqnarray}$$

また,カーネルの中身は今回用いた RBF の場合,以下で計算されるが,このとき用いるのが .gamma であり,これは以下の \(\gamma\) である.すなわち,上のパラメーターさえ抽出すればものすごく簡単な計算で予測結果を得ることができる.これは別のプログラミング言語で書き換える際に便利.

$$K(x,x’)=e^{(-\gamma|x-x’|^2)}\tag{2}$$

上記サイトより引用

つまり、式(1)に学習によって得られたサポートベクトルと定数、マイクから拾ったデータを代入すれば、出てきた数値の符号によって指パッチンかそうでないかを判定できます。

この式をプログラムとして実装して、実際に判定するとこんな感じになります。

縦軸がSVMの出力、横軸はデータのindex。ある程度大きい音がしたときのみ判定。

グラフは作ったSVMを実行しながら何回か物音を立ててみた結果です。マイク入力に対して常にFFT→SVMを行うと計算が追いつかないので、ある程度大きい音がしたときだけ判定を行うようにしています。

見て分かる通り、指パッチンしたときだけSVMの出力が正になっていることがわかります。自分で言うのもなんですが、かなりいい精度で検出できます。手や硬いものをぶつけても検出しませんし、後ろで音楽を流していてもちゃんと検出してくれます。たまに指パッチンしても検出しないときがありますが、何もしていないときに誤検出してしまうことはありませんでした。

指パッチン検出器、完成です!
あとはこれを使って色々なアクションを起こすデバイスを作っていく予定です。

最後までご覧いただきありがとうございました!
こんなことできたらいいな、というアイデアがありましたら是非コメントで教えて下さい。

ESP8266で指パッチン検出器を作る①

最近私生活がバタバタしていたので久しぶりの投稿です。ちょっと前から考えていた指パッチンで何かアクションを起こすデバイスが試行錯誤を重ねようやく完成したので、作り方をご紹介しようと思います。

今回の目標

今回の目標は「指パッチンしたら何か起こる」デバイスを作ることです。

  • 指パッチン「だけ」を検出して何かアクションを起こす。手を叩いたり何かがぶつかる音では反応しない。
  • できるだけ誤作動(指パッチンしていないのにアクション)しない。
  • コストと消費電力を抑えるため、ラズパイではなくESP8266を使う。そのため、C++でプログラムを書き、しかもコード量を減らす必要がある。

家に帰ってきたら指を鳴らしてカッコよく電気をつけたい。そんな要望に当システムがお応えします。

使用したセンサとマイコンボード

当記事でよく使っているESP8266モジュールと、アンプ付きマイクモジュールを使用しました。

指パッチンの音を解析する

指パッチンの音の特徴は?

指パッチンだけを検出するためには、指パッチンの音の特徴を捉える必要があります。マイクで取った音の信号だけでは、データがバラバラすぎて判定できません。

フーリエ変換してみる

指パッチンの音と手を叩く音は、同じ単発音ですが、人間の耳には音色の違いがはっきり分かります。その理由は音に含まれる周波数の配合が異なるためです。

そこでフーリエ変換を使って周波数分析を行います。周波数分析とは一定時間の音の時系列データの中に、各周波数成分がどのくらい入っているかを調べる手法です(ざっくり)。フーリエ変換を行うと、時系列データ(横軸が時間、縦軸が振幅)が周波数成分に変換されて、横軸が周波数、縦軸がパワースペクトルになります。

周波数分析を使えば指パッチンと手を叩く音で異なる結果が得られるはずです。
指パッチン、手を叩く、金属をぶつけるの3種類の音を周波数分析した結果がこちらです。

3種類の音をフーリエ変換した結果。指パッチン同士はなんとなく似てそう。

なんとなく指パッチンは似ている波形が出ています。逆にそれ以外は全く違う周波数的特徴を持っていることがわかります。次はこの指パッチンとそれ以外のデータを分類する判定アルゴリズムを作っていきます。

指パッチンの判定

周波数分析が出来たとして、どういう波形なら指パッチンと判断するかを決める必要があります。そのためには指パッチンに近いかどうかを定量的に評価する指標が必要です。評価の方法としては色々ありますが、相関係数を使う方法とサポートベクターマシンを使う方法を思いついたのでやってみました。

相関係数を使って判定

得られたデータがどれくらい指パッチンに似ているか定量的にわかればいいので、まず相関係数を使う方法を思いつきました。相関係数は2つのデータ(ベクトル)が似ているかどうかを数値で表すことが出来ます。

まず指パッチンを何回か録音して、指パッチンの平均的なデータを求めておきます。実際に判定するときは、この平均データと新たに得られたデータを比較することによって、得られたデータがどのくらい指パッチンに似ているか判定します。

指パッチンを20回録音して得られた平均データがこちらです。

指パッチン20回の平均データ。これを基準に相関係数を求める。

これと新たに得られたデータを比較して、相関係数が高ければ指パッチンと判定します。以下は指パッチンの平均データをx軸、新たに得られたデータをy軸にとった散布図です。教科書で見たことあるようなきれいなグラフになりました笑。

平均をデータに近い=相関が強い

相関係数はデータが似ているほど1に近くなります。逆に全く相関がなければ0になります。指パッチンと指パッチン以外の約100回のデータで実際に判定を行ってみたところ、次のようになりました。

約100回のデータに対して相関係数を計算。指パッチンはちゃんと相関係数が大きくなっている。

これを見ると、指パッチンのときはだいたい0.4以上の相関係数が出ていることがわかります。

これをトリガーにすれば実用的な判定システムが作れそうです。

長くなってきたので今回はここまでにします。
次回に続きます。

次回→相関係数で判定するデメリットとSVM(サポートベクターマシン)

ESP8266で時刻取得ができない

ESPで実行ログに現在時刻をつけて確認したいと思い、ネットから時刻を取得する方法を調べてみました。

こちらで紹介されているコードをそのまま使ってみて、ちゃんと取得できたので自分のプログラムに入れてみたのですが、なぜか取得できませんでした。

取得できない原因

原因は意外なところにあって、WiFi接続を固定IPアドレスにしていたからでした。ESPWiFiはIPアドレスやゲートウェイの設定はあってもDNSサーバーを設定する方法が無いらしく、固定IPだけ設定すると名前解決ができなくて取得に失敗するっぽいです。

  (中略)
  /* 
   *  固定IPに設定していると、名前解決できない!(=ドメイン名でアクセスできない)
   */
  IPAddress ip(192,168,1,33);
  IPAddress gateway(192,168,1,1);
  IPAddress subnet(255,255,255,0);
  WiFi.config(ip, gateway, subnet);
  
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while(WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println();
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());

解決方法

解決方法としては、mDNSとかいうライブラリを使って名前解決できるようにするか、簡易的には以下のようにNTPサーバーをIPアドレスで指定すればOKです。

(中略)
//configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
// 名前解決できないときは直接IPアドレスでNTPサーバーを指定
configTime( JST, 0, "133.243.238.244");

これだけ。簡単。
本当はちゃんとDNS使いたいけど今の環境だと色々難しいので早く回線契約したい。

帰宅したら自動で解錠するスマートロックを自作する④~指紋認証編~

前回、タクトスイッチを使って外から解錠するやつを作りました。今回はいよいよスマートロックっぽいことをしていきます。なんと指紋認証に挑戦です。意外と簡単に、かつ安くできましたのでご紹介します。

作りたいもの

前回 のつづきです。ICカードで解錠はいったん置いておきます。(作ったけど、記事にするのが面倒くさい。。。)

今回の目標は、指でセンサにふれると解錠してくれるスマートロックを作ることです。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. 指紋認証で解錠
  5. スマホを持って近づくだけで解錠

前回から追加で必要なもの

  • 指紋モジュールDY50 1200円くらい

Amazonで最安だった指紋認識モジュールです。高いやつだと4000円くらいするのにこちらは1200円なのでちょっと不安ですが、きちんと認識してくれます。若干認識までにかかる時間が長いかも?。指を置いてから0.5~1秒くらいかかります。

導線にピンヘッダをはんだ付けする

指紋モジュールから出ている線はVCC、GND、TX、RXの四本です。これをArduinoで接続するためにピンをはんだ付けします。

(以下、作業途中の写真を撮るのを忘れたのでイメージ画像です。)

モジュール付属のコネクタ・ケーブルを半分に切断して、ジャンパワイヤとピンをはんだ付けしていきます。

付属のケーブルを半分に切断
ケーブル+ジャンパワイヤ+ピンをはんだ付け。

ちなみに、モジュールの線の配置はこのようになっています。

ESP8266に接続

UART接続でESP8266と通信しますので、以下のように接続します。マイコンとの通信はソフトウェアシリアルを使うので、TXとRXをESPのD2とD3につなぎます。(追記:僕が使っているESP8266ではソフトウェアシリアルが使えませんでした!後述します。)

DY50ESP8266
VCCVin
TXD2
RXD3
GNDGND

指紋認識テスト

モジュールが接続できたら指紋認証がうまくいくかテストしてみます。ArduinoIDEのライブラリ管理から検索するか、adafruitのgitからモジュール用のドライバとサンプルプログラムをダウンロードします。

ArduinoIDEからサンプルプログラムを開いてenrollを書き込んでみます。これだけで指紋の登録ができる…と色々なサイトに書いてありますが、僕の場合は上手くいきませんでした…。プログラムを走らせてシリアルモニタを開いても「sensor not found.」と表示され、そもそも何も通信できていないようです。

色々調べた結果、原因はESP8266モジュールのソフトウェアシリアルが怪しいと結論づけました。ESP8266はデフォルトの通信レートが119200bpsであるのに対し、このモジュールは9600bpsで通信するので上手くいっていないのだと思います。ネットの記事を調べてESP8266の通信レートを9600に落とす方法を試しましたが、僕の持っているモジュールでは変更しても電源を入れ直すと通信レートが戻ってしまい上手く行きませんでした。

どうするか

ESP8266でソフトウェアシリアルが使えないならどうするか、これに結構悩みました。

案① Arduino UnoとかNanoとか他のモジュールを使う。

ただし、WiFiから解錠したり他の機器との連携は無理。NanoとESPを両方使えばできるがなんか無駄な気がする。

案② ESP8266のハードウェアシリアルを使う。

ただし、シリアルモニタが使えなくなるためデバッグができなくなる。ログをWiFi経由で確認できるようにすれば一応デバッグできる。

3日くらい悩んでましたが、今後WiFiはやっぱり使いたいのと、これまで作ってた基盤がESP用になっているのでまた1から基盤にはんだ付けし直すのが面倒という理由で案②を採用することにしました。

でもこの方法だとシリアルモニタからモジュールに数値を送信することができないんですよね…。指紋の登録時には番号を送信する必要があるので、指紋登録時は①、運用時は②という面倒くさいことになっております。

指紋を登録してみる

指紋登録だけはArduino Nanoを使って行います。adafruitのサンプルプログラムからenrollを書き込んで、指紋モジュールのTXとRXをArduinoのD2とD3につなぎます。

シリアルモニタを開いて、メッセージ通りに番号を入力して指を2回置けば登録完了です。

登録した指紋は電源を切っても保持されているので、よく使う指を全部登録したら次はESPにつないで認証できるかやってみます。

指紋を認識できるかテストしてみる

ここからはモジュールをESP8266につないで作業していきます。ソフトウェアシリアルではなくハードウェアシリアルを使うので接続はモジュールのTX、RXとESPのRX、TXです。また、プログラムの書き込みを行う際はモジュールを外さないと書き込めません。それとシリアルモニタは使えません。

書き込むプログラムはサンプルプログラムfingerprintをちょっと変えるだけでOKです。

// Adafruitのサンプルプログラムfingerprintをハードウェアシリアルに改変
// Serial.printlnは全部コメントアウトしています。

#include <Adafruit_Fingerprint.h>

const int led_pin = 16;  // LEDはD0に接続

// On Leonardo/Micro or others with hardware serial, use those! #0 is green wire, #1 is white
// uncomment this line:
// #define mySerial Serial1

// For UNO and others without hardware serial, we must use software serial...
// pin #2 is IN from sensor (GREEN wire)
// pin #3 is OUT from arduino  (WHITE wire)
// comment these two lines if using hardware serial
//SoftwareSerial mySerial(2, 3);  // ハードウェアシリアルを使うのでコメントアウト

Adafruit_Fingerprint finger = Adafruit_Fingerprint(&Serial);  // ハードウェアシリアル

void setup()  
{
  //Serial.begin(9600);
  //while (!Serial);  // For Yun/Leo/Micro/Zero/...
  //delay(100);
  //Serial.println("\n\nAdafruit finger detect test");

  // set the data rate for the sensor serial port
  finger.begin(57600);
  
  if (finger.verifyPassword()) {
    //Serial.println("Found fingerprint sensor!");
  } else {
    //Serial.println("Did not find fingerprint sensor :(");
    while (1) { delay(1); }
  }

  finger.getTemplateCount();
  //Serial.print("Sensor contains "); Serial.print(finger.templateCount); Serial.println(" templates");
  //Serial.println("Waiting for valid finger...");

  pinMode(led_pin, OUTPUT);
}

void loop()                     // run over and over again
{
  int num = getFingerprintIDez();

  // 認証成功したらLEDを光らせる
  if( num > 0 ){
    digitalWrite(led_pin, HIGH);
    delay(1000);
    digitalWrite(led_pin, LOW);
  }
  delay(50);            //don't ned to run this at full speed.
}

uint8_t getFingerprintID() {
  uint8_t p = finger.getImage();
  switch (p) {
    case FINGERPRINT_OK:
      //Serial.println("Image taken");
      break;
    case FINGERPRINT_NOFINGER:
      //Serial.println("No finger detected");
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      //Serial.println("Communication error");
      return p;
    case FINGERPRINT_IMAGEFAIL:
      //Serial.println("Imaging error");
      return p;
    default:
      //Serial.println("Unknown error");
      return p;
  }

  // OK success!

  p = finger.image2Tz();
  switch (p) {
    case FINGERPRINT_OK:
      //Serial.println("Image converted");
      break;
    case FINGERPRINT_IMAGEMESS:
      //Serial.println("Image too messy");
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      //Serial.println("Communication error");
      return p;
    case FINGERPRINT_FEATUREFAIL:
      //Serial.println("Could not find fingerprint features");
      return p;
    case FINGERPRINT_INVALIDIMAGE:
      //Serial.println("Could not find fingerprint features");
      return p;
    default:
      //Serial.println("Unknown error");
      return p;
  }
  
  // OK converted!
  p = finger.fingerFastSearch();
  if (p == FINGERPRINT_OK) {
    //Serial.println("Found a print match!");
  } else if (p == FINGERPRINT_PACKETRECIEVEERR) {
    //Serial.println("Communication error");
    return p;
  } else if (p == FINGERPRINT_NOTFOUND) {
    //Serial.println("Did not find a match");
    return p;
  } else {
    //Serial.println("Unknown error");
    return p;
  }   
  
  // found a match!
  //Serial.print("Found ID #"); Serial.print(finger.fingerID); 
  //Serial.print(" with confidence of "); Serial.println(finger.confidence); 

  return finger.fingerID;
}

// returns -1 if failed, otherwise returns ID #
int getFingerprintIDez() {
  uint8_t p = finger.getImage();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.image2Tz();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.fingerFastSearch();
  if (p != FINGERPRINT_OK)  return -1;
  
  // found a match!
  //Serial.print("Found ID #"); Serial.print(finger.fingerID); 
  //Serial.print(" with confidence of "); Serial.println(finger.confidence);
  return finger.fingerID; 
}

変更点としてはmySerialの部分をSerialに変えて、Serial.printlnをすべてコメントアウトするだけです。

シリアルモニタが使えないので、確認用のLEDをD0とGNDにつけます。認証が成功するとLEDを1秒間光らせます。

認証成功で解錠する

前回 のプログラムの解錠条件に、今回の指紋認証の結果を加えるだけです。最終的なプログラムを載せておきます。

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
//#include <wifi_setting.h>
#include <Adafruit_Fingerprint.h>
#include <MyFunctions.h>

// ルータ設定
IPAddress ip(192,168,100,102);
IPAddress gateway(192,168,100,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;
const int button_pin = 14;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&Serial);  // ハードウェアシリアル
LogBuffer action_log(64);

void door_open(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(170);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void door_close(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(10);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

void handle_log(){
  server.send(200, "text/html", action_log.to_HTML());
}

bool secret_command(){
  // HIGHがスイッチが押されていない状態
  // LOWがスイッチが押されている状態
  
  unsigned long pressed_time = millis();
  unsigned long released_time = millis();
  int now_state = LOW;
  int last_state = LOW;
  
  float button_lpf = 0.0;  // センサ信号にローパスフィルタ(LPF)をかけた値
  
  String key = "---.--..-.";  //モールス符号でO・P・E・N
  String input = "";
  while(millis() - released_time < 3000){

    button_lpf = 0.9 * button_lpf + 0.1 * digitalRead(button_pin);
    if( button_lpf > 0.8 ){
      now_state = HIGH;
    }else if( button_lpf < 0.2 ){
      now_state = LOW;
    }

    if( last_state == HIGH && now_state == LOW ){
      // ボタンが押されたとき、HIGH→LOW
      pressed_time = millis();
    }else if( last_state == LOW && now_state == HIGH){
      // ボタンが離されたとき、LOW→HIGH
      released_time = millis();

      // ボタンを押していた時間で「.」か「-」を追加
      input += (released_time - pressed_time < 300) ? "." : "-";
    }

    last_state = now_state;
    delay(1);
  }
  
  if(key == input){
    return true;
  }else{
    return false;
  }
  
}

int getFingerprintIDez() {
  uint8_t p = finger.getImage();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.image2Tz();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.fingerFastSearch();
  if (p != FINGERPRINT_OK)  return -1;
  
  // found a match!
  //action_log.add("Found ID #"); action_log.add(finger.fingerID); 
  //action_log.add(" with confidence of "); action_log.add(finger.confidence);
  return finger.fingerID; 
}

void setup() 
{
  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  //WiFi.begin(WIFI_SSID, WIFI_PWD);

  // WiFiに接続するまで待つ
  action_log.add("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    action_log.add(".");
  }
  
  action_log.add("");
  action_log.add("Connected!");
  action_log.add("IP Address: ");
  action_log.add(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.on("/log", handle_log);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);
  door_close();

  // 指紋センサ
  finger.begin(57600);
  
  if (finger.verifyPassword()) {
    action_log.add("Found fingerprint sensor!");
  } else {
    action_log.add("Did not find fingerprint sensor :(");
    while (1) { delay(1); }
  }

  finger.getTemplateCount();
  action_log.add("Sensor contains "); action_log.add(finger.templateCount); action_log.add(" templates");
  action_log.add("Waiting for valid finger...");
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }  
  door_sensor_last = door_sensor;

  // ボタンで解錠
  if(digitalRead(button_pin) == LOW){
    bool success = secret_command();
    
    if(success){
      door_open();
    }
  }

  // 指紋で解錠
  if( getFingerprintIDez() > 0 ){
    door_open();
    action_log.add("Door opened.");
  }
  
  delay(10);
}

シリアルモニタが使えないのでWiFiからログを確認できる機能を追加しています。何かメッセージを出したいときはaction_logに保存しておき、http://192.168.xxx.xxx/logで表示します。action_logのクラスはMyFunction.hに別ファイルとして書いてます。

以上です。

(追記予定)

帰宅したら自動で解錠するスマートロックを自作する②~ボタンで解錠編~

前回はドアが閉まったら自動で鍵をかけるシステムを作りましたが、それだと開けることができないので結局鍵を出さないといけなくて面倒です。そこで今回は鍵の代わりにタクトスイッチを使った暗証番号的な方法で解錠する方法をご紹介します。

作りたいもの

前回のつづきです。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. スマホを持って近づくだけで解錠

前回から追加で必要なもの

  • タクトスイッチ
押すと導通するふつうのタクトスイッチ

ドアの外側にスイッチをつける

外から押せる位置にスイッチをつけます。うちのドアの横のすき間から線を通せたので普通の導線を使っています。ドアのすき間が狭い場合はフラットケーブル等を使うと良いと思います。ESP8266側はGNDとデジタル入出力に繋ぎます。

方法を決める

ボタンを準備したら次はどんなパターンでボタンを押したら解錠するか決めます。

やり方はみなさんのオリジナリティを出せるところだと思います。例えばボタンを押す順番や、押す時間の長短などを組み合わせて秘密の暗号にします。ここでは長短の組み合わせをパスワードにしてみます。

長短の組み合わせ(モールス符号)をパスワードにしてみる

皆さんご存じモールス符号をパスワードにしてみます。例えば「OPEN」のモールス符号で「--- ・--・ ・ ―・」と押した時に解錠するようにしてみます。

新しく追加したのは関数secret_command()の部分です。ボタンを押したり離したりしたときに、digitalReadの値の立ち下がり・立ち上がりを検出して、ボタンを押していた時間で「・」か「―」を判定します。3秒以上入力がなかったら終了して、入力された符号とパスワードが一致していたら解錠します。

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

// ルータ設定
IPAddress ip(192,168,100,102);
IPAddress gateway(192,168,100,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;
const int button_pin = 14;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト


void door_open(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(170);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void door_close(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(10);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

bool secret_command(){
  // HIGHがスイッチが押されていない状態
  // LOWがスイッチが押されている状態
  
  unsigned long pressed_time = millis();
  unsigned long released_time = millis();
  int now_state = LOW;
  int last_state = LOW;
  
  float button_lpf = 0.0;  // センサ信号にローパスフィルタ(LPF)をかけた値
  
  String key = "---.--..-.";  //モールス符号でO・P・E・N
  String input = "";
  while(millis() - released_time < 3000){

    button_lpf = 0.9 * button_lpf + 0.1 * digitalRead(button_pin);
    if( button_lpf > 0.8 ){
      now_state = HIGH;
    }else if( button_lpf < 0.2 ){
      now_state = LOW;
    }

    if( last_state == HIGH && now_state == LOW ){
      // ボタンが押されたとき、HIGH→LOW
      pressed_time = millis();
    }else if( last_state == LOW && now_state == HIGH){
      // ボタンが離されたとき、LOW→HIGH
      released_time = millis();

      // ボタンを押していた時間で「.」か「-」を追加
      input += (released_time - pressed_time < 300) ? "." : "-";
    }

    last_state = now_state;
    delay(1);
  }
  
  if(key == input){
    return true;
  }else{
    return false;
  }
  
}

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

  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  WiFi.begin(WIFI_SSID, WIFI_PWD);

  // WiFiに接続するまで待つ
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);

  door_close();
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }  
  door_sensor_last = door_sensor;

  // ボタンで解錠
  if(digitalRead(button_pin) == LOW){
    bool success = secret_command();
    
    if(success){
      door_open();
    }
  }
  
}

完成!

簡単ではありますがこれだけでパスワード解錠的な機能をつけることができました。他にもテンキーなどのデバイスを付けてパスワードと一致したら解錠とかもできると思います。もっといいやり方があるよ!という方はコメントで教えてください。

ちゃんとセキュリティを考えるなら、10回入力に失敗したら一定時間操作を受け付けなくなるとかも考えたほうが良いかもしれません。今回の例ならパスワードが10ケタなので、2の10乗=1024回試されたら破られます。もう少しパスワードは長いほうが良いかも。

←前回    次回→

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を減らすなどして調整してみてください。

以上です。