GHSのObservationを実際のバイトに【ACOM→GATTマッピング 実装者向け】

bluetooth




こんな悩みはありませんか?

前回の記事で、GHSの測定値(Observation)は 「Segmentation Header + Body」のバイト列としてGATTを流れる、という全体像を見ました。でも、実装しようとすると次が気になります。

そのバイト、実際どのcharacteristicで、どう組み立てて/解読するの?

この記事は、そこに踏み込む実装者向けの回です。読めば、次の3つがクリアになります。

  1. 測定データ(Observation)が どのGATT characteristic を、どう流れるのか(Notify/Indicate、開始/停止)
  2. Flags と各フィールドが、実際のバイトにどう並ぶのか(エンディアン込み)
  3. 値が IEEE-754ではないという、実装者が必ずハマる話(IEEE-11073 FLOAT)

さらに、メーカーが自社独自の測定指標を載せたいときの対応(private MDCコード)も扱います。

Observationやデータモデル(ACOM)がまだの方は、先に GHSの中身「ACOM」とは? を。Notify/Indicate や CCCD があやしい方は BLEの基本構造【GAP / GATT / Service / Characteristic】 もどうぞ。


測定データ(Observation)はどのcharacteristicで流れるのか

GHSはBLEの標準に沿ったサービス(GHSS=Generic Health Sensor Service)で、いくつかのcharacteristicを定義しています。主なものを挙げます。

Characteristic主なProperty役割
Health Sensor FeaturesRead対応する測定の種類・デバイス特化を通知
Live Health ObservationsIndicate または Notify生成した測定データをクライアントへ報告(本命)
Stored Health ObservationsIndicate / Notify蓄積した測定データ
RACP(Record Access Control Point)Write, Indicate蓄積データへのアクセス制御
GHS Control PointWrite, Indicate測定データ送信の開始 / 停止
Observation Schedule ChangedIndicateスケジュール変更の通知

実装の中心になるのが Live Health Observations characteristic です。サーバ(センサー)は、生成した測定データ(Observation)をこのcharacteristicの Indicate または Notify でクライアントに送ります(どちらを使うかはサーバ側の選択)。

そして、その送信を制御するのが GHS Control Point です。クライアントがオペコードを書き込みます。

  • 0x01 … Liveの測定データ送信を開始
  • 0x02 … 送信を停止
  • サーバは 0x80(成功)で応答

ここは、GAP/GATT記事で扱った Notify / Indicate / CCCD の知識がそのまま効く場所です。これまでの記事で積み上げてきたGHSのデータモデルが、ここで馴染みのGATT操作に着地します。

図1:クライアントがControl Pointに開始を書き込み、サーバがLive Health Observationsで測定データをindicate/notifyする。

なぜ ATT_MTU − 4 なのか(分割の実際)

前回見た Segmentation Header(First / Last / Rolling Counter)が効くのが、ここです。仕様はこう定めています。

1件の測定データ(Observation)の合計サイズが (ATT_MTU − 4) を超えるなら、1つのHealth Observation Bodyを複数のセグメントに分けて送る。

BLEの1回の通知で運べるペイロードは ATT_MTU に縛られます。長い測定データ(波形や複合値、補足情報つきなど)はそこに収まらないので、Bodyを分割し、各セグメントの先頭に1オクテットのSegmentation Headerを付けて送ります。受信側は First / Last / カウンタを見て、元のBodyに組み立て直します。(− 4 は送信オーバーヘッド分の余白。内訳は実装時に仕様で確認を。)


Flagsを読む(2オクテット)

Bodyの先頭近くにある Flags(2オクテット) が、「このObservationにどの任意フィールドが入っているか」を1ビットずつ示します。

bit存在を示すフィールド
0Observation Type
1Time Stamp
2Measurement Duration
3Measurement Status
4Observation Id
5Patient
6Supplemental Information
7Derived From
8Is Member Of
9TLVs
10–15予約(RFU)

パースの実装は、まず Flags を読み、立っているビットに対応するフィールドだけを、定義された順序で取り出す——という流れになります。逆に生成側は、含めるフィールドに応じてFlagsを立てます。

図2:Flagsの各ビットが、対応する任意フィールドの有無を表す。

Observationを実際のバイトに組み立てる

いよいよ本題です。前回の記事で見たSpO₂測定(パルスオキシメータ、89.0%)を、実際のバイトに組んでみます。まず大前提として、GHSSは多バイト値をすべて little-endian(最下位オクテットが先頭)で送ります(仕様§1.4)。ここを外すと全部ずれます。

主なフィールドの並びは、こうなります(Observation Type と Time Stamp を含むケース)。

フィールドサイズバイト(16進, LE)意味
Segmentation Header1(First/Last/Counter)分割制御
Observation Class Type101Numeric(数値)
Length2(Body長, LE)本体の長さ
Flags203 00bit0(Type)+bit1(Time Stamp) を立てた例
Observation Type4B8 4B 02 00MDCコード(下記)
Time Stamp9(Elapsed Time 構造)測定時刻
Observation Value可変(単位+値。下記)SpO₂の値

Observation Type = 前回の答え合わせ

MDC_PULS_OXIM_SAT_O2 のMDCコードは 0x00024BB8 です。ここで前回の記事を思い出してください。

  • 上位16bit 0x0002 = partition 2
  • 下位16bit 0x4BB8 = term-code 19384
  • まとめた10進値 = 150456

前回、このコードを「partition=2 / term-code=19384 / まとめて150456」と分解しました。その数字が、そのままこのバイトに現れます。little-endianなので、ワイヤ上では B8 4B 02 00 の順になります。

単位は2オクテットのterm-code

Numericの値には単位が付きます。単位のMDCコードは partition 4(=単位)に固定なので、2オクテットのterm-codeだけで識別できます。SpO₂の単位「%」は MDC_DIM_PERCENT(4::544)です。

値は IEEE-754 ではない ← 最大のハマりどころ

そして核心。GHSの数値は、みなさんが普段使う IEEE-754 の float ではありません。 仕様§1.5が定めるのは IEEE 11073 の 32-bit FLOAT で、構造はこうです——1オクテットの10進指数(符号付き)+ 3オクテットの仮数(符号付き)

89.0 を表すと、89.0 = 890 × 10⁻¹ なので、指数 = −10xFF)、仮数 = 8900x00037A)、まとめた32bit値 = 0xFF00037A になります。もしこの4バイトを IEEE-754のfloatとしてパースすると、まったく別のとんでもない値になります(0xFF00037A を754で読むと約 −1.7×10³⁸)。「値が桁違いにおかしい」ときは、まずここを疑ってください。

図3:主要フィールドのバイト並び。Observation TypeにMDCコードが、値にIEEE-11073 FLOATが現れる。確定バイトのみ表示。

⚠️ 本記事は、フィールドの構造と要注意点を掴むことを目的にしています。完全なバイト列(Lengthの実値、Time Stampの9バイト、値部の厳密なバイト順)は、§3.2.1 のフィールド定義と §1.4(バイト順)・§1.5(FLOAT)の規則から自分で組み立て、実機で必ず検証してください。サンプル実装や資料に載っているバイト例も、そのまま鵜呑みにせず、必ず自分の環境で照合することを強くおすすめします。


自社独自の測定値を載せたい:private MDCコード

ここはメーカーに刺さる論点です。「標準のMDCに無い、自社独自の測定指標(独自アルゴリズムのスコアなど)をObservation Typeに入れたい」——よくある要望です。結論から言うと、できます。IEEE 11073-10101 は private(私的)コードの領域を定義しています。

  • Partition 1024(Private Partition):64K個ぶんの private コード空間。上位16bitを1024(0x0400)にしたコードは、自社定義として使えます。
  • 各partition内の term-code 0xF000–0xFFFF も、private用に予約されています。

これらは「公開コードが割り当てられるまでの暫定利用も可」とされており、メーカーは自社のprivateコードを製品間で一貫して管理・使用する責任を負います。実装上のうれしさは、前回強調した「箱は共通」がそのまま効くことです。Observation Type(4バイト)に private partition のコードを入れるだけで、標準のObservationの器はそのままに、独自指標を流せます。パーサやトランスポートを作り直す必要はありません。

ただし現実的な線引きも押さえておきましょう。

  • privateコードの意味を解釈できるのは 自社エコシステム内に限られます(他社の汎用クライアントは「未知のコード」として扱う)。
  • 本筋は、標準化(新しいMDCコードの申請)への移行です。privateは「暫定 or 自社用」と位置づけるのが健全です。
図4:MDCコードは partition(上位16bit)+ term-code(下位16bit)。partition 1024 と各partitionの 0xF000–0xFFFF が private。

実装で必ず引っかかる4つの落とし穴

最後に、GHS実装でハマりやすい点をまとめます。

1. 値をIEEE-754でパースして桁違いにズレる

いちばん多いのがこれ。GHSの数値は IEEE-11073 FLOAT(指数+仮数) です。754のfloatとしてmemcpyすると当然おかしくなります。

2. エンディアンの取り違え

GHSSは little-endian。MDCコードもFLOATも最下位オクテットが先頭です。B8 4B 02 00 を big-endianで読むと、まったく別のMDCコードになります。

3. ATT_MTU − 4 を忘れて分割が破綻する

測定データが (ATT_MTU − 4) を超えたら分割が必要です。Segmentation Header の First / Last / カウンタの組み立て・再結合をサボると、長い測定データで崩れます。

4. Control Pointで開始しないと測定データが来ない

「接続してCCCDも有効化したのに測定データが来ない」。GHSでは GHS Control Point に 0x01(開始)を書くまで、Liveの測定データは流れ始めません。GAP/GATT記事の「Notifyが来ない=CCCD書き忘れ」と同じ構図の、GHS版の第1容疑者です。


まとめ

3行でまとめます。

  1. 測定データは Live Health Observations characteristic を Indicate/Notify で流れ、GHS Control Point で開始/停止する
  2. Bodyは little-endian、任意フィールドの有無は Flags が1ビットずつ示す。MDCコードはそのままバイトに現れる(前回の答え合わせ)
  3. 値は IEEE-754ではなくIEEE-11073 FLOAT(指数+仮数)。ここが最大のハマりどころ

そして、標準に無い指標は private MDCコード(Partition 1024 など)で、標準の器のまま流せます。「抽象モデル(ACOM)→ 実バイト(GATT)」の橋がこれで架かりました。次回は、旧プロファイル(BLPなど)からGHSへ移行するときの実務を扱う予定です。


参考(一次資料)

  • Bluetooth SIG『Generic Health Sensor Service(GHSS)』v1.0(§1.4 バイト順 / §1.5 IEEE FLOAT / §3.1 Features / §3.2 Live Health Observations / §3.5 Control Point / Appendix A Examples)
  • Bluetooth SIG『Generic Health Sensor Design and Implementation Guide』v2(MDCコード・private コード領域・単位)
  • IEEE 11073-10101(MDC nomenclature)/ IEEE 11073-10206(ACOM)/ IEEE 11073-20601(FLOAT-Type)
  • GATT Specification Supplement(Elapsed Time 構造)

この記事はBluetooth SIGの公開仕様・実装ガイド、およびIEEEの公開情報をもとに構成しています。

コメント

タイトルとURLをコピーしました