Skip to content

novatel/novatel_edie

Repository files navigation

edie_logo

NovAtel EDIE

EDIE (Encode Decode Interface Engine) is a C++ SDK (Software Development Kit) that can encode and decode messages from NovAtel's OEM7 receivers from one format into another. For example, converting an ASCII log into a Binary log.

EDIE is maintained and published by NovAtel.

Documentation on NovAtel's data (commands, logs, messages, and responses) can be found here.

Information on the novatel_edie Python package can be found here. The rest of this document pertains only to the C++ SDK.

Table of Contents

Getting Started

EDIE can be built from source by cloning this repository git clone https://github.com/novatel/novatel_edie.git and acting according to the following instructions.

Dependencies

EDIE is a C++ 17 project which uses a CMake build system directly integrated with Conan 2 for dependency management. Information is provided below on how to acquire all of the necessary build dependencies.

Building the Project

A build can be configured using cmake as follows:

cmake -S {path_to_edie_repository} -B {path_to_build_folder}

This step will automatically use Conan to install all dependencies within the newly created build folder.

The build can then be started with:

cmake --build {path_to_build_folder}

CMake Presets

It is recommended to use CMake presets to define specific build parameters, such as compiler and build type. The CMakePresets.json file contains a few default options and you can extend it by creating a CMakeUserPresets.json file with your personal configurations.

Take the windows-msvc configure preset and its corresponding windows-msvc-debug and windows-msvc-release build presets as an example. The configure preset can be used via cmake --preset windows-msvc to configure a Windows MSVC build within an out/build folder. A debug build can then be initiated via cmake --build --preset windows-msvc-debug or a release build via cmake --build --preset windows-msvc-release.

CMake presets are especially useful when initiating builds from an IDE. Almost all IDEs have inbuilt or extension support for CMake which allow selecting between presets to configure a build.

CMake Options

The following is a list of CMake cache variables specific to this project.

  • BUILD_TESTS: Whether to build the test executables. Defaults to ON.
  • BUILD_EXAMPLES: Whether to build example programs that make basic use of EDIE's features. Defaults to ON.
  • BUILD_BENCHMARKS: Whether to build the benchmarking programs. Defaults to OFF.
  • BUILD_PYTHON: Whether to build the novatel_edie python module. Defaults to OFF.
  • Python_EXECUTABLE: The python interpreter executable to use as a source for the nanobind library which the novatel_edie python module is built upon.
  • PYTHON_INSTALL_DIR: The location to install the novatel_edie python module. During development, set this to your Python environment's site-packages directory. For example: {$WORKSPACE}/venv/Lib/site-packages/novatel_edie.

These can be set within the cacheVariables field of a configure preset:

    "configurePresets": [
        {
            "name": "my-custom-preset",
            "cacheVariables": {
                "BUILD_TESTS": "ON",
                ...
            },
            ...
        }
    ]

Conan-Driven Configurations

Instead of manually creating a cmake preset to configure a build, the conan install command can be used to create a configuration based on a conan profile. This approach is particularly well-suited to cross-compilation builds.

Whenever Conan installs dependencies, it also creates a CMakePresets.json file within the build folder. To make these presets available, add the following to your CMakeUserPresets.json file:

    "include": [
        "ConanPresets.json"
    ]

The conanfile.py is set to place a reference presets file at this location.

Usage

EDIE provides different classes for interfacing with messages, each at a different level of the decoder stack. The main use-case of EDIE is to transform the data from one format into another. Documentation on the three NovAtel formats can be found here: ASCII, Abbreviated ASCII, and Binary. Flattened binary is a format unique to EDIE that standardizes the Binary format to have consistent-length arrays. So variable-length arrays are converted into fixed-size arrays. Flattened binary covers all formats (ASCII, abbreviated ASCII, and Binary).

User Task Purpose Format Example
Person Human-readable To make the data understandable to people ASCII/Abbreviated ASCII Finding the timestamp of a BESTPOS message
Programmer Program-accessible To make the data accessible from a programming language Flattened Binary Filtering a log file for BESTPOS messages
Computer Machine-readable To have efficient transmission and storage of the data Binary Efficient long-term storage of logs

Flowchart

Use the following flowchart to determine which EDIE class you need.

flowchart TD
    START(I have NovAtel data) --> STEP_ONE{I want to manipulate}
    STEP_ONE --> LOGS{Logs}
    STEP_ONE --> COMMANDS(Commands)
    LOGS -->|Read messages| DECODE(Decode)
    LOGS -->|Write messages| ENCODER[Encoder]:::cppclass
    COMMANDS -->|Write commands| ENCODE(Encode)
    DECODE -->|File| FILE_PARSER[FileParser]:::cppclass
    DECODE -->|Byte stream| PARSER[Parser]:::cppclass
    DECODE --> MORE_CONTROL(I need more control)
    MORE_CONTROL -->|I only want the<br/>type of message| FRAMER[Framer]:::cppclass
    MORE_CONTROL -->|I only want the<br/>JSON database| JSON_READER[JsonReader]:::cppclass
    MORE_CONTROL -->|I only want the<br/>message header| HEADER_DECODER[HeaderDecoder]:::cppclass
    MORE_CONTROL -->|I only want the<br/>message body| MESSAGE_DECODER[MessageDecoder]:::cppclass
    MORE_CONTROL -->|I only want messages<br/>with a specific ID, time,<br/>decimation, etc.| FILTER[Filter]:::cppclass
        MORE_CONTROL --> SPECIAL_LOGS{Special logs}
    SPECIAL_LOGS -->|RXConfig| RXCONFIG_HANDLER[RxConfigHandler]:::cppclass
    SPECIAL_LOGS -->|Rangecmp| RANGE_DECOMPRESSOR[RangeDecompressor]:::cppclass
    ENCODE -->|In Abbreviated ASCII| COMMANDER[Commander]:::cppclass

    subgraph Legend
        CLASS[Class]:::cppclass
        ACTION(Action)
    end

    classDef cppclass fill:teal
Loading

Notes

  • The MessageDecoder and Encoder class output and input the EDIE intermediate format and not ASCII/Binary.
  • The RXCONFIG and RANGECMP** messages are handled in different classes because of their unique traits.
  • The FileParser and Parser classes automatically filter and decode RXCONFIG and RANGECMP messages. So the RxConfigHandler and RangeDecompressor classes should only be used when bypassing the Parser class.
  • The Parser class can handle any byte stream such as a file stream, serial, Ethernet, or USB.

Examples

Examples are provided in the examples folder.

File Parser Format Conversion

This example shows how to convert a file from one format to another using the FileParser class.

Run the resulting executable with the following command: converter_file_parser.exe <path_to_json_db> <input_file> <output_format>

Parser Format Conversion

This example shows how to write bytes to the internal buffer of a Parser and have it convert those bytes to a specified format.

Run the resulting executable with the following command: converter_parser.exe <path_to_json_db> <input_file> <output_format>

Piecewise Conversion

This example shows how to use the individual components of the decoder stack in order to convert a file from one format to another. It demonstrates how to use the JsonReader, Framer, Filter HeaderDecoder, MessageDecoder, Encoder classes to achieve fine-grained control over the conversion process.

Run the resulting executable with the following command: converter_components.exe <path_to_json_db> <input_file> <output_format>

Adding Message Definitions

This example shows how to dynamically add message definitions to the JsonReader class.

Run the resulting executable with the following command: converter_parser.exe <path_to_json_db> <input_file> <output_format> <msg_def_json_string>

Decompressing Range Logs

This example shows how to decompress range logs. For example go from RANGECMP4 to RANGE.

Run the resulting executable with the following command: range_decompressor.exe <path_to_json_db> <input_file> <output_format>

Command Encoding

This example shows how to use the Commander class to encode a command from Abbreviated ASCII to an ASCII or Binary command. Note that all fields, even optional ones, must be provided in the abbreviated ASCII string command.

Run the resulting executable with the following command: command_encoding.exe <path_to_json_db> <output_format> <abbreviated_ascii_command>

Converting RXConfig Logs

This example shows how to use the RxConfigHandler class to convert RXConfig logs to another format. Converting RXConfig logs requires special treatment due to their outlying message protocol.

Run the resulting executable with the following command: rxconfig_handler.exe <path_to_json_db> <input_file> <output_format>

Accessing Message Fields

There are two approaches to accessing a message's fields after decoding. The following examples show how to access the "latitude" field of a BESTPOS message:

Approach 1: Search by Field Name

Search for the desired FieldContainer by name after decoding, then use std::get<T> to get the field's value, where T is the field's type.

✅ Advantages:

  • Works with any message format (ASCII, Abbreviated ASCII, Binary, etc.)
  • No code generation required
  • Some measure of type safety with std::get<T> (you will get a std::bad_variant_access exception if you input the wrong type)

⚠️ Disadvantages:

  • Computational overhead from linear search and string comparisons

Example:

std::vector<FieldContainer> stMessage;
double latitude;
eDecoderStatus = clMessageDecoder.Decode(pucFrameBuffer, stMessage, stMetaData);

if (eDecoderStatus == STATUS::SUCCESS) {
    if (stMetaData.usMessageId == 42 /*BESTPOS*/) {
        for (const auto& field : stMessage) {
            if (field.fieldDef->name == "latitude") {
                latitude = std::get<double>(field.fieldValue);
            }
        }
    }
}

If you prefer, std::find_if from the <algorithm> library can be used instead of the for loop as follows:

if (stMetaData.usMessageId == 42/*BESTPOS*/) {
    latitude = std::get<double>(std::find_if(stMessage.begin(), stMessage.end(), [](const FieldContainer& fc) { return fc.fieldDef->name == "latitude"; })->fieldValue);
}

Approach 2: Cast to Generated Struct

If your input data is in the flattened binary format then, after decoding a message's header, you may cast it to the appropriate struct and access its fields as member variables. Struct definitions are generated by running

python scripts/gen_flat_cpp_structs.py <input_database_file>

This will generate a header file called novatel_message_definitions.hpp in your current working directory.

✅ Advantages:

  • Direct access to fields via member variables (very fast)

⚠️ Disadvantages:

  • Only works with the flattened binary format
  • Requires running Python script to generate structs
  • Uses reinterpret_cast (undefined behaviour if you cast to the wrong type)

Example:

#include "novatel_message_definitions.hpp" // TODO: insert correct relative path here
...
IntermediateHeader stHeader;
double latitude;
eDecoderStatus = clHeaderDecoder.Decode(pucFrameBuffer, stHeader, stMetaData);

if (eDecoderStatus == STATUS::SUCCESS)
{
    // Remember to skip over the header before casting
    pucFrameBuffer += stMetaData.uiHeaderLength;

    if (stMetaData.usMessageId == 42/*BESTPOS*/) {
        auto bestposLog = reinterpret_cast<struct BESTPOS*>(pucFrameBuffer);
        latitude = bestposLog->latitude;
    }
}

Code Style

Clang-Format

We format our C++ code using Clang-Format as it provides a well-defined set of rules and conventions that make it easier for developers to collaborate on and understand a codebase. Additionally, adhering to this styling guide helps catch potential coding errors and reduces the likelihood of introducing bugs through inconsistent formatting.

If the code in a pull request does not match the style rules defined in our .clang-format file, the pipeline will fail and you will have to correct any discrepancies.

EditorConfig

Non-C++ files are more loosely formatted using EditorConfig. Failures in the EditorConfig pipeline stage will have more information than Clang-Format, so manual fixes are a bit easier.

Applying Formatting Settings

Windows

You can install llvm and clang-format via the builds page. However, Visual Studio has built-in clang-format support since 2017, as described here.

The shortcuts below can be used to apply format settings to individual files.

  • Visual Studio: Ctrl + K, Ctrl + D

  • VS Code: Shift + Alt + F

Linux

If you are using linux, you simply use the commands below to install clang-format and apply the settings.

sudo apt install clang-format

find src examples \( -name '*.cpp' -o -name '*.hpp' \) -exec clang-format -Werror --style=file -i {} +

Regression Testing

Regression testing is a naive form of testing in that it will fail for all functional changes, even if they are intentional. When you have intentional changes in an MR, you can simply update the files in regression/targets in your MR.

To see the regression differences, you can download the artifacts from the failed pipeline, copy the artifacts from the regression folder and diff them with the artifacts in the regression/targets folder. One way to do this is just copy-paste them in, and then look at the git diff.

API Stability

Currently, we do not guarantee API stability as development progresses. While we intend to minimize the frequency of changes that cause old things to break, please be aware that we retain the right to add, remove, or modify components.

Authors

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

The EDIE (Encode Decode Interface Engine) software development kit allows interfacing and decoding data output from NovAtel's OEM7 receivers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors