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.
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.
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.
- C++ 17 compiler
- Windows
- MSVC (recommended): Download and install both the Build Tools for Visual Studio 2026 and the Microsoft Visual C++ v14 Redistributable.
- g++: Install as part of the MSYS2 tool collection.
- Linux
- Install the toolchains for g++ or clang using your distribution's package manager.
- Windows
- CMake (>=3.15)
- Installers can be found on the downloads page. For Linux it is recommended to use your distribution's package manager instead.
- Conan (>=2.4.0)
- Follow the installation instructions from the Conan 2 documentation.
- nanobind (>=2.0.0)
- Only required if building the
novatel_ediepython module. - Install using pip with
pip install nanobind.
- Only required if building the
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}
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.
The following is a list of CMake cache variables specific to this project.
BUILD_TESTS: Whether to build the test executables. Defaults toON.BUILD_EXAMPLES: Whether to build example programs that make basic use of EDIE's features. Defaults toON.BUILD_BENCHMARKS: Whether to build the benchmarking programs. Defaults toOFF.BUILD_PYTHON: Whether to build thenovatel_ediepython module. Defaults toOFF.Python_EXECUTABLE: The python interpreter executable to use as a source for the nanobind library which thenovatel_ediepython module is built upon.PYTHON_INSTALL_DIR: The location to install thenovatel_ediepython 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",
...
},
...
}
]
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.
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 |
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
- 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 are provided in the examples folder.
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>
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>
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>
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>
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>
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>
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>
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:
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 astd::bad_variant_accessexception if you input the wrong type)
- 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);
}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)
- 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;
}
}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.
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.
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
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 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.
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.
This project is licensed under the MIT License - see the LICENSE file for details.
