同じコードなのに、iOSだけエラーになる謎

「Androidでは完璧に動くのに、iOSでだけBluetooth接続がエラーになる」

BLE(Bluetooth Low Energy)アプリを開発した経験者なら、一度は遭遇したことがあるはずです。エラーメッセージは Write is not permitted。デバイスへのデータ送信が、なぜかiOSだけ拒否されるのです。

実はこれ、iOSとAndroidでBluetooth通信の「ルールの厳しさ」が違うことが原因です。 そして、この違いを理解するには、BLE通信の仕組みそのものを理解する必要があります。

この記事では、プログラミング経験者向けに、デパートの例えを使ってBLE通信の全体像を徹底解説します。読み終わる頃には、「サービス」「キャラクタリスティック」「UUID」といった専門用語が、すんなり理解できるようになります。

そもそもBLE通信って何?

BLE(Bluetooth Low Energy)は、スマートウォッチ、ワイヤレスイヤホン、IoTデバイスなど、低消費電力で通信する機器に使われる無線通信技術です。

従来のBluetooth(クラシックBluetooth)と比べて:

  • 消費電力が1/10以下
  • 接続が高速(数百ミリ秒)
  • 小さなデータを頻繁に送る用途に最適

だからこそ、腕時計のような小型デバイスに採用されているわけです。

BLE通信は「デパート」だと考えよう

BLE通信を理解する最大のコツは、デバイスを「デパート」として想像することです。

デパートの構造で理解するBLE

想像してください。あなた(スマホアプリ)が、大きなデパート(BLEデバイス)に買い物に行く場面を。

デパートには複数のフロアがあります。

  • 3階:心拍計測フロア
  • 4階:バッテリー情報フロア
  • 5階:デバイス情報フロア

このフロアが、BLEでは「サービス (Service)」と呼ばれるものです。機能ごとに分かれた大きな区画です。

各フロアには、いくつかの棚や窓口があります。

  • 心拍フロアの「現在の心拍数の掲示板」
  • 心拍フロアの「測定間隔の設定カウンター」
  • バッテリーフロアの「残量表示の棚」

この棚や窓口が「キャラクタリスティック (Characteristic)」です。実際にデータを読み書きするのは、必ずこの棚を経由します。

そして、各フロアと棚には固有の番号札がついています。

  • 心拍フロア:UUID 0000180D-0000-1000-8000-00805F9B34FB
  • 現在の心拍数棚:UUID 00002A37-0000-1000-8000-00805F9B34FB

この長い番号が「UUID (Universally Unique Identifier)」です。世界中で重複しない、住所のようなものです。

実際の通信の流れ

1. スマホ「心拍数が知りたいな」
2. スマホ「心拍フロア (UUID: 180D) に行こう」
3. スマホ「心拍数の棚 (UUID: 2A37) を見つけた!」
4. スマホ「この棚のデータを読み取ろう」
5. デバイス「現在の心拍数は72です」

このように、サービス(フロア)を特定してから、キャラクタリスティック(棚)にアクセスするという2段階構造になっています。

なぜこんなに複雑な構造なの?

「単純にデータを送受信すればいいじゃん」と思いますよね。でも、BLEデバイスは1台で複数の機能を持っています。

例えば、スマートウォッチは:

  • 心拍数を測る機能
  • 歩数を記録する機能
  • 通知を表示する機能
  • 時刻を合わせる機能
  • バッテリー残量を報告する機能

これらすべてが1つのデバイスに同居しています。もしフロア(サービス)で分けずに、すべての棚が1階に並んでいたら...混乱しますよね。

だからこそ、機能ごとに「フロア(サービス)」を分けて、その中に関連する「棚(キャラクタリスティック)」を配置するという構造が採用されているのです。

棚には「使い方のルール」がある - iOSエラーの正体

ここが、冒頭の「iOSでだけエラーになる」問題の核心です。

デパートの棚には、それぞれ使い方のルールがあります。BLEでは、これを「プロパティ (Property)」と呼びます。

Write(書き込み)権限の棚

「商品を置ける棚」です。お客さん(スマホ)が自由に物(データ)を置けます。

例:アラーム設定の棚プロパティ: Write
→ スマホから「朝7時にアラーム」というデータを書き込める

Read(読み取り)権限の棚

「商品を見るだけの棚」です。お客さんは中身を確認できますが、勝手に変更はできません。

例:バッテリー残量の棚プロパティ: Read
→ スマホから「今何%?」と読み取れるが、書き換えはできない

Notify(通知)専用の窓口

これが少し特殊です。お客さんは触れませんが、中身が変わると店員(デバイス)が自動的に「変わったよ!」と教えてくれる窓口です。

例:心拍数の掲示板プロパティ: Notify
→ デバイスが心拍数を測定するたびに、自動的にスマホに通知が飛んでくる
→ スマホからは書き込みも読み取りもできない(通知を受けるだけ)

iOSとAndroidで動作が違う理由

ここが重要です。

iOSは「超厳格な門番」

  1. デバイスが接続時に「0001番の棚はNotify(通知)専用です」と宣言する
  2. iOSは「わかりました。この棚は見る専用ですね」と厳密に記録する
  3. 開発者が誤って「0001番の棚にデータを書き込もう!」とコードを書く
  4. iOS「待て! その棚は通知専用だ。Write権限がないぞ!」Write is not permitted エラー

Androidは「ゆるい門番」

同じ状況でも:

  1. デバイスが「0001番はNotify専用です」と宣言
  2. 開発者が「0001番に書き込もう!」とする
  3. Android「ルール違反だけど...とりあえずデバイスに投げてみるか」
  4. デバイス側が拒否すればエラーになるが、場合によっては通ってしまう

このため、Androidで動いたコードがiOSでエラーになるという現象が起きるのです。

UUIDは長すぎて不便...「ハンドル」という受付番号

UUIDは 0000180D-0000-1000-8000-00805F9B34FB のように、非常に長い文字列です。毎回この長い住所を通信で送るのは効率が悪いですよね。

そこで、デバイスに接続すると、**接続中だけ使える短い「受付番号」**が自動的に発行されます。これを「ハンドル (Handle)」と呼びます。

長いUUID: 00002A37-0000-1000-8000-00805F9B34FB
↓ 接続すると自動的に割り当てられる短いハンドル: 0x0020 (10進数で32番)

通信中は、いちいち長いUUIDを使わず、この短いハンドル番号でやり取りします。デパートで言えば「心拍フロアの現在値棚」ではなく「20番窓口」と呼ぶようなものです。

パケットキャプチャツールでBLE通信を覗くと、このハンドル番号が頻繁に出てきます。

Nordic UART サービス - RXとTXの話

実際の市販BLEデバイス(Pavlok、Arduino系モジュール、M5Stackなど)では、「Nordic UART サービス」という仕組みが非常によく使われます。

これは、昔ながらのシリアル通信(RS-232のような文字のやり取り)を模した通信方法です。

RXとTXって何?

電子工作や通信の世界では、データの送受信を「RX(受信)」「TX(送信)」と呼びます。

ここが混乱しやすいポイントです。立場によって名前が逆転します。

デバイス視点:

  • RX (Receive): デバイス側が受け取る端子 = スマホから見ると「送信先」
  • TX (Transmit): デバイス側が送る端子 = スマホから見ると「受信元」

デパートの例えで言うと:

RXの棚(Write権限あり)
→ お客さん(スマホ)が商品(データ)を置く棚
→ 店員(デバイス)がそこから商品を受け取る
→ スマホ視点では「送信先」

TXの窓口(Notify権限あり)
→ 店員(デバイス)が情報を掲示する窓口
→ お客さん(スマホ)がそこから情報を受け取る
→ スマホ視点では「受信元」

実際のコードで正しい棚を探す

iOSのSwiftコードで、書き込み先(RX)を探すなら:

for characteristic in service.characteristics ?? [] {
    // 書き込み可能な棚を探す(RX)
    if characteristic.properties.contains(.write) || 
       characteristic.properties.contains(.writeWithoutResponse) {
        print("これがRX(書き込み用)の棚!")
        rxCharacteristic = characteristic
    }
    
    // 通知を受け取る窓口を探す(TX)
    if characteristic.properties.contains(.notify) {
        print("これがTX(通知用)の窓口!")
        txCharacteristic = characteristic
        // 通知の購読を開始
        peripheral.setNotifyValue(true, for: characteristic)
    }
}

よくあるミス: UUIDが 0001 だから書き込めるだろうと決めつけるのは危険です。必ずプロパティを確認しましょう。

HEX(16進数)データの読み方 - 実例で理解する

最後に、実際に送受信されるデータの話です。

BLE通信では、データは「HEX(16進数)」という形式で送られます。

HEXって何?

普段私たちは「0, 1, 2, 3...9」の10種類の数字を使います(10進数)。

HEXは「0〜9」に加えて「A, B, C, D, E, F」も使う、16種類の数字です。

10進数: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
HEX:    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,  A,  B,  C,  D,  E,  F

なぜHEXを使うのか? コンピュータの内部では、すべてのデータは「0と1」(2進数)で表現されています。HEXは、この0と1の長い羅列を人間が読みやすく、かつコンパクトに表記するための方法なんです。

2進数: 10100000 01000000
↓ 4ビットずつ区切る
HEX:   A0       40

実際のデバイス通信例

Pavlokという電気ショックウェアラブルデバイスの例:

送信: A0:40
意味: 「これから設定変更しますよ」という準備コマンド

送信: A8:4E:01:32:00:00
意味: 「振動モードで強度50で作動させて」という命令
      A8 = 振動コマンド
      4E = サブコマンド
      01 = モード指定
      32 = 強度(16進数の32 = 10進数の50)
      00:00 = オプション

送信: 6B:01:00:00:00
意味: 「デバイス