zcbor

repository·main·Indexed 41 days ago

https://github.com/nordicsemi/zcbor

A lightweight CBOR (Concise Binary Object Representation) library for C/C++ designed for microcontrollers. It features a standalone C library for encoding and decoding, and Python-based tooling for schema-driven validation, data conversion between YAML/JSON/CBOR, and C code generation based on CDDL (Concise Data Definition Language) schemas.

Tokens
9.5K
Snippets
19
Records
38
Agent score
50%

What's inside zcbor

  1. Overview of zcbor

    main

    zcbor is a low-footprint CBOR (Concise Binary Object Representation) library written in C (C++ compatible), specifically designed for microcontrollers.

    It provides two primary ways to work with CBOR:

    1. C Library: A standalone, lightweight library for encoding and decoding CBOR data. It can be used directly or as the foundation for generated code.
    2. Python Tooling: A script and module (zcbor.py) used for schema-driven validation, data conversion (YAML/JSON/CBOR), and C code generation based on CDDL (Concise Data Definition Language) schemas.

    Key use cases include:

    • Using the C library for manual encoding/decoding in constrained environments.
    • Using the Python tool to generate optimized C code that automatically validates data against a CDDL schema.
    • Using the Python tool to validate or convert data formats (e.g., converting a YAML config to a CBOR payload for transmission).
  2. Understand the zcbor Python script architecture

    main

    The zcbor.py script is a tool used to generate C code from CDDL (Concise Data Definition Language) descriptions. The script's logic is organized into five primary classes:

    1. CddlParser: Recursively parses CDDL strings into a tree of type objects.
    2. CddlXcoder: A base class for DataTranslator and CodeGenerator providing common logic for member naming and type inference.
    3. DataTranslator: Used for host-side data manipulation. It converts between CBOR, YAML, JSON, and internal Python representations while validating against CDDL.
    4. CodeGenerator: Constructs the C decoding/encoding functions and the struct definitions required to hold the data.
    5. CodeRenderer: Takes the output from CodeGenerator and produces the final C files (.c, .h) and an optional cmake file.

    To use the tool, you typically provide a CDDL file, and the script generates the necessary C infrastructure to handle those specific data structures.

  3. Handling fragmented payloads in C

    main

    zcbor supports encoding and decoding payloads in sections (fragments), which is useful for protocols where data is received in multiple packets. You can introduce a new section by calling zcbor_update_state().

    Constraints

    Section boundaries cannot fall within a CBOR header or a simple value. The following cannot be split between sections:

    • Numbers and simple values (integers, floats, bools, undefined, nil)
    • Tags
    • Headers of lists, maps, tstrs, and bstrs

    If a boundary must occur within one of these, you must manually splice the remaining bytes of the current section with the start of the next section (a buffer of ~8 bytes is usually sufficient).

    Supported Fragmented Types

    • Lists and Maps: Can span multiple sections as long as individual elements are not split.
    • Strings: Text and byte strings can be split across sections if ZCBOR_FRAGMENTS is enabled and the *str_fragments_*() APIs are used.
  4. Understand CDDL syntax and constraints in zcbor

    main

    zcbor uses a subset of CDDL (Concise Data Definition Language) to define data structures. You can use it to specify exact values, ranges, sizes, and repetition patterns.

    Value and Range Constraints

    • Exact Value: Foo = 3 (uint must be 3).
    • Range: Foo = -100..100 (int between -100 and 100).
    • Text String: Foo = "hello" (tstr must be "hello").
    • Boolean: Foo = false (bool must be false).

    Size and Type Constraints

    • .size: Restricts integer or string length in bytes. E.g., Foo = uint .size 4.
    • .cbor / .cborseq: Ensures a bstr contains valid CBOR data of a specific type. E.g., Foo = bstr .cbor Bar.

    Repetition Patterns

    • ?: 0 or 1 time.
    • *: 0 or more times.
    • +: 1 or more times.
    • x*y: Between $x$ and $y$ times, inclusive. E.g., {4*8(int => bstr)} is a map with 4 to 8 key/value pairs.

    Note: The actual number of entries supported for * and + is limited by the default_max_qty value.

  5. Understand the generated C API and state management

    main

    The C code generated by zcbor follows a consistent pattern for all decoding and encoding functions.

    Function Signature

    Every generated (and non-generated) function follows this signature: bool <name>(zcbor_state_t *state, <type> *result)

    • Return Value: Returns true on success, false on failure.
    • State Management: All functions operate on a zcbor_state_t object, which tracks:
      • Current position and end of the payload.
      • Current position within a list or map and the maximum expected elements.
      • A list of backup states used to restore the position if an optional element fails to decode.

    Special Functions

    Two specialized functions exist that deviate from the standard signature by accepting function pointers:

    • zcbor_multi_decode / zcbor_multi_encode
    • zcbor_present_decode / zcbor_present_encode
  6. Integrate generated zcbor code with CMake

    main

    When you run zcbor code with the --output-cmake <path> flag, a CMake file is created. This file defines a target library containing both the generated and non-generated source files, along with the necessary include directories.

    To use it in your project:

    1. Generate the files using --output-cmake.
    2. Include the generated CMake file in your project's CMakeLists.txt using add_subdirectory() or include().
    3. Link the generated target into your project.

    You can also use --copy-sources to copy the non-generated source files to the same location as your generated files.

  7. Generate C code from CDDL

    main

    Use the zcbor code command to generate C structs and API functions based on your CDDL descriptions. The generated code is C++ compatible and includes validation logic to ensure input data adheres to the restrictions (type, content, repetition) defined in the CDDL.

    Generated Files

    • Types Header: Always generated.
    • Decoding Functions: Generated if --decode or -d is specified.
    • Encoding Functions: Generated if --encode or -e is specified.
    • CMake File: Generated if --output-cmake is specified.

    Command Examples

    Generate decoding code:

    zcbor code -c pet.cddl -d -t Pet --oc pet_decode.c --oh pet_decode.h

    Generate both encoding and decoding code:

    python3 <zcbor base>/zcbor/zcbor.py code --decode --encode -c pet.cddl -t Pet --oc pet_decode.c --oh pet_decode.h
    # Example: Generate decoding code for the 'Pet' type
    zcbor code -c pet.cddl -d -t Pet --oc pet_decode.c --oh pet_decode.h
  8. Migrate Python usage to zcbor v. 0.9.99

    main

    In zcbor v. 0.9.99, the from_cddl() and __init__() functions were updated to accept only keyword-only arguments. To ensure compatibility, you must pass all arguments to these constructors or functions as keyword arguments rather than positional arguments.

    Example:

    # Old way (positional)
    obj = MyClass(arg1, arg2)
    
    # New way (keyword-only)
    obj = MyClass(arg1=arg1, arg2=arg2)
    # Old way (positional)
    obj = MyClass(arg1, arg2)
    
    # New way (keyword-only)
    obj = MyClass(arg1=arg1, arg2=arg2)
  9. Configure CodeGenerator entry types

    main

    When using CodeGenerator, you must specify which types are "entry types". These are the outermost types or the specific types the user expects to encounter directly.

    Why this matters:

    • Direct Access: Entry types receive public wrapper functions in the generated C code.
    • Handling Nested CBOR: If you have a type defined as Foo = bstr .cbor Bar, the generated code usually automatically decodes the bstr into a Bar struct. If you prefer to handle the bstr manually as a raw byte string, you must add Foo to the entry_types list.
  10. How to use the zcbor C library

    main

    The C library is located in include/ and src/. You can integrate it into your project by including these files directly.

    Zephyr Integration

    If you are using the Zephyr RTOS, you can enable the library by setting the CONFIG_ZCBOR configuration option.

    The zcbor State Object

    All encoding and decoding operations require a zcbor_state_t object. These are most easily initialized using the provided macros:

    • ZCBOR_STATE_D(decode_state, n, payload, payload_len, elem_count, n_flags): Initializes a decoding state. The resulting decode_state[0] is ready for use with decoding APIs. The parameter n specifies the number of backup states to include.
    • ZCBOR_STATE_E(encode_state, n, payload, payload_len, 0): Initializes an encoding state. The resulting encode_state[0] is ready for use with encoding APIs.

    Key State Members

    • elem_count: Tracks the number of encoded objects in the current list or map. It resets when entering nested structures and restores when exiting. This is used for managing state during nested encoding/decoding.
    • n_flags: Used during decoding of maps with unknown ordering to enable searching via zcbor_unordered_map_search().
    /** Initialize a decoding state (could include an array of backup states).
     *  After calling this, decode_state[0] is ready to be used with the decoding APIs. */
    ZCBOR_STATE_D(decode_state, n, payload, payload_len, elem_count, n_flags);
    
    /** Initialize an encoding state (could include an array of backup states). */
    ZCBOR_STATE_E(encode_state, n, payload, payload_len, 0);
  11. Build and run the zcbor 'hello world' sample

    main

    To verify your installation and see a basic usage of the encoding and decoding API, you can build and run the provided hello_world sample. This sample demonstrates instantiating zcbor state variables and using them to encode and decode the string "Hello World".

    Note: This specific sample uses the library API directly and does not utilize the zcbor.py script tool.

    mkdir build
    cmake . -Bbuild
    make -C build
    ./build/app
  12. Build and run the pet sample project

    main

    To build the sample project using CMake and Make, follow these steps:

    1. Create a build directory.
    2. Configure the project with CMake.
    3. Build the project using Make.
    4. Execute the resulting application.
    mkdir build
    cmake . -Bbuild
    make -C build
    ./build/app
    mkdir build
    cmake . -Bbuild
    make -C build
    build/app