nanopbによりマイコンでProtocol Buffersを使用する方法

# 記事の概要


# Protocol Buffersとは何か？
データ構造をGoogleの策定した形式に従ってシリアライズしたものをProtocol Buffersと呼びます。

このProtocol Buffersを使う利点は、様々な機器間で送受信するデータを共通のデータ構造で扱えることです。

例えば、サーバとアプリ、アプリとマイコン、マイコンとマイコンでそれぞれ別のデータ構造を用いて通信していては管理が煩雑になります。
共通のデータ構造を持ち、共通の形式でシリアライズされたProtocol Buffersを使えば、間違いが起きにくいというわけです。

Protocol Buffersの意義について、詳しくは以下の記事をご参照ください。

[今さらProtocol Buffersと、手に馴染む道具の話](https://qiita.com/yugui/items/160737021d25d761b353)

# 環境
本記事では以下の環境で作業しています。
本記事では以下の開発環境の構築方法については解説しません。

マイコンはSTMでも、ルネサスでも、他のどれでも同様に使えます。


- OS
    - Windows 10
- Python3
    - anaconda3
- マイコン
    - Nordic nRF52832(ARM系）

# nanopbのダウンロード

マイコンに組み込める小サイズなライブラリnanopbを使用します。
以下をダウンロードしてください。

https://github.com/nanopb/nanopb.git

# protoファイルの作成
データ構造を定義します。これは自分の作成するアプリケーションの仕様に合わせて自由に作成できます。

今回は以下のmy_test.proptを作成しました。
3つのデータ構造体です。
これらは長さやid、そしてデータ配列から成ります。

```
syntax = "proto3";
import "nanopb.proto";

message TestOne {
  int32 length = 1;
  bytes first_data = 2 [(nanopb).max_size = 5, (nanopb).fixed_length = true];
}

message TestTwo {
  int32 length = 1;
  bytes second_data = 2 [(nanopb).max_size = 8, (nanopb).fixed_length = true];
}

message TestThree {
  int32 length = 1;
  int64 id = 2;
  bytes third_data = 3 [(nanopb).max_size = 16, (nanopb).fixed_length = true];
}
```

ここでデータ配列はあらかじめサイズを設定しました。 `(nanopb).max_size` で配列のサイズを、 `(nanopb).fixed_length` でサイズは固定であることを指定してあります。
他のデータ型の設定方法の詳細は以下をご参照ください。
https://jpa.kapsi.fi/nanopb/docs/concepts.html#data-types


また、protoファイルにおいて `(nanopb)` を使用するためには、以下の例のように `import "nanopb.proto";` が必要になります。
これがないと、　`Option "(nanopb)" unknown.` というエラーメッセージが表示されます。

https://github.com/nanopb/nanopb/blob/master/tests/options/proto3_options.proto


# コンパイル
マイコンに組み込むファイルを自動生成します。
そのために、まずは以下のツールをインストールしてください。

```console
python -m pip install protobuf grpcio-tools
```

anaconda3のコマンドプロンプトにおいて `python ../../generator/nanopb_generator.py YOUR_FILE.proto` を実行します。
階層の場所とファイル名は各位の環境に合わせて修正ください。

```console
(base) D:\nanopb\examples\my_test>python ../../generator/nanopb_generator.py my_test.proto
Writing to my_test.pb.h and my_test.pb.c
```

成功すれば、*.pb.h と *.pb.c が生成されます。

# マイコンに組み込む
今回はNordicのnRF52382を例に説明しますが、手順は他のマイコンでも同様です。

上で自動生成されたファイルmy_test.pb.h と my_test.pb.c、およびnanopbの階層にあるファイル、pb.h、pb_common.c、pb_common.h、pb_decode.c、pb_decode.h、pb_encode.c、pb_encode.hを自分のマイコン開発環境にコピーします。

今回は適当なサンプルプロジェクトの中に放り込んでみました。

![開発環境.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/dc16b8da-8195-3bed-54a7-684a55859040.png)

# エンコーダ
送信データをシリアライズします。

nanopbのexampleフォルダにあるサンプルプログラムのsimple.cを参照して以下を作成しました。

送受信するメッセージのデータは `message1_buffer[]` に格納します。
メッセージのデータサイズは `message1_length` に格納します。

```C:
    uint8_t message1_buffer[128];
    size_t message1_length;
    bool result;

    /* Encode our message */
    {
        /* Allocate space on the stack to store the message data. */
        TestOne message1 = TestOne_init_zero;
        TestTwo message2 = TestTwo_init_zero;
        TestThree message3 = TestThree_init_zero;
        
        /* Create a stream that will write to our buffer. */
        pb_ostream_t stream1 = pb_ostream_from_buffer(message1_buffer, sizeof(message1_buffer));
        
        /* Fill in the data */
        uint8_t sample_data1[5] = "Hello";
        message1.length = 11;
        for(int i=0; i<5; i++)
        {
            message1.first_data[i] = sample_data1[i];
        }
        
        /* Now we are ready to encode the message! */
        result = pb_encode(&stream1, TestOne_fields, &message1);
        message1_length = stream1.bytes_written;
        
        /* Then just check for any errors.. */
        if (!result)
        {
            printf("Encoding failed: %s\n", PB_GET_ERROR(&stream1));
            return 1;
        }
    }
```

## メッセージ実体の作成
まずはデータを格納する実体を作成します。

```
        TestOne message1 = TestOne_init_zero;
        TestTwo message2 = TestTwo_init_zero;
        TestThree message3 = TestThree_init_zero;
```

データ型は自動生成したmy_test.pb.hに定義されています。
my_test.proptで設定した通りのデータ構造体が作成されているはずです。

```C:my_test.pb.h
/* Struct definitions */
typedef struct _TestOne {
    int32_t length;
    pb_byte_t first_data[5];
} TestOne;

typedef struct _TestThree {
    int32_t length;
    int64_t id;
    pb_byte_t third_data[16];
} TestThree;

typedef struct _TestTwo {
    int32_t length;
    pb_byte_t second_data[8];
} TestTwo;
```

初期化定数の `*_init_zero` は自動生成したmy_test.pb.hに作成されています。
0で初期化する以外にも、初期値を設定する`*__init_default` も用意されています。

```C:my_test.pb.h
/* Initializer values for message structs */
#define TestOne_init_default                     {0, {0}}
#define TestTwo_init_default                     {0, {0}}
#define TestThree_init_default                   {0, 0, {0}}
#define TestOne_init_zero                        {0, {0}}
#define TestTwo_init_zero                        {0, {0}}
#define TestThree_init_zero                      {0, 0, {0}}
```

## 送信用stereamの作成
stremを作成します。
message1_buffer[]を引数に設定することで、これにシリアライズしたデータが格納されるようになります。

```C
        /* Create a stream that will write to our buffer. */
        pb_ostream_t stream1 = pb_ostream_from_buffer(message1_buffer, sizeof(message1_buffer));
```

## 送信データの準備
送信データを用意します。

```C:
        /* Fill in the data */
        uint8_t sample_data1[5] = "Hello";
        message1.length = 11;
        for(int i=0; i<5; i++)
        {
            message1.first_data[i] = sample_data1[i];
        }
```

## 送信データのシリアライズ
stream を用いてmessage1_buffer[]にシリアライズした送信データを格納します。

```
        /* Now we are ready to encode the message! */
        result = pb_encode(&stream1, TestOne_fields, &message1);
        message1_length = stream1.bytes_written;
```

以下のように`pb_encode` の実行前後を比較することで、message1_buffer[]にシリアライズされたデータが格納されていることが分かります。

![encoder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/bd271ab8-75fe-46fd-522e-19eba3062b92.png)
![encoder2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/79fb5269-2390-75eb-2acb-64beaa3f9ce6.png)

# デコーダ
受信したデータをデシリアライズします。
このサンプルプログラムでは、先にエンコードしたデータを使用していますが、実際には送信元から受信したシリアライズ・データをmessage1_buffer[]に格納します。

nanopbのexampleフォルダにあるサンプルプログラムのsimple.cを参照して以下を作成しました。


```C:
    {
        /* Allocate space for the decoded message. */
        TestOne message1 = TestOne_init_zero;
        TestTwo message2 = TestTwo_init_zero;
        TestThree message3 = TestThree_init_zero;
        
        /* Create a stream that reads from the buffer. */
        pb_istream_t stream1 = pb_istream_from_buffer(message1_buffer, message1_length);
        
        /* Now we are ready to decode the message. */
        result = pb_decode(&stream1, TestOne_fields, &message1);
        
        /* Check for errors... */
        if (!result)
        {
            printf("Decoding failed: %s\n", PB_GET_ERROR(&stream1));
            return 1;
        }
    }
```

##  メッセージ実体の作成
まずはデータを格納する実体を作成します。
これはエンコーダと同様なので説明は省略します。

## 受信用straemの作成
streamを作成します。
受信データを格納したmessage1_buffer[]をstreamに渡すことで、デシリアライズの準備をします。

```
        /* Create a stream that reads from the buffer. */
        pb_istream_t stream1 = pb_istream_from_buffer(message1_buffer, message1_length);
```


## 受信データのデシリアライズ
streamを介して受信データを構造体に代入します。

```
        /* Now we are ready to decode the message. */
        result = pb_decode(&stream1, TestOne_fields, &message1);
```

自動的に構造体の定義通りにデータが格納されるので、バグが減らせて、コード作成も容易になります。

以下のように`pb_decode` の実行前後を比較することで、構造体のmessage1にデシリアライズされたデータが自動的に格納されていることが分かります。

![decoder1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/bf500dac-647e-9f89-31e8-8e3234aa99ed.png)
![decoder2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150620/41482ee3-0eea-ef21-c8a3-43213a296598.png)


今回は構造体を1つしか試しませんでしたが、他の作成した2つについても同様のことができます。

# まとめ
以上のようにnanopbを使用することで、簡単にProtocol Buffersをマイコンに導入できることが分かりました。

IoTが盛んになり様々な機器やサーバ同士を連結するようになったことで、送受信データの管理が複雑化しています。
データ管理を容易にするProtocol Buffersは、今後に使用する機会が増えていくと思われます。