ESP32 1.0.3のBLEUUIDについて

検証したら、他にもおかしいことがありました。

概要

ESP32 1.0.3のBLEUUIDはおかしいので、気をつけましょう。

検証コード

#include "BLEDevice.h"

void setup() {
  Serial.begin(115200);
  delay(1000);

  BLEUUID uuid("00001812-0000-1000-8000-00805F9B34FB");
  Serial.printf("uuid     : %s\n", uuid.toString().c_str());

  BLEUUID uuid16("1812");
  Serial.printf("uuid16   : %s\n", uuid16.toString().c_str());

  BLEUUID uuid16_2(uuid16.toString().c_str());
  Serial.printf("uuid16_2 : %s\n", uuid16_2.toString().c_str());

  BLEUUID uuid32("00001812");
  Serial.printf("uuid32   : %s\n", uuid32.toString().c_str());
}

void loop() {
}

実行結果

uuid     : 00001812-0000-1000-8000-00805f9b34
uuid16   : 1812-0000-1000-8000-00805f9b34fb
uuid16_2 : <NULL>
uuid32   : 00001812-0000-1000-8000-00805f9b34fb

内部で小文字に置き換えているので、00001812-0000-1000-8000-00805f9b34fbと出て欲しいのですが、普通に指定すると最後の2文字が削られます。

内部データも削られているので、おそらく動くように思えますが、最後の2文字だけ違うデバイスがあったら間違えて接続してしまいます。

短縮形で渡すと4文字の場合、先頭の0000が削られます。そのため00001812-0000-1000-8000-00805f9b34fbとはマッチングしません。

短縮形で8文字で渡すと正しく表示されますが、内部データは短縮形4文字の0000が削られた形で保存されているので、マッチングしません!

まとめ

HIDなどの4文字短縮デバイスを使う場合には4文字で指定しないと動きません。修正したものをプルリクエストで投げてみましたが、現状の1.0.3は上記の問題がありますのでご注意ください。

M5StickC(ESP32)でダイソーのBluetoothシャッターを操作(1.0.3対応版)

ESP32ライブラリ1.0.2のときに検証したものを、最新バージョンの1.0.3で検証しなおしました。M5StickCで実験しましたが、コード的には単なるESP32です。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

1.0.2での問題点は以下の2点でした。

  • リセットがかかる
  • 同一CharacteristicUUIDがあると1つしか取得できない

1個目のリセットがかかるについては、修正されておりました。2個目についてはまだ修正されていませんので、対策をする必要があります。

同一CharacteristicUUIDの取得方法

getCharacteristics()関数

/**
 * @brief This function is designed to get characteristics map when we have multiple characteristics with the same UUID
 */
void BLERemoteService::getCharacteristics(std::map<uint16_t, BLERemoteCharacteristic*>* pCharacteristicMap) {
	pCharacteristicMap = &m_characteristicMapByHandle;
}  // Get the characteristics map.

上記の関数で、参照渡しでm_characteristicMapByHandle構造体を引き渡しているように見えますが、引数が参照ではなくコピーなので、実際には値が取得できないようです。

上記に詳しい解説があります。

getCharacteristicsByHandle()関数

こちら宣言はBLERemoteService.hにありますが、BLERemoteService.cppで実装されていません。

しかし、よく考えれば実装されていないってことは、自分で実装してしまえばいいのです!

解決方法

getCharacteristicsByHandle()関数を自分で作成

std::map<uint16_t, BLERemoteCharacteristic*>* BLERemoteService::getCharacteristicsByHandle() {
  if (!m_haveCharacteristics) {
    retrieveCharacteristics();
  }
  return &m_characteristicMapByHandle;
}

関数的には単純ですので、すぐに実装が可能です。1.0.3では実装されていないので、ライブラリに手を入れずに、自分のプロジェクトの中に追加することで利用可能になります。

Scanのサンプル(GitHub)

#include "BLEDevice.h"

// 1.0.3で未定義のメソッドを実装する
std::map<uint16_t, BLERemoteCharacteristic*>* BLERemoteService::getCharacteristicsByHandle() {
  if (!m_haveCharacteristics) {
    retrieveCharacteristics();
  }
  return &m_characteristicMapByHandle;
}

// 検索するBLEデバイス。serviceUUIDを調べる場合には空にする(例はHuman Interface Device"00001812-0000-1000-8000-00805f9b34fb")
static BLEUUID serviceUUID("1812");

static BLEAdvertisedDevice* myDevice;

// 接続してCharacteristic一覧を取得
bool connectToServer() {
  Serial.print("接続先 : ");
  Serial.println(myDevice->getAddress().toString().c_str());
  Serial.println("start createClient");
  BLEClient*  pClient  = BLEDevice::createClient();
  Serial.println("end createClient");
  Serial.println("start connect");
  pClient->connect(myDevice);
  Serial.println("end connect");

  // サービス取得
  Serial.println("start getService");
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  Serial.println("end getService");
  if (pRemoteService == nullptr) {
    pClient->disconnect();
    return false;
  }

  // Characteristic一覧
  Serial.println("characteristic一覧");
  std::map<uint16_t, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristicsByHandle();
  for (std::map<uint16_t, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
    Serial.print(" - characteristic UUID : ");
    Serial.print(i->second->getUUID().toString().c_str());
    Serial.print(" Broadcast:");
    Serial.print(i->second->canBroadcast()?'O':'X');
    Serial.print(" Read:");
    Serial.print(i->second->canRead()?'O':'X');
    Serial.print(" WriteNoResponse:");
    Serial.print(i->second->canWriteNoResponse()?'O':'X');
    Serial.print(" Write:");
    Serial.print(i->second->canWrite()?'O':'X');
    Serial.print(" Notify:");
    Serial.print(i->second->canNotify()?'O':'X');
    Serial.print(" Indicate:");
    Serial.print(i->second->canIndicate()?'O':'X');
    Serial.println();
  }

  // stop
  Serial.println("プログラム停止!");
  while (1) delay(1000);

  return true;
}

// 検索したデバイスを受信するコールバック関数
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE デバイス発見 : ");
      Serial.println(advertisedDevice.toString().c_str());

      if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
        // 指定デバイスだったら接続する
        BLEDevice::getScan()->stop();
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
      }
    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("BLEデバイス検索開始...");
  BLEDevice::init("");

  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
}

void loop() {
  if (myDevice != NULL) {
    connectToServer();
  }

  delay(1000);
}

1.0.2のときにはリセット回避のために、黒魔術を使っていましたが1.0.3の場合にはライブラリ関数を、自分で実装するだけで実現できました。

BLEデバイス検索開始...
BLE デバイス発見 : Name: AB Shutter3       , Address: ff:ff:XX:XX:XX:XX, appearance: 961, serviceUUID: 1812-0000-1000-8000-00805f9b34fb
接続先 : ff:ff:XX:XX:XX:XX
start createClient
end createClient
start connect
end connect
start getService
end getService
characteristic一覧
 - characteristic UUID : 2a4e-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:O Write:X Notify:X Indicate:X
 - characteristic UUID : 2a4d-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
 - characteristic UUID : 2a4d-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
 - characteristic UUID : 2a4b-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 2a4a-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 2a4c-0000-1000-8000-00805f9b34fb Broadcast:X Read:X WriteNoResponse:O Write:X Notify:X Indicate:X
プログラム停止!

実行結果が上記です。

ボタン状態取得のサンプル(GitHub)

/**
   A BLE client example that is rich in capabilities.
   There is a lot new capabilities implemented.
   author unknown
   updated by chegewara
*/

#include "BLEDevice.h"

// 1.0.3で未定義のメソッドを実装する
std::map<uint16_t, BLERemoteCharacteristic*>* BLERemoteService::getCharacteristicsByHandle() {
  if (!m_haveCharacteristics) {
    retrieveCharacteristics();
  }
  return &m_characteristicMapByHandle;
}

// The remote service we wish to connect to.
static BLEUUID serviceUUID("1812");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLEAdvertisedDevice* myDevice;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
  Serial.print("Notify callback for characteristic ");
  Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
  Serial.print("(");
  Serial.print(pBLERemoteCharacteristic->getHandle());
  Serial.print(") of data length ");
  Serial.print(length);
  Serial.print(" data: ");
  for ( int i = 0 ; i < length ; i++ ) {
    Serial.printf( "%02X ", pData[i] );
  }

  Serial.println();
}

class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {
    }

    void onDisconnect(BLEClient* pclient) {
      connected = false;
      Serial.println("onDisconnect");
    }
};

bool connectToServer() {
  Serial.print("Forming a connection to ");
  Serial.println(myDevice->getAddress().toString().c_str());

  BLEClient*  pClient  = BLEDevice::createClient();
  Serial.println(" - Created client");

  pClient->setClientCallbacks(new MyClientCallback());

  // Connect to the remove BLE Server.
  pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
  Serial.println(" - Connected to server");

  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(serviceUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found our service");

  std::map<uint16_t, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristicsByHandle();
  for (std::map<uint16_t, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
    if (i->second->canNotify()) {
      Serial.println(" - Add Notify");
      i->second->registerForNotify(notifyCallback);
    }
  }

  connected = true;
  return true;
}
/**
   Scan for BLE servers and find the first one that advertises the service we are looking for.
*/
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    /**
        Called for each advertising BLE server.
    */
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE Advertised Device found: ");
      Serial.println(advertisedDevice.toString().c_str());

      // We have found a device, let us now see if it contains the service we are looking for.
      if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

        BLEDevice::getScan()->stop();
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
        doConnect = true;
        doScan = true;

      } // Found our server
    } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
  } else if (doScan) {
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }

  delay(1000); // Delay a second between loops.
} // End of loop

BLE_clientに最低限の手を入れるだけで動くようになりました。

Starting Arduino BLE Client application...
BLE Advertised Device found: Name: AB Shutter3       , Address: ff:ff:XX:XX:XX:XX, appearance: 961, serviceUUID: 1812-0000-1000-8000-00805f9b34fb
Forming a connection to ff:ff:XX:XX:XX:XX
 - Created client
 - Connected to server
 - Found our service
 - Add Notify
 - Add Notify
We are now connected to the BLE Server.
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(19) of data length 2 data: 01 00 
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(19) of data length 2 data: 00 00 
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(23) of data length 2 data: 00 28 
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(19) of data length 2 data: 01 00 
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(19) of data length 2 data: 00 00 
Notify callback for characteristic 2a4d-0000-1000-8000-00805f9b34fb(23) of data length 2 data: 00 00 

上記が実行結果です。2つあるキーのうち、iOSキーを押した場合、ハンドルID19の01 00(ボリュームアップ)と00 00(キーアップ)が飛んできます。

Androidキーを押した場合には、ハンドルID23の00 28(エンター)が押されたのち、ハンドルID19の01 00(ボリュームアップ)と00 00(キーアップ)がきて、ハンドルID23の00 00(キーアップ)が飛んできます。

新規問題点

上記でも報告されていますが、UUIDの桁数がおかしいです。

1.0.2 : 00001812-0000-1000-8000-00805f9b34fb
1.0.3 : 1812-0000-1000-8000-00805f9b34fb

先頭の0が消えています。

std::string BLEUUID::toString() {
	if (!m_valueSet) return "<NULL>";   // If we have no value, nothing to format.
	// If the UUIDs are 16 or 32 bit, pad correctly.

	if (m_uuid.len == ESP_UUID_LEN_16) {  // If the UUID is 16bit, pad correctly.
		char hex[5];
		snprintf(hex, sizeof(hex), "%04x", m_uuid.uuid.uuid16);
		return std::string(hex) + "-0000-1000-8000-00805f9b34fb";
	} // End 16bit UUID

	if (m_uuid.len == ESP_UUID_LEN_32) {  // If the UUID is 32bit, pad correctly.
		char hex[9];
		snprintf(hex, sizeof(hex), "%08x", m_uuid.uuid.uuid32);
		return std::string(hex) + "-0000-1000-8000-00805f9b34fb";
	} // End 32bit UUID

原因はBLEUUID.cppの上記箇所で、1812などの4文字の短縮形の場合m_uuid.uuid.uuid16を利用するのですが、先頭に0000を追加するの忘れています。

まとめ

まだまだ不安定なESP32のBluetoothライブラリです。

上記3件は修正のプルリクエストを出してみました。取り込まれるといいんですが、、、

getCharacteristics()は参照渡しにしたほうがシンプルなのですが、定義自体が変わってしまうので、最小限の修正で済むようにしています。

M5StickCのFritzingパーツ作成

配線図がそろそろ欲しくなったのですが、Fritzing用のパーツが無かったので作ってみました。

概要

Fritzingは無料で使える回路図エディタで、かんたんにきれいな図を作ることができます。しかしながら一般的なパーツデータは内蔵されていますが、製品データなどは自分で作成するか、誰かが作成したデータを利用する必要があります。

作り方

同じく無料で使えるInkscapeでM5StickCの絵を準備します。私の場合にはすでにエクセルのオートシェイプで描画したデータがあったので、Inkscapeに貼り付けて、サイズを実寸に縮小したものを利用しました。

イチからInkscapeで描画するのは結構たいへんですが、写真をエクセルに貼り付けて、同じようなものをオートシェイプで作るのはそれほど難しくなかったです。

ピン設定

ピンのリンクをする場所を、適当な黒円を作ってピンの場所に設置しました。その後IDをconnector0pinから連番に変更するとFritzingでピンの対応がされました。

データ修正

エクセルで作ったデータには、なにか無駄なオブジェクトが入っているようで、Fritzingに取るこむと、ものすごい大きな四角いオブジェクトがありました。保存したSVGファイルをテキストエディタで開いて、defs以下のオブジェクトをすべて消すとうまく取り込めました。

作成物

上記においておきました。

まとめ

謎のオブジェクトにはちょっとハマりましたが、それ以外は比較的かんたんにつくることができると思います。ただしデザインルールなどは守っていません。あとブレッドボードの画像以外は作っていないので、雛形にしたICのままです。

M5StickCの消費電流 その2

省電力系の設定を計測してみました。

現時点の情報ですので最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

USB接続のテスターを利用して、消費電流を計測しています。供給電圧は5Vですので、内部的にDCDCなどを経由してロスがでています。

充電機能は基本的にAXP経由で止めていますが、止めても少しだけ流れています。また、動作時の温度などで消費電流が変わる気がしますので絶対値ではなく、オプションを変更した場合の増減値を参考値として利用してください。

測定値は瞬間電流を目でみて記録していますので、精度は低いです。

CPU周波数

周波数(MHz)消費電流(mA)増減(mA)
24095.50.0
16082.1-13.4
8074.4-21.1
4065.7-29.8
2060.5-35.0
1059.0-36.5

標準が240MHzで、下げた場合の消費電流の推移です。10と20だとほとんど差がないんですね。

スリープ

状態消費電流(mA)増減(mA)
電源OFF13.30.0
DeepSleepEXT0WakeUp17.03.7
DeepSleepEXT1WakeUp17.23.9
DeepSleepTimerWakeUp17.84.5
DeepSleepTouchWakeUp17.03.7
DeepSleepULPWakeUp17-33
LightSleepEXT0WakeUp18.45.1
LightSleepEXT1WakeUp18.24.9
LightSleepGPIOWakeUp18.35.0
LightSleepTimerWakeUp18.24.9
LightSleepTouchWakeUp18.24.9
LightSleepULPWakeUp18.2-19.3
WAKE98.885.5

ソースコードはGitHubのサンプルを利用しています。DeepSleepとLightSleepはもっと差があると思いましたが、こんなに差が少ないんですね。

ULPは動作していないときには省電力ですが、起動時には結構電力使っていました。LightSleepは増減が少なかったのですが、DeepSleepは非常に電力使っていたのでちょっと気になります。

スリープ設定

状態消費電流(mA)増減(mA)
標準17.00.0
PERIPH OFF17.00.0
SLOW_MEM OFF16.8-0.2
FAST_MEM OFF16.8-0.2
ALL OFF16.8-0.2

下記コードを使って設定値をコメントアウトしながら計測しました。GPIOをPULLUPやHIGH設定にしていないので、周辺機器を切っても電流に差はありませんでした。

メモリはどちらかを切ると消費電流が下がりましたが、両方切っても変わらない謎の動きです。

void setup() {
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH  , ESP_PD_OPTION_OFF);
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);

  esp_deep_sleep_start();
}

void loop() {
}

AXPスリープ

状態消費電流(mA)増減(mA)
通常99.70.0
AxpSleep67.9-31.8

AXPのスリープです。主に画面への電源供給が止まります。画面の明るさを7とかにしたのとそれほど変化がなかったです。

まとめ

スリープの消費電流の結果が意外でした。GPIOのpinModeを変更した場合や、通信をした場合の計測もしていきたいと思います。

M5StickCの消費電流 その1

液晶の明るさとかを変更しながら、消費電力を計りました。精度はそれほど高くなく、値もふらつくので概算として利用してください。

現時点の情報ですので最新情報はM5StickC非公式日本語リファレンスを確認してください。

画面の明るさ

明るさ消費電力(mA)増減(mA)
072.50
172.50
272.50
372.50
472.50
572.50
672.50
772.70.2
873.91.4
977.75.2
1083.711.2
1191.318.8
12100.728.2

0-6は実際には暗すぎてほぼ見えません。消費電力の変化もありませんので、液晶の最低電圧以下かもしれません。

ボタン(MPU6886)

状態消費電力(mA)増減(mA)
Off72.50
BtnA On72.70.2
BtnB On180.1107.6

えーっと、右側ボタンのBtnBを押すと100mAも消費電力が増えます、、、

回路の抵抗値間違えてない?

ボタン(SH200Q)

状態消費電力(mA)増減(mA)
Off76.50
BtnA On76.70.2
BtnB On76.70.2

もう一台で調べてみましたら、普通の結果です。MPU6886は全部おかしいのか、私の一台だけおかしいのか、誰か検証お願いします。

ちなみに左ボタンはAXPが管理していて、消費電力は特になかったです。

BtnAとBtnBは回路的にプルアップされていて、ボタンを押すとLOWになるので、その分消費電力が発生すると思っていましたが、予想外の結果が出てしまいました。

内蔵赤色LED

状態消費電力(mA)増減(mA)
Off95.60
On116.721.1

20mAぐらいなので、普通ですよね?

内蔵赤外線LED(IR)

状態消費電力(mA)増減(mA)
Off95.90
On121.825.9

赤色LEDより消費電力少しだけ多いんですね。

まとめ

ボタンの結果が意外すぎて、混乱しています。他の項目も少しずつ調べていきたいと思います!

M5StickCのマイクを使ってみる その1

サンプルスケッチを動かす以上していなかったマイクを触ってみました。もう少し掘り下げて調べる必要がありそうです。

現時点での情報のため、最新情報はM5StickC非公式日本語リファレンスを確認お願いします。

概要

M5StickCにはI2S経由で接続されているマイクが内蔵されています。I2Sからの受信は別タスクで実行され、DMA転送されてバッファに保存されています。

マイクのスペック

型番SPM1423
データシートhttps://github.com/m5stack/M5-Schematic/blob/master/Core/SPM1423HM4H-B.pdf
製造元Knowles Electronics, LLC.
方向無指向性
周波数範囲100Hz ~ 10kHz
感度-22dB ±3dB @ 94dB SPL
S/N比61.5dB
電圧範囲1.6V ~ 3.6V(2.8V供給)
電流 – 供給600µA

サンプルスケッチ(GitHub)

#include <M5StickC.h>
#include <driver/i2s.h>

#define PIN_CLK  0
#define PIN_DATA 34
#define READ_LEN (2 * 256)
uint8_t BUFFER[READ_LEN] = {0};

uint16_t oldy[160];
int16_t *adcBuffer = NULL;

void i2sInit()
{
   i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate =  44100,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 2,
    .dma_buf_len = 128,
   };

   i2s_pin_config_t pin_config;
   pin_config.bck_io_num   = I2S_PIN_NO_CHANGE;
   pin_config.ws_io_num    = PIN_CLK;
   pin_config.data_out_num = I2S_PIN_NO_CHANGE;
   pin_config.data_in_num  = PIN_DATA;
  
   
   i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
   i2s_set_pin(I2S_NUM_0, &pin_config);
   i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
}



void mic_record_task (void* arg)
{   
  size_t bytesread;
  while(1){
    i2s_read(I2S_NUM_0,(char*) BUFFER, READ_LEN, &bytesread, (100 / portTICK_RATE_MS));
    adcBuffer = (int16_t *)BUFFER;
    showSignal();
    vTaskDelay(100 / portTICK_RATE_MS);
  }
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.println("mic test");

  i2sInit();
  xTaskCreate(mic_record_task, "mic_record_task", 2048, NULL, 1, NULL);
}


void showSignal(){
  // Offset
  int32_t offset_sum = 0;
  for (int n = 0; n < 160; n++) {
    offset_sum += (int16_t)adcBuffer[n];
  }
  int offset_val = -( offset_sum / 160 );

  // Auto Gain
  int max_val = 200;
  for (int n = 0; n < 160; n++) {
    int16_t val = (int16_t)adcBuffer[n] + offset_val;
    if ( max_val < abs(val) ) {
      max_val = abs(val);
    }
  }
  
  int y;
  for (int n = 0; n < 160; n++){
    y = adcBuffer[n] + offset_val;
    y = map(y, -max_val, max_val, 10, 70);
    M5.Lcd.drawPixel(n, oldy[n],WHITE);
    M5.Lcd.drawPixel(n,y,BLACK);
    oldy[n] = y;
  }
}

void loop() {
  printf("loop cycling\n");
  vTaskDelay(1000 / portTICK_RATE_MS); // otherwise the main task wastes half of the cpu cycles
}

オフィシャルのGitHubにある最新版のサンプルスケッチを参考にしています。0.1.0のサンプルスケッチはデータがuintだったりといろいろおかしいので、修正されたものになります。

私の手元の2台を動かしたところ、無音でも-1500前後数値でしたので、表示部分のデータ平均を取得して、そこを中央に表示するオフセットを行っています。

また、かなり大きな音がしないとグラフがでないので、最大値と最小値を計算して、グラフが拡大するようにしています。

検証

検証にはefuさんの多機能 高精度 テスト信号発生ソフトWaveGeneを利用させていただきました。

上記のように1000Hzのサイン波をパソコンから再生し、イヤホンを使ってM5StickCのマイク入力を行いました。

結果、上記のようにサイン波を画面に表示することができました。再生周波数を変更することで、画面上の表示も変化しています。

まとめ

サンプルスケッチを動かすところまではできましたが、細かい詳細までは検証できていません。

また、このサンプルは100msのスリープを入れているので秒間10回しか描画しません。そして1度に受け取っているデータが256個のため、全44,100サンプリング中2,560サンプリングしか受信できていないことになります。

256個受け取ったデータも、画面上には横160個分しか表示していないので、96サンプリングは捨てています。なんとなく波形を表示させたり、FFTで周波数分析をする分には問題ありませんが、ちゃんと利用するためにはかなり手をいれないとダメな気がします。

ESP32の選び方 2019年9月

ESP32の選び方をまとめてみました。

ESP32

純粋なESP32はフラッシュなどを内蔵していない部品であり、無線のアンテナもないので日本ではお金をかけて技適を取得しないと利用できません。

日本国内で利用する場合には技適マークのついている商品を利用する必要があるので、気をつけて選びましょう。

ESP32-WROOM-32

技適取得済みのパッケージ版です。左側が単品で、右側がブレイクアウトして使いやすくしたものです。自分で基板を作成する場合にはよいのですが、通常用途では使いにくいためおすすめしません。

初期には開発ボードがなかったので、単品を使っていた事例が多かったですが、現状はいろいろ回路を追加しないと動かないのでおすすめしません。

ESP32開発ボード

ESP32-WROOM-32に動作に必要な回路やUSB接続を追加した開発ボードです。左2つは同じように見えますが、ピンの数が違います。

機能的にはほぼ同じようなものですが、ボードの大きさが何種類かあります。一番左が片側15ピンの30ピンモデル。左から2個目は片側19ピンの38ピンモデル。また横幅も狭いものや広いものがあります。

個人的には小さい30ピンモデルが使いやすいのでよく使っていますが、ピンが少ないので使いたくても使えないピンがあります。

私がよく使っているのが、上のブレッドボードですが、縦17ピンしかありません。そのため小さいサイズの開発ボードしか使うことができません。

また、横幅が広いと普通の片側5列のブレッドボードを使うと、片側1列しか利用することができません。

上記のサンハヤトの片側6列あるブレッドボードか、左右の耳が取れるブレッドボードを連結して使うと便利だと思います。

上記がESP32の開発元が販売しているリファレンス機となります。これが一番サイズが大きいですが、安定動作するはずです。

日本国内に限っては上記ボードが、利用者が多いようです。

M5StickC

いまは、商品をメインで使っています。液晶画面と小さいバッテリー、赤いLEDライトと赤外線送信に2つのボタンと電源ボタンを内蔵しています。

通常用途であれば十分利用できるのですが、小さい分使えるピンが少ないです。実際のところそんなにピンを使う用途って無いので、80%ぐらいの用途はこれでカバーできる気もします。

また、現在は製造が追いついておらず、商品が品薄です。

上記が正規取扱い店です。Amazonだとまだ品薄なので、中国からの並行輸入品が販売されています。M5Stack Official Storeが安いのですが、海外発送なので1週間から2週間程度届くまでに時間がかかります。

本体と合体することで拡張できるHATや、ケーブルで接続することで使えるUNITもありますので機能拡張も可能です。

M5Stack

M5StackはM5StickCより大きく、種類もたくさんあります。BASIC、GRAY、FIRE、GOがありますが、まず1台であれば加速度センサーがついていて、標準的なGRAYがおすすめだと思います。

上記動画で選び方が解説されていますので、参考にしてください。機能拡張はM5StickCと同じケーブルで接続するUNITの他に、積み重ねて使うModuleがあります。

その他の開発ボード

いろいろありますが、商品的にあまり安定供給されていないイメージがありますのでおすすめしません。

カメラが利用したい場合には、以下の選択肢もあります。

Arduino以外のOS搭載ボード

obnizはJavaScriptで開発できるボードです。最近OSだけも販売をはじめましたので、通常のESP32開発ボードにOSを入れて使うこともできます。Nefry BTはArduino IDEで開発することができますが、ブラウザを利用した管理画面などがあります。

その他PythonやJavaScriptで開発ができるOSもありますので、通常のESP32開発ボードにいれて使うことも可能です。

まとめ

Espressif社のESP32-DevKitCがリファレンス機ですが、あまり流通していないのでこれという標準機が存在していません。

画面がついていて、箱にはいっているM5Stack社の製品が人気です。ただ海外製品が多いので、使っている部品が途中から変わって、バージョンアップしていることがありますので注意してください。

Arduinoのはじめ方 2019年9月

いまからArduinoをはじめる方法を紹介します。

[無料] Autodesk Tinkercad

AutodeskはAutoCADという有名なCADソフトを作っている老舗企業です。主に学生向けにArduinoなどのプログラミングや、3Dデザインをブラウザ上で学習することができます。

Arduinoもブラウザ上でコードをいれて、実行をすると動きをシミュレートしてくれます。無料で使えるので、ちょっとどんな感じかなと試してみるのに適していますが、中身が英語で部分的にしか日本語化していないのがネックです。

また、登録しないと利用できないので、ちょっと不便ですね。機能的にはかなり充実していますが、このサイトだけだとチュートリアル的なものが足りないので教材は別に準備したほうがいいと思います。

ELEGOO Arduino用スターターキット レベルアップ

私が最初に買った学習用のキットですが、Arduino UNO互換機の他に学習で利用する部品が含まれています。とりあえず持っていたほうが便利な部品が一式含まれているのと、日本語でのドキュメントが充実しています。

ドキュメント自体はCDで付属していますが、サイトからダウンロードも可能です。このドキュメントはよくできているので、これをダウンロードして、Autodesk Tinkercad上で無料でためしてみるのもいいかもしれません。

予算があるのであれば、実物を動かしながら学習したほうが理解は早いと思います。LEDとかを実際に利用してみて、抵抗を入れ忘れて壊すみたいな体験をしたほうが記憶には残りやすい気がします。

より部品数が多いキットや、スマートカーが入っているキットもあります。どんなことができるかは事前にドキュメントをダウンロードして見比べることもできます。

ELEGOO以外にも似たようなセットがありますが、ドキュメントはELEGOOが一番充実していると思います。

もしくは、書籍とその中で利用している部品のセットもあります。こちらの方が初学者向けの内容ではあります。実際の書籍がほしい場合にはこのセットがおすすめです。

ESP32

Arduino UNOは非常に使いやすく、標準的なボードですが無線通信ができません。そのため無線を内蔵しているESP32もおすすめします。最初の一台はUNOと部品セットの方が使いやすいと思いますが、2台目には通信ができるESP32がいいと思います。

ただ外付けの部品を使わないとか、完成品のユニットを接続しかしないのであれば、1台目からESP32のケース付きのボードが使いやすいです。

たとえば、M5StackかM5StickCなどの液晶画面とバッテリー付きESP32の商品に、上記のような温度センサーや、人感センサー、ボタンなどを組み合わせて使う場合には、箱付きの商品を最初から使ったほうが簡単です。

まとめ

単価でいうと部品を自分で組み合わせた方が安いですが、ケースなどを揃えると高くなってしまいます。自分専用のセットで部品を壊しながら学ぶのであればUNOのセット。複数人で簡単にできる学習用であれば箱入りの商品が無難です。

おすすめは、Arduino UNOのセットを購入し、その後にプレーンなESP32で無線を使ってみて、最後にM5StackかM5StickCなどの箱付き製品を使うのが、どっぷり楽しめます。

電子工作は動いている分には問題ないのですが、動かない場合の問題切り分けに追加機材が必要になったりします。ESP32の箱付き商品で問題が出た場合、プレーンなESP32でも同じ問題がでるかを確認したり、UNOで動作検証をしたりと複数持っていたほうが嬉しい場合も多いです。

M5StickCで赤外線リモコンを使う

本体に内蔵している赤外線ユニットを利用してNECフォーマットの赤外線送信を実験しました。現時点の情報ですので最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

M5StickCにはGPIO9に赤外線リモコン用のIRが内蔵されており、汎用的なリモコンとして利用が可能です。

ローレベルな関数を利用して、直接送信することも可能ですが、一般的な利用を想定して外部ライブラリを利用しています。

利用ライブラリ(IRremoteESP8266)

IRremoteESP8266はArduinoの標準ライブラリからインポートして利用することができるライブラリで、IRremoteという有名な赤外線リモコンのライブラリをESP8266とESP32に特化した形でフォークしたものです。

元になったIRremoteは受信しかできませんが、IRremoteESP8266を利用することで送信も可能です。

受信テスト

送信できたのかを確認するためには、受信環境を用意したほうがテスト簡単です。テレビとそのリモコンがあれば、送信コードを調べることで最低限確認ができますが、前回利用した赤外線受信ユニットを利用して実験を行いました。

まずは、本物のリモコンを操作して、どんなコードが飛んでいるのかを確認して、そのコードと同じものを送信できるようにします。

受信側はIRremoteESP8266のサンプルスケッチであるIRrecvDumpV2を利用すると簡単に分析が可能です。受信ユニットを接続したPINの番号を変更するだけで動きました。もちろん前回実験したときと同じデータが受信できました。

送信側コード(GitHub)

#include <M5StickC.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <IRutils.h>

const uint16_t kIrLed = 9;              // M5StickCはGPIO9にIRが内蔵

IRsend irsend(kIrLed);                  // IR送信を宣言

const uint32_t CUSTOMER_CODE = 0x00ff;  // カスタマーコードをセット

// リモコンコード保存用構造体
struct REMOTE {
  char name[9];
  uint8_t command;
};

// リモコンコード一覧
REMOTE remote[] = {
  { "POWER" , 0x45 },
  { "VOL+"  , 0x46 },
  { "VOL-"  , 0x15 },
  { "0"     , 0x16 },
  { "1"     , 0x0c },
  { "2"     , 0x18 },
  { "3"     , 0x5e },
  { "4"     , 0x08 },
  { "5"     , 0x1c },
  { "6"     , 0x5a },
  { "7"     , 0x42 },
  { "8"     , 0x52 },
  { "9"     , 0x4a },
};

int cursor = 0; // カーソル位置

void setup() {
  M5.begin();     // M5StickC初期化
  irsend.begin(); // IR初期化

  // リモコン項目表示
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 8);
  for ( int i = 0 ; i < ( sizeof(remote) / sizeof(REMOTE) ) ; i++ ) {
    M5.Lcd.print((cursor == i) ? ">" : " ");
    M5.Lcd.println(remote[i].name);
  }
}

void loop() {
  M5.update();  // ボタン状態更新

  // M5ボタンで送信
  if ( M5.BtnA.wasPressed() ) {
    // 送信4Byte(カスタマーコード2Byte+リモコンコード+反転リモコンコード)
    uint64_t send = 0;
    send = (uint64_t)reverseBits(CUSTOMER_CODE >> 8, 8) << 24;    // カスタマーコード(上位8bit)
    send += (uint64_t)reverseBits(CUSTOMER_CODE & 0xff, 8) << 16; // カスタマーコード(下位8bit)
    send += reverseBits(remote[cursor].command, 8) << 8;          // リモコンコードを順番入れ替えて送信
    send += reverseBits(remote[cursor].command, 8) ^ 0xff;        // リモコンコードのビット反転(パリティ)
    irsend.sendNEC(send);                                         // 送信

    // デバッグ表示
    Serial.printf("Send IR : 0x%08LX", send);
    Serial.printf("(customer=0x%04X, ", CUSTOMER_CODE);
    Serial.printf("command=0x%02X)\n", remote[cursor].command);
  }

  // 右ボタンでカーソル移動
  if ( M5.BtnB.wasPressed() ) {
    cursor++;
    cursor = cursor % ( sizeof(remote) / sizeof(REMOTE) );

    // カーソル描画
    M5.Lcd.setCursor(0, 8);
    for ( int i = 0 ; i < ( sizeof(remote) / sizeof(REMOTE) ) ; i++ ) {
      M5.Lcd.println((cursor == i) ? ">" : " ");
    }
  }

  delay(100);
}

上記を実行するとリモコンの一覧がでてきて、右ボタン(Bボタン)でカーソルが動き、下ボタン(Aボタン)で送信をします。

IRremoteESP8266のサンプルスケッチであるIRsendDemoのPINを9に変更してまずは動かしたほうがわかりやすいと思いますが、PIN番号を指定して宣言すればあとは実際に送信するコードを指定するだけで動きます。

赤外線リモコンのコードは会社によって複数フォーマットがあり、今回は一番有名なNECフォーマットで送信しています。

NECフォーマットの特徴としては2バイトのカスタマーコード(会社)があり、ここで自分が利用するコード以外が来たら無視する設定になっています。その後に送信するリモコンコードと、そのコードを反転したパリティの4バイトを送るのですが、ビットの並びが下位から上位の順番なのでreverseBits()関数を利用して順番を入れ替えています。

Send IR : 0x00FFA25D(customer=0x00FF, command=0x45)

上記が0x45(POWERボタン)のコードを送ったときのデータですが、ビットの並びが違うので、実際に送信されたデータを見ただけだとすぐに解析ができません。

実際に利用する場合には、実物のリモコンが送信している0x00FFA25DをPOWERボタンとしてデータ保持しているほうがシンプルなコードになります。

シンプル送信側コード(GitHub)

#include <M5StickC.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>

const uint16_t kIrLed = 9;              // M5StickCはGPIO9にIRが内蔵

IRsend irsend(kIrLed);                  // IR送信を宣言

// リモコンコード保存用構造体
struct REMOTE {
  char name[9];
  uint64_t command;
};

// リモコンコード一覧
REMOTE remote[] = {
  { "POWER" , 0x00FFA25DUL },
  { "VOL+"  , 0x00FF629DUL },
  { "VOL-"  , 0x00FFA857UL },
  { "0"     , 0x00FF6897UL },
  { "1"     , 0x00FF30CFUL },
  { "2"     , 0x00FF18E7UL },
  { "3"     , 0x00FF7A85UL },
  { "4"     , 0x00FF10EFUL },
  { "5"     , 0x00FF38C7UL },
  { "6"     , 0x00FF5AA5UL },
  { "7"     , 0x00FF42BDUL },
  { "8"     , 0x00FF4AB5UL },
  { "9"     , 0x00FF52ADUL },
};

int cursor = 0; // カーソル位置

void setup() {
  M5.begin();     // M5StickC初期化
  irsend.begin(); // IR初期化

  // リモコン項目表示
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 8);
  for ( int i = 0 ; i < ( sizeof(remote) / sizeof(REMOTE) ) ; i++ ) {
    M5.Lcd.print((cursor == i) ? ">" : " ");
    M5.Lcd.println(remote[i].name);
  }
}

void loop() {
  M5.update();  // ボタン状態更新

  // M5ボタンで送信
  if ( M5.BtnA.wasPressed() ) {
    // 送信4Byte(カスタマーコード2Byte+リモコンコード+反転リモコンコード)
    irsend.sendNEC(remote[cursor].command);

    // デバッグ表示
    Serial.printf("Send IR : 0x%08LX", remote[cursor].command);
  }

  // 右ボタンでカーソル移動
  if ( M5.BtnB.wasPressed() ) {
    cursor++;
    cursor = cursor % ( sizeof(remote) / sizeof(REMOTE) );

    // カーソル描画
    M5.Lcd.setCursor(0, 8);
    for ( int i = 0 ; i < ( sizeof(remote) / sizeof(REMOTE) ) ; i++ ) {
      M5.Lcd.println((cursor == i) ? ">" : " ");
    }
  }

  delay(100);
}

実物のリモコンが手元にある場合には、受信して確認したデータをそのまま送信したほうが楽だと思います。ネット上で送信データを調べた場合には内部的な数値なのか、送信時のデータなのかを確認してから利用してください。

まとめ

赤外線リモコンは、データ送信時にビット配列を入れ替えるところがちょっと面倒です。またM5StickCのリモコンデータは受信側の性能とあると思いますが、2メートルぐらいの範囲ぐらいまで安定して送信できました。

ただし、実験で利用したELEGOOのリモコンもほぼ同じ距離だったので、安いリモコンはこれぐらいの距離が限界なのかもしれません。

M5StickCのAXP192で外部電源を使う

まだまだ謎の多いAXP192ですが、ちょっとだけわかってきました。現時点の情報なので最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

M5StickCには外部電源用に5V INの端子があります。しかしながらAXP192のデータシート上は2.9Vから6.3Vまでの入力を許容しています。

また、3.8V以上の入力があると自動起動をするようです。

外部電源の実験

2台のM5StickCを利用して、GNDを共有してから、片方のBAT端子からもう片方の5V INに接続してみました。安定化電源が壊れたので簡易測定です。

BAT側のM5StickCが4V以上の場合

5V IN側のM5StickCが充電されながら動いていました。

BAT側のM5StickCが3.7V前後の場合

5V IN側のM5StickCで充電はされなくなりましたが、外部電源で動いていました。

BAT側のM5StickCが3.5V前後の場合

この辺から外部電源を使わなくなりました。

外部電源との電源連動(GitHub)

#include <M5StickC.h>

// 最後に電圧確認した時間
int lastVinTime = 0;

// AXPを電源オフする
void axp_halt(){
    Wire1.beginTransmission(0x34);
    Wire1.write(0x32);
    Wire1.endTransmission();
    Wire1.requestFrom(0x34, 1);
    uint8_t buf = Wire1.read();

    Wire1.beginTransmission(0x34);
    Wire1.write(0x32);
    Wire1.write(buf | 0x80); // halt bit
    Wire1.endTransmission();
}

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(WHITE);
}

void loop() {
  if( (M5.Axp.GetVinData()*1.7) < 3.0 ){
    if( lastVinTime + 5000 < millis() ){
      // 3.0V以下が5秒以上で電源オフ
      axp_halt();
    }
  } else {
    // 最終電圧確認時間更新
    lastVinTime = millis();
  }

  delay(500);
}

外部電源からの入力をM5.Axp.GetVinData()で監視して、一定以下の電圧が指定秒数以上続くと電源を落とすサンプルです。

電源オフは本家ライブラリにプルリクエストを出しているので、そのうち取り込まれる可能性がありますが、現時点では自作する必要があります。

電源オンは5V INに3.7V以上の電源を接続したときに自動起動します。このため5Vで動作する回路とM5StickCを連携させ、外部電源がなくなったらプログラムで電源オフにして、回路から5Vが供給すると電源オンになる連携が可能になります。

まとめ

AXP192のデータシート上2.9Vからの動作になっていましたが、安定動作させるには3.5V以上の電圧が必要でした。この電圧だと18650などのリチウムイオン充電池の直結は厳しいかな?

M5StickCの内蔵電池はかなり容量が小さいので、18650や16340であれば直結でも動きそうですが、安定動作にはやっぱり5Vに昇圧したほうが良さそうでした。

安定化電源が壊れて使えなかったので、ちょっと機材購入して追加で調査したいと思います。