ラベル ホームオートメーション の投稿を表示しています。 すべての投稿を表示
ラベル ホームオートメーション の投稿を表示しています。 すべての投稿を表示

2020年8月23日日曜日

【家の自動化プロジェクト】#4 Node.jsでWEBインターフェースを作る

どうもcaketetuです。
今回は、前回製作したデバイスにアクセスするためのインターフェースをNode.jsで作ります。
より具体的にはRaspberry Pi上にWEBサーバを作り、スマホやPCのブラウザからアクセスし指示コマンドを受け付けるようにします。受信した指示コマンドをBLE信号に変換し前回製作したデバイスに飛ばせるようにします。要するに以下の経路を作ります。

webブラウザ →  <<socket.io通信>  →  webサーバ  ->  <<BLE通信>>  →  BLEデバイス



ちなみに現在のRaspberry Piの様子は…



こんな感じになってます。
3B+です。OSはちょっと古いStretch。
Amazonで安く購入できたタッチパネル付き液晶を付けている以外は特にいじっていません。いずれは音楽プレイヤー機能、キオスク機能、フォトフレーム機能を搭載したいと考えております。



PythonでBLE

bluepyでPythonからBLE信号を出せるようにします。
送信と読み込みのサンプルを貼っておきます。

ble_write.py
# -*- coding: utf-8 -*-
import time
import bluepy

HANDLE_LED = 0x002d		#ハンドル
devadr = "3C:71:BF:F0:66:26"   # アドレス

def main():
    peri = bluepy.btle.Peripheral()
    peri.connect(devadr, bluepy.btle.ADDR_TYPE_PUBLIC)
    peri.writeCharacteristic(HANDLE_LED, b'Ms' )
    time.sleep(5)
    peri.disconnect()

if __name__ == "__main__":
    main()
ble_read.py
# -*- coding: utf-8 -*-
import bluepy
import time

HANDLE_SERIAL = 0x002d		#ハンドル
devadr = "24:6F:28:9D:C6:6A"   # アドレス

def main():
    peri = bluepy.btle.Peripheral()
    peri.connect(devadr, bluepy.btle.ADDR_TYPE_PUBLIC)
    time.sleep(1)
    serialnum = peri.readCharacteristic(HANDLE_SERIAL)
    print(len(serialnum))
    print( serialnum.decode('utf-8') )
    peri.disconnect()

if __name__ == "__main__":
    main()

bluepyの簡単な使い方ですが、
まず 
peri = bluepy.btle.Peripheral()で宣言 、
peri.connect(デバイスアドレス, bluepy.btle.ADDR_TYPE_PUBLIC) 
で接続します。読み込みは 
データ = peri.readCharacteristic(ハンドル)
送信は 
peri.writeCharacteristic(ハンドル, データ )
です。最後に
peri.disconnect()
でクローズしましょう。

デバイスのハンドル、アドレスの調べ方はここでご紹介しています。

ここで、前回作ったデバイスの通信コードですが、
//*****命令コード表****
CMD_SERVO_MOVE 0x78 //照明用サーボ動作指令
命令コード + 角度(2byte) + 動作時間(2byte)
CMD_LIGHT_LIV   0x80 //リビング照明
命令コードのみ
CMD_LIGHT_ENT 0x81 //エントランス照明
命令コードのみ
CMD_IR_SEND 0x79 //エアコン、データ書き込み
命令コード  + ボタン + 温度+ 風力+ 動作モード + ON/OFF (全1byte)
CMD_IR_POWER_OFF 0x82 //エアコンOFF
命令コードのみ
CMD_IR_REPEAT 0x83 //エアコン指令をリピート
命令コードのみ
REQ_DATA 0x50 //全データリクエスト
   命令コードのみ(読み取りで実行)

となっていました。これにあわせて作るとこんな感じになります。参考にどうぞ。
コードを書く時詰まったのがバイト操作です。WEBソケット通信の命令は文字列なのに対しBLE通信はバイト列になっているので数値を送るときはバイト単位の結合、分離が必要です。これはpythonのpack,unpackモジュールで実現できます。



Raspberry Pi上にWEBブラウザを作る

WEBブラウザの作り方はこちらに別記事でまとめました。これに上記で作成したPython
プログラムを呼び出すコードを追加することでWEBブラウザから指示を出せるようになります。現在は以下のようになってます。

app.js
	
//********************************************************** */
//      モジュールオブジェクトの初期化
//********************************************************** */
var express = require('express');
var app = express();
var http = require('http').Server(app);
const io = require('socket.io')(http);          //Socket.io
const execSync = require('child_process').execSync; //外部コマンド実行(同期)

//********************************************************** */
//      グローバル変数
//********************************************************** */
//エアコン送信パラメータ
var ac_para = ['b_off', '27', 'auto', 'cooling', 'off'];

//********************************************************** */
//      EXPRESS関連
//********************************************************** */
//サーバー実装の前に、エラーハンドリングを記載します。
process.on('uncaughtException', function(err) {
    console.log(err);
  });
//テンプレートエンジンの指定s
app.set("view engine", "ejs");
//ルーティング
app.get('/', (req, res)=> { res.render('index'); });
//3000番ポートで待ち受けてサーバー開始
http.listen(3000, function () {
    console.log("Start server at port:3000...");
});

//********************************************************** */
//    SocketIOリクエスト処理
//********************************************************** */
io.sockets.on("connection", function (socket) {
    //=====照明コントロール=====
    socket.on("light", function (msg) {
        var res;
        if(msg=='living'){
            res = request_shellsync("python3 ble_light_ctrl.py servo 55 1000");
        }else if(msg=='entrance'){
            res = request_shellsync("python3 ble_light_ctrl.py servo 115 1000");
        }
        console.log(res.toString());
    });
    //=====エアコンコントロール=====
    socket.on("aircon", function (msg) {
      var res;
      var cmd = "python3 ble_aircon_ctrl.py ";
      if(msg.cmd=='send'){
          cmd += msg.btn + " ";
          cmd += msg.temp + " ";
          cmd += msg.level + " ";
          cmd += msg.select;
          res = request_shellsync(cmd);
      }else if(msg.cmd=='off'){
          cmd += "off";
          res = request_shellsync(cmd);
      }
      console.log(res.toString());
    });
    //=====画面コントロール=====
    socket.on("display", function (msg) {
      var res;
      if(msg=='on'){
          res = request_shellsync("vcgencmd display_power 1");
      }else if(msg=='off'){
          res = request_shellsync("vcgencmd display_power 0");
      }
      console.log(res.toString());
    });

    //端末データリクエスト
    socket.on("data_req", function (msg) {
        var res;
        res = request_shellsync("python3 ble_read.py");
        socket.emit("req_data", res.toString());
        console.log(res.toString());
    });
});

  //********************************************************** */
  //Shellに指示を投げる関数 同期
  //********************************************************** */
  var request_shellsync = function(command){
    var str_cmd =  command;   //コマンド列生成
    //child_processでシェルコマンドを実行
    result = execSync(str_cmd, function(error, stdout, stderr) {
      if (error !== null) {
        console.log('exec error: ' + error);    //エラーなら表示
      }
    });
    return result;  //結果を返す
  }

index.ejs
	
<!DOCTYPE html><html>
    <head>
        <title>Home Ctrl</title>
    </head>
    <body>
        <script src="/socket.io/socket.io.js"></script>
        <script type="text/javascript">
            var socket = io.connect('http://ip_adress:3000');
            //ライトコントロール
            function cmd_light(cmd) {
                socket.emit("light",cmd);
            }
            //エアコンコントロール
            function cmd_aircon(cmd) {
                var btn = document.getElementById('ac_btn');
                var temp = document.getElementById('ac_temp');
                var level = document.getElementById('ac_level');
                var select = document.getElementById('ac_select');
                var send_data = {
                    "cmd" : cmd,
                    "btn" : btn.value,
                    "temp" : temp.value,
                    "level" : level.value,
                    "select" : select.value,
                }
                socket.emit("aircon",send_data);
            }
            function cmd_display(cmd) {
                socket.emit("display",cmd);
            }
            //端末データリクエスト
            function cmd_request() {
                socket.emit("data_req",0);
            }
            //リクエストデータ受信時
            socket.on("req_data", function (data) {
                document.getElementById("request_data").innerHTML=data;
            });

        </script>
        <div class="title"><h1>My Home Automation</h1></div>
        <!-- 端末データ表示 -->
        <div class="function">
            <div class="fc_title">Sensor data</div>
            <div id="request_data">Info:</div>
            <button type="button" onClick="cmd_request()">Data Request</button></br>
        </div>
        <!-- 照明コントロールボタン -->
        <div class="function">
            <div class="fc_title">light switch</div>
            <button type="button" onClick="cmd_light('living')">Living</button>
            <button type="button" onClick="cmd_light('entrance')">Entrance</button>
        </div>
        <!-- エアコン操作ボタン -->
        <div class="function">
            <div class="fc_title">Air Con Controll</div>
            <select id="ac_btn">
                <option value="off">off</option>
                <option value="up">up</option>
                <option value="down">down</option>
                <option value="tistop">tistop</option>
                <option value="tiwkup">tiwkup</option>
                <option value="wind_lev">wind_lev</option>
                <option value="wind_dir">wind_dir</option>
            </select>
            <select id="ac_temp">
                <option value="19">19</option>
                <option value="20">20</option>
                <option value="21">21</option>
                <option value="22">22</option>
                <option value="23">23</option>
                <option value="24">24</option>
                <option value="25">25</option>
                <option value="26">26</option>
                <option value="27">27</option>
                <option value="28">28</option>
                <option value="29">29</option>
            </select>
            <select id="ac_level">
                <option value="auto">auto</option>
                <option value="strong">strong</option>
                <option value="nomal">nomal</option>
                <option value="week">week</option>
            </select>
            <select id="ac_select">
                <option value="cooling">cooling</option>
                <option value="dehumid">dehumid</option>
                <option value="heating">heating</option>
            </select>
            <button type="button" onClick="cmd_aircon('send')">send</button>
            <button type="button" onClick="cmd_aircon('off')">off</button>
        </div>
        <!-- 画面バックライト制御ボタン -->
        <div class="function">
            <div class="fc_title">Display Controll</div>
            <button type="button" onClick="cmd_display('on')">on</button>
            <button type="button" onClick="cmd_display('off')">off</button>
        </div>
    </body>
</html>

Node.jsで外部プログラムを呼び出すにはchild_processモジュールを使います。
const execSync = require('child_process').execSync; //外部コマンド実行(同期)

//********************************************************** */
//Shellに指示を投げる関数 同期
//********************************************************** */
var request_shellsync = function(command){
  var str_cmd =  command;   //コマンド列生成
  //child_processでシェルコマンドを実行
  result = execSync(str_cmd, function(error, stdout, stderr) {
    if (error !== null) {
      console.log('exec error: ' + error);    //エラーなら表示
    }
  });
  return result;  //結果を返す
}

res = request_shellsync("vcgencmd display_power 1");
端末で実行するようなコマンドを文字列で渡すだけなので楽です。pythonだけではなく
シェルコマンドや多言語のプログラムも同じように実行できます。
同じ手法でRaspberry Piの画面をON/OFFするコードも作っています。
実行コマンドは
vcgencmd display_power 1    //画面ON
vcgencmd display_power 0  //画面OFF
です。覚えておくといつか使う時があるかも。



サーバ起動!

ディレクトリ内でnode app.jsを打ち込みサーバを起動します。ブラウザから"http://アドレス:ポート"にアクセスするとコントローラ画面が表示されます。
動作の様子







今回は以上です。インターフェースの見た目がだいぶアレですがかなり実用的になったと思います。私も日常生活では非常に活用していて、電気を消すためいちいち立ち上がったりしなくてよいので便利になったなぁと思います。さらにIoTしてる感による近未来感が良い。

とはいえRaspberry Piでサーバーを動かしている割にはまだまだ機能がショボいので、もっとデバイスを作って機能を増やし、より快適な自動化生活を目指します。

2020年8月21日金曜日

【家の自動化プロジェクト】#3 一つ目のBLEデバイス

どうも、caketetuです。
相変わらず家の自動化プロジェクトを進めています。今回は一つ目のデバイスが完成したので
ご紹介します。



外観


前回でESP32を使ったベースに小型液晶とサブプロセッサとしてのArduino、環境センサ
を乗っけたデバイスを作りました。今回はさらに機能を足して最終的に以下機能を持った
デバイスになりました。(リンクは個別記事です。参考にどうぞ)

・照明操作(アナログサーボ) アナログサーボを動かす
・エアコン操作(赤外線LED) 赤外線でエアコンを操作する
・環境測定(環境センサ) 自動化プロジェクト#2
・画面表示(小型液晶) 自動化プロジェクト#2

全部乗せ感がすごいですね(笑)


回路設計



回路はこんな感じです。赤外線LEDとサーボはAruduinoを介して接続しているので
ちょっとごちゃついています。ESP32のBLEを使うと割込みが不安定になることが
あったのでこのような構成にしました。

照度センサが標準でついているのは寝るときに画面を消すためです。<<意外と重要>>





ハード設計


画像中央のコアユニット部です。タミヤのユニバーサルプレートに合わせて
設計することで他の工作にも応用が利くように工夫しました。


画像左側のユニットにくっついているのはエアコン操作用の赤外線照射
モジュールです。LEDを三個直列につなげて照射範囲を広くしています。
結構遠くでも反応してくれます(5mぐらい?)
2自由度あるので調整も割と簡単です。


画像上部に乗っているのは照明操作用のサーボモータです。一軸で二つの照明を操作
出来ます。既製品のスイッチに左右のネジでクランプして固定します。
既存のモノの面積がとても小さいので、ちゃんと固定するのに苦労しました。



ソフト

上記の個別記事で紹介したサンプルプログラムをこちらで作ったBLEデバイスのプロトタイプにぶち込んで作りました。参考までにすべて載せておきます。

BLE_Unit_Firmware1.0.c      ESP32のメインプログラム
code_list.h                        通信コードヘッダ
Arduino_sub_prossor1.0.c    Aruduino 側プログラム
IR_Ctrl.h                          エアコン操作ヘッダ 

通信プロトコルはESP32側の136行目から始まる関数とcode_list.hに定義されており、
BLEデータの先頭バイトで識別します。
現在、以下のように定義されています。

//*****命令コード表****
CMD_SERVO_MOVE   0x78   //照明用サーボ動作指令  
        命令コード +  角度(2byte) + 動作時間(2byte)
CMD_LIGHT_LIV      0x80   //リビング照明
        命令コードのみ
CMD_LIGHT_ENT       0x81   //エントランス照明
        命令コードのみ
CMD_IR_SEND           0x79   //エアコン、データ書き込み
        命令コード  + ボタン + 温度+ 風力+ 動作モード + ON/OFF (全1byte)
CMD_IR_POWER_OFF 0x82   //エアコンOFF
        命令コードのみ
CMD_IR_REPEAT        0x83   //エアコン指令をリピート
        命令コードのみ
REQ_DATA                 0x50   //全データリクエスト
   命令コードのみ(読み取りで実行)

現在、通信エラーはめったに発生していませんが、チェックサムなどを用いた
エラーチェックをしたほうがいいかもしれませんね。

また、パラメータ設定指令などを作ってパラメータを保存できるようにするのも課題です。



今回は以上です。若干ゴツゴツした見た目が意外とお気に入りです。
次回はRaspberry piにWEBサーバを作りそこから操作できるようにしたいと思います。











2020年7月29日水曜日

【ESP32】 BLEでRaspberry Piと通信する

どうもcaketetuです。
最近暇つぶしにFalloutというゲームを買ってみたら、かなりハマってしまって
猛烈な勢いで時間を消費しています。短期間にゲームをやりこむと燃え尽きて
すぐ飽きてしまうのでほどほどにして現実でもクラフトしましょう。
今回はホームオートメーションの核となるBLEでRaspberryPiと通信してみます。



こちらにBLEの基礎について詳しい解説がありました。実際はライブラリに隠れているので
そこまで意識することはありませんがザっと読んでおくと大変勉強になります。

・BLEプログラムのプロトタイプを作る

ちょっと勉強して理解したつもりになったところでArduinoIDEのサンプルからBLEデバイスの
プロトタイプを作ってみました。

//==========================================================
//    Include Header
//==========================================================
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>

//*****ヘッダ都度部******

//*********************

//==========================================================
//    Macro Definition
//==========================================================
#define SERVER_NAME "ESP32_BLE_TEST"     // サーバー名 各自設定する
//====シグナル定義====
//#define SIGNAL_ERROR      'E'         // (異常発生:Error)
//#define SIGNAL_REQUEST    'r'         // デバイス状態取得要求
//====UUID設定====
#define SERVICE_UUID               "28b0883b-7ec3-4b46-8f64-8559ae036e4e"      // サービスのUUID
#define CHARACTERISTIC_UUID_Notify "2049779d-88a9-403a-9c59-c7df79e1dd7c"      // NotifyUUID
#define CHARACTERISTIC_UUID_RX     "9348db8a-7c61-4c6e-b12d-643625993b84"      // 受信用UUID
#define CHARACTERISTIC_UUID_TX     "e303cfe5-b463-4b82-bbc3-e0789945b499"      // 送信用UUID

//*****マクロ都度部******

//*********************

//==========================================================
//    Function Prototype
//==========================================================
void BLE_Setup(void);

//****関数プロトタイプ都度部*****

//****************************

//==========================================================
//    Global Variable
//==========================================================
BLECharacteristic *pCharacteristicTX;   // 送信用キャラクタリスティック
BLECharacteristic *pCharacteristicRX;   // 受信用キャラクタリスティック
BLECharacteristic *pCharacteristicDD;   // データ送信要求キャラクタリスティック
BLEServer *pServer = NULL;
bool deviceConnected = false;           // デバイスの接続状態
bool bInAlarm  = false;                 // デバイス異常判定
bool oldDeviceConnected = false;        //

//*****グローバル変数都度部******

//****************************

//----------------------------------------------------------
//    接続・切断時コールバック関数
//----------------------------------------------------------
class funcServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    }
    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};

//----------------------------------------------------------
//    シグナル受信時のコールバック
//----------------------------------------------------------
class funcReceiveCallback: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristicRX) {
      // シリアルモニタに表示する
      std::string rxValue = pCharacteristicRX->getValue();
      if (rxValue.length() > 0) {
        //受信したデータを全部表示
        Serial.print("RECV_DATA: ");
        for (int i = 0; i < rxValue.length(); i++) Serial.print(rxValue[i]);
        Serial.println();

        //*******************受信時処理の都度部**********************************

        
        //*******************************************************
      }
    }
    void onRead(BLECharacteristic *pCharacteristicRX) {
      
      //*******************受信時処理の都度部**********************************
      char send_str[] = "Hello world";
      pCharacteristicRX->setValue(send_str);
      Serial.println("BLE:Data Request");
      //*******************************************************
    }
};

//----------------------------------------------------------
//    BLEセットアップ関数
//----------------------------------------------------------
void BLE_Setup(void) {
  BLEDevice::init(SERVER_NAME);   // 初期化処理を行ってBLEデバイスを初期化する
  // Serverオブジェクトを作成してコールバックを設定する
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new funcServerCallbacks());
  // Serviceオブジェクトを作成して準備処理を実行する
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Notify用のキャラクタリスティックを作成する
  pCharacteristicTX = pService->createCharacteristic(
                        CHARACTERISTIC_UUID_TX,
                        BLECharacteristic::PROPERTY_NOTIFY
                      );
  pCharacteristicTX->addDescriptor(new BLE2902());

  // 受信用キャラクタリスティックを作成してシグナル受信時のコールバックを設定する
  pCharacteristicRX = pService->createCharacteristic(
                        CHARACTERISTIC_UUID_RX,
                        BLECharacteristic::PROPERTY_WRITE
                      );
  pCharacteristicRX->setCallbacks(new funcReceiveCallback());

  // サービスを開始して、SERVICE_UUIDでアドバタイジングを開始する
  pService->start();
  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->start();
}

//----------------------------------------------------------
//    Set Up Function
//----------------------------------------------------------
void setup() {
  Serial.begin(115200);  //シリアル設定 
  BLE_Setup();           //BLEセットアップ
}

//----------------------------------------------------------
//    Main Loop Function
//----------------------------------------------------------
void loop() {
  // notifyしたいときはここに書く
  if (deviceConnected) {}
  
  //接続段時の処理
  if (!deviceConnected && oldDeviceConnected) {
    delay(500); // give the bluetooth stack the chance to get things ready
    pServer->startAdvertising(); // restart advertising
    //Serial.println("start advertising");
    oldDeviceConnected = deviceConnected;
  }
  //接続開始時の処理
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;   // do stuff here on connecting
  }
  delay(100);
}

「都度部」と書かれたところに固有の処理を書くことで簡単にBLEデバイスを作れるように
しました。

具体的にはセントラルからデータが送信されると68行目のコールバック関数
が呼ばれ、データはrxValue[]に入っているのでこの値にその下の都度部に処理を書きます。

また、セントラルから読み出し指示があった時は84行目のonRead関数が呼ばれるので
pCharacteristicRX->setValue()にデータをセットすれば送信することができます。

本来は機能ごとにUUIDを振るのが正解っぽいですが送信データの先頭で処理を
場合分けすればいいかなと思ったのでUUIDは一つだけ使っています。



このプログラムでは単体テストできるようにセントラルから来たデータを
シリアルで書き出す処理、読み取り指示があった時は指定文字列を送信するコード
になっています。次にRaspberri Piから接続してデータをやり取りしてみます。


・Raspberry piから接続する

作ったデバイスにRaspberry Piから接続してみます。今回はテストなので
gattoolというソフトを使って直接値を見てみます。
まずはアドレスを調べます。

pi@raspberrypi:~ $ sudo hcitool lescan
LE Scan ...
3C:71:BF:F1:E6:7E (unknown)
3C:71:BF:F1:E6:7E ESP32_BLE_TEST
41:DD:FB:51:61:AB (unknown)
41:DD:FB:51:61:AB (unknown)

アドレスがわかったらCtrl+Cで止めてしまいましょう。私の場合3C:71:BF:F1:E6:7E
であることがわかります。ESP32側のプログラムと一致しますね。
gatttool -b アドレス -I でデバイスを指定しconnectで接続します。

pi@raspberrypi:~ $ gatttool -b 3C:71:BF:F1:E6:7E -I
[3C:71:BF:F1:E6:7E][LE]> connect
Attempting to connect to 3C:71:BF:F1:E6:7E
Connection successful

characteristicsでキャラクタリスティックを見つけましょう。

[3C:71:BF:F1:E6:7E][LE]> characteristics
handle: 0x0002, char properties: 0x20, char value handle: 0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0015, char properties: 0x02, char value handle: 0x0016, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0017, char properties: 0x02, char value handle: 0x0018, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0019, char properties: 0x02, char value handle: 0x001a, uuid: 00002aa6-0000-1000-8000-00805f9b34fb
handle: 0x0029, char properties: 0x10, char value handle: 0x002a, uuid: e303cfe5-b463-4b82-bbc3-e0789945b499
handle: 0x002c, char properties: 0x08, char value handle: 0x002d, uuid: 9348db8a-7c61-4c6e-b12d-643625993b84

今回はCHARACTERISTIC_UUID_RXにアクセスしたいのでハンドルは0x002dになります。
このハンドルを使って通信をしてみましょう。char-read-hnd ハンドル で読み取り、
char-write-cmdでデータを送信します。

[3C:71:BF:F1:E6:7E][LE]> char-read-hnd 0x002d
Characteristic value/descriptor: 48 65 6c 6c 6f 20 77 6f 72 6c 64
[3C:71:BF:F1:E6:7E][LE]> char-write-cmd 0x002d 30313233



ArduinoIEDのシリアルモニタを見るとなんかでています。Raspberry Pi側は
16進数になっているのでわかりずらいですが、アスキーコードに直すとちゃんと
データがやり取りできていることがわかります。



これで無事通信できていることがわかりました。まだBLEについて理解できていない
部分があるので使い方が間違っているかもしれませんが、データを送受信できるように
なったのでとりあえず動かせるようになります。今回作ったプロトタイププログラム
に色々追加してホームオートメーション用BLEネットワークを構築していきたいと
思います。

参考にさせて頂いたサイト

2020年7月12日日曜日

【家の自動化プロジェクト】#2 モジュールをテストしてみた

どうも。caketetuです。
前回はESP32を使ってBLE子機ユニットと拡張モジュールを作りました。
これだけではただマイコンを囲った箱ですので接続できても何もできません。
よって今回は、拡張モジュールにいくつか機能を付け足し、テストしてみよう
と思います。

前回より、以下のモジュールを追加しました。
 ・環境センサモジュール
 ・ミニI/Fパネル
 ・サブプロセッサモジュール



これが全面のミニI/Fパネルです。ここで紹介したSSD1306 OLED
ディスプレイと3つのボタン、赤青のLEDがついています。これで
単体でも最低限の操作をできるようになると思います。




モジュール右側に追加した環境センサモジュールです。使用してるセンサは
BME280とCCS811で気温、湿度、気圧、CO2濃度、総揮発性有機化合物(TVOC)
を測定できます。どちらもI2C接続なので配線は電源合わせ4線で済みます。




モジュール上側に追加したサブプロセッサモジュールです。Arduinoを
マイコン単体で動作させ、シリアル通信で接続します。ESP32はBLEを使ったとき
動作が不安定になる感じがあったので、ハードウェア制御は別のマイコンを
使うことにしました。マイコン単体をArduino化させるのはここで紹介しています。



本ユニット全体の回路としてはこんな感じになってます。本体には
デバック用として圧電ブザーを付けました。子機デバイス一つ目としては
ほとんど完成だと思います。



ここからソフトを書き込んでモジュールごとにチェックしていきます。
まずミニI/Fパネルから動かしていきます。ボタン操作でページを変える
サンプルを作ってみました。
I/Fパネルサンプルプログラム
//*********************************************************
//    Easy_IF_pannel_test
//    ESP32_Core_Unit向け簡易IFパネルテストプログラム
//    1.0   2020 7/3  作成
//*********************************************************

//==========================================================
//    Include Header
//==========================================================
#include <Wire.h>             //I2Cヘッダ(OLED)
#include <Adafruit_GFX.h>     //Adafruit グラフィックヘッダ(OLED)
#include <Adafruit_SSD1306.h> //Adafruit LCDヘッダ(OLED)

//==========================================================
//    Macro Definition
//==========================================================
//ディスプレイ関連
#define SCREEN_WIDTH 128    //OLEDピクセルX
#define SCREEN_HEIGHT 64    //OLEDピクセルY
#define OLED_RESET     -1   //OLEDリセット(本モジュールは無し)
//ピン定義
#define BUZZER_PIN 23
#define LED1 12
#define LED2 14
#define SW1 13
#define SW2 27
#define SW3 26

#define PAGE_MAX 2    //表示ページ最大
#define KEY_OFF 150   //ボタンウェイト時間

//==========================================================
//    Global Variable
//==========================================================
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);   //ディスプレイクラス
int page = 0;     //ページ


//==========================================================
//    Function Prototype
//==========================================================
void show_disp0(void);
void show_disp1(void);
void show_disp2(void);


//----------------------------------------------------------
//    Set Up Function
//----------------------------------------------------------
void setup() {
  Serial.begin(115200);   //シリアル初期化
  ledcSetup(1,12000, 8);  //ブザーPWM初期化

  //ピン設定
  pinMode(LED1,OUTPUT);
  pinMode(LED2,OUTPUT);
  digitalWrite(LED1,HIGH);
  digitalWrite(LED2,HIGH);
  pinMode(SW1,INPUT);
  pinMode(SW2,INPUT);
  pinMode(SW3,INPUT);

  //OLED設定
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // 失敗の時は無限ループ
  }
  display.display();      //ディスプレイ表示
  delay(1000);            //ちょっと待つ
  display.clearDisplay(); //ディスプレイクリア
  
}

//----------------------------------------------------------
//    Main Loop Function
//----------------------------------------------------------
void loop() {
  if(digitalRead(SW1)==LOW){        //ボタン1動作
    ledcAttachPin(BUZZER_PIN,1);    //ブザーピンセット
    digitalWrite(LED1,LOW);         //LED1 ON
    digitalWrite(LED2,LOW);         //LED2 ON
    ledcWriteNote(1, NOTE_C, 4);    //ブザーを鳴らす
    delay(KEY_OFF);                 //指定時間待つ(チャタリング防止)
    ledcDetachPin(BUZZER_PIN);      //ブザーピンを解除
    page++;   //次のページへ
  }else if(digitalRead(SW2)==LOW){  //ボタン2動作
    ledcAttachPin(BUZZER_PIN,1);    //ブザーピンセット
    digitalWrite(LED1,LOW);         //LED1 ON
    ledcWriteNote(1, NOTE_G, 4);    //ブザーを鳴らす
    delay(KEY_OFF);                 //指定時間待つ(チャタリング防止)
    ledcDetachPin(BUZZER_PIN);      //ブザーピンを解除
    page=0;   //最初のページに戻る
  }else if(digitalRead(SW3)==LOW){  //ボタン3動作
    ledcAttachPin(BUZZER_PIN,1);    //ブザーピンセット
    digitalWrite(LED2,LOW);         //LED2 ON
    ledcWriteNote(1, NOTE_A, 4);    //ブザーを鳴らす
    delay(KEY_OFF);                 //指定時間待つ(チャタリング防止)
    ledcDetachPin(BUZZER_PIN);      //ブザーピンを解除
    page--;   //前のページへ
  }
  
  digitalWrite(LED1,HIGH);    //LED1 OFF
  digitalWrite(LED2,HIGH);    //LED2 OFF

  if(page>PAGE_MAX) page=0;         //ページ上限
  else if(page<0) page = PAGE_MAX;  //ページ下限

  //pageによって表示を切り替え
  switch(page){
    case 0: show_disp0(); break;
    case 1: show_disp1(); break;
    case 2: show_disp2(); break;
    default: break;
  }
}

//----------------------------------------------------------
//    Show Display0 Function
//----------------------------------------------------------
void show_disp0(void){
  display.clearDisplay();   //ディスプレイクリア
  display.setTextSize(2);   //テキストサイズ設定
  display.setTextColor(SSD1306_WHITE);    //テキストカラー設定
  display.setCursor(10, 0);     //表示場所設定
  display.println(F("Page 0")); //テキスト出力
  display.display();            //画面表示
}

//----------------------------------------------------------
//    Show Display1 Function
//----------------------------------------------------------
void show_disp1(void){
  display.clearDisplay();   //ディスプレイクリア
  display.setTextSize(2);   //テキストサイズ設定
  display.setTextColor(SSD1306_WHITE);    //テキストカラー設定
  display.setCursor(10, 0);     //表示場所設定
  display.println(F("Page 1")); //テキスト出力
  display.display();            //画面表示
}

//----------------------------------------------------------
//    Show Display2Function
//----------------------------------------------------------
void show_disp2(void){
  display.clearDisplay();   //ディスプレイクリア
  display.setTextSize(2);   //テキストサイズ設定
  display.setTextColor(SSD1306_WHITE);    //テキストカラー設定
  display.setCursor(10, 0);     //表示場所設定
  display.println(F("Page 2")); //テキスト出力
  display.display();            //画面表示
}



こんな感じで動きます。画面も小さいので本当に最低限といった感じです。



次に環境センサモジュールを動かしていきます。
使用したセンサは結構有名でライブラリが豊富にそろっていますので
そちらを使っていくのが楽でしょう。私はSparkFunBME280とSparkFunCCS811
を使うことにしました。とりあえずセンサ値を垂れ流すサンプルを載せます。
//*********************************************************
//    Environmental_Sensor_Test
//    ESP32_Core_Unit向け環境センサユニットテストプログラム
//    1.0   2020 7/3  作成 BME280に対応
//    1.1   2020 7/3  作成 CCS811に対応
//*********************************************************
//==========================================================
//    Include Header
//==========================================================
#include <Wire.h>
#include "SparkFunBME280.h"
#include "SparkFunCCS811.h"

//==========================================================
//    Macro Definition
//==========================================================
#define SDA 18    //I2C SDA PIN
#define SCL 19    //I2C SCL PIN

#define BME280_ADDR 0x76    //センサアドレス BME280
#define CCS811_ADDR 0x5A    //センサアドレス CCS811

//==========================================================
//    Global Variable
//==========================================================
BME280 sen_bme280;    //センサクラス BME280
CCS811 sen_ccs811(CCS811_ADDR);    //センサクラス CCS811
//センサデータ
double temp_act = 0.0, press_act = 0.0,hum_act=0.0;
unsigned short co2=0, tvoc=0;

//==========================================================
//    Function Prototype
//==========================================================

//----------------------------------------------------------
//    Set Up Function
//----------------------------------------------------------
void setup() {
  Serial.begin(115200);   //シリアル初期化

  //I2C設定&センサ初期化
  Wire.begin( SDA, SCL, 40000);           //Pin SDA, Pin SCL, Frequency
  sen_bme280.setI2CAddress(BME280_ADDR);  //BME280 アドレスセット
  //BME280 セットアップ
  if(sen_bme280.beginI2C() == false){ 
    Serial.println("Sensor BME280 connect failed");   //エラーなら停止
    while(1);
  }
  //CCS811 セットアップ
  if (sen_ccs811.begin() == false){
      Serial.print("Sensor CCS811 connect failed");   //エラーなら停止
      while (1);
  }
  Serial.println("start ");
}


//----------------------------------------------------------
//    Main Loop Function
//----------------------------------------------------------
void loop() {
  //センサ値取得 BME280
  temp_act = sen_bme280.readTempC();
  press_act = sen_bme280.readFloatPressure();
  hum_act = sen_bme280.readFloatHumidity();

  //センサ値取得 CCS811
  if (sen_ccs811.dataAvailable()){
    sen_ccs811.readAlgorithmResults();
    co2 = sen_ccs811.getCO2();
    tvoc = sen_ccs811.getTVOC();
  }

  //シリアル出力
  Serial.print(" Humidity: ");
  Serial.print(hum_act, 0);
  Serial.print(" Pressure: ");
  Serial.print(press_act, 0);
  Serial.print(" Temp: ");
  Serial.print(temp_act, 2);

  Serial.print(" CO2: ");
  Serial.print(co2);
  Serial.print(" tVOC: ");
  Serial.print(tvoc);
  
  Serial.println();

  delay(500);   //ちょっと待つ
}

動きはこんな感じです。
値は取れているので通信はOKのようです。しかし、値は何となく違う感じ
がしています。実用にはセンサ値を分析してフィルタや補正を考えてやる
必要がありそうです。


今回、拡張モジュールを追加してIoT機器っぽくなりました。しかしこれだけではちょっと
物足りません。部屋のものをいろいろ操作できるぐらいでないとホームオートメーション
とは言えません。次回はESP32から部屋の機器を何かを操作できるようにしてみたい
と思います。





2020年7月11日土曜日

【家の自動化プロジェクト】#1 BLE子機の製作1

どうもcaketetuです。

去年末からHA(ホームオートメーション)に興味があり、色々作って遊んでおりました。
Google Assistantの力を借りて音声認識で色々動かせるところまでいったのですが
ちょっとお粗末な部分もあったのでリニューアルも兼ねて作り直してみようと思います。


構成としてはRaspBerryPiがBLE親機になり複数のBLE子機を一括管理します。
そこにNode.jsでwebサーバーを作り、スマホやパソコンでアクセスし命令を飛ばす
仕組みです。今回は実際の指令を実行するBLE子機ユニットをESP32で作っていこう
と思います。



設計はこんな感じ。ESP32モジュールを入れるためのただの箱です。
穴がたくさん開いている板はタミヤのユニバーサルプレートで、
これを共通規格として用いることで上下左右に拡張できる所が
本計画のミソになっています。




内部の回路です。これもIOを使いやすいように引っ張ってきてるだけの基板
です。GND、3.3V、Vinはよく使うのでピンを多くしています。



各パーツになります。ユニバーサルプレートを使用しているので
パーツをそろえる時間がぐっと節約できます。こういった板状のものを
印刷すると時間かかりますからね。




組み立てた様子がこちら。しっかりと組みあがりました。
タミヤのユニバーサルプレートは設計しやすい寸法になっており、
穴でカットすれば簡単に精度よく加工できるので重宝しています。



次に拡張パーツを作っていきます。本計画は拡張性の高い基本設計に
拡張パーツをどんどん付け足せるようにすることで機能を簡単に
増やすことができます。PLCにモジュールを足す感じにヒントを得ました。



こんな感じで拡張していきます。最初の基本設計から全面のIFパネルと
上、横に回路を取り付けられる拡張モジュールを増やしました。




わーっと出力し…



はい、全部乗せ完成です。
拡張モジュールの使い方としては今のところ以下を考えています。

 ・温湿度センサ、CO2センサなどをまとめた、”環境センサモジュール”
 ・マイコンを乗っけてピンとリソースを拡張する "サブプロセッサモジュール"
 ・モータ制御に対応した、”モータコントローラモジュール”

次回は拡張モジュールに回路を追加していくつかのサンプルを動かしてみようと思います。