🦷

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 通信処理を実装すると、息子にも遊んでもらえそう。
クリスマスに向けて良いおもちゃが作れそうです。

jig.jp Engineers' Blog

Discussion