IoTプロトコル

CBOR / Protocol Buffers

効率的なバイナリデータ形式。

概要

IoT機器が送信するデータを効率的に符号化するバイナリ形式として、CBOR(Concise Binary Object Representation)とProtocol Buffers(Protobuf)は代表的な存在です。どちらもJSONと同様にさまざまなデータ型をシリアライズできますが、テキストではなくバイナリ形式のため、より小さなサイズ・高速なパース処理が特徴です。

CBORはIETFが2013年(RFC 7049)・2020年(RFC 8949)に標準化した形式です。JSONのデータモデル(オブジェクト・配列・文字列・数値・真偽値・null)を踏襲しながら、バイナリに変換することでサイズを削減します。CoAPのペイロードとして明示的に推奨されており、IETFの多くのIoT関連RFCで採用されています。スキーマ定義がなくても使える「自己記述型」フォーマットです。

Protocol BuffersはGoogleが2008年にオープンソース化したバイナリシリアライズ形式です。.protoファイルでスキーマを事前定義し、コードジェネレーターでシリアライズ/デシリアライズコードを自動生成します。フィールドが番号(タグ)で管理されるため非常にコンパクトで、Googleの内部システムや高性能gRPCの基盤として使われています。


歴史・背景

CBORの歴史

JSONはそのまま使うにはテキストオーバーヘッドが大きく、特に制約機器・低帯域通信(LoRaWAN等)では問題でした。MessagePack(2010年)が先行してバイナリJSON的な位置付けで登場しましたが、IETFの標準には至りませんでした。

CBORは6LoWPAN・CoAPとの統合を念頭に設計され、2013年にRFC 7049として制定。その後改訂版のRFC 8949(2020年)が現行標準です。IETFのIoT関連規格(SUIT、OSCORE、EDHOC等)では多くがCBORを採用しており、「IoT界のJSON」的な位置付けになっています。

Protocol Buffersの歴史

Googleは2001年頃、社内システムの大量データシリアライズのためにProtobufを開発しました。XMLが遅すぎた時代の解決策です。2008年にオープンソース化(proto2)、2016年にproto3が登場しデフォルトになりました。gRPCのペイロード形式として2015年に普及が加速しました。


技術仕様

CBOR のデータモデル

CBORはJSONの6型に加えて、バイト文字列・タグ付きデータ・浮動小数点(16/32/64bit)・符号なし整数などをネイティブでサポートします。

主要データ型(Major Types):
0: 符号なし整数
1: 符号付き整数
2: バイト文字列
3: テキスト文字列
4: 配列
5: マップ(オブジェクト)
6: タグ(追加意味情報)
7: 浮動小数点・特殊値

エンコード例

JSONとCBORのバイト列比較:

import json
import cbor2

data = {
    "device_id": "s01",
    "temperature": 25.3,
    "online": True
}

json_bytes = json.dumps(data).encode()
# b'{"device_id": "s01", "temperature": 25.3, "online": true}'
# 長さ: 56バイト

cbor_bytes = cbor2.dumps(data)
# 長さ: 約33バイト(約41%削減)

print(f"JSON: {len(json_bytes)} bytes")
print(f"CBOR: {len(cbor_bytes)} bytes")
print(f"CBOR hex: {cbor_bytes.hex()}")
# a3 69 64657669636...: 辞書型(3要素) + キー/値...

CBOR のバイトレイアウト詳細

例: {"a": 1} をCBORでエンコード
a1                  # map(1) - 1要素のマップ
   61               # text(1) - 1文字のキー
      61            # "a"
   01               # unsigned(1) - 値1

例: {"temperature": 25.3}
a1                  # map(1)
   6b               # text(11) - 11文字
      74656d706572617475726f # "temperature"
   f9 3f73          # float16: 25.3(2バイト浮動小数点!)

CBORは16ビット浮動小数点(half precision)もサポートしており、センサー値のような中程度精度の数値には2バイトで表現できます。

Protocol Buffers の仕様

.protoファイルの定義(proto3)

syntax = "proto3";

package iot.sensors;

// センサーデータメッセージ定義
message SensorData {
  string device_id = 1;
  double temperature = 2;
  double humidity = 3;
  int64 timestamp_ms = 4;
  repeated Alert alerts = 5;
  bool online = 6;
}

message Alert {
  string type = 1;
  float threshold = 2;
  float current_value = 3;
}

// OTA マニフェスト
message OtaManifest {
  string version = 1;
  string url = 2;
  bytes sha256 = 3;     // バイナリをそのまま表現可能
  uint32 size_bytes = 4;
}

ワイヤーフォーマット(Wire Format)

Protobufはフィールド番号(タグ)とワイヤータイプを組み合わせて各フィールドをエンコードします。

Tag = (field_number << 3) | wire_type

Wire types:
0: Varint (int32, int64, bool, enum)
1: 64-bit (fixed64, double)
2: Length-delimited (string, bytes, embedded message)
5: 32-bit (fixed32, float)

例: device_id = "s01" (field 1, wire type 2)
0a              # tag: (1 << 3) | 2 = 0x0a
03              # length: 3バイト
73 30 31        # "s01"

例: temperature = 25.3 (field 2, wire type 1)
11              # tag: (2 << 3) | 1 = 0x11
9a 99 99 99    # double 25.3 (リトルエンディアン)
99 99 39 40

サイズ比較

フォーマットバイト数注記
JSON100% (基準)可読性最高
CBOR50〜70%スキーマ不要
MessagePack55〜75%スキーマ不要
Protocol Buffers30〜50%スキーマ必須

動作原理

CBORのパース(Cライブラリ)

// tinycbor(Qtのオープンソースライブラリ)使用例
#include "cbor.h"

void encode_sensor_data(uint8_t *buf, size_t buf_size,
                         float temp, float humidity) {
    CborEncoder encoder, mapEncoder;
    cbor_encoder_init(&encoder, buf, buf_size, 0);
    
    cbor_encoder_create_map(&encoder, &mapEncoder, 3);
    
    cbor_encode_text_stringz(&mapEncoder, "device_id");
    cbor_encode_text_stringz(&mapEncoder, "sensor_001");
    
    cbor_encode_text_stringz(&mapEncoder, "temperature");
    cbor_encode_float(&mapEncoder, temp);
    
    cbor_encode_text_stringz(&mapEncoder, "humidity");
    cbor_encode_float(&mapEncoder, humidity);
    
    cbor_encoder_close_container(&encoder, &mapEncoder);
}

void decode_cbor(const uint8_t *buf, size_t buf_size) {
    CborParser parser;
    CborValue it;
    cbor_parser_init(buf, buf_size, 0, &parser, &it);
    
    CborValue map;
    cbor_value_enter_container(&it, &map);
    
    while (!cbor_value_at_end(&map)) {
        char key[32];
        size_t len = sizeof(key);
        cbor_value_copy_text_string(&map, key, &len, &map);
        
        if (strcmp(key, "temperature") == 0) {
            float temp;
            cbor_value_get_float(&map, &temp);
            printf("温度: %.1f\n", temp);
        }
        cbor_value_advance(&map);
    }
}

Protocol Buffersのコード生成

# .protoからC++コードを生成
protoc --cpp_out=. sensor_data.proto
# → sensor_data.pb.h, sensor_data.pb.cc が生成される

# Pythonコードを生成
protoc --python_out=. sensor_data.proto
# → sensor_data_pb2.py が生成される

# nanopb(組み込み向け軽量Cコード生成)
python nanopb/generator/nanopb_generator.py sensor_data.proto
# → sensor_data.pb.h, sensor_data.pb.c が生成される(動的メモリ不使用)
// 生成されたC++コードの使用例
#include "sensor_data.pb.h"

// シリアライズ
iot::sensors::SensorData msg;
msg.set_device_id("sensor_001");
msg.set_temperature(25.3);
msg.set_humidity(60.5);
msg.set_timestamp_ms(1736942400000LL);

std::string serialized;
msg.SerializeToString(&serialized);

// デシリアライズ
iot::sensors::SensorData received;
received.ParseFromString(received_data);
float temp = received.temperature();
# Python での Protobuf 使用
from sensor_data_pb2 import SensorData
import time

# シリアライズ
msg = SensorData()
msg.device_id = "sensor_001"
msg.temperature = 25.3
msg.humidity = 60.5
msg.timestamp_ms = int(time.time() * 1000)

binary = msg.SerializeToString()
print(f"Protobuf: {len(binary)} bytes")

# デシリアライズ
received = SensorData()
received.ParseFromString(binary)
print(f"温度: {received.temperature}")

nanopb による組み込み向け実装

// nanopb(動的メモリ不使用の組み込み向けProtobuf)
#include "pb_encode.h"
#include "pb_decode.h"
#include "sensor_data.pb.h"

bool encode_sensor_data(uint8_t *buffer, size_t buffer_size,
                         size_t *message_length) {
    iot_sensors_SensorData message = iot_sensors_SensorData_init_zero;
    
    strcpy(message.device_id, "sensor_001");
    message.temperature = 25.3f;
    message.humidity = 60.5f;
    message.online = true;
    
    pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
    bool status = pb_encode(&stream, iot_sensors_SensorData_fields, &message);
    
    *message_length = stream.bytes_written;
    return status;
}

用途・ユースケース

LoRaWAN での省サイズ送信

LoRaWANのペイロード上限は最大242バイト(SF7)〜51バイト(SF12)と非常に小さく、バイナリエンコードが必須です。

import struct
import cbor2

# LoRaWAN向け超コンパクトCBOR
data = {
    1: 253,   # temp × 10 = 25.3℃(整数キーで最小化)
    2: 605,   # humidity × 10 = 60.5%
    3: 87,    # battery %
}
payload = cbor2.dumps(data)
# a3 01 18fd 02 19025d 03 18 57
# 9バイト!(JSONの"{"t":25.3,"h":60.5,"b":87}" = 28バイト)

gRPCサービスでのIoTデータ収集

syntax = "proto3";

service SensorService {
  // 単一データ送信
  rpc SendData (SensorData) returns (SendResponse);
  
  // ストリーミング送信(多数のデータを連続送信)
  rpc StreamData (stream SensorData) returns (StreamSummary);
  
  // サーバーストリーミング(コマンド受信)
  rpc ReceiveCommands (DeviceInfo) returns (stream Command);
}
# gRPC クライアント(Raspberry Pi から)
import grpc
from sensor_data_pb2_grpc import SensorServiceStub
from sensor_data_pb2 import SensorData, DeviceInfo

channel = grpc.secure_channel(
    'grpc.example.com:443',
    grpc.ssl_channel_credentials()
)
stub = SensorServiceStub(channel)

# ストリーミング送信
def generate_data():
    for _ in range(100):
        yield SensorData(
            device_id="rpi_001",
            temperature=read_temp(),
            timestamp_ms=int(time.time() * 1000)
        )
        time.sleep(1)

response = stub.StreamData(generate_data())

CoAP + CBOR(IoTの王道組み合わせ)

from aiocoap import Context, Message, resource
from aiocoap.numbers.codes import Code
import cbor2

class TemperatureResource(resource.Resource):
    async def render_get(self, request):
        data = {
            "device": "coap_sensor_01",
            "temp": read_temperature(),
            "unit": "celsius"
        }
        # CBOR形式で返す(Content-Format: 60)
        return Message(
            code=Code.CONTENT,
            payload=cbor2.dumps(data),
            content_format=60  # CBOR
        )

実装・開発のポイント

フォーマット選択ガイド

状況推奨フォーマット理由
LoRaWAN / LPWACBOR(整数キー)最小バイト数
gRPC サービスProtocol BuffersgRPCのデファクト
CoAP ペイロードCBORIETF標準推奨
MQTTペイロード(帯域制約あり)MessagePack or CBOR既存エコシステム
開発・デバッグ段階JSON可読性最優先
大規模Googleインフラ連携Protocol BuffersGoogleエコシステム

スキーマ進化(後方互換性)

Protobufはフィールド番号が変わらなければ後方互換性を維持できます。

// v1.0
message SensorData {
  string device_id = 1;
  double temperature = 2;
  // フィールド3は将来のために予約(reserved)
}

// v2.0(後方互換)
message SensorData {
  string device_id = 1;
  double temperature = 2;
  double humidity = 3;     // 追加OK(古いパーサーは無視)
  // フィールド4を削除する場合はreservedを使う
  reserved 4;
  reserved "old_field_name";
}

ベンチマーク(ESP32での実測目安)

フォーマット100バイト相当エンコードパース
JSON(cJSON)〜500μs〜800μs
CBOR(tinycbor)〜100μs〜150μs
Protobuf(nanopb)〜80μs〜100μs

他技術との比較

項目CBORProtobufMessagePackJSON
RFC標準○(8949)××○(8259)
スキーマ要否不要必要(.proto)不要不要
自己記述性×
サイズ最小
組み込み適合◎(nanopb)
CoAP対応◎(推奨)
gRPC対応◎(標準)×

JSONの可読性を活かした開発・デバッグと、CBORやProtobufの効率性を活かした本番運用という使い分けが実践的なアプローチです。CoAPとの組み合わせにはCBOR、高性能サービス間通信にはProtobufが定番です。LoRaWANLPWAなど低帯域通信では整数キーを使ったCBORが特に有効です。

関連用語

参考リンク