クロスマートアドベントカレンダー19日目の記事です。
クロスマートでは普段Webフロントエンドを書いているのですが、タイトルにある通り、最近は電子工作にハマっております。
今回はXIAO ESP32S3という親指の先っちょほどの可愛い小型ボードを使っていくつか工作したのでその成果を晒していこうと思います。
きっかけ
電子工作にハマったきっかけはつい2~3ヶ月前に開催された社内ハッカソンでした。
クロスマートは主に、
- 卸業者様向けの受注管理サービス
- 飲食店向けの発注管理サービス
を提供しているのですが、社内ハッカソンではこのうちの「発注」部分をIoT機器を組み合わせることで簡略化する仕組みを作りました。
普段の業務では扱わない概念に触れ、慣れない言語で作業し、足りない部品があれば秋葉原で調達する.....それはもう楽しかったわけです。
社内ハッカソン終了後も余韻は続き、AIと壁打ちしながら実装計画を立て部品を買い集め、たまに失敗したりしながら組み上げていく過程を楽しみ今に至ります。
作ったものその1
IoT電池Mabeee(マビー)の出力制御デバイス
- ハコフグ型のおもちゃの中にIoT電池Mabeee(マビー)が入っています
- 手元のデバイスでBLE接続処理と電池の出力コントロールをしています
なぜ作ったか
マビーという単三電池の出力を自由にコントロールできるIoTデバイスがあり、子どものおもちゃを作るのにぴったりなんです。
しかしコントロール用の公式ios/androidアプリは、手持ちの端末で既にサポート終了しておりこれに替わるものを自作する必要がありました。
どうやるか
macOS用アプリが公開されていたのでそれを使ってマビーを操作しつつ、実際の通信内容を見て模倣することにしました。
通信内容はXcodeのadditional toolsに同梱されているPacketLoggerを使って見ることができます。
通信内容を見てみる
先ほどのmacOS用アプリが公開しているAPI
| path | 概要 |
|---|---|
| /scan/start | スキャンを開始 |
| /devices/:id/connect | :idで指定したデバイスに接続 |
| /devices/:id/set | :idで指定したデバイスに値を設定 |
/scan/start
スキャン開始直後からマビーのアドバタイズを確認できます

/devices/:id/connect
定義されているサービス、キャラクタリスティックを確認できます

/devices/:id/set
値のハンドルと実際に送信されている値を確認できます

ここまででわかったこと
- 更新対象のハンドルは
0x0011である - そのハンドルのキャラクタリスティックidは
BPF53006-D813-45C6-8B61-B453EE2C74D9である - pwm_duty=50(出力値50)で更新した場合、
01 32 00 00 00という値が送られる
大まかな実装
補足事項
大まかな流れ重視で処理の詳細は所々省略しています
BLEスキャン・接続処理の部分も標準的な内容なので省略しています
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
// MaBeeeで確認できたサービスとキャラクタリスティック
static BLEUUID serviceUUID("b9f5ff00-d813-46c6-8b61-b453ee2c74d9");
static BLEUUID charUUID ("b9f53006-d813-46c6-8b61-b453ee2c74d9");
BLEScan *pBLEScan;
BLEAdvertisedDevice *targetDevice = nullptr;
BLEClient *client = nullptr;
// スキャン時のコールバック
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) override {
// デバイス名に"MaBee"が含まれていたら*targetDeviceに保持する処理
}
};
// スキャン開始
void startScan() {
// pBLEScan->start();でスキャンを開始する一般的な実装
}
// MaBeeeに接続
bool connectToMaBeee(BLEAdvertisedDevice *device) {
// BLEDevice::createClient() で client 作成
// client->connect(device) で接続
}
// 値を送信する処理
void sendDutyToMabee(int duty) {
if (!client || !client->isConnected()) {
Serial.println("(not connected, skip send)");
return;
}
BLERemoteService *service = client->getService(serviceUUID);
if (!service) {
Serial.println("Service not found");
return;
}
BLERemoteCharacteristic *ch = service->getCharacteristic(charUUID);
if (!ch) {
Serial.println("Characteristic not found");
return;
}
uint8_t buf[5] = {0x01, (uint8_t)duty, 0x00, 0x00, 0x00};
ch->writeValue(buf, sizeof(buf), true);
Serial.print("write duty = ");
Serial.println(duty);
}
void setup() {
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
// 'MaBeeeという名前のデバイスがあるかチェック'
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
//
pBLEScan->setActiveScan(true);
}
void loop() {
// ボタン押した時の処理
// スキャン開始
// mabeeeに接続など
}
作ったものその2
直近1時間で雨が降るかどうかを教えてくれるデバイス
- タクトスイッチを押下すると予め登録しておいた地点で直近1時間で雨が降るか教えてくれます
なぜ作ったか
- 健康面を考慮しチャリンコを多用する生活をしているため降雨情報はとても重要
- 最近予期せぬ雨が多い
- 朝の子どもの送迎など慌ただしい時間にいちいちスマホで付近の雨雲情報を見るのが面倒
- OLEDディスプレイを使って何か作りたい
そんな背景もあって、ボタンポチ!だけで「1時間以内に雨が降るか」を教えてくれるデバイスを作ることにしました。
どうやるか
玄関に置くことを想定しているので独立した外部電源の確保と、頻繁に充電せずとも使い続けられる省電力性を備える必要があります。
XIAO ESP32C3はディープスリープ機能とバッテリーコネクタを備えているのでこの点をクリアできます。
ちなみに今回の実装ではディープスリープとバッテリーコネクタは使えませんでした。ディープスリープの方は後ほど触れますが、バッテリーコネクタの方は購入したリポ電池の端子が合わないという凡ミスでした。
天気APIは、APIキー不要&無料で利用できるOpen-Meteoがお手軽そうだったので利用することにしました。
OLEDディスプレイ制御
大部分のコードを生成AIに任せており、正直なところこれといった感想が無いのですが、難しい印象のOLEDディスプレイ制御周りが専用ライブラリのおかげでだいぶシンプルに書ける点が驚きでした。実際はI2Cという通信方式でデータがやり取りされているのですが、隠蔽され非常に理解しやすいインターフェイスになっていると感じます。
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
// ここでdisplayインスタンスを生成
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// =====================
// OLED 表示 / OFF
// =====================
void oledOn() {
display.ssd1306_command(SSD1306_DISPLAYON);
}
void oledOff() {
display.clearDisplay();
display.display();
display.ssd1306_command(SSD1306_DISPLAYOFF);
}
// API取得中表示
void showCheckingOnOLED() {
oledOn();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Checking...");
display.display();
}
// 結果表示
void showResultOnOLED(bool willRain, float maxP) {
oledOn();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Next 1 hour");
display.setTextSize(2);
display.setCursor(0, 18);
display.println(willRain ? "RAIN" : "NO RAIN");
display.setTextSize(1);
display.setCursor(0, 52);
display.print("max ");
display.print(maxP, 1);
display.print(" mm");
display.display();
}
ハマりポイント
ディープスリープ後、タクトスイッチを押下すると起床する想定だったのですが全く復帰できずハマりました。どうやら以下の関係があるらしく、ディープスリープ状態でGPIOは全て無効状態にも関わらず、GPIOに起床トリガーを設定していたことが原因だったようです。
| モード | 何が生きているか? |
|---|---|
| ディープスリープ | RTC |
| ライトスリープ | GPIO |
ちなみにピンリストを見るとRTC IOがどこにも見つからないのですがこれはどういうことでしょうか...?今回のような実装ではそもそも無理だったのでしょうか...

最終的にはライトスリープを使用することで起床できるようになりました。
void goLightSleepWaitButton() {
Serial.println("Entering light sleep. Press button to wake.");
Serial.flush();
// OLED OFF
oledOff();
delay(100);
// ボタン設定(通常HIGH、押すとLOW)
pinMode(BUTTON_PIN, INPUT_PULLUP);
// GPIO2 を LOW レベルで起床トリガにする
gpio_wakeup_enable((gpio_num_t)WAKE_GPIO, GPIO_INTR_LOW_LEVEL);
// GPIO wakeup を有効化
esp_sleep_enable_gpio_wakeup();
delay(50);
// light sleep に入る
esp_light_sleep_start();
Serial.println("Woke up from light sleep");
}

