Skip to content
Published on

FingerScore ハードウェア 4 — BLE 無線通信の設計(GATT からモバイル連携まで)

Authors

はじめに

FingerScore はラケットスポーツ(テニス、バドミントン、卓球、スカッシュ)のためのスマートスコアリングプラットフォームです。指にはめる小さな BLE リングがあり、簡単なジェスチャー(指を軽くトントンと叩く、あるいはボタンを押す動作)で点数を上げると、その情報が無線で携帯やウェブアプリに伝わります。試合中にスコアボードへ走ったり紙に書いたりする必要はなく、指の動作ひとつで点数が記録されます。

このシリーズでは、そのハードウェアを実際に作れるよう段階的に教えます。1〜3 回ではマイコンの選定、センサー(加速度計でジェスチャー検出)、ファームウェアの基礎を扱いました。今回の第 4 回のテーマは無線通信です。リングがどれだけ賢くジェスチャーを検出しても、その信号を携帯へ安定して送れなければ意味がありません。

なぜ BLE(Bluetooth Low Energy)なのか。なぜ Wi-Fi やクラシック Bluetooth ではないのか。「点数が 1 上がった」という情報をどう携帯へ届けるのか。ペアリングは安全か。本記事ではそのすべてをゼロから説明します。電子工学の専攻者でなくても追えるよう、たとえと具体的なコードの骨格を併せて提供します。


1. なぜ BLE か — 低電力の哲学

無線通信の候補

指にはめるリングは電池がとても小さいです。コインセル(CR2032)ひとつ、あるいは爪ほどの LiPo 電池が精一杯です。ですから無線方式を選ぶ際の最重要基準は電力です。

方式平均消費電力距離データ量FingerScore 適合性
Wi-Fi非常に高い(数百 mA)長い非常に大きい不適(電池が数時間)
クラシック Bluetooth高い(数十 mA)大きい不適
BLE非常に低い(平均 数十 uA)中(10m 前後)小さい(数バイト)最適
ANT+低い小さい可能だが携帯の対応が弱い
LoRa低い非常に長い非常に小さい過剰(距離は不要)

FingerScore が送るデータは「点数が 1 上がった」「セットが終わった」といった小さなイベントです。一度に数バイトで十分です。大きなデータを速く送る必要は全くありません。一方で電池は数日〜数週間もたせなければなりません。この条件に最も合うのが BLE です。

BLE はどう電力を節約するか

核心は**「ほとんどの時間は寝て、ほんの一瞬だけ起きる」**ことです。BLE 無線チップは普段ほぼオフで、決まった短い時間(connection interval)にだけ起きてデータをやり取りします。起きている時間が 1% にも満たないことが多いです。

BLE ラジオの時間使用(概念図)

電力
  ^
  |   .         .         .         .
  |   |         |         |         |     <- 短い送受信区間(数 ms)
  |   |         |         |         |
  |___|_________|_________|_________|____> 時間
      <-------->                          <- ほぼ sleep(数十〜数百 ms)
       interval

この「一瞬起きる周期」を connection interval と呼び、後で詳しく扱います。この周期を長くするほど電力は節約できますが反応は遅くなります。トレードオフです。


2. BLE の二本柱 — GAP と GATT

BLE を学び始めて最も紛らわしいのが GAP と GATT という二つの略語です。たとえで整理しましょう。

  • GAP(Generic Access Profile):「誰がどう接続するか」を扱う規則。パーティーで互いを発見し挨拶しペアになる段階だと思ってください。
  • GATT(Generic Attribute Profile):「接続したあと、どんなデータをどうやり取りするか」を扱う規則。挨拶のあとの実際の会話の形式です。

GAP — 発見と接続

BLE 機器には通常二つの役割があります。

  • Peripheral(周辺機器):自分の存在を広告(advertising)する側。FingerScore リングがこれです。
  • Central(中心機器):広告をスキャンして接続を仕掛ける側。普通は携帯です。

リングは周期的に「ここにいるよ、FingerScore リングだよ」という小さなパケット(advertising packet)を発信します。携帯は周囲をスキャンしてこのパケットを見つけると接続を試みます。

[FingerScore リング]  --- advertising --->  (空中)  <--- scanning ---  [携帯]
   Peripheral                                                          Central

発見後:
[リング] <========= connection established =========> [携帯]

広告パケットには機器名、サービス UUID(後述)、送信出力などを載せられます。ただしサイズ制限(レガシー広告で 31 バイト)があり、必要なものだけを入れます。

GATT — データの構造

接続されたら GATT の世界です。GATT はデータを**サービス(Service) → キャラクタリスティック(Characteristic) → ディスクリプタ(Descriptor)**という階層で整理します。

  • サービス(Service):関連データの束。例)「スコアサービス」「バッテリーサービス」。
  • キャラクタリスティック(Characteristic):実際のデータ一片。例)「現在のスコア」「セット番号」「バッテリー残量」。
  • ディスクリプタ(Descriptor):キャラクタリスティックのメタデータ。例)人が読む説明、通知設定。

たとえるとサービスはタンス、キャラクタリスティックは引き出しひとつ、ディスクリプタは引き出しのラベルです。

GATT サーバー(FingerScore リング)の構造

Service: Battery Service(標準, UUID 0x180F)
  └─ Characteristic: Battery Level(0x2A19) [Read, Notify]

Service: FingerScore Score Service(カスタム 128-bit UUID)
  ├─ Characteristic: Score State        [Read, Notify]
  ├─ Characteristic: Match Control      [Write]
  └─ Characteristic: Device Config      [Read, Write]

重要な点:一般に Peripheral(リング)が GATT サーバーCentral(携帯)が GATT クライアントになります。データを保持し提供する側がリングです。


3. キャラクタリスティックの動作 — Read, Write, Notify

キャラクタリスティックはいくつかの属性(property)を持ちます。FingerScore で最も重要な三つを見ましょう。

属性方向意味FingerScore 例
Read携帯がリングに要求携帯が値を読む現在スコアを一度照会
Write携帯がリングに送る携帯が値を書く「試合リセット」命令送信
Notifyリングが携帯に押し出す値が変わるとリングが通知点数が上がると即通知

FingerScore の核心は Notify です。携帯が常に「点数変わった?」と問い合わせる(ポーリング)方式は電力の無駄が大きいです。代わりに点数が実際に変わったときだけリングが携帯へ「今変わったよ!」と知らせるのが効率的です。

Notify が動作する原理

携帯は接続後、通知を受けたいキャラクタリスティックの特殊なディスクリプタである **CCCD(Client Characteristic Configuration Descriptor)**に値を書きます。ここに 1 を書くと「通知をオンに」という意味です。以降、リングがスコアキャラクタリスティックを更新するたびに自動で携帯へ届きます。

1. 携帯: Score State キャラクタリスティックの CCCD に 0x0001 を Write(購読)
2. リング: ジェスチャー検出 -> 点数 +1 -> Score State の値を更新
3. リング: 更新値を即座に携帯へ Notify 送信
4. 携帯: コールバックで新スコア受信 -> UI 更新

ちなみに Notify に似た Indicate があります。違いは**確認応答(ACK)**の有無です。Notify は送りっぱなし(速いが欠落あり)、Indicate は携帯が受領を返さないと次を送りません(遅いが信頼性が高い)。点数のように欠落してはいけないデータは Indicate が安全な場合もありますが、通常は Notify にシーケンス番号を付けて欠落を検知します。これはパケット設計の節で扱います。


4. カスタム GATT サービスの設計 — スコアイベント

標準サービス(バッテリー、心拍など)は UUID が決まっていますが、FingerScore のスコアサービスは独自のものなので 128 ビットのカスタム UUID を自作します。UUID ジェネレーターでランダムな UUID をひとつ取りベースにし、サービス/キャラクタリスティックごとに一部のバイトだけ変えるのが慣例です。

Base UUID:   6E40XXXX-B5A3-F393-E0A9-E50E24DCCA9E   (例)

Score Service       : 6E400001-...
 ├ Score State char : 6E400002-...   [Notify, Read]
 ├ Match Control char: 6E400003-...   [Write]
 └ Device Config char: 6E400004-...   [Read, Write]

スコアパケットの設計

「点数が上がった」をどうバイトで表すか。単純に点数の数字だけ送ると、パケットが欠落したとき携帯に知る術がありません。そこで小さくても賢いパケット構造を設計します。

Score State パケット(8 bytes)

Offset  Size  Field          説明
0       1     seq            シーケンス番号(0-255 循環)。欠落検知用
1       1     event_type     0=点数, 1=セット終了, 2=試合終了, 3=リセット
2       1     player         0=自分, 1=相手
3       1     score_self     自分の現在スコア
4       1     score_oppo     相手の現在スコア
5       1     set_self       自分のセット数
6       1     set_oppo       相手のセット数
7       1     battery_pct    バッテリー残量(%)

この設計の利点:

  • シーケンス番号で携帯が「5 の次に 7 が来た? 6 が欠落した」と検知できます。すると携帯が Read で現在状態を読み直し同期します。
  • 点数を累積値(現在スコア)で送るため、1〜2 個のパケットが欠落しても次のパケットで自動復旧します(増分でなく絶対値なので安全)。
  • 8 バイトは BLE 標準 MTU(23 バイト、実ペイロード 20 バイト)に十分収まります。

設計原則:小さな IoT 機器では「増分(+1)」より「現在状態の全体」を送るほうが欠落に強いです。パケットがひとつ消えても次のパケットが真実を持っているからです。


5. ファームウェアのコード骨格(Peripheral 側)

実チップ(例:Nordic nRF52、ESP32)ごとに SDK は異なりますが、概念は似ています。ここでは擬似コードに近い C の骨格で流れを示します。中括弧やポインタはすべてコードフェンス内にあるので安全です。

// スコア状態を持つ構造体
typedef struct {
    uint8_t seq;
    uint8_t event_type;
    uint8_t player;
    uint8_t score_self;
    uint8_t score_oppo;
    uint8_t set_self;
    uint8_t set_oppo;
    uint8_t battery_pct;
} score_packet_t;

static score_packet_t g_score;
static uint16_t score_char_handle;  // GATT キャラクタリスティックのハンドル

// ジェスチャー検出コールバック(加速度計の割り込みから呼ばれる)
void on_gesture_score(uint8_t which_player) {
    g_score.seq++;                       // シーケンス増加
    g_score.event_type = 0;              // 点数イベント
    g_score.player = which_player;
    if (which_player == 0) g_score.score_self++;
    else                   g_score.score_oppo++;
    g_score.battery_pct = battery_read_percent();

    // キャラクタリスティック値を更新 + Notify 送信
    ble_notify(score_char_handle,
               (uint8_t*)&g_score,
               sizeof(g_score));
}

核心は、ジェスチャー割り込みが来たら構造体を更新し ble_notify() を呼ぶことだけです。普段 MCU はスリープしており、加速度計が「トン!」という衝撃を検知すると割り込みで起き、この関数を実行して再び眠ります。

Write 処理(携帯 -> リング命令)

携帯が「試合リセット」などの命令を送ると Write コールバックが呼ばれます。

// Match Control キャラクタリスティックに Write が来たとき
void on_match_control_write(const uint8_t *data, uint16_t len) {
    if (len < 1) return;
    uint8_t cmd = data[0];
    switch (cmd) {
        case 0x01:  // 試合リセット
            memset(&g_score, 0, sizeof(g_score));
            break;
        case 0x02:  // セット開始
            g_score.score_self = 0;
            g_score.score_oppo = 0;
            break;
        default:
            break;  // 未知の命令は無視
    }
}

6. ペアリングとセキュリティ — LE Secure Connections

他人の携帯が自分のスコアを横取りしたり、偽リングが携帯に付くと困ります。BLE はこのためのセキュリティ手順を用意しています。

セキュリティの 3 段階

  1. Pairing(ペアリング):二機器が一時鍵を交換し暗号化チャネルを作る過程。
  2. Bonding(ボンディング):ペアリング後に鍵を保存し、次に会えば自動で信頼すること。
  3. Encryption(暗号化):実データを暗号化して盗聴を防ぐこと。

最新 BLE(4.2 以上)は LE Secure Connections に対応します。これは ECDH(楕円曲線ディフィー・ヘルマン)鍵交換を使い、中間者(MITM)がパケットをすべて覗いても鍵を割り出せないようにします。旧来の LE Legacy Pairing より遥かに安全です。

ペアリング方式の選択

FingerScore リングにはディスプレイもキーパッドもありません。こうした機器は通常、次のいずれかを使います。

方式必要 UIMITM 防御FingerScore
Just Worksなし弱いデフォルト(利便性優先)
Passkey Entry数字入力強いUI がなく不可
Numeric Comparison画面 2 つ強いUI がなく不可
OOB別チャネル(NFC など)強いNFC 追加で可能

リングには UI がないので通常は Just Works で始めますが、スコアデータは機微度が低いので実用上問題ありません。ただしファームウェア更新のような機微な操作には追加認証(アプリのアカウントに基づくトークンなど)を重ねるとよいです。


7. モバイル・ウェブ連携

Web Bluetooth(ウェブアプリ)

Chrome 系ブラウザは Web Bluetooth API で BLE 機器に直接アクセスできます。セキュリティ上、ユーザーがボタンを押して明示的に機器を選んだときだけ接続されます。

const SCORE_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const SCORE_CHAR = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';

async function connectRing() {
  // ユーザーに機器選択ポップアップを表示
  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: [SCORE_SERVICE] }],
  });

  const server = await device.gatt.connect();
  const service = await server.getPrimaryService(SCORE_SERVICE);
  const ch = await service.getCharacteristic(SCORE_CHAR);

  // Notify 購読
  await ch.startNotifications();
  ch.addEventListener('characteristicvaluechanged', (e) => {
    const v = e.target.value; // DataView
    const seq = v.getUint8(0);
    const scoreSelf = v.getUint8(3);
    const scoreOppo = v.getUint8(4);
    updateScoreboard(seq, scoreSelf, scoreOppo);
  });
}

Android(Kotlin)概要

Android ネイティブは BluetoothGatt API を使います。コールバックベースなので流れは似ています。

private val gattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            g.discoverServices()   // サービス探索開始
        }
    }

    override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
        val ch = g.getService(SCORE_SERVICE_UUID)
            .getCharacteristic(SCORE_CHAR_UUID)
        g.setCharacteristicNotification(ch, true)
        // CCCD に通知有効化の値を書く
        val cccd = ch.getDescriptor(CCCD_UUID)
        cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        g.writeDescriptor(cccd)
    }

    override fun onCharacteristicChanged(g: BluetoothGatt, ch: BluetoothGattCharacteristic) {
        val data = ch.value
        val scoreSelf = data[3].toInt() and 0xFF
        val scoreOppo = data[4].toInt() and 0xFF
        // UI スレッドでスコアボード更新
    }
}

iOS は CoreBluetooth(CBCentralManager, CBPeripheral)でほぼ同じ概念を扱います。


8. 省電力 — connection interval とパケット頻度

接続が維持されている間もさらに電力を節約できます。鍵となるパラメータは次のとおりです。

パラメータ意味トレードオフ
Connection Interval起きて通信する周期(7.5ms-4s)短い=速い・電力増、長い=遅い・電力減
Slave Latency送るものがなければ飛ばせる回数上げると電力減、反応性は維持
Supervision Timeoutこの時間応答がないと切断と判断短すぎ=誤切断、長すぎ=切断検知が遅い

FingerScore は点数が頻繁に変わりません(数秒〜数十秒に一度)。ですから長い connection interval + 高い slave latency の組み合わせが理想です。普段はほぼ寝て、点数が変わった瞬間に Notify で起きればよいのです。

電力 vs 反応性(概念)

interval 短い(15ms)  : 即反応, 電池 数日
interval 中(100ms)   : ほぼ即, 電池 1-2 週
interval 長い(500ms) : 0.5 秒遅延, 電池 数週

スコア記録は 0.1-0.5 秒の遅延が問題にならない -> 長い interval を選ぶ

ファームウェアで接続後に connection parameter update を要求し、携帯へ「もっとゆったりした周期に変えよう」と提案できます。


9. アンテナと電波 — 2.4GHz と人体

BLE は 2.4GHz 帯を使います。この周波数は水によく吸収されますが、人体の大部分は水です。リングは指にはめるのでアンテナが皮膚に非常に近いです。これが通信距離を縮める主因です。

対応策:

  • アンテナ配置:指の内側(肌に触れる面)ではなく外側(空気側)へアンテナが向くよう PCB を配置します。
  • グラウンドキープアウト:アンテナ周辺の銅(グラウンド)を空けないと放射しにくくなります。これは第 6 回 PCB 設計で深掘りします。
  • 出力調整:送信出力(TX power)を適切に上げますが、上げすぎると電池を食います。

人体への影響については、BLE の送信出力は非常に低いです(通常 0dBm = 1mW、通話よりはるかに弱い)。通常使用で健康問題は報告されておらず、規制機関の SAR(電磁波吸収率)基準内で動作します。なお認証(FCC/KC)は第 6 回で扱います。


10. デバッグ — 見えない無線を見る方法

無線は目に見えずデバッグが難しいです。幸い良いツールがあります。

  • nRF Connect(モバイルアプリ):携帯で周囲の BLE 機器をスキャンし、サービス・キャラクタリスティックを展開し、自分で Read/Write/Notify を試せます。ファームウェア開発初期に必須です。アプリがなくてもこれでリングの GATT 構造が正しく作れているか確認します。
  • BLE スニファー:nRF52840 ドングル + Wireshark で空中のパケットを直接キャプチャします。接続がなぜ切れるのか、どのパケットが欠落するのかなど深い問題を追うときに使います。
  • ロギング(RTT/UART):チップからシリアルログを取り、ファームウェア内部状態を見ます。
デバッグの段階別ツール

段階 1: nRF Connect アプリで GATT 構造を確認(サービスが見えるか?)
段階 2: nRF Connect で Notify 購読 -> ジェスチャー時に値が変わるか?
段階 3: ダメなら RTT ログでファームウェア内部を確認
段階 4: それでもダメならスニファーで空中パケットをキャプチャ

11. よくある落とし穴

実際に BLE ファームウェアを作るとほぼ全員が一度は踏む地雷です。

  • 接続がよく切れる:supervision timeout が短すぎる、またはリングがスリップに入りラジオを長く切りすぎて応答を逃す場合。スリープと connection interval のバランスを取る必要があります。
  • MTU 問題:標準 MTU は 23 バイト(実ペイロード 20)。もっと大きなデータを送るには接続後に MTU 交渉が必要です。FingerScore の 8 バイトパケットは交渉なしで十分で、あえて小さく保った設計が活きます。
  • CCCD を有効化せず Notify を期待:携帯が通知購読(CCCD 書き込み)をしていないのに値だけ更新すると、携帯は何も受け取れません。初心者が最も詰まる地点です。
  • 広告が頻繁/まれすぎ:広告周期が長すぎると携帯がリングを見つけるのが遅れ、短すぎると電池を浪費します。通常 100ms〜1s の間で折衷します。
  • ボンディング情報のもつれ:携帯でペアリングを消したのにリングが覚えていると再接続できません。リングに「ボンディング初期化」動作(例:長押し)を入れておくとよいです。

12. おわりに — 次回予告

今回の第 4 回では FingerScore の無線神経網である BLE をゼロから設計しました。GAP/GATT の構造、Notify ベースのスコア配信、欠落に強いパケット設計、LE Secure Connections のセキュリティ、Web Bluetooth と Android 連携、そして省電力とデバッグまで触れました。これでリングは点数を賢く、かつ節約しながら携帯へ送れます。

しかしここには大きな前提がひとつあります。リングに電源が入っていることです。指ほどの機器の中にどうやって電池を入れ、数日もたせ、安全に充電するのでしょうか。

次回の第 5 回電源・電池・回路設計では、電力予算の計算から電池選び(コインセル vs LiPo)、充電 IC と保護回路、LDO とバックコンバータ、そして発熱・ESD・安全まで — リングを本当に点け続けるすべてを扱います。


参考資料