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がいいんじゃないかな~と思います。





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





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

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


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

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