NanoPB: Як обробляти необов'язкові поля в C

Див. також: C++ version: How to handle optional fields in C++

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

Визначення Proto

Спочатку створіть файл .proto з необов’язковими полями:

optional.proto
syntax = "proto3";

package example;

message OptionalMessage {
  optional uint32 id = 1;
  optional string name = 2;
  optional float temperature = 3;
  bool active = 4;  // Звичайне (необов'язкове) поле
}

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

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

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

optional.options
example.OptionalMessage.name max_size:64

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

generate_nanopb_optional.sh
protoc --nanopb_out=. optional.proto

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

Приклад на C

Ось повний приклад на C для обробки необов’язкових полів:

optional_example.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "optional.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"

int main() {
    // Буфер для закодованого повідомлення
    uint8_t buffer[256];
    size_t message_length;
    
    // --- КОДУВАННЯ ---
    example_OptionalMessage message = example_OptionalMessage_init_zero;
    
    // Встановлення необов'язкових полів
    message.id = 42;
    message.has_id = true;  // Важливо: встановіть прапорець has_*
    
    strncpy(message.name, "Sensor1", sizeof(message.name) - 1);
    message.has_name = true;  // Важливо: встановіть прапорець has_*
    
    message.temperature = 23.5f;
    message.has_temperature = true;  // Важливо: встановіть прапорець has_*
    
    // Звичайне поле (прапорець has_* не потрібен)
    message.active = true;
    
    // Створення потоку для кодування
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    // Кодування повідомлення
    if (!pb_encode(&ostream, example_OptionalMessage_fields, &message)) {
        printf("Помилка кодування: %s\n", PB_GET_ERROR(&ostream));
        return 1;
    }
    
    message_length = ostream.bytes_written;
    printf("Закодовано %zu байтів\n", message_length);
    
    // Друк шістнадцяткового дампу закодованих даних
    printf("Закодовані дані: ");
    for (size_t i = 0; i < message_length; i++) {
        printf("%02x ", buffer[i]);
    }
    printf("\n");
    
    // --- ДЕКОДУВАННЯ ---
    example_OptionalMessage decoded = example_OptionalMessage_init_zero;
    
    // Створення потоку для декодування
    pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
    
    // Декодування повідомлення
    if (!pb_decode(&istream, example_OptionalMessage_fields, &decoded)) {
        printf("Помилка декодування: %s\n", PB_GET_ERROR(&istream));
        return 1;
    }
    
    // Друк декодованих значень
    printf("Декодовані значення:\n");
    if (decoded.has_id) {
        printf("  id: %u\n", (unsigned int)decoded.id);
    } else {
        printf("  id: (не встановлено)\n");
    }
    
    if (decoded.has_name) {
        printf("  name: %s\n", decoded.name);
    } else {
        printf("  name: (не встановлено)\n");
    }
    
    if (decoded.has_temperature) {
        printf("  temperature: %f\n", decoded.temperature);
    } else {
        printf("  temperature: (не встановлено)\n");
    }
    
    printf("  active: %s\n", decoded.active ? "true" : "false");
    
    return 0;
}

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

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

compile_optional_example.sh
gcc -o optional_example optional_example.c optional.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.

Скрипт тестування на Python

Щоб перевірити кодування, ви можете використати бібліотеку protobuf для Python:

test_optional.py
import optional_pb2

# Читання двійкових даних
with open('encoded.bin', 'rb') as f:
    data = f.read()

# Декодування
msg = optional_pb2.OptionalMessage()
msg.ParseFromString(data)

print("Декодовані Python значення:")
print(f"  id: {msg.id if msg.HasField('id') else '(не встановлено)'}")
print(f"  name: {msg.name if msg.HasField('name') else '(не встановлено)'}")
print(f"  temperature: {msg.temperature if msg.HasField('temperature') else '(не встановлено)'}")
print(f"  active: {msg.active}")

Спочатку скомпілюйте визначення protobuf для Python:

compile_python_optional.sh
protoc --python_out=. optional.proto

Потім змініть приклад на C, щоб зберегти закодовані дані у файл:

save_encoded_optional.c
// Після кодування додайте це:
FILE *f = fopen("encoded.bin", "wb");
fwrite(buffer, 1, message_length, f);
fclose(f);

Приклад з відсутніми необов’язковими полями

Ось приклад, де деякі необов’язкові поля не встановлено:

optional_partial_example.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "optional.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"

int main() {
    uint8_t buffer[256];
    size_t message_length;
    
    // --- КОДУВАННЯ ---
    example_OptionalMessage message = example_OptionalMessage_init_zero;
    
    // Встановлення лише деяких необов'язкових полів
    message.id = 42;
    message.has_id = true;
    
    // name та temperature не встановлено (прапорці has_* залишаються false)
    
    // Звичайне поле завжди встановлено
    message.active = true;
    
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    if (!pb_encode(&ostream, example_OptionalMessage_fields, &message)) {
        printf("Помилка кодування: %s\n", PB_GET_ERROR(&ostream));
        return 1;
    }
    
    message_length = ostream.bytes_written;
    printf("Закодовано %zu байтів (частково)\n", message_length);
    
    // --- ДЕКОДУВАННЯ ---
    example_OptionalMessage decoded = example_OptionalMessage_init_zero;
    
    pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
    
    if (!pb_decode(&istream, example_OptionalMessage_fields, &decoded)) {
        printf("Помилка декодування: %s\n", PB_GET_ERROR(&istream));
        return 1;
    }
    
    printf("Декодовані значення:\n");
    printf("  id: %s\n", decoded.has_id ? "встановлено" : "не встановлено");
    printf("  name: %s\n", decoded.has_name ? "встановлено" : "не встановлено");
    printf("  temperature: %s\n", decoded.has_temperature ? "встановлено" : "не встановлено");
    printf("  active: %s\n", decoded.active ? "true" : "false");
    
    return 0;
}

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

Коли використовувати необов’язкові поля

Очікуваний вивід (повний приклад)

optional_full_expected_output.txt
Закодовано 19 байтів
Закодовані дані: 08 2a 12 07 53 65 6e 73 6f 72 31 1d 00 00 bc 41 30 01 
Декодовані значення:
  id: 42
  name: Sensor1
  temperature: 23.500000
  active: true

Очікуваний вивід (частковий приклад)

optional_partial_expected_output.txt
Закодовано 4 байтів (частково)
Закодовані дані: 08 2a 30 01 
Декодовані значення:
  id: встановлено
  name: не встановлено
  temperature: не встановлено
  active: true

Зверніть увагу, що часткове кодування набагато менше (4 байти проти 19 байтів), оскільки необов’язкові поля пропущено.

More NanoPB posts


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