🛜

ESP32とiPhoneでBLE通信:SwiftとArduinoでLEDを制御する(送信編)

に公開

はじめに

Bluetooth Low Energy(BLE)を用いて、iPhoneからESP32へ信号を送信し、マイコン側でLEDをON/OFF制御するシンプルな仕組みを構築する。本記事では「スマートフォンからマイコンへ値を送る」ことに焦点を当てる。

BLE通信には双方向性があり、マイコン側のセンサー値などをスマートフォンに送信する実装も可能であるが、そちらはやや構成が異なるため、別記事にて紹介予定である。

今回は「iPhoneからBLE経由でLEDを操作する」ことを通して、BLE通信の基本的な構成や実装方法を理解することを目的とする。

1. 開発環境と前提条件

本記事では以下の開発環境を前提としている。

  • Arduino IDE(ESP32開発ボード向けの設定が完了していること)
  • Xcode16
  • iPhone実機(iPadでも可、BLE通信のためシミュレータは使用不可)
  • ESP32開発ボード(XIAO ESP32S3とDevKitC-VE WROVER-Eで動作確認済み)
  • LED等を接続した回路

2. BLE通信の基礎知識

BLE(Bluetooth Low Energy)は、低消費電力かつ短距離通信を特徴としたBluetoothの規格である。センサー端末などの組み込み機器とスマートフォンを接続する用途で広く用いられている。

GATTの構造

BLEは「GATT(Generic Attribute Profile)」と呼ばれるモデルに基づいて通信を行う。GATTには以下のような構成要素がある。

  • Peripheral:サービスを提供する側(本記事ではESP32)
  • Central:サービスに接続し、データの読み書きを行う側(本記事ではiPhone)
  • Service:通信対象を機能単位でまとめた論理的なまとまり
  • Characteristic:実際の値(データ)をやり取りするエンドポイント

UUIDとは何か

ServiceやCharacteristicは、それぞれUUID(Universally Unique Identifier)で識別される。BLE通信では、Central(iPhone)とPeripheral(ESP32)が同じUUIDを認識していることで、正しい通信が可能になる。

なお、本実装ではUUIDによるフィルタリングではなく、「デバイス名」によりPeripheralを識別しているため、BLEサービス側のUUIDは任意である。
セキュリティを高めたい場合は、あらかじめPeripheral側になんらかのトークンを記録しておき、Centralから送られてきたトークンとの照合処理を挟めば良いだろう。

BLEではペアリング後に暗号化通信が有効になるが、今回のようにペアリングを行わないケースでは暗号化されず通信内容は平文でやりとりされる。

UUIDは自動生成できるツールがたくさんある。筆者はこちらのサイトを利用した。

3. ESP32側の実装(Peripheral)

本プロジェクトでは、ESP32をBLE Peripheralとして動作させ、iPhoneからの書き込みを受信してLEDのON/OFFを制御する。また、今回はデバイス名をLED-Deviceとする。

使用ライブラリ

NimBLEライブラリをインストールする。

実装コード

以下、コードをブロックごとに丁寧に解説していく。

ヘッダーファイルのインクルード

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
  • BLE通信に必要なライブラリを読み込む。

定数定義

#define SERVICE_UUID        "951d154b-7c11-adc9-e6ac-3ec1b4cb77bd"
#define CHARACTERISTIC_UUID "bdadc788-d355-0d84-f5af-11b799a05f62"
#define LED_PIN D1
  • LED_PIN:LED制御用のGPIOピン(ボードや回路によって物理ピンは異なるので自身の環境のものを指定してください)。

グローバル変数

BLECharacteristic *pCharacteristic;
  • キャラクタリスティック(送信先にあたるもの)を他の関数でも使えるように、ポインタで保持。

書き込み時の処理クラス

class LEDControlCallback : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pChar) override {
    String value = String(pChar->getValue().c_str());
    digitalWrite(LED_PIN, value == "1" ? HIGH : LOW);
  }
};
  • キャラクタリスティックに書き込みがあったときに呼ばれる処理。
  • 受け取った値が "1" なら LED を ON、その他なら OFF。

クライアント接続/切断時の処理クラス

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) override {
    Serial.println("Client connected");
  }
  void onDisconnect(BLEServer* pServer) override {
    Serial.println("Client disconnected → restart advertising");
    pServer->getAdvertising()->start();
  }
};
  • クライアントが接続されたときと切断されたときの処理。
  • 切断時に advertising を再スタートすることで、再接続ができるようにしている。

setup() 関数

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  • シリアルモニタ用に通信を開始。
  • LEDピンを出力モードに設定。
  BLEDevice::init("LED-Device");
  • BLEデバイスとして初期化し、デバイス名を "LED-Device" に設定。
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());
  • BLEサーバーを作成し、先ほど定義した接続・切断時のコールバックを設定。
  BLEService *pService = pServer->createService(SERVICE_UUID);
  • サーバー内にサービスを作成(UUIDを使って識別される)。
  pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_WRITE
  );
  pCharacteristic->setCallbacks(new LEDControlCallback());
  • 書き込み専用キャラクタリスティックを作成し、LED制御のコールバックを紐づけ。
  pService->start();
  • サービスを開始。
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->start();
  Serial.println("BLE advertising as LED-Device...");
}
  • BLEアドバタイジング(周囲に存在を知らせる信号)を開始。
  • 他のデバイスがこのESP32に接続できるようになる。

loop() 関数

void loop() {
  // 何もしない
}
  • BLEの処理は全てイベント駆動型(コールバック)で行われるため、ループ内には何も書く必要がない。

4. iOS側の実装(Central)

SwiftとCoreBluetoothを使って、iPhoneをBLE Centralとして動作させる。

Bluetooth使用許可

プロジェクトのTargetinfoタブにあるプロパティにPrivacy - Bluetooth Always Usage Descriptionを追加する。

BLEManager.swift:通信ロジック

  • BLEの初期化とスキャン
  • デバイス名によるフィルタリング("LED-Device")
  • 接続後、サービス・キャラクタリスティックを探索し、書き込み用のものを保持
  • 書き込みメソッド send(_:) を用意

CoreBluetoothフレームワークを使って Bluetooth Low Energy (BLE) デバイス(ここでは "LED-Device")と通信する Swift のクラスを使用する。

BLEManager クラスは、BLE デバイスをスキャン・接続・通信するための処理をまとめたもの。

以下、コードをブロックごとに解説していく。

インポートとクラス定義

import Foundation
import CoreBluetooth
  • Foundation: 基本的な機能(データ型、日付、文字列など)を提供。
  • CoreBluetooth: Bluetooth 通信に必要なフレームワーク。
class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
  • BLEManager: BLE の管理クラス。
  • NSObject: CoreBluetooth のデリゲートとして必要なベースクラス。
  • ObservableObject: SwiftUIと連携するためのプロトコル。@Published プロパティを使ってUIと状態を連動できるようになる。
  • CBCentralManagerDelegate: BLEのスキャンや接続の処理を受け取るデリゲート。
  • CBPeripheralDelegate: 接続したBLEデバイス(Peripheral)からの通知を受け取るデリゲート。

プロパティ定義

@Published var isConnected = false
  • @Published によって、接続状態をUI側にリアルタイムで通知する。
private var centralManager: CBCentralManager!
private var peripheral: CBPeripheral?
private var writeChar: CBCharacteristic?
  • centralManager: BLEの中心となるマネージャー(スキャンや接続を行う)。
  • peripheral: 接続先のBLEデバイス。
  • writeChar: 書き込み可能なキャラクタリスティック(BLEデバイスにデータを送るためのもの)。

peripheralは「どのデバイスに送るか」を表し、writeChar は「デバイスのどの機能に書き込むか」を表す。デバイスが持つ、書き込み用の「ポスト」のようなもの。

private let targetName = "LED-Device"
  • 接続したいデバイスの名前を指定(スキャン中にこの名前のデバイスを探す)。

初期化処理

override init() {
    super.init()
    centralManager = CBCentralManager(delegate: self, queue: nil)
}
  • override init(): コンストラクタ。NSObject を継承しているため、super.init() を呼び出す。
  • CBCentralManager(delegate:queue:):
    • delegate: self:このクラス自身が CBCentralManagerDelegate を実装しているため、BLEイベントを受け取る対象として自分を指定。
    • queue: nil:メインスレッドで BLE 処理を行う(UIと直接やりとりしたい場合は nil でOK)。

Bluetooth状態の監視

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    if central.state == .poweredOn {
        central.scanForPeripherals(withServices: nil, options: nil)
    }
}
  • BLE の電源状態が変わったときに呼ばれるデリゲートメソッド。
  • .poweredOn のときにのみ、スキャンを開始。
  • withServices: nil:特定のサービスに絞らず、すべての周辺機器を対象にスキャン。

デバイスの検出と接続

func centralManager(_ central: CBCentralManager, didDiscover p: CBPeripheral,
                    advertisementData: [String : Any], rssi RSSI: NSNumber) {
    if let name = p.name, name == targetName {
        peripheral = p
        central.stopScan()
        central.connect(p, options: nil)
        p.delegate = self
    }
}
  • スキャン中にデバイスが見つかると呼ばれる。
  • p.name で名前を確認し、"LED-Device" なら接続処理へ。
  • central.stopScan():スキャン終了(すでに目的のデバイスが見つかったため)。
  • central.connect(p):接続を開始。
  • p.delegate = self:このクラスが CBPeripheralDelegate を実装しているので、自分をデリゲートに設定。

接続完了後の処理

func centralManager(_ central: CBCentralManager, didConnect p: CBPeripheral) {
    isConnected = true
    p.discoverServices(nil)
}
  • デバイスに接続完了すると呼ばれる。
  • isConnected = true:SwiftUI に接続状態を反映。
  • discoverServices(nil):サービス(機能のまとまり)を検索。nil にするとすべてのサービスを検索。

サービスの検索完了時

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    for service in peripheral.services ?? [] {
        peripheral.discoverCharacteristics(nil, for: service)
    }
}
  • 各サービスについて、さらにその中にある「キャラクタリスティック(特性)」を検索。
  • これにより、読み書きできる情報や機能にアクセスできるようになる。

キャラクタリスティック(特性)の検出

func peripheral(_ peripheral: CBPeripheral,
                didDiscoverCharacteristicsFor service: CBService,
                error: Error?) {
    for char in service.characteristics ?? [] {
        if char.properties.contains(.write) {
            writeChar = char
        }
    }
}
  • キャラクタリスティックの中から .write(書き込み可能)なものを探し、writeChar に保存。
  • これでやっと、デバイスに文字列などのデータを送る準備が整う。

切断時の再接続処理(なくてもOK)

func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("切断されました: \(peripheral.name ?? "Unknown")")
        isConnected = false
        peripheral.delegate = self
        central.connect(peripheral, options: nil)
}
  • 通信が切断された場合の再接続処理。
  • isConnectedがfalseに戻るのでUIにも反映される。

データ送信処理

    func send(_ value: String) {
        guard let peripheral = peripheral,
              let char = writeChar,
              let data = value.data(using: .utf8)
        else { return }

        peripheral.writeValue(data, for: char, type: .withResponse)
    }
}
  • send(_:) は BLE デバイスに文字列データを送るメソッド。
  • value.data(using: .utf8) で文字列をデータに変換。
  • writeValue(_:for:type:) でデータを送信。
    • type: .withResponse:送信後にPeripheral側からレスポンスを受け取る設定(GATTレベルで応答しているので意識する必要はない)

動作フロー確認

このクラス BLEManager の全体の動作フローを改めて確認する。

  1. 初期化時に BLE 状態を監視。
  2. BLE が ON になったらスキャン開始。
  3. 目的のデバイスが見つかったら接続。
  4. 接続後、サービス・キャラクタリスティックを探索。
  5. 書き込み可能なキャラクタリスティックを取得。
  6. send(_:) メソッドで文字列データを送信。

ContentView.swift:UIの構成

SwiftUIでUIを構築し、BLEManagerの send() メソッドを呼び出す。ボタンのタップでLEDをON/OFFする。

import SwiftUI

struct ContentView: View {
    @StateObject private var ble = BLEManager()

    var body: some View {
        VStack(spacing: 30) {
            Text(ble.isConnected ? "接続中" : "接続待ち...")
                .font(.title)
            
            Button("LED ON") {
                ble.send("1")
            }
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
            

            Button("LED OFF") {
                ble.send("0")
            }
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.green)
            .foregroundColor(.white)
            .cornerRadius(10)

        }
        .padding()
    }
}

5. 実行と確認

  1. ESP32にスケッチを書き込む
  2. Xcodeでアプリをビルドし、iPhone実機で起動
  3. Bluetoothの使用をアプリに許可
  4. 「接続完了」と表示されればBLE接続完了
  5. 「LED ON」「LED OFF」ボタンをタップして、LEDが点灯・消灯することを確認

現在の実装では、アプリを終了(タスクキル)すると接続が解除され他の端末から接続できるようになる。

おわりに

本記事では、BLE通信を用いてiPhoneからESP32へ値を送信し、LEDの制御を行う基本的なシステムを構築した。BLEは低消費電力かつそこそこの距離で通信できるので非常に使い勝手が良く、今回実装方法を学んだことでIoT開発の幅をまた広げることができた。また、ESP32からiPhoneへセンサー値等を通知する「Notify通信」の実装も今後紹介予定である。

今回紹介したサンプルコードは以下に掲載している。LED_PINを自身の環境に合わせれば動作するはずなのでぜひ試して頂きたい。
https://github.com/myml12/ESP32-BLE-LED

Discussion