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

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

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経由で操作できるようにしてみます。


【Arduino】アナログサーボをいい感じに動かす

どうもcaketetuです。今回はArduinoでアナログサーボを動かすテストです。
ただ動かすだけでは味気ないので、使いやすくスムーズに動かせるようにしてみます。

・とりあえず動かしてみる

まずはよくあるライブラリを使った簡単な実装です。

#include <Servo.h>

#define SERVO_PIN 5

Servo myservo;
 
void setup() {
    Serial.begin(115200);
    myservo.attach(SERVO_PIN);    //サーボ初期化
    myservo.write(90);            //初期角度90に設定
    delay(500);                   //移動まで待つ
}
 
void loop() {
    myservo.write(0);
    Serial.println("Servo move 0");
    delay(1000);
    myservo.write(90);
    Serial.println("Servo move 90");
    delay(1000);
    myservo.write(180);
    Serial.println("Servo move 180");
    delay(1000);
}

動作の様子です



・スムーズに動かしてみる

上のような簡単な実装では角度を与えたときにサーボの能力限界の速さで
動こうとするため、ゆっくり動かすのは不可能です。よって随時Delayを入れて
少しづつ指示を出すようにします。具体的には、指定した角度に何msで
移動せよという関数を作ります。

#include <Servo.h>

#define SERVO_PIN 5

int servo_move(int pos, int msec );

Servo servo;
int pos_now;    //角度保存用
 
void setup() {
    Serial.begin(115200);
    servo.attach(SERVO_PIN);    //サーボ初期化
    servo.write(90);            //初期角度90に設定
    delay(500);                 //移動まで待つ
    pos_now=90;                 //現在角度保存
    servo.detach();             //サーボピン解除
}
 
void loop() {
    //myservo.write(0);
    servo_move(0,1000);
    Serial.println(pos_now,DEC);
    Serial.println("Servo move 0");
    //myservo.write(90);
    servo_move(90,1000);
    Serial.println(pos_now,DEC);
    Serial.println("Servo move 90");
    //myservo.write(180);
    servo_move(180,1000);
    Serial.println(pos_now,DEC);
    Serial.println("Servo move 180");
}

//----------------------------------------------------------
//    サーボ動作関数    deg;-90~90 msec: 移動時間[ms]
//----------------------------------------------------------
int servo_move(int pos, int msec ){
  if(pos>180 || pos<0) return -1;    //posが範囲外ならリターン
  int d_pos = pos - pos_now;          //角度偏差計算
  int s_delay = abs(int(msec/d_pos)); //角度偏差からディレイ時間計算
  if(s_delay<0) return -1;            //ディレイ時間が0以下ならリターン(速すぎるため)
  
  servo.attach(SERVO_PIN);    //サーボ初期化
  
  if (d_pos > 0) { //d_posが0以上の時、プラス向きに動作
    for (int i = pos_now; i<=pos; i++) {
        servo.write(i);   //サーボ書き込み
        delay(s_delay);   //s_delayまで待つ
        pos_now = i;      //pos_now保存
    }
  }else if(d_pos < 0) { //d_posが0以下の時、マイナス向きに動作
    for(int i = pos_now; i>=pos; i--) {
        servo.write(i);   //サーボ書き込み
        delay(s_delay);   //s_delayまで待つ
        pos_now = i;      //pos_now保存
    }
  }else{ //d_posが0の時
  }
  
  servo.detach();   //サーボピン解除
}

動作の様子です
サーボの動きがゆっくり、スムーズなものになりました。


・割込みで実行してみる

ここまではメイン関数に書いていましたが、メイン関数はLCDの描写や通信
なんかで忙しいのでバックグラウンドで動くようにしたい。
ついでにサーボを動かした後はサーボをOFFし余計な電力消費や騒音を抑えます。

#include <MsTimer2.h>
#include <Servo.h>

#define SERVO_PIN 5

int servo_set(int pos, int msec);

Servo servo;                //サーボクラス
volatile int pos_now=90;    //角度保存用
volatile int pos_target=90; //ターゲットポジション
volatile int servo_flg=0;   //サーボ移動フラグ

//サーボタイマー
void timer_servo() {
   if(pos_target>pos_now){
      servo_flg = 1;
      pos_now++;
      servo.write(pos_now);
   }else if(pos_target<pos_now){
      servo_flg = 1;
      pos_now--;
      servo.write(pos_now);
   }else{
      servo_flg = 0;
      servo.detach();   //サーボピン解除
      MsTimer2::stop();
   }
}

//サーボ角度セット
int servo_set(int pos, int msec) {
    if(pos>180 || pos<0) return -1;    //posが範囲外ならリターン
    if(pos == pos_now)   return -1;    //pos=pos_nowならリターン
    int d_pos = pos-pos_now;          //角度偏差計算
    int s_delay = abs(int(msec/d_pos)); //角度偏差からディレイ時間計算
    pos_target = pos;
    servo.attach(SERVO_PIN);    //サーボ初期化
    servo_flg = 1;              //フラグON
    MsTimer2::set(s_delay, timer_servo);  //タイマセット
    MsTimer2::start();                    //タイマスタート
    return 1;           //正常リターン
}
 
void setup() {
    Serial.begin(115200);       //シリアルセット
    servo.attach(SERVO_PIN);    //サーボ初期化
    servo.write(90);            //初期角度90に設定
    delay(500);                 //移動まで待つ
    pos_now=90;                 //現在角度保存
    pos_target=90;              //現在角度保存
    servo.detach();             //サーボピン解除
}
 
void loop() {
    servo_set(0,1000);
    while(servo_flg);
    //delay(5000);
    servo_set(90,1000);
    while(servo_flg);
    //delay(5000);
    servo_set(180,1000);
    while(servo_flg);
    //delay(5000);
}

動きは変わらないので動画は割愛します。サーボの動きは同じですが、
サーボ動作中もメイン関数で色々できるのでより効率的なプログラミングができます。


今回は以上です。実はまだ割込みを細かくするだとかサーボの指示をDuty比で
指示できる関数を使ったりだとかまだ改善の余地はあります。もっと精密な動作
が必要になった時は実装してみようかなと思います。

2020年7月17日金曜日

Bloggerでプログラムコードを見やすく載せるメモ

どうもcaketetuです。
私はブログにプログラムコードを載せる時、短いコードは直接記載、長いコードは
google driveからリンクを貼って表示していました。しかし見栄えが悲惨な状況
だったので、今回はちょっとだけ見やすくしてみようと思います。

私は組み込み系はそこそこですが、WEB系はイマイチのため本記事は
以下のような人たち向けになるかと思います。
  • ちょっと見栄えを良くできればいい人
  • HTMLやCSSはあんまりいじりたくない人
  • 外部のサーバやソフトを使いたくない人




1、短いコードを記事に埋め込むとき

code-prettifyを使用します。少しHTMLをいじることになりますが、
比較的簡単にコードを色分けして表示することができます。

#include <Wire.h>
#include <PCA9685.h>            //PCA9685用ヘッダーファイル(秋月電子通商作成)

PCA9685 pwm = PCA9685(0x40);    //PCA9685のアドレス指定(アドレスジャンパ未接続時)

#define SERVOMIN 700            //最小パルス幅 (標準的なサーボパルスに設定)
#define SERVOMAX 2300            //最大パルス幅 (標準的なサーボパルスに設定)
int LED = 13;

void setup() {
 pwm.begin();                   //初期設定 (アドレス0x40用)
 pwm.setPWMFreq(240);            //PWM周期を60Hzに設定 (アドレス0x40用)

  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  Serial.println("Start reading.");
}

int n=0;
char buff[255];
                //手首R 肘 肩 
int angle[16] = {120,150,170,90,120,130,70,92,108,110,50,60,90,10,30,60};
int counter = 0;

void loop() {
//サンプルソース 16chすべてのチャンネルより0度~180度の移動を繰り返します。
  if(Serial.available()){
    delay(10);
    digitalWrite(LED, HIGH);
    while(Serial.available()){ 
      buff[counter] = Serial.read();
      counter++;
    }
    Serial.println(buff);
    Serial.println(counter,DEC);
    if(buff[0] == 'S' && counter == 17){
          for(int i=0; i<16; i++) angle[i] = buff[i+1]*2;
      }
  }else{
      digitalWrite(LED, LOW);
      counter=0;
  }
  
  for(int i=0; i<16; i++) servo_write(i, angle[i]);
}
void servo_write(int ch, int ang){ //動かすサーボチャンネルと角度を指定
  ang = map(ang, 0, 180, SERVOMIN, SERVOMAX); //角度(0~180)をPWMのパルス幅(150~600)に変換
  pwm.setPWM(ch, 0, ang);
}
bloggerでの導入方法はデザイン画面を開きテーマ→HTMLの編集を選びます。
ここでブログのHTMLソースを編集できます。<head>直下に
以下コードを追加します。
<script src='https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?skin=desert' type='text/javascript'/>

これだけでも動作しますが、スタイルをいじってもうちょっと見やすくしてみます。
テーマデザイナー→上級者向け―→CSSを編集
ここに以下コードを追加します。

    .prettyprint ol.linenums > li {
	list-style-type: decimal;	/*行数を1づつ表示*/
} 

pre.prettyprint {
	margin: 5x;
	margin-bottom:4rem; 
	padding: 10px; 
	padding-left:20px; 		/*行番号が隠れないように追記*/ 
	/*max-height: 300px;*/ /*縦サイズは記事毎に設定するため削除*/ 
	overflow: auto; 		/*横スクロールバー*/
	/*background-color: #f8f8f8;*/ /*背景を変えたいとき*/
	border-radius:4px;		/*かどをまるく*/
}
    

記事に載せる時は編集画面の一番左の鉛筆マークからHTMLビューを選択します。
コードを乗っけたいところに以下のように追記します。
<pre class="prettyprint lang-言語 linenums">
	載せたいコード
</pre>
"言語"は以下参照
"bsh", "c", "cc", "cpp", "cs", "csh", "cyc", "cv", "htm", "html", "java", "js", "m", "mxml", "perl", "pl", "pm", "py", "rb", "sh", "xhtml", "xml", "xsl".

このとき、不等号や&があると、ソースコードと認識しうまくいかないため
これらを文字列として認識させる必要があります。こちらのサイトを使って
変換すればうまくいくと思います。

あとはHTMLビューと作成ビューを切り替えながら編集していくと比較的
簡単にできると思います。HTMLビューで編集するのは慣れていないと大変なので
私は作成ビューで一通り編集した後HTMLビューでソースコードを追加することに
しています。

見た目はこの記事のようになります。このスタイルはdesertテーマに少し手を
加える形で簡単に作っています。もう少しこだわりたい人はHTMLソースからテーマを
変えたり、CSSをいじるとブログにあったデザインになると思います。





2、長いコードをファイルごと載せる時

 googleドライブに共有可能なフォルダを作りその中にコードを入れ
 googleドキュメントで編集します。

 googleドキュメントのアドオン→アドオンを取得からcode blockを導入します。



あとは言語、テーマを選びコードをすべて選択してformatするだけです。
私はxcodeがいいんじゃないかな~と思います。





編集が終わったら右上の共有からリンクを取得します。これをブログに
貼り付けます。
これで長いコードもブログを圧迫することなくきれいに見せることができます。





ちょっとひと手間ですが、劇的に見やすくすることができたと思います。私は
作ること中心で記事の体裁にはあんまり時間をかけたくないのでこれぐらいが
ちょうどいいですね。

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