Flutter アプリから M5StickC Plus に Bluetooth 通信してみる
この記事は、jig.jp Engineers' Blog Advent Calendar 2025 12月3日の記事です。
少し前ですが、BALA-C PLUS ESP32 バランスロボットキット を安く買いました。
これに付いてくる M5StickC Plus は UIFlow という Scratch 風のビジュアルプログラミングでコーディングできるので、10 歳の息子でもハードルが低いです。
これで「コントローラーで自由に動かせるラジコンを作りたい」となり、UIFlow 1 にある Remote+ という UI (おそらく UIFlow 2 にはない?) で、息子と一緒にリモコンを作ってスマホから操作してみたのですが、サーバ経由なのでどうしてもラグがあるのと、インターネット接続が必須というところに不満がありました。
「ラグ無しで動かせるコントローラーを作りたい!」と思い、今回はその準備段階として、Flutter アプリから Bluetooth で送った文字列を M5StickC Plus で表示させてみました。
構成
UI は筆者が得意とする Flutter でサクッと作りたいなと思い、調べてみると flutter_blue_plus というおあつらえ向きのパッケージも見つかったので、これで作ることにしました。
筆者は Bluetooth に対する知識があまりなく AI に聞きつつ調べながらでしたが、「これでいけそう」という形が見えてきました。
M5StickC Plus 側
M5StickC Plus のやることは、BLE サーバを構築し、Write 可能な Characteristic への書き込みイベントを受け取って処理することになります。
Characteristic とはざっくり言えばファイルのようなものです。
ここに値を読み書きすることでセントラル(親機)とペリフェラル(子機)の間でやり取りします。
今回使う Write 含め、以下の 3 つのプロパティがあります。「M5StickC のジャイロの値を Flutter で受け取りたい」といったケースでは Notify が使えそうですね。
- Read → セントラルがペリフェラルから読み取る
- Write → セントラルがペリフェラルへ書き込む
- Notify → ペリフェラル側の値が変わったら、セントラルへ通知する
UIFlow のブロックを眺めるに、UIFlow でも実装できそうでしたが、まずはパッと動くものを作りたく、 .ino ファイルを Copilot に出力してもらってそれを焼く形で進めました。
(今思うと、MicroPython を出力してもらえば UIFlow でも出来たのかもしれません)
結果、「画面は真っ暗だけどなんか通信できてそう」というところまでは行けたのですが、どうしてもディスプレイが真っ暗になってしまい、M5.Lcd の部分は手作業で修正する形になりました。
以下はコードの一部です。
void setup() {
M5.begin();
Serial.begin(115200);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(4);
M5.Lcd.setCursor(10, 10);
M5.Lcd.println("Booting...");
BLEDevice::init(DEVICE_NAME);
BLEServer *server = BLEDevice::createServer();
server->setCallbacks(new ServerCallbacks());
BLEService *service = server->createService(SERVICE_UUID);
writeChar = service->createCharacteristic(WRITE_UUID, BLECharacteristic::PROPERTY_WRITE);
writeChar->setCallbacks(new WriteCallbacks());
service->start();
BLEAdvertising *advertising = server->getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->setScanResponse(true);
advertising->start();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.println("Advertising...");
}
Flutter 側
こちらもまず Copilot にコードを書いてもらい、後から勉強するという形で進めましたが、順を追っていくとそれほど難しくはありませんでした。
まず以下のようなコードで M5StickC を見つけます。先の M5StickC のコードを踏まえて読むと、どこが対になっているのか分かって読みやすかったです。
Future<ScanResult> _scanForTarget() async {
final targetUuid = _serviceUuid.str.toLowerCase();
final completer = Completer<ScanResult>();
await FlutterBluePlus.stopScan();
await FlutterBluePlus.startScan(
withServices: [_serviceUuid],
timeout: const Duration(seconds: 10),
);
late final StreamSubscription<List<ScanResult>> subscription;
subscription = FlutterBluePlus.scanResults.listen(
(results) {
for (final result in results) {
final advertisedUuids = result.advertisementData.serviceUuids
.map((e) => e.toString().toLowerCase())
.toList();
final deviceName = (result.advertisementData.advName).toLowerCase();
final matchesUuid = advertisedUuids.contains(targetUuid);
final matchesName = deviceName.contains(_deviceNameHint);
if (matchesUuid || matchesName) {
if (!completer.isCompleted) {
completer.complete(result);
}
break;
}
}
},
onError: (e) {
if (!completer.isCompleted) {
completer.completeError(e);
}
},
);
try {
return await completer.future.timeout(const Duration(seconds: 10));
} finally {
await subscription.cancel();
await FlutterBluePlus.stopScan();
}
}
その後、見つけた M5StickC の Write Characteristic を見つけます。
※そういえば license: License.free は自分で書き足しました。おそらく AI が持ってる知識が古かったものと思われます。
final scanResult = await _scanForTarget();
final device = scanResult.device;
await device.connect(
timeout: const Duration(seconds: 10),
license: License.free,
);
final services = await device.discoverServices();
final service = services.firstWhere((s) => s.uuid == _serviceUuid);
final characteristic = service.characteristics.firstWhere(
(c) =>
c.uuid == _writeUuid &&
(c.properties.write || c.properties.writeWithoutResponse),
);
Write Characteristic に書き込みを行うのは非常に簡単で、以下のコードで OK です。
text には TextField から取得した文字を入れるようにして、任意の文字を送信できるようにしました。
final payload = utf8.encode(text);
await characteristic.write(payload, withoutResponse: false);
結果
以下の通り、文字を入力すればその文字が M5StickC Plus の画面に表示されるようになりました。

まとめ
後は M5StickC Plus とのプロトコルを決めて、右のタイヤを回転・左のタイヤを回転などできるようにすれば、Bluetooth でラグなく操作できるラジコンが作れそうです。
Copilot がすごいのもありますが、 flutter_blue_plus が素晴らしいですね。思ってた以上にサクッとここまで出来てしまいました。
M5StickC Plus から情報を受け取って動くなにかも作りたくなりましたし、UIFlow 2 で Bluetooth 通信処理を実装すると、息子にも遊んでもらえそう。
クリスマスに向けて良いおもちゃが作れそうです。
Discussion