こんな悩みはありませんか?
前回の記事で、GHSの測定値(Observation)は 「Segmentation Header + Body」のバイト列としてGATTを流れる、という全体像を見ました。でも、実装しようとすると次が気になります。
「そのバイト、実際どのcharacteristicで、どう組み立てて/解読するの?」
この記事は、そこに踏み込む実装者向けの回です。読めば、次の3つがクリアになります。
- 測定データ(Observation)が どのGATT characteristic を、どう流れるのか(Notify/Indicate、開始/停止)
- Flags と各フィールドが、実際のバイトにどう並ぶのか(エンディアン込み)
- 値が 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 Features | Read | 対応する測定の種類・デバイス特化を通知 |
| Live Health Observations | Indicate または Notify | 生成した測定データをクライアントへ報告(本命) |
| Stored Health Observations | Indicate / Notify | 蓄積した測定データ |
| RACP(Record Access Control Point) | Write, Indicate | 蓄積データへのアクセス制御 |
| GHS Control Point | Write, Indicate | 測定データ送信の開始 / 停止 |
| Observation Schedule Changed | Indicate | スケジュール変更の通知 |
実装の中心になるのが Live Health Observations characteristic です。サーバ(センサー)は、生成した測定データ(Observation)をこのcharacteristicの Indicate または Notify でクライアントに送ります(どちらを使うかはサーバ側の選択)。
そして、その送信を制御するのが GHS Control Point です。クライアントがオペコードを書き込みます。
0x01… Liveの測定データ送信を開始0x02… 送信を停止- サーバは
0x80(成功)で応答
ここは、GAP/GATT記事で扱った Notify / Indicate / CCCD の知識がそのまま効く場所です。これまでの記事で積み上げてきたGHSのデータモデルが、ここで馴染みのGATT操作に着地します。

なぜ 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 | 存在を示すフィールド |
|---|---|
| 0 | Observation Type |
| 1 | Time Stamp |
| 2 | Measurement Duration |
| 3 | Measurement Status |
| 4 | Observation Id |
| 5 | Patient |
| 6 | Supplemental Information |
| 7 | Derived From |
| 8 | Is Member Of |
| 9 | TLVs |
| 10–15 | 予約(RFU) |
パースの実装は、まず Flags を読み、立っているビットに対応するフィールドだけを、定義された順序で取り出す——という流れになります。逆に生成側は、含めるフィールドに応じてFlagsを立てます。

Observationを実際のバイトに組み立てる
いよいよ本題です。前回の記事で見たSpO₂測定(パルスオキシメータ、89.0%)を、実際のバイトに組んでみます。まず大前提として、GHSSは多バイト値をすべて little-endian(最下位オクテットが先頭)で送ります(仕様§1.4)。ここを外すと全部ずれます。
主なフィールドの並びは、こうなります(Observation Type と Time Stamp を含むケース)。
| フィールド | サイズ | バイト(16進, LE) | 意味 |
|---|---|---|---|
| Segmentation Header | 1 | (First/Last/Counter) | 分割制御 |
| Observation Class Type | 1 | 01 | Numeric(数値) |
| Length | 2 | (Body長, LE) | 本体の長さ |
| Flags | 2 | 03 00 | bit0(Type)+bit1(Time Stamp) を立てた例 |
| Observation Type | 4 | B8 4B 02 00 | MDCコード(下記) |
| Time Stamp | 9 | (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⁻¹ なので、指数 = −1(0xFF)、仮数 = 890(0x00037A)、まとめた32bit値 = 0xFF00037A になります。もしこの4バイトを IEEE-754のfloatとしてパースすると、まったく別のとんでもない値になります(0xFF00037A を754で読むと約 −1.7×10³⁸)。「値が桁違いにおかしい」ときは、まずここを疑ってください。

⚠️ 本記事は、フィールドの構造と要注意点を掴むことを目的にしています。完全なバイト列(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つの落とし穴
最後に、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行でまとめます。
- 測定データは Live Health Observations characteristic を Indicate/Notify で流れ、GHS Control Point で開始/停止する
- Bodyは little-endian、任意フィールドの有無は Flags が1ビットずつ示す。MDCコードはそのままバイトに現れる(前回の答え合わせ)
- 値は 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の公開情報をもとに構成しています。


コメント