NanoPB: Як обробляти користувацькі конвертери масивів в C++

Див. також: C version: How to handle custom array converters in C

NanoPB — це оптимізована за розміром коду реалізація Protocol Buffers для вбудованих систем. Ця публікація показує, як обробляти користувацькі конвертери масивів у C++ за допомогою NanoPB, на основі реалізації KKS-Firmware.

Визначення Proto

Спочатку створіть файл .proto з repeated-полями:

custom_array.proto
syntax = "proto3";

package example;

message CustomArrayMessage {
  repeated uint32 values = 1;
}

Генерація коду NanoPB

Згенеруйте код NanoPB за допомогою файлу .options, щоб указати максимальну кількість:

Створіть custom_array.options:

custom_array.options
example.CustomArrayMessage.values max_count:20

Потім згенеруйте:

generate_nanopb_custom_array.sh
protoc --nanopb_out=. custom_array.proto

Це згенерує custom_array.pb.h та custom_array.pb.c.

Приклад на C++ з користувацьким конвертером масиву

Ось повний приклад на C++, що реалізує користувацький конвертер масиву з параметрами offset та size:

custom_array_example.cpp
#include <stdio.h>
#include <algorithm>
#include <array>
#include <limits>
#include "custom_array.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"

// Користувацький конвертер масиву з шаблонними параметрами offset та size
template<class ITEM_CONVERTER, class CONTAINER, size_t OFFSET=0, size_t SIZE=std::numeric_limits<size_t>::max()/2>
class ArrayConverterWithOffsetAndSize {
public:
    static bool encodeCallback(pb_ostream_t *stream, const pb_field_t *field, const CONTAINER &container) {
        size_t i = 0;
        // Використовуємо цикл for для ітерації по контейнеру
        for (const auto &item : container) {
            i++; // Перший індекс, який ми перевіряємо, — 1
            if(i <= OFFSET) { // <=, а не "<", оскільки ми ++ перед цим рядком
                continue;
            }
            if(i > OFFSET + SIZE) {
                break;
            }
            // Кодуємо елемент
            if (!ITEM_CONVERTER::encodeCallback(stream, field, item)) {
                return false;
            }
        }
        return true;
    }

    static bool decodeCallback(pb_istream_t *stream, const pb_field_t *field, CONTAINER &container) {
        // Додаємо елемент до контейнера
        // Примітка: Це спрощений приклад — реальна реалізація залежить від типу контейнера
        return false; // Не реалізовано в цьому прикладі
    }
};

// Простий конвертер uint32
class UInt32Converter {
public:
    static bool encodeCallback(pb_ostream_t *stream, const pb_field_t *field, uint32_t value) {
        if (!pb_encode_tag_for_field(stream, field))
            return false;
        return pb_encode_varint(stream, value);
    }
};

int main() {
    // Буфер для закодованого повідомлення
    uint8_t buffer[256];
    size_t message_length;
    
    // Створюємо масив з 10 елементами
    std::array<uint32_t, 10> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // --- КОДУВАННЯ ---
    example_CustomArrayMessage message = example_CustomArrayMessage_init_zero;
    
    // Використовуємо користувацький конвертер: кодуємо елементи 3-7 (OFFSET=2, SIZE=5)
    using CustomConverter = ArrayConverterWithOffsetAndSize<UInt32Converter, std::array<uint32_t, 10>, 2, 5>;
    
    // Налаштовуємо callback
    message.values.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
        const std::array<uint32_t, 10>* container = (const std::array<uint32_t, 10>*)*arg;
        return CustomConverter::encodeCallback(stream, field, *container);
    };
    message.values.arg = &values;
    
    // Створюємо потік для кодування
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    // Кодуємо повідомлення
    if (!pb_encode(&ostream, example_CustomArrayMessage_fields, &message)) {
        printf("Помилка кодування: %s\n", PB_GET_ERROR(&ostream));
        return 1;
    }
    
    message_length = ostream.bytes_written;
    printf("Закодовано %zu байтів (елементи 3-7)\n", message_length);
    
    // Виводимо hex-дамп
    printf("Закодовані дані: ");
    for (size_t i = 0; i < message_length; i++) {
        printf("%02x ", buffer[i]);
    }
    printf("\n");
    
    // Виводимо, що було закодовано
    printf("Закодовані значення: ");
    for (size_t i = 2; i < 2 + 5 && i < values.size(); i++) {
        printf("%u ", values[i]);
    }
    printf("\n");
    
    return 0;
}

Команда компіляції

Скомпілюйте приклад з nanopb. NanoPB зазвичай використовується шляхом безпосереднього включення вихідних файлів у ваш проєкт:

compile_custom_array_example.sh
g++ -o custom_array_example custom_array_example.cpp custom_array.pb.c pb_common.c pb_encode.c pb_decode.c -I.

Примітка: Вихідні файли NanoPB (pb_common.c, pb_encode.c, pb_decode.c) потрібно компілювати разом з вашим проєктом. Їх можна отримати з NanoPB GitHub repository.

Ключові моменти

  • Шаблонні параметри: OFFSET та SIZE керують тим, які елементи кодувати
  • Логіка зсуву: Пропустити перші OFFSET елементів (індексація з 1 у циклі)
  • Обмеження розміру: Закодувати не більше SIZE елементів після зсуву
  • Патерн callback: Використовувати pb_callback_t з користувацькими функціями encode/decode
  • Гнучкість: Можна кодувати будь-який зріз масиву без копіювання
  • Ефективність пам’яті: Не потрібно створювати тимчасові масиви
  • Патерн KKS-Firmware: Базується на ArrayConverterWithOffsetAndSize з KKS-Firmware

Коли використовувати користувацькі конвертери масивів

  • Коли потрібно закодувати підмножину великого масиву
  • Коли ви хочете уникнути копіювання даних
  • Коли реалізуєте пагінацію або розбиття на частини
  • Коли структура масиву не відповідає вимогам protobuf
  • Коли потрібна користувацька логіка кодування для масивів

Очікуваний вивід

custom_array_expected_output.txt
Закодовано 10 байтів (елементи 3-7)
Закодовані дані: 08 03 08 04 08 05 08 06 08 07 
Закодовані значення: 3 4 5 6 7 

Зверніть увагу, що закодовуються лише елементи 3, 4, 5, 6, 7 (5 елементів, починаючи зі зсуву 2).

Додатково: Підтримка декодування

Щоб підтримати декодування, розширте конвертер:

decode_callback_snippet.cpp
static bool decodeCallback(pb_istream_t *stream, const pb_field_t *field, CONTAINER &container) {
    uint64_t value;
    if (!pb_decode_varint(stream, &value))
        return false;
    
    // Додаємо до контейнера (реалізація залежить від типу контейнера)
    // Для std::array можна відстежувати окремий індекс
    return true;
}

Реальне використання

Цей патерн використовується в KKS-Firmware для:

  • Кодування лише активних каналів з більшого масиву каналів
  • Надсилання часткових даних датчиків для економії пропускної здатності
  • Реалізації диференціальних оновлень
  • Ефективної обробки розріджених масивів

More NanoPB posts


Дивіться схожі статті за категоріями: Embedded C/C++ Protocol Buffers