ラベル IoT の投稿を表示しています。 すべての投稿を表示
ラベル IoT の投稿を表示しています。 すべての投稿を表示

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月18日土曜日

【Arduino】赤外線でエアコンを操作する

どうもcaketetuです。現在、自宅のスマートホーム化を進めています。
その中でエアコンの自動化は必要不可欠と言えます。IoT対応のエアコンを
買えば楽ですが、価格が高く、賃貸なので簡単にエアコン交換ができません。
よって今回は後付けでもエアコン自動化できるように赤外線LEDとセンサーを使って
Arduinoから操作してみます。

早速アマゾンで赤外線リモコンセットを購入します。
私が購入したのはこれです。
もう売り切れでしたが、同じようなやつはたくさんありました。


・赤外線信号を調べる

「Arduino 赤外線」とかで調べると赤外線コードやライブラリがたくさん出て
きますが、残念なことに我が家のエアコンは対応していませんでした。
なのでエアコンのリモコンから出ている信号を調べなければなりません。

Arduinoに赤外線センサーを取り付けて信号を読み出します。
以下のプログラムを使用します。

#define READ_PIN 2
#define LOW_STATE 0
#define HIGH_STATE 1

void setup(){
  Serial.begin(115200UL);
  pinMode(READ_PIN,INPUT);

  Serial.println("Ready to receive");
}

void waitLow() {
  while (digitalRead(READ_PIN)==LOW) {
    ;
  }
}

int waitHigh() {
  unsigned long start = micros();
  while (digitalRead(READ_PIN)==HIGH) {
    if (micros() - start > 5000000) {
      return 1;
    }
  }
  return 0;
}

unsigned long now = micros();
unsigned long lastStateChangedMicros = micros();
int state = HIGH_STATE;

void loop() {
    if (state == LOW_STATE) {
      waitLow();
    } else {
      int ret = waitHigh();
      if (ret == 1) {
        Serial.print("\n");
        return;
      }
    }

    now = micros();
    Serial.print((now - lastStateChangedMicros) / 10, DEC);
    Serial.print(",");
    lastStateChangedMicros = now;
    if (state == HIGH_STATE) {
      state = LOW_STATE;
    } else {
      state = HIGH_STATE;
    }
}

また、Arduinoとセンサの配線は次の通りです。

Arduino センサー
 IO2-----OUT
 GND----GND
 5V-----VCC

実行しリモコンを照射しながらシリアルモニタで見てみると数字の羅列が出力されます。
これは赤外線のONとOFFが切り替わる間の時間を表しています。これを解析していきます。

おそらくAEHAフォーマットで40ぐらいが1Tで120ぐらいが3Tでしょうか。
ボタンを何回も押していき、変化した値を見て信号を解析するのですが
いちいちやっていては大変なのでデコードプログラムをVSのC#で作りました。
GUIのプログラムを全部乗せるのは大変なのでコア部分だけ載せます。

		private void TimerSerial_Tick(object sender, EventArgs e)
        {
            //二秒ごとにバッファからすべて吐き出す
            String buffer;  //シリアルバッファ―
            buffer = Serial1.ReadExisting();    //バッファからすべて読み出す
            RTB_Raw_Data.Text = buffer;         //バッファをすべて書き出す
            if (buffer.Length > 500)
            {
                string[] arr = buffer.Split(',');   //','で区切って取り出す
                int ms, digit=0;        //時間、桁数
                int code_size=0;        //HEXコードサイズ
                UInt16 ir_sig = 0x00;   //HEXデータ
                //有功データから一つとびに値を見る
                for (int i = 4; i < arr.Length; i+=2)
                {
                    int.TryParse(arr[i], out ms);   //データを整数に直す
                    if (ms > 80) ir_sig |= 0x80;    //データが80以上(=1)なら先頭ビットを1に

                    digit++;        //桁数プラス
                    if ( digit>=8 ) //桁数が8なら表示
                    {
                        digit = 0;  //桁数リセット
                        String sighex = string.Format("{0,3:X2}", ir_sig);  //HEXにフォーマット指定
                        RTB_IR_Signal.Text += sighex + " "; //表示
                        ir_sig = 0x00;  //ir_sigリセット
                        code_size++;    //コードサイズ更新
                    }

                    ir_sig /= 2;    //ir_sigの桁をひとつ下げる
                }
                RTB_Raw_Data.Text += "  DataSize=" + arr.Length.ToString() + "\n";  //データサイズ表示
                RTB_IR_Signal.Text += "  DataSize=" + code_size.ToString() + "\n";  //データサイズ表示

            }
        }

簡単に説明すると
  1. シリアルバッファからすべて読み出す
  2. ","ごとに分割してそれぞれをint型に直す。
  3. 最初のいらない部分を除いて一つ飛ばしに値を読んでいき80以上なら一番左のbitを1に、それ以外なら0にする。その後右にビットシフト(/2)する。
  4. 8回ごとにデータを16進数でTEXTに追加する。
VC#を選んだのは実装がラクだったからです。アルゴリズムが分かれば他言語でも簡単に実装できると思います。

検証の様子です。
結構取りこぼしてますが信号数33個が正常なデータと見ていいでしょう。



以下が検証から得られたデータです。~はビット反転を表します。


・信号を送信する

調べられた値を使って信号を送信してみます。解析時と逆の手順で16進数の
コードをON、OFF信号に変換して送信します。

メインループ
#include "IR_Ctrl.h"

//赤外線送信データ
byte ir_hexdata[33];

void setup() {
  // put your setup code here, to run once:
  ir_Init();                  //赤外線モジュール初期化
  for(int i=0;i<33;i++){
    ir_hexdata[i]=i;
  }
  delay(500);
}

void loop() {
  // put your main code here, to run repeatedly:
  ir_send(ir_hexdata);    //赤外線照射
  delay(2000);
}
赤外線リモコン送信ヘッダ
//*********************************************************
//    IR Control header
//    
//*********************************************************
#define IR_SEND_PIN  6  //define ir-signal send pin
#define IR_BUTTON 11
#define IR_MODE 25
#define IR_TEMP 13
#define IR_POWER 27

#define IR_POWER_ON  0xD1
#define IR_POWER_OFF 0cC1

#define IR_BTN_OFF 0x13
#define IR_BTN_UP  0x44
#define IR_BTN_DOWN 0x43
#define IR_BTN_TISTOP 0x31
#define IR_BTN_TIWKUP 0x22
#define IR_BTN_WIND_LEVEL 0x42
#define IR_BTN_WIND_DIRECTION 0x81

#define IR_WIND_AUTO    0x50
#define IR_WIND_STRONG  0x40
#define IR_WIND_NOMAL   0x30
#define IR_WIND_WEAK    0x20

#define IR_MODE_COOLING  0x03
#define IR_MODE_DEHUMID  0x05
#define IR_MODE_HEATING  0x06


//----------------------------------------------------------
//    sumple send data
//----------------------------------------------------------
byte ir_heating[33] = {0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 
                      0x33, 0x92, 0x6D, 0x13, 0xEC, 0x50, 0xAF, 0x00,
                      0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,  
                      0xFF, 0x56, 0xA9, 0xD1, 0x2E, 0x00, 0xFF, 0x00, 0xFF};

//01  10  00  40  BF  FF  00  CC  33  92  6D  13  EC  50  AF  00  FF  00  FF  00  FF  00  FF  00  FF  56  A9  D1  2E  00  FF  00  FF 


byte ir_cooling[33] = {0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 
                      0x33, 0x92, 0x6D, 0x13, 0xEC, 0x68, 0x97, 0x00,
                      0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,  
                      0xFF, 0x53, 0xAC, 0xD1, 0x2E, 0x00, 0xFF, 0x00, 0xFF};

//01  10  00  40  BF  FF  00  CC  33  92  6D  13  EC  68  97  00  FF  00  FF  00  FF  00  FF  00  FF  53  AC  D1  2E  00  FF  00  FF 

byte ir_dehumid[33] = {0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 
                      0x33, 0x92, 0x6D, 0x13, 0xEC, 0x68, 0x97, 0x00,
                      0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,  
                      0xFF, 0x25, 0xDA, 0xD1, 0x2E, 0x00, 0xFF, 0x00, 0xFF};

// 01  10  00  40  BF  FF  00  CC  33  92  6D  13  EC  68  97  00  FF  00  FF  00  FF  00  FF  00  FF  25  DA  D1  2E  00  FF  00  FF 

byte ir_off[33]      = {0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 
                      0x33, 0x92, 0x6D, 0x13, 0xEC, 0x6C, 0x93, 0x00,
                      0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,  
                      0xFF, 0x53, 0xAC, 0xC1, 0x3E, 0x00, 0xFF, 0x00, 0xFF};


//----------------------------------------------------------
//    Initialize
//----------------------------------------------------------
void ir_Init( void ){
      //check led setup
      pinMode(IR_SEND_PIN,OUTPUT);    //led pin set output
      digitalWrite(IR_SEND_PIN,LOW); //set initial value(high-level off)  
}

//----------------------------------------------------------
//    IR send signals
//----------------------------------------------------------
void sendSignal( unsigned short *data ,int data_len) {
  //int dataSize = sizeof(data) / sizeof(data[0]);
  int dataSize = data_len;
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = data[cnt];
    unsigned long us = micros();
    do {
      digitalWrite(IR_SEND_PIN, 1 - (cnt&1));
      delayMicroseconds(10);
      digitalWrite(IR_SEND_PIN, 0);
      delayMicroseconds(8);
    } while (long(us + len - micros()) > 0); // 送信時間に達するまでループ
  }
}

//----------------------------------------------------------
//    IR send bytes
//----------------------------------------------------------
void ir_send(byte *data){
    int khz = 38; // 38kHz carrier frequency for the NEC protocol
    unsigned short send_buf[16] = {390, 390, 390, 390, 390, 390, 390, 390,
                    390, 390, 390, 390, 390, 390, 390, 390};    //send buffer
    unsigned short for_data[2] = {3340,1700};   //head data
    unsigned short low_data[2] = {1240,390};    //tail data
    byte hex;
    
    sendSignal(for_data, sizeof(for_data) / sizeof(for_data[0]));
    
    for(int i=0;i<33;i++){
        hex = data[i];
        for(int j=1;j<16; j+=2){
          send_buf[j] = 410;
          if( hex&0x01 == 0x01 ){ send_buf[j] = 1240;}
          hex/=2;
        }
        sendSignal(send_buf, sizeof(send_buf) / sizeof(send_buf[0]));
    }
    
    sendSignal(low_data, sizeof(low_data) / sizeof(low_data[0]));
}
実行環境によって多少の調整が必要です。
delayMicroseconds()の値を調整するとうまくいくかもしれません。

Arduinoを二つ使って受信、送信をテストします。
テストでは、01,02,03・・・の信号を送信しています。



・エアコンを操作する

実際にエアコンを操作してみます。LEDをピン直結では発光が弱いので
LEDを3個使いトランジスタで信号を増幅して照射します。

照射モジュール


動作の様子
無事信号が認識されました。ちょっと光軸がずれていても認識してくれます。




無事Arduinoからエアコンを操作することができました。情報も結構出ていたので
簡単なのかなと思いましたが、意外と解析にてこずって時間がかかりました。
赤外線の知識、信号の作り方、信号の検証となかなかハードです。ライブラリ
もあるので対応していたらそちらを使ったほうがラクにできると思います。

スマートホーム化に際しては、これをWEB経由で操作できるようにしてみます。


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

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







2020年6月7日日曜日

スマホ搭載可ラジコンロボットを作る 回路・ソフト編

前回の記事の続きです。今回は回路・ソフト編です。

アクチュエータは駆動部にサーボ2個、スマホホルダ部にサーボ二個(同軸)
なのでそれらをPS3コントローラから操作できるようにします。
KONDOの同じサーボのみ使用しているのでかなり単純にできます。

・回路
KONDOのICS通信は一線式、マイコンはTxとRxの二線式なので
回路上で対応が必要です。今回はKONDO純正のICS変換基盤を使用
しました。純正なので簡単&安心ですが、値段がちょっと高いので
次回は自作したいところです。

回路も基盤の取説に乗っていたものをそのまま使います。
下記の通りに接続します。
ESP32              ICS変換基盤
Vin------------------------- Vout
3.3V------------------------IOREF   
GND------------------------GND
RX2(GPIO 16)--------------TX
TX2(GPIO 17)--------------RX
GPIO 2----------------------EN_IN

シリアルポート2を使用しましたが、たぶん他のポートでも大丈夫です。
一点、ICS変換基盤のピンヘッダは逆接続ができるようになっているのでサーボ
を接続するときは注意が必要です。電源側が信号線になります。




・ソフト
開発環境はArduinoIDEのESP32ライブラリを使用します。
追加が必要なライブラリはPs3ControllerとICS_Classの二つです。
Ps3Controllerはライブラリマネージャから、ICS_ClassはKONDOの
サイトからダウンロードできます。

ICSクラスは公式に詳しく説明があるのでそちらを参照のこと。
Ps3Controllerの使い方はExsampleを見れば大体わかりますが

Ps3.begin(PS3のMACアドレス);

をsetup関数で唱えてると接続待ち状態になります。

Ps3.isConnected()

で接続状態を監視し、接続されていたらPs3.dataクラスにデータが
入っていますので

int rx = Ps3.data.analog.stick.rx;

のようにしてボタンの状態を取得します。あとはボタン状態によって
処理を書くだけです。ESP32+PS3コントローラで簡単にラジコンロボット
を作れますね。

ちょっとつまずいたのは駆動の部分です。アナログスティックのx軸値、y軸値
から車輪二つの回転数に変換するのですが、より直感的に操作できるように
するにはどうするか悩みました。

最終的にtan^-1で極座標変換して角度を出し、角度によって
左右の回転数を変える処理にしました。これではその場で回転できま
せんのでL2とR2を超信知旋回に割り当てています。
私はこれが操作しやすいと思いますが、ボタンはたくさんありますので
自分に合わせた操作方法を作っていくのも楽しいと思います。
今回使用したプログラム



今回はスマホをそのまま搭載できるラジコンを作ったらいろいろ使えるんじゃないか?
というアイデアでサクサク製作しました。簡単ハードで特に目新しい部分は
なかったですが、ESP32+サーボ+PS3コントローラの組み合わせはボタン
の割り当てと制御部分の作り方次第で二足歩行ロボットやアームロボットにも
使えるのでかなり応用性が高いと思います。

動作の様子








スマホ搭載可ラジコンロボットを作る ハード編

お久しぶりです。
コロナショックで自宅作業(趣味)が非常に多いcaketetuです。
昨年は新しい環境になったこともありいろいろ忙しかったので
更新できませんでしたが、電子工作は細々してました。

今回は最近作ったスマホ搭載ラジコンの解説ハード編です。






 


解説と言っても特に目新しいところは無いです。
駆動部は差動二輪型、モータはKONDO KRS2572HVを回転モードで使用。
スマホホルダー部もモータは KRS2572HV×2をダブルサーボとして
使用しています。

ちなみに操作はPS3コントローラを使用。



横からの図。スマホホルダーはタイミングプーリを介してサーボに接続されて
おり、角度を変えることができます。正直ダイレクトドライブで良かった
ですが、タイミングプーリを使った機構を使いたかったのでこのように
しました。

↓頑張ったプーリー部です↓





3Dプリンタも工夫すれば精度のいいパーツも作れるんですね~。






上から。CPUはESP32モジュールを使用。BLEとWiFiを簡単に使用できるうえ
ライブラリも充実・IOもそこそこ多いということでかなり多用しております。
詳しくはソフト編で紹介しますが、シリアルサーボを使用しているので
回路もソフトも非常に簡単に済みます。(値段は高くなりますが)



このロボットの使い方としてはスマホのカメラをネットワークカメラにできる
ソフトがあるので、それをら使用して監視カメラやオンライン飲み会のおもちゃ
として活躍できるはず。私もオン飲みで使用しましたが自宅を探索させたりして
結構盛り上がりました。

動きはこんな感じ。




次回、回路&ソフト編です。





2018年8月18日土曜日

Node.js 色々(html socket.io child_proces)

前回はnodeをwindows10にインストールし,ブラウザにHallo worldを表示
してみました.次回RaspberryPiにインストールしてみるといいましたが,
かなり昔の話で方法を忘れてしまいました.こまめにメモするんだった・・・

今回は一通りセットアップが終わったRaspberryPiを使用し,
私が良く使うモジュールについてメモしたいと思います.

RaspberryPiはsambaでファイル共有し,sshでリモート接続できるように
なっています.とりあえず適当なテストフォルダを用意しtest.js,test.htmlを
作りました.細かいことはわかりませんが,jsはサーバで実行されるプログラムで
htmlはクライアント(ブラウザ)で実行されるプログラムという認識でいいのでしょうか.



・html
接続があった場合,外部のhtmlファイルを表示させてみようと思います.
何をするにもまずはこれからですね.
test.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Client</title>
</head>
<body>
  <p>Hello World!!</p>
</body>
</html>

test.js
var app = require('http').createServer(handler)
var fs = require('fs');

app.listen(3000); // HTTPポートオープン

//接続があった場合の処理
function handler (req, res) {
//ファイルリード
  fs.readFile(__dirname + '/test.html',
  //エラーの場合の処理
  function (err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading test.html');
    }
//通常処理
    res.writeHead(200);
    console.log("Server Started!!");
    res.end(data);
  });
}

windowsのターミナルから接続し,作ったディレクトリに移動します.
node testでサーバを起動できます.
ブラウザから「RasPiのアドレス:3000」にアクセスします.
「Hello World!!」が表示できたかと思います.



・socket.io
サーバとクライアントで通信させてみようと思います.
これができるとブラウザ経由でRasPiを操作できるようになります.

htmlファイルはボタンを追加し,massageを送れるようにします.
emitでデータを送ります.送るデータはjson形式というらしい.また,
socket.ioモジュールを読み込みます.
test.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Client</title>
  <script src="/socket.io/socket.io.js"></script>
</head>
<body>
  <p>Hello World!!</p>
  <input type="button" value="on" onclick="send('hello!!')"/>
</body>
<script>
var socket = io.connect();

function send(send_data){
    socket.emit('massage',{ data:send_data});
};
</script>
</html>

jsファイルは受け取ったメッセージをコンソール上に表示します.
モジュールとしてsocket.ioを追加.onで受信した場合の処理を記述します.
test.js

var app = require('http').createServer(handler)
var fs = require('fs');
var io = require('socket.io')(app); //socket.io

app.listen(3000); // HTTPポートオープン

//接続があった場合の処理
function handler (req, res) {
//ファイルリード
  fs.readFile(__dirname + '/test.html',
  //エラーの場合の処理
  function (err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading test.html');
    }
//通常処理
    res.writeHead(200);
    console.log("Server Started!!");
    res.end(data);
  });
};

//アクセスがあった場合
io.on('connection', function (socket) {
  //massageを受信した場合
  socket.on('massage', function (data) {
    // クライアントから受け取ったデータを出力する
    console.log(data);
  });
});

同じようにnodeを起動し,ブラウザからアクセスします.
ボタンを押したら { data: 'hello!!' } が表示されると思います.



・child_proces
サーバー上のshellコマンドなどを動かしたい時,child_procesで動かすことができます.

test.jsのみ書き換え,先ほどのボタン入力があった場合に「ls -l」を実行してみたいと
思います.child_processモジュールを追加し,socket.onのところにコードを追加します.
test.js

var app = require('http').createServer(handler)
var fs = require('fs');
var io = require('socket.io')(app); //socket.io
var exec = require('child_process').exec; //child_process

app.listen(3000); // HTTPポートオープン

//接続があった場合の処理
function handler (req, res) {
//ファイルリード
  fs.readFile(__dirname + '/test.html',
  //エラーの場合の処理
  function (err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading test.html');
    }
//通常処理
    res.writeHead(200);
    console.log("Server Started!!");
    res.end(data);
  });
};

//アクセスがあった場合
io.on('connection', function (socket) {
  //massageを受信した場合
  socket.on('massage', function (data) {
    // クライアントから受け取ったデータを出力する
    console.log(data);
    
    //ls -lを実行
    exec('ls -l', function(err, stdout, stderr){
  console.log(stdout);
  });
  });
});


同じようにnodeを起動し,ブラウザからアクセスします.
ボタンを押したら以下のように表示されると思います.
{ data: 'hello!!' }
合計 28
drwxr-xr-x 41 pi pi  4096  8月 18 21:34 node_modules
-rw-r--r--  1 pi pi 10624  8月 18 21:34 package-lock.json
-rw-r--r--  1 pi pi   250  8月 18 21:34 package.json
-rwxr--r--  1 pi pi   381  8月 18 21:35 test.html
-rwxr--r--  1 pi pi   976  8月 18 21:50 test.js




ここまでで,3つのNodeモジュールを使用してみました.
実はこの3つだけで非常に幅広いことが可能になります.
例えば,ブラウザ経由で音楽をかけたり,RaspPiに接続された温度計の情報を
ブラウザに表示したりすることが可能かと思います.夢が広がりますね.

あと良く使うのはexpressモジュールですが,まだうまく扱えていないので
次回まとめたいと思います.



2018年1月24日水曜日

温湿計付きWiFi時計を作ってみた2

前回の続きでソフト編です.

やりたいことは,
1.時間をネットワークで自動同期.
2.BME280から温度・湿度・気圧を取得
3.7セグに時間を表示
4.液晶に時計と温度等のグラフを表示
です.

1.時間をネットワークで自動同期.
NTPClientライブラリを使用します.
https://github.com/arduino-libraries/NTPClient
zipを解凍しArduinoのlibrariesフォルダに入れます.
使い方はサイトにあるサンプルのとおりです.
日本時間に合わせるには以下のように初期化します.
NTPClient timeClient(ntpUDP, "jp.pool.ntp.org", 3600*9, 60000);


2.BME280から温度・湿度・気圧を取得
ボッシュ社のセンサで,温度,湿度,気圧を取得することができます.
マイコンとの接続はI2Cです.
データシート
特に深く考えずに値を取得できればいいので,適当なヘッダを作りました.
何らかのフィルタをかけてくれていますが,動いたので良しとしました.
元のサンプルプログラムを見つけられませんでした.すいません.


3.7セグに時間を表示
マトリクスLCDドライバ,HT16K33を使用しています.接続はI2Cです.
コレも深く考えずヘッダを作成.以前の記事を参考にしています.
数字の点灯パターンをあらかじめ宣言しておきます.


4.液晶に時計と温度等のグラフを表示
コレを一番頑張りました.マイコンから液晶を触るのはとても大変で
何時間もデータシートとにらめっこすることになりますが,
しかしこの液晶は使いやすいライブラリが公開されているのでプログラミングは楽です.
Adafruit_GFX.hとAdafruit_ST7735.hを使用します.
コチラを参考にさせて頂きました.

4つの画面(時計,温度,湿度,気圧)をボタンで切り替えられるようになっています.
それぞれの画面は必要領域のみ更新し,チラつきを抑えます.
また100ms割り込みでボタンを監視し,一定時間以上入力がなければ
自動でOFFになります.
コレの画面のレイアウトに開発時間の大半を費やしました.




結構気に入っていてたまに画面を切り替えてはニヤニヤしています笑
実用性もなかなかあり,時計はもちろんエアコンや加湿器の
電源を入れる目安にしています.
ネットワーク上から見れたりすると便利かなと思いますので時間が
あればいじりたいと思います.

以下使用プログラム
ESP-WROOM-02_digital_clock
BME280
SEG7_LED

2017年10月22日日曜日

温湿計付きWiFi時計を作ってみた

お久しぶりです.
気づいたら2ヶ月経ってました.中々の忙しさでした.
しかし何もやっていなかったわけではなく,ESP-WROOM-02を
使って温湿計付きWiFi時計を作っていました.今回ようやく形に
なったのでメモに残したいと思います.

概観はこんな感じ.



温度センサ,LCD,7セグメントLEDを搭載しています.
回路はまじめに設計するのが面倒だったので
ユニバーサル基盤に全部載せしました.

使用したモジュールは以下のとおり
赤色LEDディスプレイ7セグメントLED
128*160 TFT液晶ディスプレイモジュール
BME280 大気圧センサ 温湿度センサー
ESP-WROOM-02ピッチ変換済みモジュール
16x8LEDマトリクスドライバーモジュール

回路図は作ってませんが,ESP-WROOM-02はピンに余裕がないので
工夫が必要です.現在の割り当ては

4,5  ->  I2C(7seg,BMP280)
15->LCDバックライト制御
0->画面切り替えスイッチ
12,13,14,16->SPI(LCD)

となっています.ピン0,15は使用するのはよくないとされていますが,
私の使い方では問題ありませんでした.

また,バックライト制御は内部でGNDがLED-とつながっているのか
カソード側で制御できませんでした.そのためこのサイトで紹介されている
アノード側での制御を試したところ,うまく動作できました.

動作の様子です.




趣味で電子工作を始めて2年ぐらい,初めて実用的なものを
製作した気がします笑.

長くなりそうなのでソフトは次回にしようと思います.
それではまた.


2017年8月13日日曜日

Rasoberry Pi minidlnaを使ってPS4からアクセス

どうもcake tetuです.

先日PS4を購入しました.
PS4のメディアプレーヤーがなかなか使いやすそうだったので
Raspbery piにminidlnaをインストールしメディアサーバーにしてみます.

実は我が家には一応ファイルサーバーがあります.



画像からわかる通り初代Raspberry piを使用しています.
そのため,速度はかなり遅いです.
まあ自宅内でファイルの受け渡しぐらいしか使用してないので
あまり気にしませんが.

こいつにminidlnaをインストールします.
コチラを参考にさせて頂きました.

とりあえずテストということで公開フォルダはHDD上ではなくは
/home/pi上にしました.

PS4からアクセス!!



キターー!! 



適当な動画を再生してみます.
PS4のメディアプレーヤーはコントローラ―で操作しやすく,
とても扱いやすいです.これはなかなか便利.
そして,データベースを勝手に作ってくれるので,探す手間が省けます.
データベースは60sで更新になっていますが,サービスの再起動

$sudo service minidlna restart

で更新してくれる模様.


関係ない話ですが,Raspberry piでファイルサーバーは省エネで良いです.
しかし,LANが100base-TなのとUSB2.0しかないのでとても遅い.
本当は小型PCなんかでサーバを作りたいですが,金が…
金が貯まるまで頑張れ初代RaspPi.