Fixposition SDK 0.0.0-heads/main-0-g5c7edb5
Collection of c++ libraries and apps for use with Fixposition products on Linux
Loading...
Searching...
No Matches
Parsing and decoding FP_A messages
1/**
2 * \verbatim
3 * ___ ___
4 * \ \ / /
5 * \ \/ / Copyright (c) Fixposition AG (www.fixposition.com) and contributors
6 * / /\ \ License: see the LICENSE file
7 * /__/ \__\
8 * \endverbatim
9 *
10 * @file
11 * @brief Fixposition SDK examples: introduction to the parser and FP_A message decoding
12 *
13 * To build and run:
14 *
15 * make
16 * ./build/parser_intro
17 *
18 * This program demonstrates how to use the fpsdk::common::parser functions to parse FP_A protocol
19 * messages from data received from the Vision-RTK 2 sensor as well as how to decode those messages
20 * for futher processing.
21 *
22 * This file is both the source code of this example as well as the documentation on how it works.
23 */
24
25/* LIBC/STL */
26#include <cinttypes>
27#include <cstdint>
28#include <cstdlib>
29
30/* EXTERNAL */
31
32/* Fixposition SDK */
33#include <fpsdk_common/app.hpp>
35#include <fpsdk_common/math.hpp>
38#include <fpsdk_common/time.hpp>
40
41/* PACKAGE */
42
43/* ****************************************************************************************************************** */
44
45using namespace fpsdk::common::time;
46using namespace fpsdk::common::app;
47using namespace fpsdk::common::parser;
48using namespace fpsdk::common::parser::fpa;
49using namespace fpsdk::common::logging;
50using namespace fpsdk::common::trafo;
51using namespace fpsdk::common::math;
52
53// ---------------------------------------------------------------------------------------------------------------------
54
55// This is an example of data received from the Vision-RTK 2 sensor. Note that the data is a long sequence of bytes.
56// Even though it looks like strings, do not treat data received from the sensor as strings. Always treat it as a
57// sequence of bytes that may have any value, including the string nul terminator (0x00) character. For readability
58// the data here is formatted as one FP_A message per line. Do not assume this is how a program would receive the data
59// from the sensor in a real application. Depending on the connection and implementation the app receives the data
60// in chunks, perhaps as small as one byte at a time. Or there may be errors (some detailed below) in transmissions.
61// To extract complete message frames from such a stream of data we can use the *parser*.
62// clang-format off
63const uint8_t SAMPLE_DATA_1[] = {
64 // The data starts with what looks like an incomplete FP_A-ODOMETRY...
65 /* (1) */ "2650,0.00283,-0.00305,-0.02449,0.00108,0.00054,0.00057,-0.00009,-0.00002,0.00024,"
66 "fp_vrtk2-integ_6912e460-1703*3C\r\n"
67 // Then it seems we're receiving correct FP_A messages as expected...
68 /* (2) */ "$FP,ODOMSTATUS,1,2348,574451.500000,2,2,1,1,1,1,,0,,,,,,0,1,3,8,8,3,5,5,,0,6,,,,,,,,,,,,*2D\r\n"
69 /* (3) */ "$FP,EOE,1,2348,574451.500000,FUSION*6C\r\n"
70 // ..and more messages...
71 /* (4) */ "$FP,ODOMETRY,2,2348,574452.000000,4278387.7000,635620.5134,4672339.9355,-0.603913,0.313038,-0.179283,"
72 "0.710741,0.0003,-0.0009,0.0012,-0.00110,0.00128,0.00026,0.2101,0.1256,9.8099,4,0,8,8,-1,0.00073,"
73 "0.00294,0.00107,0.00141,-0.00170,-0.00083,0.02271,0.00042,0.02650,0.00283,-0.00305,-0.02448,0.00101,"
74 "0.00053,0.00056,-0.00008,-0.00002,0.00021,fp_vrtk2-integ_6912e460-1703*17\r\n"
75 /* (5) */ "$FP,ODOMSTATUS,1,2348,574452.000000,2,2,1,1,1,1,,0,,,,,,0,1,3,8,8,3,5,5,,0,6,,,,,,,,,,,,*2B\r\n"
76 /* (6) */ "$FP,EOE,1,2348,574452.000000,FUSION*6A\r\n"
77 // ..and more messages...
78 /* (7) */ "$FP,ODOMETRY,2,2348,574452.500000,4278387.6984,635620.5126,4672339.9366,0.313004,-0.179369,0.710598,"
79 "0.0002,-0.0008,0.0009,0.00060,-0.00117,0.00038,0.2129,0.1266,9.8185,4,0,8,8,-1,0.00073,0.00294,0.00107,"
80 "0.00141,-0.00169,-0.00083,0.02270,0.00042,0.02650,0.00283,-0.00305,-0.02448,0.00098,0.00053,0.00056,"
81 "-0.00008,-0.00002,0.00019,fp_vrtk2-integ_6912e460-1703*01\r\n"
82 /* (8) */ "$FP,ODOMSTATUS,1,2348,574452.500000,2,2,1,1,1,1,,0,,,,,,0,1,3,8,8,3,5,5,,0,6,,,,,,,,,,,,*2E\r\n"
83 /* (9) */ "$FP,EOE,1,2348,574452.500000,FUSION*6F\r\n"
84 // Hmmm... there seems to be some kind of errors in the reception of the data. Perhaps a bad cable or a shaky connector?
85 /* (10) */ "$FP,ODOM3TRY,2,2348,574453.000000,4278387.6988,6\xaa\x55\xaa\x55.604077,0.312982\xba\xad\xc0\xff\xee.71"
86 "0587,0.0010,-0.0005,0.0018,0.00368,-0.00044,0.00092,0.2163,0.1214,9.8162,4,0,8,8,-1,0.00073,0.00294,"
87 "0.00107,0.00141,-0.00170,-0.00083,0.02269,0.00042,0.02650,0.00283,-0.00305,-0.02448,0.00100,0.00053,"
88 "0.00056,-0.00008,-0.00002,0.00021,fp_vrtk2-integ_6912e460-1703*12\r\n"
89 // Ok, back to normal it seems..
90 /* (11) */ "$FP,ODOMSTATUS,1,2348,574453.000000,2,2,1,1,1,1,,0,,,,,,0,1,3,8,8,3,5,5,,0,6,,,,,,,,,,,,*2A\r\n"
91 /* (12) */ "$FP,EOE,1,2348,574453.000000,FUSION*6B\r\n"
92 // The data ends in an incomplete FP_A-ODOMETRY message...
93 /* (13) */ "$FP,ODOMETRY,2,2348,579519.500000,4278387.7342,635620.5918,4672339.8925,-0.700505,0.286"
94};
95const std::size_t SAMPLE_SIZE_1 = sizeof(SAMPLE_DATA_1) - 1; // -1 because "" adds \0
96// A second chunk of data, which is the continuation from the above
97const uint8_t SAMPLE_DATA_2[] = {
98 /* (13) */"891,-0.222201,0.614502,0.0002,0.0000,-0.0002,-0.00071,-0.00146,-0.00001,0.2974,0.0215,9.8060,4,0,8,8,-1,"
99 "0.00025,0.00493,0.00060,0.00099,-0.00161,-0.00033,0.02743,0.00044,0.03268,0.00316,-0.00342,-0.02989,"
100 "0.00094,0.00066,0.00049,-0.00023,-0.00003,0.00007,fp_vrtk2-integ_6912e460-1703*18\r\n"
101 /* (14) */"$FP,ODOMSTATUS,1,2348,579519.500000,2,2,1,1,1,1,,0,,,,,,0,1,3,8,8,3,5,5,,0,6,,,,,,,,,,,,*2D\r\n"
102 /* (15) */"$FP,EOE,1,2348,579519.500000,FUSION*6C\r\n"
103 /* (16) */"$FP,ODOMETRY,2,2348,579520.000000,,,,-0.700563,0.286828,-0.222129,0.614491,0.0013,-0.0012,0.0004,"
104 "-0.00513,-0.00055,-0.00092,0.2984,0.0225,9.8047,4,0,8,8,-1,0.00025,0.00493,0.00060,0.00099,-0.00161,"
105 "-0.00033,0.02743,0.00044,0.03268,0.00316,-0.00342,-0.02989,0.00094,0.00066,0.00048,-0.00024,-0.00003,"
106 "0.00007,fp_vrtk2-integ_6912e460-1703*35\r\n"
107 /* (17) */"$FP,ODOMSTATUS,1,2348,579520.00000"
108};
109const std::size_t SAMPLE_SIZE_2 = sizeof(SAMPLE_DATA_2) - 1; // -1 because "" adds \0
110// clang-format on
111
112int main(int /*argc*/, char** /*argv*/)
113{
114#ifndef NDEBUG
116#endif
117 LoggingSetParams({ LoggingLevel::TRACE });
118
119 // -----------------------------------------------------------------------------------------------------------------
120 // Experiment 1
121 // -----------------------------------------------------------------------------------------------------------------
122 NOTICE("----- Parsing the data into messages -----");
123
124 // The first step is to *parse* the data. This process splits the data into message frames. Note that "parsing" is
125 // not the same as "decoding" the messages. "Parsing" (as per Fixposition SDK definition) refers to splitting a
126 // stream of data into individual messages (als known as "frames"). The Parser recgonises a variety of protocols and
127 // it can reliably split data into messages of all the protocols it understands. It can do so in streams of mixed
128 // data, such as FP_A, NMEA and NOV_B messages in the same stream.
129
130 // Create a parser instance. Note that one parser should be used per stream of data. If multiple streams (inputs)
131 // are handled, they should all be processed by a separate Parser instance.
132 Parser parser;
133
134 // Feed data to the parser. We'll have to check that we don't overwhelm the parser by feeding too much data.
135 // The details for this are explained in the fpsdk::common::parser documentation in fpsdk_common/parser.hpp.
136 INFO("Adding SAMPLE_DATA_1 (%" PRIuMAX " bytes)", SAMPLE_SIZE_1);
137 if (!parser.Add(SAMPLE_DATA_1, SAMPLE_SIZE_1)) {
138 WARNING("Parser overflow, SAMPLE_DATA_1 is too large!");
139 }
140
141 // Now we can process the data in the parser and we get back the data we fed in above message by message. On
142 // success, the ParserMsg object is populated with the part of the input data that corresponds to a message along
143 // with some meta data, such as a message name. The naming of the messages is explained in the fpsdk::common::parser
144 // documentation.
145 ParserMsg msg;
146 while (parser.Process(msg)) {
147 INFO("Message %-20s (size %" PRIuMAX " bytes)", msg.name_.c_str(), msg.data_.size());
148 // DEBUG_HEXDUMP(msg.data_.data(), msg.data_.size(), NULL, NULL); // Hexdump of the raw message data
149 }
150 // This should print the following output:
151 //
152 // Adding SAMPLE_DATA_1 (1810 bytes)
153 // Message OTHER (size 114 bytes) (1)
154 // Message FP_A-ODOMSTATUS (size 93 bytes) (2)
155 // Message FP_A-EOE (size 40 bytes) (3)
156 // Message FP_A-ODOMETRY (size 372 bytes) (4)
157 // Message FP_A-ODOMSTATUS (size 93 bytes) (5)
158 // Message FP_A-EOE (size 40 bytes) (6)
159 // Message FP_A-ODOMETRY (size 362 bytes) (7)
160 // Message FP_A-ODOMSTATUS (size 93 bytes) (8)
161 // Message FP_A-EOE (size 40 bytes) (9)
162 // Message OTHER (size 256 bytes) (10)
163 // Message OTHER (size 87 bytes) (10)
164 // Message FP_A-ODOMSTATUS (size 93 bytes) (11)
165 // Message FP_A-EOE (size 40 bytes) (12)
166 //
167 // We can observe the following parser behaviour:
168 //
169 // - A total of 1723 bytes of the 1810 input bytes are output in 13 messages (1) - (13)
170 // - Message (1) is of the "fake" type Protocol::OTHER, which means it is data that doesn't match any of the known
171 // protocols. Even though it is a partial FP_A message, the parser cannot recognise this as the frame is
172 // incomplete and therefore does not pass the "looks like an FP_A message" check.
173 // - Similarly, the corrupt message (13) is output as OTHER messages. Since a OTHER message is limited to 256 bytes,
174 // we see two such messages being emitted from the parser.
175 // - Message (14) is missing. This is expected as it is the beginning of a FP_A message and the parser cannot yet
176 // determine that is _not_ such a message. For this decision it would need more data.
177 // - That is, 87 byte (1810 bytes input minus 1723 bytes output) are "stuck" in the parser.
178
179 // Let's add more data, namely the rest of the partial FP_A-ODOMETRY now "stuck" in the parser, and process more
180 INFO("Adding SAMPLE_DATA_2 (%" PRIuMAX " bytes)", SAMPLE_SIZE_2);
181 if (!parser.Add(SAMPLE_DATA_2, SAMPLE_SIZE_2)) {
182 WARNING("Parser overflow, SAMPLE_DATA_2 is too large!");
183 }
184 while (parser.Process(msg)) {
185 INFO("Message %-20s (size %" PRIuMAX " bytes)", msg.name_.c_str(), msg.data_.size());
186 // DEBUG_HEXDUMP(msg.data_.data(), msg.data_.size(), NULL, NULL); // Hexdump of the raw message data
187 }
188 // This should print the following output:
189 //
190 // Adding SAMPLE_DATA_2 (793 bytes)
191 // Message FP_A-ODOMETRY (size 374 bytes) (13)
192 // Message FP_A-ODOMSTATUS (size 93 bytes) (14)
193 // Message FP_A-EOE (size 40 bytes) (15)
194 // Message FP_A-ODOMETRY (size 339 bytes) (16)
195 //
196 // We can observe the following parser behaviour:
197 //
198 // - A total of 846 byte are output in messages. Note that at the time we added the second chunk of data (793 bytes)
199 // there were still 87 bytes from the first chunk left. So in total the parser had 880 bytes of data.
200 // - The parser now had enough data to output message (13)
201 // - Also there was data for messages (14) - (16)
202 // - There must be 34 (880 - 846) bytes "stuck" in the parser now
203
204 // The parser has a "flush" mode that forces "stuck" bytes to be output even though they look like the beginning of
205 // a valid message:
206 INFO("Flushing parser");
207 while (parser.Flush(msg)) {
208 INFO("Message %-20s (size %" PRIuMAX " bytes)", msg.name_.c_str(), msg.data_.size());
209 DEBUG_HEXDUMP(msg.data_.data(), msg.data_.size(), NULL, NULL);
210 }
211 // This should print:
212 //
213 // Message OTHER (size 34 bytes) (17)
214 // 0x0000 00000 24 46 50 2c 4f 44 4f 4d 53 54 41 54 55 53 2c 31 |$FP,ODOMSTATUS,1|
215 // 0x0010 00016 2c 32 33 34 38 2c 35 37 39 35 32 30 2e 30 30 30 |,2348,579520.000|
216 // 0x0020 00032 30 30 |00 |
217 //
218 // And indeed it returned the 34 bytes that were stuck in the parser and indeed they are the incomplete
219 // FP_A-ODOMSTATUS message at the end of the second junk.
220
221 // -----------------------------------------------------------------------------------------------------------------
222 // Experiment 2
223 // -----------------------------------------------------------------------------------------------------------------
224 NOTICE("----- Decoding the data in the messages -----");
225
226 // We have now seen how to *parse* the messages. Let's have a closer look at how to *decode* the payload (contents)
227 // of some of the messages.
228
229 // Reset the parser to assert it's empty and back to the initial state. Then, add all data we have.
230 INFO("Adding SAMPLE_DATA_1+SAMPLE_DATA_2 (%" PRIuMAX " bytes)", SAMPLE_SIZE_1 + SAMPLE_SIZE_2);
231 parser.Reset();
232 if (!parser.Add(SAMPLE_DATA_1, SAMPLE_SIZE_1) || !parser.Add(SAMPLE_DATA_2, SAMPLE_SIZE_2)) {
233 WARNING("Parser overflow, SAMPLE_DATA_1+SAMPLE_DATA_2 is too large!");
234 }
235
236 // Loop though all messages and have a closer look at FP_A-ODOMETRY
237 while (parser.Process(msg)) {
238 // Is it a FP_A-ODOMETRY?
239 if (msg.name_ == FpaOdometryPayload::MSG_NAME) {
240 INFO("Message %-20s (size %" PRIuMAX " bytes)", msg.name_.c_str(), msg.data_.size());
241 // We can now try to decode it
242 FpaOdometryPayload payload;
243 if (payload.SetFromMsg(msg.data_.data(), msg.data_.size())) {
244 INFO("Decode OK");
245 }
246 // Decoding failed
247 else {
248 INFO("Decode fail");
249 }
250 }
251 }
252 // We should get:
253 //
254 // Adding SAMPLE_DATA_1+SAMPLE_DATA_2 (2603 bytes)
255 // Message FP_A-ODOMETRY (size 372 bytes) (4)
256 // Message OK
257 // Message FP_A-ODOMETRY (size 362 bytes) (7)
258 // Message decode fail <---- !!!
259 // Message FP_A-ODOMETRY (size 374 bytes) (13)
260 // Message OK
261 // Message FP_A-ODOMETRY (size 339 bytes) (16)
262 // Message OK
263 //
264 // We again see all four FP_A-ODOMETRY messges. However, the second messge (7) failed to decode. If we have a close
265 // look at SAMPLE_DATA_1 we can see that even though it is a valid FP_A message frame (the checksum is correct) and
266 // the message type ("ODOMETRY") is present, the number of fields does not match the specification for FP_A-ODOMETRY
267 // and therefore FpaOdometryPayload::SetFromMsg() complains. This method does various checks to make sure the data
268 // it decodes matches the corresponding message specification.
269
270 // -----------------------------------------------------------------------------------------------------------------
271 // Experiment 3
272 // -----------------------------------------------------------------------------------------------------------------
273 NOTICE("----- Using decoded data in the messages -----");
274
275 // We repeat the parsing and decoding and now try to *use* some of the data.
276
277 // Reset the parser to assert it's empty and back to the initial state. Then, add all data we have.
278 INFO("Adding SAMPLE_DATA_1+SAMPLE_DATA_2 (%" PRIuMAX " bytes)", SAMPLE_SIZE_1 + SAMPLE_SIZE_2);
279 parser.Reset();
280 if (!parser.Add(SAMPLE_DATA_1, SAMPLE_SIZE_1) || !parser.Add(SAMPLE_DATA_2, SAMPLE_SIZE_2)) {
281 WARNING("Parser overflow, SAMPLE_DATA_1+SAMPLE_DATA_2 is too large!");
282 }
283
284 // Loop though all messages and have a closer look at some of the data in the valid FP_A-ODOMETRY messages
285 while (parser.Process(msg)) {
286 // Ignore messages other than FP_A-ODOMETRY
287 if (msg.name_ != FpaOdometryPayload::MSG_NAME) {
288 continue;
289 }
290 // Ignore invalid FP_A-ODOMETRY messages
291 FpaOdometryPayload payload;
292 if (!payload.SetFromMsg(msg.data_.data(), msg.data_.size())) {
293 continue;
294 }
295
296 INFO("Message %-20s (size %" PRIuMAX " bytes)", msg.name_.c_str(), msg.data_.size());
297
298 // Some the message fields are optional and in principle any field can be a "null" (empty) field. It is highly
299 // recommended to *always* check if the desired fields are valid.
300
301 // For example we can check if the time is available. It it is, we can convert the GPS time to UTC for the
302 // debugging purposes here. (Note that you should probably not use UTC time for anything other than to display
303 // time to humans.)
304
305 // GPS week number and time of week can be indpendently valid or invalid
306 if (payload.gps_time.week.valid && payload.gps_time.tow.valid) {
307 Time time;
308 // We should also handle the week number and/or time of week values not being in range
309 if (time.SetWnoTow({ payload.gps_time.week.value, payload.gps_time.tow.value, WnoTow::Sys::GPS })) {
310 INFO("GPS time %04d:%010.3f (%s)", payload.gps_time.week.value, payload.gps_time.tow.value,
311 time.StrUtcTime().c_str());
312 } else {
313 INFO("GPS time %04d:%010.3f is bad", payload.gps_time.week.value, payload.gps_time.tow.value);
314 }
315 } else {
316 INFO("GPS time not available");
317 }
318
319 // The position data can should be checked for availability, too
320 if (payload.pos.valid) {
321 INFO("Position: [ %.1f, %.1f, %.1f ]", payload.pos.values[0], payload.pos.values[1],
322 payload.pos.values[2]);
323 } else {
324 INFO("Position not available");
325 }
326
327 // We can transform the position to latititude, longitude and height (and lat/lon to degrees). Note that
328 // TfWgs84LlhEcef() should not be used for anything other than debugging. Instead, an appropriately configured
329 // fpsdk::common::trafo::Transformer instance should be used to transform from the input coordinate reference
330 // system (determined by the correction data service used with the sensor) to the desired output coordinate
331 // reference system (which may or may not be WGS-84). See the fusion_epoch example.
332 if (payload.pos.valid) {
333 const auto llh = TfWgs84LlhEcef({ payload.pos.values[0], payload.pos.values[1], payload.pos.values[2] });
334 INFO("LLH: %.6f %.6f %.1f", RadToDeg(llh(0)), RadToDeg(llh(1)), llh(2));
335 }
336 }
337 // This should output:
338 // Adding SAMPLE_DATA_1+SAMPLE_DATA_2 (2603 bytes)
339 // Message FP_A-ODOMETRY (size 372 bytes) (4)
340 // GPS time 2348:574452.000 (2025-01-11 15:33:54.000)
341 // Position: [ 4278387.7, 635620.5, 4672339.9 ]
342 // LLH: 47.400296 8.450363 459.5
343 // Message FP_A-ODOMETRY (size 374 bytes) (13)
344 // GPS time 2348:579519.500 (2025-01-11 16:58:21.500)
345 // Position: [ 4278387.7, 635620.6, 4672339.9 ]
346 // LLH: 47.400296 8.450364 459.5
347 // Message FP_A-ODOMETRY (size 339 bytes) (16)
348 // GPS time 2348:579520.000: 2025-01-11 16:58:22.000
349 // Position not available
350
351 return EXIT_SUCCESS;
352}
353
354/* ****************************************************************************************************************** */
Fixposition SDK: Utilities for apps.
Helper to print a strack trace on SIGSEGV and SIGABRT.
Definition app.hpp:139
Message parser class.
Definition parser.hpp:165
bool Flush(ParserMsg &msg)
Get remaining data from parser as OTHER message(s)
void Reset()
Reset parser.
bool Process(ParserMsg &msg)
Process data in parser, return message.
bool Add(const uint8_t *data, const std::size_t size)
Add data to parser.
std::string StrUtcTime(const int prec=3) const
Stringify as year, month, day, hour, minute and second time (UTC)
bool SetWnoTow(const WnoTow &wnotow)
Set time from GNSS (GPS, Galileo, BeiDou) time (atomic)
Fixposition SDK: Parser FP_A routines and types.
Fixposition SDK: Logging.
#define NOTICE(...)
Print a notice message.
Definition logging.hpp:85
#define WARNING(...)
Print a warning message.
Definition logging.hpp:81
#define DEBUG_HEXDUMP(data, size, prefix,...)
Print a debug hexdump.
Definition logging.hpp:97
#define INFO(...)
Print a info message.
Definition logging.hpp:89
Fixposition SDK: Math utilities.
Utilities for apps.
Definition app.hpp:34
Math utilities.
Definition math.hpp:35
constexpr T RadToDeg(T radians)
Convert radians to degrees.
Definition math.hpp:61
Parser FP_A routines and types.
Definition fpa.hpp:96
Time utilities.
Definition time.hpp:39
Transformation utilities.
Definition trafo.hpp:37
Eigen::Vector3d TfWgs84LlhEcef(const Eigen::Vector3d &ecef)
Convert ECEF (x, y, z) coordinates to geodetic coordinates (latitude, longitude, height)
Fixposition SDK: Parser.
Message frame output by the Parser.
Definition types.hpp:96
std::string name_
Name of the message.
Definition types.hpp:100
std::vector< uint8_t > data_
Message data.
Definition types.hpp:99
std::array< double, 3 > values
Values.
Definition fpa.hpp:497
FpaInt week
GPS week number, range 0-9999, or null if time not (yet) available.
Definition fpa.hpp:523
FpaFloat tow
GPS time of week, range 0.000-604799.999999, or null if time not (yet) available.
Definition fpa.hpp:524
bool valid
Data is valid.
Definition fpa.hpp:478
FpaFloat3 pos
Position, X/Y/Z components.
Definition fpa.hpp:749
FP_A-ODOMETRY (version 2) messages payload (ECEF)
Definition fpa.hpp:781
bool SetFromMsg(const uint8_t *msg, const std::size_t msg_size) final
Set data from message.
Fixposition SDK: Time utilities.
Fixposition SDK: Transformation utilities.