Skip to content

Support for Advanced Navigation ANPP format#861

Open
joe-saronic wants to merge 2 commits into
rtklibexplorer:mainfrom
joe-saronic:anpp
Open

Support for Advanced Navigation ANPP format#861
joe-saronic wants to merge 2 commits into
rtklibexplorer:mainfrom
joe-saronic:anpp

Conversation

@joe-saronic
Copy link
Copy Markdown

@joe-saronic joe-saronic commented May 19, 2026

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 opt field is searched for -RCVR{n}, where n must 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.

@joe-saronic
Copy link
Copy Markdown
Author

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.

Comment thread src/convrnx.c Outdated
Comment thread src/rcv/adnav.c
Comment thread src/rcv/adnav.c Outdated
Comment thread src/rcv/adnav.c

/* map ANPP (sys, freq_code) to rtklib observation code ---------------------*/
static uint8_t anpp2code(uint8_t anpp_sys, uint8_t freq_code)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I have an off-by-one here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/rcv/adnav.c Outdated
Comment thread src/rcv/adnav.c
return 0;
}
anpp->time.time = (time_t)U4(p + 4);
anpp->time.sec = U4(p + 8) * 1e-6;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

@joe-saronic joe-saronic Jun 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@joe-saronic joe-saronic force-pushed the anpp branch 2 times, most recently from 83d180b to e6fb96d Compare June 3, 2026 05:00
@joe-saronic
Copy link
Copy Markdown
Author

@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 rtklib-ffi rust crate I'm augmenting.

Comment thread src/rcv/adnav.c
case ANPP_SYS_GPS:
switch (freq_code) {
case 1: return CODE_L1C; /* L1 C/A */
case 2: return CODE_L1C; /* L1 C */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L1 C is likely rinex L1L (or L1S or L1X)

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this might really be L1M, for non-mil it is likely rinex L1W.

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess rinex L2W, except dup of L2M below?

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If their receiver is Trimble based then 5X otherwise guess L5Q.

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess L3X for Trimble otherwise L3Q.

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also likely rinex L2W.

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimble L2X otherwise guess L2L (or L2S).

Comment thread src/rcv/adnav.c
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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimble L6X otherwise L6L.

Comment thread src/rcv/adnav.c
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 ?? */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimble L5X otherwise L5Q.

Comment thread src/rcv/adnav.c
return 0;
}
anpp->time.time = (time_t)U4(p + 4);
anpp->time.sec = U4(p + 8) * 1e-6;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/rcv/adnav.c
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ourairquality
Copy link
Copy Markdown

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants