Support for Advanced Navigation ANPP format#861
Conversation
020b239 to
b4f5fd4
Compare
|
Hi @rtklibexplorer. This is my first PR to RTKLIB. I'm curious if there is any procedure you have for reviewing/merging, and what standard of testing you expect for incoming PRs. |
|
|
||
| /* map ANPP (sys, freq_code) to rtklib observation code ---------------------*/ | ||
| static uint8_t anpp2code(uint8_t anpp_sys, uint8_t freq_code) | ||
| { |
There was a problem hiding this comment.
Perhaps add all the documented signals. For BDS it appears to be just B1 B2 B3 and might the following not be the most relevant reference. Would they limit Trimble receiver processing to just these signals, and BDS2 has been largely retired now and there is no reference to the B1C / B2A signals. For QZSS there is no definition for L1C/B for J04 and J08. This reference looks a little stale.
https://docs.advancednavigation.com/boreas-d/ANPP/RawSatelliteDataPacket.htm#Raw_Satellite_Data_Packet
There was a problem hiding this comment.
Will add. From the bottom of that page: © 2026 Advanced Navigation. All Rights Reserved. Version:2.5.1 Date: 2026-03-24. I know for a fact that it contains the latest version of the messages for 60 and 61 because the firmware including them was recently updated. That being said, I don't discount the possibility that a more recent source is available, so will look around.
There was a problem hiding this comment.
Also, I have an off-by-one here
There was a problem hiding this comment.
I've cut the Gordian knot here and reached out directly to the source (Advanced Navigation). Hoping this will get resolved soon, since I frankly don't know enough to get these mappings absolutely right.
| return 0; | ||
| } | ||
| anpp->time.time = (time_t)U4(p + 4); | ||
| anpp->time.sec = U4(p + 8) * 1e-6; |
There was a problem hiding this comment.
Could further abstract these raw packet accesses adding a bounds check if you wanted an extra layer of defence in depth. See for example my branch and the septentrio decoder where it defines e.g. the following, and I have the same for some other decoders in other private branches used for testing.
static uint32_t U4(const raw_t *raw, size_t index) {
RTKBOUNDSCHECK(raw->buff, sizeof(raw->buff), index + 3);
RTKBOUNDSCHECK(raw->buff, raw->len, index + 3);
uint32_t u;
memcpy(&u, raw->buff + index, 4);
return u;
}
then this becomes the following and you have a little extra peace of mind, and avoiding pointer arithmetic makes the code easier to translate to a higher level language with bounds checked array access.
anpp->time.time = (time_t)U4(raw, 5 + 4);
anpp->time.sec = U4(raw, 5 + 8) * 1e-6;
There was a problem hiding this comment.
For most of these, the bounds check is overkill since they are fixed known-good offsets. Out of curiosity though, which branch are you referring to. I did not see it among the merged or unmerged ones I spot-checked.
There was a problem hiding this comment.
Fwiw personally would also like to see it move away from pointer arithmetic. These functions also check against the raw->len and not just the buffer size. https://github.com/ourairquality/RTKLIB/blob/oaq/src/rcv/septentrio.c#L447
There was a problem hiding this comment.
@ourairquality I adapted your work into this branch. I like the idea, but I'm pretty sure you have an off-by-one error by checking > instead of >= when comparing sizes since you are not going past the end of the element of interest. I've solved this by passing the index and element size separately, which keeps in the spirit of doing less pointer math explicitly rather well I think.
There was a problem hiding this comment.
I've isolated the bounds changes to the second commit.
In other news, Advanced Navigation has gotten back to me saying that their engineers would take a look and help find the correct mapping between their enums and the RTKLIB ones.
83d180b to
e6fb96d
Compare
|
@ourairquality I've addressed as much of your feedback as I could for the moment. Waiting for feedback from Advanced Navigation. In the meantime, I've continued testing this with the |
| case ANPP_SYS_GPS: | ||
| switch (freq_code) { | ||
| case 1: return CODE_L1C; /* L1 C/A */ | ||
| case 2: return CODE_L1C; /* L1 C */ |
There was a problem hiding this comment.
L1 C is likely rinex L1L (or L1S or L1X)
| case 1: return CODE_L1C; /* L1 C/A */ | ||
| case 2: return CODE_L1C; /* L1 C */ | ||
| case 3: return CODE_L1P; /* L1 P */ | ||
| case 4: return CODE_L1M; /* L1 M */ |
There was a problem hiding this comment.
While this might really be L1M, for non-mil it is likely rinex L1W.
| case 3: return CODE_L1P; /* L1 P */ | ||
| case 4: return CODE_L1M; /* L1 M */ | ||
| case 5: return CODE_L2C; /* L2 C */ | ||
| case 6: return CODE_L2P; /* L2 P */ |
There was a problem hiding this comment.
Guess rinex L2W, except dup of L2M below?
| case 5: return CODE_L2C; /* L2 C */ | ||
| case 6: return CODE_L2P; /* L2 P */ | ||
| case 7: return CODE_L2M; /* L2 M */ | ||
| case 8: return CODE_L5X; /* L5 */ |
There was a problem hiding this comment.
If their receiver is Trimble based then 5X otherwise guess L5Q.
| case 3: return CODE_L1P; /* G1 P */ | ||
| case 5: return CODE_L2C; /* G2 C/A */ | ||
| case 6: return CODE_L2P; /* G2 P */ | ||
| case 8: return CODE_L3X; /* G3 */ |
There was a problem hiding this comment.
Guess L3X for Trimble otherwise L3Q.
| case 4: return CODE_L1M; /* L1 M */ | ||
| case 5: return CODE_L2C; /* L2 C */ | ||
| case 6: return CODE_L2P; /* L2 P */ | ||
| case 7: return CODE_L2M; /* L2 M */ |
| case 1: return CODE_L1C; /* L1 C/A */ | ||
| case 2: return CODE_L1C; /* L2 C */ | ||
| case 3: return CODE_L1Z; /* L1 SAIF */ | ||
| case 5: return CODE_L2X; /* L2 C */ |
There was a problem hiding this comment.
Trimble L2X otherwise guess L2L (or L2S).
| case 2: return CODE_L1C; /* L2 C */ | ||
| case 3: return CODE_L1Z; /* L1 SAIF */ | ||
| case 5: return CODE_L2X; /* L2 C */ | ||
| case 6: return CODE_L6X; /* LEX */ |
| case 3: return CODE_L1Z; /* L1 SAIF */ | ||
| case 5: return CODE_L2X; /* L2 C */ | ||
| case 6: return CODE_L6X; /* LEX */ | ||
| case 8: return CODE_L5Z; /* L5 ?? */ |
| return 0; | ||
| } | ||
| anpp->time.time = (time_t)U4(p + 4); | ||
| anpp->time.sec = U4(p + 8) * 1e-6; |
There was a problem hiding this comment.
Fwiw personally would also like to see it move away from pointer arithmetic. These functions also check against the raw->len and not just the buffer size. https://github.com/ourairquality/RTKLIB/blob/oaq/src/rcv/septentrio.c#L447
| static float R4(uint8_t *p) {float r; memcpy(&r,p,4); return r;} | ||
| static double R8(uint8_t *p) {double r; memcpy(&r,p,8); return r;} | ||
| static uint8_t U1(const raw_t *raw, size_t index) { | ||
| RTKBOUNDSCHECK(raw->buff, sizeof(raw->buff), index, 1); |
There was a problem hiding this comment.
Thank you, well spotted on the bound check 'off by one' so yes that should be index >= size, and could you please consider adopting that and have the caller pass in the index of the last byte rather than the element size.
Would like to keep the bounds check a primitive as simple as possible and the element size is redundant, the caller can simply add that to the index less one in these functions. Where this is a case that includes unaligned multi-byte accesses into a byte sized array.
This same bound check function is intended to also be used for aligned access into an array in which case it would use the size and index in elements rather than bytes, so the element size is still not needed and to have it would be confusing, the element size passed in would then be 0.
This is not the pointer arithmetic style that I would personally like to avoid, the base of the array and the size and index are passed to the callee and array access is still relative to the array base. The pointer arithmetic code style that I would personally like to avoid is passing a pointer into a array as a new array base - something that does not translate well to higher level languages although some languages do support this. There is no issue passing in an index into the middle of an array if the callee is also passed the base and size - that should translate well to higher level languages where an array object and an integer index would be passed in.
In any case, with the rest of the code abstract the details of the bound check implementation can be easily change and the code style is already easier to translate to a higher level language and the bounds is checked, and it's good to have some eyes looking over the code, I make endless mistakes, thank you.
Btw if the bounds checking becomes a performance issue then the test could be inlined, added to the macro in rtklib.h, with only the reporting handled out of line.
|
Imported the patch into my branch and updated as to personal preference, the raw buffer access with bounds checking, avoiding the pointer arithmetic, the signals mapping guess, also noted a few other integration issues, and reworked a little. Are there anpp files that can be shared for testing? |
Format defined here: https://docs.advancednavigation.com/boreas-d/ANPP/Advanced%20Navigation%20Packet.htm
This implementation only handles the raw satellite measurements, not the entire protocol.
Receiver selection is done in the same way as for SBF format: the
optfield is searched for-RCVR{n}, wherenmust be an integer in the range [0-255] since ANPP theoretically supports that many antennae per unit.Testing
Compared raw output from Boreas D-90 parsed with https://github.com/saronic-technologies/liban-rs/ (see saronic-technologies/liban-rs#18) to result of parsing with rust bindings to this library in https://github.com/kpwebb/rtklib-ffi/ (see joe-saronic/rtklib-ffi#1). Verified that obs fragments are handled correctly.
Inspected output of convbin from two receivers in a well-known dataset to ensure correct numbers of observations, constellations, etc. Main dataset can not be provided, but a small edited sample is available in the integration tests of the rust wrapper.