BLEの基本構造とGATT設計
BLE(Bluetooth Low Energy)では、機器側がペリフェラル、スマホ側がセントラルになるのが一般的です。データのやり取りはGATTという構造の上で行われます。
Service(機能のまとまり)
└ Characteristic(実際のデータ項目)
├ Read : アプリから読み出す
├ Write : アプリから書き込む
└ Notify : 機器側から変化を通知する(ポーリング不要)
例)測定器の場合
Service: Measurement
├ Characteristic: current_value (Notify) ← 測定値の連続通知
├ Characteristic: device_status (Read) ← バッテリー・エラー状態
└ Characteristic: command (Write) ← 測定開始/停止の指示ここでの設計判断が、後の実装の楽さを大きく左右します。「アプリが定期的に読みに行く(Read)」のか「機器が変化時に通知する(Notify)」のかは、電池持ちにも応答性にも直結します。連続的に変化する値はNotify、状態の確認はRead、が基本方針です。
最初に決めるべきGATTの分担
機器メーカーとアプリ開発会社が別々の場合、ここで揉めることが非常に多いポイントです。決めておくべきなのは次の5点です。
- どの値をNotifyにするか — 増やしすぎると電池を消耗し、少なすぎるとアプリ側がポーリングして結局消耗します
- データのエンコード — リトルエンディアンか、固定小数点か、スケール係数はいくつか。ここの認識違いは値が10倍ずれる不具合になります
- エラーの伝え方 — GATTのエラーコードで返すのか、専用のCharacteristicで状態を持たせるのか
- ファームウェア更新(DFU)の有無 — 後から必要になると機器側の作り直しになります。最初に決めてください
- ペアリングの要否 — 暗号化が必要か、都度接続でよいか。業務用途では「誰でも接続できる」が問題になることがあります
// アプリ側で値の解釈を間違えないよう、仕様を単体テストで固定する例
test('温度Characteristicは 0.01℃ 単位のint16(LE)として解釈される', () => {
const raw = new Uint8Array([0x64, 0x09]); // 0x0964 = 2404
expect(parseTemperature(raw)).toBeCloseTo(24.04); // 2404 * 0.01
});弊社の場合は機器側の組込み開発とアプリ開発を同じチームで担当できるため、この分担を「仕様書のやり取り」ではなくその場の設計で決められます。機器とアプリが別会社に分かれている案件で最も時間を取られるのが、この擦り合わせです。
接続は切れる前提で設計する
BLEは必ず切れます。電波環境、機器の省電力動作、OSの都合、端末のスリープ。切れることを異常として扱う設計にすると、現場で使い物になりません。
実装で押さえるべき点は3つです。
- 自動再接続 — 切断イベントを検知したら、指数バックオフで再接続を試みる。無限に即時リトライすると電池を消耗します
- 状態の可視化 — 「接続中/再接続中/切断」を画面に出す。作業者が「今つながっているのか」を判断できないアプリは信用されません
- データの取りこぼし防止 — 切断中に機器側で測定した値をどう扱うか。機器側にバッファを持たせ、再接続時にまとめて吸い上げる設計が確実です
バックグラウンド動作の制約
「アプリを閉じていても測定を続けたい」という要望はよく出ますが、ここはOSの制約が最も厳しい領域です。
| iOS | Android | |
|---|---|---|
| バックグラウンド接続 | Background Modes(bluetooth-central)で可能 | Foreground Serviceの併用が実質必須 |
| スキャンの制約 | サービスUUID指定が必須。間隔も延びる | Doze mode・電池最適化の影響を受ける |
| アプリ終了後 | 状態復元(restoration)で一部復帰可能 | 端末メーカー独自の最適化で停止されることがある |
とくにAndroidは、端末メーカーごとの独自の省電力機能でバックグラウンド動作が止められることがあります。要件にバックグラウンド動作が含まれる場合は、実際に使う端末で早期に検証してください。「開発機では動いたのに現場の端末では止まる」が起きやすい領域です。
iOS/Androidの権限の違い
iOS
Info.plist に NSBluetoothAlwaysUsageDescription(利用目的の説明)が必須
→ 説明文が不十分だと審査でリジェクトされる
Android 12 (API 31) 以降
BLUETOOTH_SCAN … スキャンに必要(実行時権限)
BLUETOOTH_CONNECT … 接続に必要(実行時権限)
※ 位置情報を推定しない場合は neverForLocation を宣言すると
位置情報権限の要求を避けられる
Android 11 以前
ACCESS_FINE_LOCATION が必要(BLEスキャン=位置推定とみなされるため)ここはOSバージョンによる分岐が多く、対応OSの下限をどこに置くかで実装量が変わります。業務用途で古い端末が残っている場合は、権限まわりだけで想定外の工数が発生することがあるため、対象端末の棚卸しを先に行ってください。
データ量とMTU、大容量転送の限界
BLEは省電力な代わりに、一度に送れるデータ量が小さいのが特徴です。初期のMTUは非常に小さく、交渉して拡張しても数百バイト程度に収まります。
この制約から、次の設計判断が必要になります。
- 分割送信と組み立て — 大きなデータはパケットに分け、順序番号を付けてアプリ側で再構成する
- 再送制御 — 欠落を検出して再送を要求する仕組み。Notifyは到達保証がないため、確認応答の設計が要ります
- そもそも送らない — 機器側で集計・間引きしてから送る。生データを全部送る設計は、たいてい破綻します
機器内に蓄積したログを一括で吸い上げるような要件では、BLEにこだわらずWi-FiやUSBを併用する判断も現実的です。「何を、どのくらいの頻度で、どれだけ送るか」を先に数字で出すと、方式選定を誤りません。
実案件でハマった5つのこと
- スケール係数の認識違い — 機器側は0.01単位、アプリ側は0.1単位で解釈しており、値が10倍ずれた。仕様書ではなくテストコードで固定すべきでした
- 手袋をした指で操作できない — 現場作業者が手袋着用で、タップ判定が効かなかった。ボタンを大きくし、静電容量式に反応する手袋の使用を案内して解決
- 複数台の機器が近くにあり、意図しない機器につながる — アドバタイズに機器固有IDを載せ、アプリ側で選択・記憶する仕組みを追加
- 屋外で画面が見えない — 直射日光下でコントラストが不足。配色を見直し、輝度を最大にする設定を追加
- 特定メーカーのAndroid端末でだけ再接続しない — 端末独自の省電力機能が原因。使用端末を確定させ、その端末で検証する体制に変更
3つ目以降は、いずれも実装の問題ではなく現場環境の問題です。BLE案件では、実際に使う場所・端末・作業者の状態を早い段階で確認することが、技術的な設計と同じくらい重要になります。
よくある質問(FAQ)
Q. BLE対応アプリはiOSとAndroidで作りが変わりますか?
変わります。APIも権限の要求方法も異なり、Androidは12以降で実行時権限が追加されました。クロスプラットフォーム開発でも、この差はプラグイン越しに現れます。
Q. アプリをバックグラウンドにしてもBLE通信を続けられますか?
条件付きで可能です。iOSはBackground Modes、AndroidはForeground Serviceが前提になり、Androidは端末メーカー独自の省電力機能の影響も受けます。実機での早期検証が必須です。
Q. BLEの通信速度はどのくらいですか?
一度に送れるデータ量は数百バイト程度です。ログの一括転送には向かず、分割送信と再送制御、あるいは機器側での間引きが必要になります。
Q. 機器側のファームウェアも合わせて開発できますか?
できます。弊社はマイコンを使った組込み開発とアプリ開発の両方を自社で行っているため、GATT設計を機器側とアプリ側で分けずに決められます。
まとめ
この記事のまとめ
- GATT設計(Notify/Read/Writeの割り当てとエンコード)が実装の楽さを決める
- BLEは必ず切れる。自動再接続・状態表示・機器側バッファを前提に設計する
- バックグラウンド動作はOSと端末メーカーの制約が強い。実機で早期検証する
- Android 12以降の実行時権限など、対応OS下限で実装量が変わる
- 一度に送れるデータは小さい。大容量転送はそもそも設計を見直す
- 現場環境(手袋・直射日光・複数台)の確認が、技術設計と同じくらい重要
テクノスフィアは、マイコンを使った機器側の開発とスマートフォンアプリの開発を自社で一貫して行っています。機器メーカー様からの「アプリ側だけお願いしたい」というご相談も、既存機器の仕様解析からご一緒できます。