はじめてのBLE通信、iOSからESP32のLチカ

アドバタイジングのイメージ
アドバタイジングのイメージ

ESP32にLEDを繋いで、それをiOSからBluetooth通信でLチカさせてみました。この記事ではその詳しい方法をお伝えしたいと思います。

▼ ESP32のデータをiOSで受信したい方は、こちらの記事をご覧になってください。

▼ ESP32同士での通信でしたら「ESP-NOW」という方法もあります。ペアリングの必要がないので、手軽に相互通信ができます。

はじめに

はじめに、Bluetoothの概要をご説明します。

「Bluetooth」名前の由来

Bluetoothは直訳すると青い歯です。その由来は10世紀のデンマークとノルウェーの王様の「Harald Blåtand Gormsen」の名前から来ているそうです。Blåtandを英語に翻訳するとBluetoothなのです。Harald王は北欧を統一した王様で、乱立する無線通信規格も Harald王のようにBluetoothで統一したいという願いが込められています。

Bluetooth Logo
Bluetooth Logo

また、こちらのよく見るBluetoothのロゴですが、これは北欧長枝ルーン文字を使ってHarald Blåtandの頭文字「H」と「B」を組み合わせて作られたものだそうです。

Bluetoothの消費電力

さて、Bluetoothですが現在ではBluetoothバージョン4.0が主流となっています。BLE(Bluetooth Low Energy)などが追加されました。それ以前の古い規格はクラシックBluetoothなどと呼ばれます。

この記事でもBLEを扱っていきます。

まず、BLE規格では圧倒的に消費電力が抑えられました。こちらを御覧ください。WiFi通信とクラシックBluetooth、そしてBLEのスペックを比較したものです。

WiFiクラシックBluetoothBLE
IEEE規格IEEE802.11gIEEE802.15.1-
通信距離30m15m10m
通信速度54Mbps1Mbps1Mbps
消費電力約300mW約50mW**50μW〜1mW**
電池交換数時間数日**数年**

BLEはなんと、WiFiの消費電力の1/300〜1/6000に抑えられています。通信速度はそれほど出ませんが、IoTなどのセンサロガー用途でしたら十分です。なにより、BLEのおかげでバッテリー交換ができない状況下でもデバイスの運用が可能になりました。ちなみにBLEの平均消費電流は0.05mA〜3mA程度です。これはLEDをひとつ点灯する程度またはそれ以下の消費電流となります。

BLEのネットワークアーキテクチャ

BLEのネットワークアーキテクチャの一部を大雑把に説明します。

トポロジー(Topology)ネットワークの接続形態には、スター(Star)、メッシュ(Mesh)、クラスターツリー(Cluster Tree)の3つがあります。

ここではスターのみを解説します。

スタートポロジー接続

スタートポロジー接続状態
スタートポロジー接続状態

スターとは、中心となるノードから放射状に他の端末を接続する星状の構成です。中心ノードの端末をセントラル(Central)と呼び、端末はペリフェラル(Peripheral)と呼ばれます。この関係がかなり重要で、プログラミングに入るときにもごっちゃにならないように気をつけてください。

例えば、センサデバイスがついた複数のESP32を、iPhoneを使って双方向通信したい場合、iPhoneがセントラルになり、ESP32がペリフェラルになります。本記事でもこの関係で説明していきます。もちろん場合によっては、スマートフォンをペリフェラルにすることも可能ですし、ESP32をセントラルにすることも可能です。

またBLEでは、セントラルのリソースが許す限り、理論上数千台のペリフェラルを同時接続することが可能なようです。

マスタとスレイブ

複数のペリフェラル端末とセントラルでパケットのやり取りをするときに、同時で通信されるとパケットが衝突して正常な通信ができません。そこで、BLEではマスタ・スレイブのアクセス制御方式を採用します。マスタ側が順番に一つ一つのスレイブへリクエストを投げて(ポーリング)通信するといった方法です。

よって、セントラルノードがマスタとなり、ペリフェラルがスレイブとなります。

アドバタイジング

アドバタイジングのイメージ
アドバタイジングのイメージ

アドバタイジング(Advertising)とは、簡単に言えばBLE通信を開始するにあたっての準備や挨拶みないなものです。BLEではネットワーク接続せずにアドバタイジング(Advertising)通信を行います。

BLE通信を開始するには、まずセントラルがアドバタイズ(advertise)パケット送信(ブロードキャスト)する必要があります。アドバタイズパケットには、ペリフェラルのサービスUUIDを含めます。

周辺機器のペリフェラルがアドバタイズパケットを受け、サービスUUIDが一致すればセントラルへ参加要請を返信します。その時ペリフェラルのもつキャラクタリスティック情報を含めて送信します。(後ほど説明)

セントラルはペリフェラルのキャラクタリスティックの仕様を受けてBLE通信を開始する流れになります。

ちなみに、BLE通信にはお互い送受信できるコネクトモードの他に、ペリフェラルから一方向に送信するだけのブロードキャストモードがあります。ブロードキャストモードの方が、手順が少なく省電力で動作できます。

クライアントとサーバ

サーバ(Server)はセンサデータや特定の機能をサービスとして提供する側です。クライアント(Client)はサーバから提供されたサービスを利用する側です。よって通常はペリフェラルがサーバとなり、セントラルがクライアントとなります。この記事で紹介するプログラムも、ここまでの関係をしっかり把握していれば理解が進むと思います。

iPhoneESP32
セントラルペリフェラル
マスタスレイブ
クライアントサーバ

GATT

GATT(General Attribute Profile)とは、インタフェース構造化の一つです。ネット通信におけるTCPやUDPのプロトコル(約束事)に近いものと思ってください。セントラルがペリフェラルからのアドバタイズを受けて接続要求すると、1対1のGATT通信を始めることが可能となります。

GATT通信ではサービス(Service)キャラクタリスティック(Characteristic)の概念でデータのやり取りを行います。

キャラクタリスティックは大雑把に言うと、そのペリフェラル端末がどんなことができるのかという内容を含んだデータ構造です。例えば、Characteristicの「Value」にはデータが入れられます。また、「Property」にはRead、Write、Notifyなどの属性があります。

Property役割
Readペリフェラルからデータを読む
Writeペリフェラルにデータを書き込む
Notify状態が変化したらセントラルへデータ送信させる

他にもプロパティ属性がありますが、この記事ではiPhone(セントラル)からESP32(ペリフェラル)へ命令をだしてLチカを行うので、Write属性を使えばよさそうです。

UUID

GATTのサービスやキャラクタリスティックを識別するにはUUID(Universally Unique Identifier)というIDを使います。UUIDは理論上重複することのない128bitのIDです。

例: 4fafc201-1fb5-459e-8fcc-c5c9c331914b

ユニークなUUIDが必要な場合は、こちらのUUIDジェネレータサービスを使って生成できます。

GATT通信を行うには、ペリフェラルが所有しているサービスUUIDと、キャラクタリスティックUUIDの最低2つを知る必要があります。もしペリフェラルがキャラクタリスティックを複数持っていれば2つ以上になります。

たいへん大雑把ではありますが、ここでBLE通信の解説は終えたいと思います。他にもBLEにはさまざまな用語や機能がたくさんあります。本記事では紹介しきれませんので、より詳しく知りたい方はこれらの本などをご参考になさってみてください。それではBluetooth Lチカのプログラミングへ進みましょう。

BLEの参考書籍

Bluetooth Lチカプログラム

概要

iOSとESP32でBluetooth Lチカ
iOSとESP32でBluetooth Lチカ

BluetoothでLチカするまでの流れをカンタンに説明します。

  1. iOSとESP32をアドバタイジングします
  2. サービスのUUIDとキャラクタリスティックのUUIDが一致したらBLE通信が開始されます
  3. iOSのボタンを押すと"0"または"1"の文字列を送信します
  4. ESP32で"0"を受け取ったらLEDを消灯、"1"を受け取ったらLEDを点灯します

ESP32のGPIO13にLEDのアノードを接続しました。1kΩ程度の抵抗を介してLEDのカソードをGNDへ落としてください。

ESP32(ペリフェラル)

まずはESP32のペリフェラル側のプログラムです。ESP32はNodeMCU-32Sで動かしました。ESP32-DevKitCでもM5Stackでも動作すると思います。UUIDはそのままでも構いませんが、ペリフェラル端末を複数台用意したり実用化する場合は変更してください。

//
//  server.ino
//
//  Created by 101010.fun on 2021/07/14.
//

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// こちらのジェネレータでUUIDを生成してください
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
static int LED_PIN = 13;



class MyCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* pCharacteristic) {
        std::string value = pCharacteristic->getValue();

        if (value.length() > 0) {
            String ledState = value.c_str();
            Serial.println(ledState);
            if (ledState == "0") {
                digitalWrite(LED_PIN, LOW);
            }
            else if (ledState == "1") {
                digitalWrite(LED_PIN, HIGH);
            }

        }
    }

};


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

    pinMode(LED_PIN, OUTPUT);

    BLEDevice::init("ESP32_BLE_SERVER"); // この名前がスマホなどに表示される
    BLEServer* pServer = BLEDevice::createServer();
    BLEService* pService = pServer->createService(SERVICE_UUID);
    BLECharacteristic* pCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    ); // キャラクタリスティックの作成 → 「僕はこんなデータをやり取りするよできるよ」的な宣言

    pCharacteristic->setCallbacks(new MyCallbacks());
    pService->start();
    BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);  // iPhone接続の問題に役立つ
    pAdvertising->setMinPreferred(0x12);
    BLEDevice::startAdvertising();
    Serial.println("Characteristic defined! Now you can read it in your phone!");
}

void loop() {
    delay(2000);
}

iOS(セントラル)

CoreBluetoothをインポートし、CBCentralManagerDelegateCBPeripheralDelegateを実装します。BLEでデータを送るだけの必要最小限なコードにとどめてあります。実用するときはBluetooth接続切断時などのエラー処理を詰めてください。

//
//  BleLedBlinkerViewController.swift
//
//  Created by 101010.fun on 2021/07/14.
//

import UIKit
import CoreBluetooth

class BleLedBlinkerViewController: UIViewController {

    @IBOutlet weak var ledButton:UIButton!
    

    let kUARTServiceUUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b" // サービス
    let kTXCharacteristicUUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8" // ペリフェラルへ送信用

    var centralManager: CBCentralManager!
    var peripheral: CBPeripheral!
    var serviceUUID : CBUUID!
    var kTXCBCharacteristic: CBCharacteristic?
    var charcteristicUUIDs: [CBUUID]!

    var stateLed: Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ledButton.setTitleColor(.gray, for: .normal)
        setup()
    }

    
    
    private func setup() {
        print("setup...")

        centralManager = CBCentralManager()
        centralManager.delegate = self as CBCentralManagerDelegate

        serviceUUID = CBUUID(string: kUARTServiceUUID)
        charcteristicUUIDs = [CBUUID(string: kTXCharacteristicUUID)]
   }
    
    @IBAction func tappedLed(_ sender:UIButton) {
        guard let peripheral = self.peripheral else {
            return
        }
        
        guard let kTXCBCharacteristic = kTXCBCharacteristic else {
            return
        }

        var str:String!
        self.stateLed = !self.stateLed
        if self.stateLed {
            sender.setTitleColor(.orange, for: .normal)
            str = "1"
        } else{
            sender.setTitleColor(.gray, for: .normal)
            str = "0"
        }

        let writeData = str.data(using: .utf8)!
        peripheral.writeValue(writeData, for: kTXCBCharacteristic, type: .withResponse)
    }


}



//MARK : - CBCentralManagerDelegate
extension BleLedBlinkerViewController: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("CentralManager didUpdateState")

        switch central.state {
            
        //電源ONを待って、スキャンする
        case CBManagerState.poweredOn:
            let services: [CBUUID] = [serviceUUID]
            centralManager?.scanForPeripherals(withServices: services,
                                               options: nil)
        default:
            break
        }
    }
    
    /// ペリフェラルを発見すると呼ばれる
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        
        self.peripheral = peripheral
        centralManager?.stopScan()
        
        //接続開始
        central.connect(peripheral, options: nil)
        print("  - centralManager didDiscover")
    }
    
    /// 接続されると呼ばれる
    func centralManager(_ central: CBCentralManager,
                        didConnect peripheral: CBPeripheral) {
        
        peripheral.delegate = self
        peripheral.discoverServices([serviceUUID])
        print("  - centralManager didConnect")
    }
    
    /// 切断されると呼ばれる?
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print(#function)
        if error != nil {
            print(error.debugDescription)
            setup() // リトライ
            return
        }
    }
}

//MARK : - CBPeripheralDelegate
extension BleLedBlinkerViewController: CBPeripheralDelegate {
    
    /// サービス発見時に呼ばれる
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverServices error: Error?) {
        
        if error != nil {
            print(error.debugDescription)
            return
        }
        
        //キャリアクタリスティク探索開始
        if let service = peripheral.services?.first {
            print("Searching characteristic...")
            peripheral.discoverCharacteristics(charcteristicUUIDs,
                                               for: service)
        }
    }
    
    /// キャリアクタリスティク発見時に呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        
        if error != nil {
            print(error.debugDescription)
            return
        }

        for characteristics in service.characteristics! {
            if(characteristics.uuid == CBUUID(string: kTXCharacteristicUUID)) {
//                peripheral.setNotifyValue(true, for: (service.characteristics?[1])!)
                self.kTXCBCharacteristic = characteristics
            }
        }
        
        print("  - Characteristic didDiscovered")

    }
    
    /// データ送信時に呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        print(#function)
        if error != nil {
            print(error.debugDescription)
            return
        }
    }   
}

Bluetooth開発に役に立つツール

最後に、Bluetooth開発をする際におすすめのアプリを紹介しておきます。こちらのアプリは、ペリフェラルのサービスやキャラクタリスティック情報をスキャンできるアプリです。UUIDなどで何かつまずいた場合に役立ちますので入れておくと便利と思います。

BLE Scanner

Bluepixel Technologies LLP

Download on theApp Store Get it on Google Play
記事に関するご質問などがあればTwitterへお返事ください。
この記事で紹介した商品
ESP32搭載ボード
ESP32の書籍
Arduinoで人気の周辺パーツ
関連記事