2020年8月29日土曜日

【STM32】HALライブラリ色々(タイマ割込み、PWM出力、AD変換、シリアル通信、エンコーダモード)

どうもcaketetuです。
前回、HALライブラリでLチカできました。今回はその他使いそうな機能のテストプログラムのメモです。


タイマ割込み

特別な設定なく一定間隔でのタイマ割込みをかけます。TIM15を使用し、間隔は1sです。
タイマ割込みでLチカするのでIO出力の設定も必要になります。IO出力は前回参照です。

CubeMX設定

赤部分がデフォルトから変更した箇所です。割込み周波数は1Hzなのでソース周波数(ここでは32MHz)をPrescaler(10000カウント)で割り、さらにそれをCounter Period(3200カウント)分カウントアップすると1Hzになります。画像は間違ってますね…Counter Period=3199が正しいです。

ここで設定値は0からのカウントなので例えば10000カウントしたければ10000-1=9999を設定します。10000-1と書いても大丈夫なようです。




サンプルプログラム

割込みでLEDをON-OFFします。割込みをmain.cに持ってきたかったので新しく関数を作りstm32f3xx.it.cで呼び出すようにしています。

//Private user code(60行目付近)
/* USER CODE BEGIN 0 */
void tim15_tick(void){
	HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_2);
}

//stm32f3xx_it.c内
void TIM1_BRK_TIM15_IRQHandler(void)
{
  /* USER CODE BEGIN TIM1_BRK_TIM15_IRQn 0 */
  tim15_tick();		//関数を追加
  /* USER CODE END TIM1_BRK_TIM15_IRQn 0 */
  HAL_TIM_IRQHandler(&htim15);
  /* USER CODE BEGIN TIM1_BRK_TIM15_IRQn 1 */

  /* USER CODE END TIM1_BRK_TIM15_IRQn 1 */
}

//mainループ(100行目付近)
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim15);	//タイマスタート
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */



PWM出力

TIM1をPWM出力に割り当てます。周期は1kHzです。PWMでLEDの光量を操作します。Prescalerは32カウント、Counter Periodは1000カウントで1KHzですね。

CubeMX設定

赤部分がデフォルトから変更した箇所です。

サンプルプログラム

上記でCounter Period=0~999で設定したので、この間の数値でデューティ比を決めます。
50%なら499になりますね。
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM1_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);		//PWMスタート
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);		//PWMスタート
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
		for(int i=0; i<999; i++){
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, i);		//PWM出力
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 999-i);	//PWM出力
			HAL_Delay(1);
		}

		for(int i=0; i<999; i++){
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 999-i);	//PWM出力
			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, i);		//PWM出力
			HAL_Delay(1);
		}
  }
  /* USER CODE END 3 */



AD変換

ADC1、CH4でAD変換します。デバックにPWMを使うので上記方法で設定しておきます。
今回は最も簡単なワンショットでの測定です。ADCの分解能が12bitなのでPWMのCounter periodは4095に設定します。これでAD値をそのままぶち込めばぴったりDuty比0~100になりますね。

CubeMX設定

赤部分がデフォルトから変更した箇所です。

サンプルプログラム

AD変換値をそのままPWM出力しLEDを点灯させます。

  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	//AD変換
	HAL_ADC_Start(&hadc1);
	HAL_ADC_PollForConversion(&hadc1, 100);
	HAL_ADC_Stop(&hadc1);
	int ad_val = HAL_ADC_GetValue(&hadc1);

	//PWM出力
	__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, ad_val);		//PWM出力
  }
  /* USER CODE END 3 */



シリアル通信

送信値をそのまま返すエコーバックを作ってみます。UART2を使用し、受信は割込みで送信はメイン関数で行います。

CubeMX設定

赤部分がデフォルトから変更した箇所です。NVIC設定で割込みを有効にします。




サンプルプログラム

//グローバル変数定義(50行目付近)
/* USER CODE BEGIN PV */
//シリアル関連
char recv_buf[256];
char recv_data[256];
char recv_data_len=0;
/* USER CODE END PV */

//mainループ(100行目付近)
  /* USER CODE BEGIN 2 */
  HAL_UART_Receive_IT(&huart1, &recv_buf, 1);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	//データがあるとき
	if(recv_data_len){
		HAL_Delay(10);
		//データをすべて読み出し
		HAL_UART_Transmit( &huart1, recv_data, recv_data_len, 0xFFFF);
		recv_data_len=0;
	}
  }
  /* USER CODE END 3 */
}

//コールバック関数(230行目付近)
/* USER CODE BEGIN 4 */
//シリアル受信割込みコールバック関数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{
  HAL_UART_Receive_IT(&huart1, &recv_buf, 1);
  recv_data[recv_data_len] =  recv_buf[0];
  recv_data_len++;
}
/* USER CODE END 4 */



エンコーダモード

タイマ機能のエンコーダモードを使用し、エンコーダ入力をカウントします。使用しているエンコーダはFaulhaver1524モータについてきたIEH2-4096です。これはエンコーダが二相あり回転方向が分かるやつなのでEncoder Modeを二相に設定します。

CubeMX設定

赤部分がデフォルトから変更した箇所です。今回はTIM2、TIM3でモータ二つ分の入力をとってみます。どちらも設定は同じです。デバックでシリアルを使うので上記の要領で設定します。




サンプルプログラム

カウント値をシリアルで送信します。値が増えていれば正転、減っていれば逆転になります。

  /* USER CODE BEGIN 2 */
  HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL); //読み取りスタート
  HAL_TIM_Encoder_Start(&htim3,TIM_CHANNEL_ALL); //読み取りスタート
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	int enc1 = TIM2 -> CNT; 		//TIM2の値を取り出してcntに格納
	int enc2 = TIM3 -> CNT; 		//TIM3の値を取り出してcntに格納

	//シリアル書き出し
	char buf[100];
	sprintf(buf , "%d %d \r\n",enc1, enc2 );
	HAL_UART_Transmit( &huart1, buf, strlen(buf) + 1, 0xFFFF);
	HAL_Delay(100);
  }
  /* USER CODE END 3 */
}


動作の様子






今回は以上です。とりあえず試してみたい機能はできたと思います。次回から本格的なハードウェア制御用マイコンとして使っていきたいですね。

2020年8月23日日曜日

【STM32】HALライブラリでLチカする

最近、ロボットのハードウェア制御用にnucleo-f446re秋月のSTM32F303K8T6
使っています。

一応、一通りの環境を整えたので、今回は秋月で購入できるSTM32F303K8T6マイコンで
Lチカするまでのメモです。




これが今回使うマイコンと変換基板。

ピッチ0.8とはんだ付けがそれほど難しくなく、Max72Mhzのマイコンが変換基板と
合わせても一個440円で手に入ります。かなりコスパが良いです。



ST-LINKを使う時の最小動作回路はこんな感じです。



私はNucleo搭載のST-LINKで書き込んでいます。ST-LINKのデバック機能はすべて使ってみたわけではないので、時間があればいじってみたいですね。


開発にはSTM32CubeMXSW4STM32を使っていきます。ソフトのインストールは省略しますが、特に難しかったところはなかったと思います。




STM32CubeMXの設定

STM32CubeMXはGUIベースでマイコンのクロックやペリフェラル関係を設定でき、コードを生成してくれる便利なソフトです。


起動したらまず一番上のFile→New Projectから新規プロジェクトを作成します。
左のセレクトボックスからマイコンを選択します。今回はSTM32F303K8を選択します。
右に候補が出るのでチェックしてStart Projectを押します。




ピン設定をしたいところですが、忘れないうちにクロック設定をしてしまいましょう。
上の画像は内部クロックを使用した設定の一例です。



プロジェクト設定も先に済ませます。プロジェクト名ファイルパスとIDEの設定をします。
今回はSystem Workbench for STM32を使いたいのでプルダウンメニューから選択します。また、Aprication StructureをAdvancedにしておきます(これはよくわかっていない)。




いよいよピン設定です。灰色のピンはまだ設定されていないので右クリックで機能を
割り当てることができます。デバックで使いそうなところはかぶせたくないので先に
埋めておきます。




PA8,PA9から出力してLEDを点灯したいので、ピンを右クリックしてGPIO_OUTPUTを選択します。最後に右上のGENERATE CODEを押すとプロジェクトが生成されます。




SW4STM32でプログラミング

System Workbench for STM32(SW4STM32)でプログラミングしていきます。
上記事で生成したプロジェクトフォルダに.projectがあるはずなのでSW4STM32を指定して
実行します。


Core/Src/main.cに既にメイン関数も生成されていますので、Lチカのプログラムを追加していきます。

main.c 100行目付近
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_8);	//PA8 出力切り替え
	  HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_9);	//PA9 出力切り替え
	  HAL_Delay(100);		//100ms停止
  }

ここで注意があり、プログラムはUSER CODE BIGINUSER CODE ENDのコメントの
間に書きます。こうしないとCubeMXを更新したときに消えてしまいます。

左上のハンマーみたいなマークでビルドし上の緑色の再生ボタンのようなマークで書き込んで実行します。

無事Lチカできました。



ちなみに、STM32CubeMXの設定を流用して新規プロジェクトを作りたいときは。

1:プロジェクト用の新規フォルダを作成
2:そこに流用したいプロジェクトの.iocファイルをコピー
3:コピーした.iocファイルでGENERATE CODEする。

で可能です。結構便利です。


今回は以上です。まだLEDチカチカできただけなので、次回はCPUの基本的な機能のサンプルプログラムを使ってみたいと思います。

【家の自動化プロジェクト】#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月22日土曜日

ルンバを買った話

どうも、caketetuです。
ちょっと前になりますが、我が家にルンバが赴任したのでちょっとしたレビューです。

これが買ったやつ、ROOMBA E5です




動いている様子がコチラ
音はうるさいですが、頑張っている感があってカワイイ





週2回、一か月ぐらい1Rの部屋で使用してみた感じですが、
清掃力というか吸引力はとても強い。一回の清掃でずいぶんきれいになります。

AIはあまりよくないようで、おなじところを何回も行ったり来たりしていることも
あります。しかし、駆動時間は長いのでそれでカバーしている感じです。

音はかなりうるさい。基本家にいないときに動かすことを想定しているのでしょう。

あとは床はかなりきれいにしないといけません。靴下一個もアウトです。
逆に床にモノを置かない生活を心がけるようになりました。



以上です。本体に改造はしない(予定)ですが、IFTTT連携などである程度コントロールできるので、時間があればチャレンジしてみようと思います。






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年8月20日木曜日

【Node.js】webサーバを作る

どうもcaketetuです。
相変わらず家の自動化プロジェクトを進めております。今回はIoTで大活躍するNode.jsを
使ってRaspberry Pi上にWEBサーバーを作ります。テンプレートエンジンにEJS、フレームワークとしてExpressを使用しますが、ジェネレータは使用せずに作ります。

Node.jsの導入は省略。

Expressではファイルやデータの場所が決まっているようです。適当な場所にディレクトリを作り、以下のようにファイルを配置します。

Home---views---index.ejs     ←WEBページ(HTML)
          |
            ---app.js ←プログラム本体(javascript)

ディレクトリ内にいる状態で以下コマンドでExpressをインストールします。
$ npm init
$ npm install express --save

とりあえずWEBページを表示するプログラムは以下の通りです。

app.js
//モジュールオブジェクトの初期化
var express = require('express');       
var app = express();
var http = require('http').Server(app);

//サーバー実装の前に、エラーハンドリングを記載
process.on('uncaughtException', function(err) {
    console.log(err);
  });

//テンプレートエンジンの指定
app.set("view engine", "ejs");

//ルーティング
app.get('/', (req, res)=> { res.render('index'); });

//3000番ポートで待ち受けてサーバー開始
http.listen(3000, function () {
    console.log("Start server at port:3000...");
});
index.ejs
<!DOCTYPE html><html>
    <head>
        <title>Home Ctrl</title>
    </head>
    <body>
        <h1>My Home Automation</h1s>
    </body>
</html>

アプリの実行は以下コマンドです。
$ node app

http://IPアドレス:ポート でWEBブラウザからアクセスできます。


これだけではまだ何もできませんのでSocket.IOでブラウザ経由でRaspberry Piに指示を
出せるようにしてみます。
$ npm install socket.io --save

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://アドレス:3000');
            function send_msg(cmd) {
                socket.emit("light", cmd);
            }
        </script>
        <h1>My Home Automation</h1>
        <button type="button" onClick="send_msg('living')">Living</button>
        <button type="button" onClick="send_msg('entrance')">Entrance</button>
    </body>
</html>
app.js
//モジュールオブジェクトの初期化
var express = require('express');       
var app = express();
var http = require('http').Server(app);
const io = require('socket.io')(http);          //Socket.io

//サーバー実装の前に、エラーハンドリングを記載します。
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) {
        console.log(msg);
    });
});

socket.emitでデータ送信socket.onでデータ受信します。これでボタンをブラウザの
ボタンを押してRaspberry pi に指示を出したり、Raspberry pi からセンサデータ
を受け取ったりできるようになります。



今回は以上です。ウェブブラウザベースで作ることでパソコンからもスマホからも同じようにアクセスできるようになります。



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ネットワークを構築していきたいと
思います。

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