diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml
index cc9ee916..c523db87 100644
--- a/.github/workflows/release_build.yml
+++ b/.github/workflows/release_build.yml
@@ -394,6 +394,29 @@ jobs:
name: python_dist
path: python/dist
+ # Check autogenerated signal definitions.
+ test_signal_defs:
+ name: GNSS Signal Definitions
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+
+ - name: Install Python Requirements
+ run: |
+ pip install -r requirements.txt
+
+ - name: Check Signal Definitions
+ run: |
+ python fusion_engine_client/messages/signal_def_gen.py --check-only
+
# Create a release only on a tag (not on a branch push).
release:
name: Create Release
diff --git a/python/examples/binary_message_decode.py b/python/examples/binary_message_decode.py
index e49561f1..77e6e5f2 100755
--- a/python/examples/binary_message_decode.py
+++ b/python/examples/binary_message_decode.py
@@ -125,7 +125,7 @@
payload.unpack(buffer=contents, offset=header.calcsize(),
message_version=header.message_version)
logger.info("Decoded payload contents: %s" % str(payload))
- except ValueError as e:
+ except Exception as e:
logger.warning(str(e))
logger.warning("Unable to decode payload contents.")
else:
diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py
index 37032781..3e854e96 100755
--- a/python/fusion_engine_client/analysis/analyzer.py
+++ b/python/fusion_engine_client/analysis/analyzer.py
@@ -195,6 +195,8 @@ def __init__(self,
self._mapbox_token_missing = False
+ self._gnss_signals_data = None
+
if self.output_dir is not None:
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
@@ -1135,13 +1137,12 @@ def _plot_data(name, idx, source_id, marker_style=None):
self._add_figure(name="map", figure=figure, title="Vehicle Trajectory (Map)", config={'scrollZoom': True})
def plot_gnss_skyplot(self, decimate=True):
- # Read the satellite data.
- result = self.reader.read(message_types=[GNSSSatelliteMessage], **self.params)
- data = result[GNSSSatelliteMessage.MESSAGE_TYPE]
-
- if len(data.p1_time) == 0:
- self.logger.info('No satellite data available. Skipping sky plot.')
+ # Read the GNSS signal data.
+ data = self._get_gnss_signals_data()
+ if len(data.messages) == 0:
+ self.logger.info('No GNSS signal data available. Skipping sky plot.')
return
+ have_gnss_signals_message = not data.using_legacy_satellite_message
# Setup the figure.
figure = go.Figure()
@@ -1149,24 +1150,52 @@ def plot_gnss_skyplot(self, decimate=True):
figure['layout']['polar']['radialaxis'].update(range=[90, 0])
figure['layout']['polar']['angularaxis'].update(visible=False)
- # Assign colors to each satellite.
- data_by_sv = GNSSSatelliteMessage.group_by_sv(data)
- svs = sorted(list(data_by_sv.keys()))
- color_by_sv = self._assign_colors(svs)
+ # Assign colors by PRN.
+ sv_hashes = np.unique(data.sv_data['sv_hash'])
+ prns = np.unique([get_prn(h) for h in sv_hashes])
+ color_by_prn = self._assign_colors(prns)
+
+ # List the available signal types for each SV.
+ signal_hashes = np.unique(data.signal_data['signal_hash'])
+ signal_types_by_sv = defaultdict(list)
+ for signal_hash in signal_hashes:
+ signal_type = get_signal_type(signal_hash)
+ signal_types_by_sv[get_satellite_hash(signal_hash)].append(signal_type)
+
+ # Convert the full list of signals for all time epochs to corresponding satellites.
+ all_signal_sv_hashes = np.array([get_satellite_hash(s) for s in data.signal_data['signal_hash']])
# Plot each satellite.
indices_by_system = defaultdict(list)
color_by_sv_format = []
color_by_cn0_format = []
- for sv in svs:
- name = satellite_to_string(sv, short=False)
- system = get_system(sv)
- sv_data = data_by_sv[sv]
-
- p1_time = sv_data['p1_time']
- az_deg = sv_data['azimuth_deg']
- el_deg = sv_data['elevation_deg']
- cn0_dbhz = sv_data['cn0_dbhz']
+ for sv_hash in sv_hashes:
+ sv_id = SatelliteID(sv_hash=sv_hash)
+ name = sv_id.to_string(short=False)
+ system = sv_id.get_satellite_type()
+
+ idx = data.sv_data['sv_hash'] == sv_hash
+ p1_time = data.sv_data['p1_time'][idx]
+ az_deg = data.sv_data['azimuth_deg'][idx]
+ el_deg = data.sv_data['elevation_deg'][idx]
+
+ # Get the C/N0 data for all signals from this satellite, then find the max at each epoch. We'll use that to
+ # set the color-by-C/N0 scale.
+ #
+ # Reference: https://stackoverflow.com/a/43094244
+ idx = all_signal_sv_hashes == sv_hash
+ cn0_per_epoch = np.split(data.signal_data['cn0_dbhz'][idx],
+ np.unique(data.signal_data['p1_time'][idx], return_index=True)[1][1:])
+ max_cn0_dbhz = np.array([max(cn0) for cn0 in cn0_per_epoch])
+
+ if have_gnss_signals_message:
+ sv_signal_types = signal_types_by_sv[sv_hash]
+ signal_type_str = ", ".join([pretty_print_gnss_enum(t, omit_satellite_type=True,
+ omit_component_hint=True)
+ for t in sv_signal_types])
+ name_str = f'{name} ({signal_type_str})'
+ else:
+ name_str = name
# Decimate the data to 30 second intervals.
if decimate and len(p1_time) > 1:
@@ -1178,24 +1207,25 @@ def plot_gnss_skyplot(self, decimate=True):
p1_time = p1_time[idx]
az_deg = az_deg[idx]
el_deg = el_deg[idx]
- cn0_dbhz = cn0_dbhz[idx]
+ max_cn0_dbhz = max_cn0_dbhz[idx]
# Plot the data. We set styles for both coloring by SV and by C/N0. We'll add buttons below to switch
# between styles.
- color_by_sv_format.append({'color': color_by_sv[sv]})
+ color_by_sv_format.append({'color': color_by_prn[sv_id.get_prn()]})
color_by_cn0_format.append({'cmin': 20, 'cmax': 55, 'colorscale': 'RdBu', 'showscale': True,
- 'colorbar': {'x': 0}, 'color': cn0_dbhz})
+ 'colorbar': {'x': 0}, 'color': max_cn0_dbhz})
text = ['P1: %.1f sec
(Az, El): (%.2f, %.2f) deg
C/N0: %.1f dB-Hz' %
- (t, a, e, c) for t, a, e, c in zip(p1_time, az_deg, el_deg, cn0_dbhz)]
+ (t, a, e, c) for t, a, e, c in zip(p1_time, az_deg, el_deg, max_cn0_dbhz)]
figure.add_trace(go.Scatterpolargl(r=el_deg, theta=(90 - az_deg), text=text,
- name=name, hoverinfo='name+text', hoverlabel={'namelength': -1},
+ name=name_str, hoverinfo='name+text', hoverlabel={'namelength': -1},
mode='markers', marker=color_by_sv_format[-1]))
indices_by_system[system].append(len(figure.data) - 1)
# Add selection buttons for each system and for choosing between coloring by SV and C/N0.
num_traces = len(figure.data)
- buttons = [dict(label='All', method='restyle', args=['visible', [True] * num_traces])]
+ num_svs = len(sv_hashes)
+ buttons = [dict(label=f'All ({num_svs})', method='restyle', args=['visible', [True] * num_traces])]
for system, indices in sorted(indices_by_system.items()):
if len(indices) == 0:
continue
@@ -1230,53 +1260,58 @@ def plot_gnss_skyplot(self, decimate=True):
self._add_figure(name='gnss_skyplot', figure=figure, title='GNSS Sky Plot')
def plot_gnss_cn0(self):
- # The legacy GNSSSatelliteMessage contains data per satellite, not per signal. The plotted C/N0 values will
- # reflect the L1 signal, unless L1 is not being tracked.
- result = self.reader.read(message_types=[GNSSSatelliteMessage], **self.params)
- data = result[GNSSSatelliteMessage.MESSAGE_TYPE]
-
- if len(data.p1_time) == 0:
- self.logger.info('No satellite data available. Skipping C/N0 plot.')
+ # Read the GNSS signal data.
+ data = self._get_gnss_signals_data()
+ if len(data.messages) == 0:
+ self.logger.info('No GNSS signal data available. Skipping C/N0 plot.')
return
+ have_gnss_signals_message = not data.using_legacy_satellite_message
# Setup the figure.
+ title = 'C/N0'
+ if not have_gnss_signals_message:
+ title += ' (L1 Only)'
figure = make_subplots(
rows=1, cols=1, print_grid=False, shared_xaxes=True,
- subplot_titles=['C/N0 (L1 Only)'])
+ subplot_titles=[title])
figure['layout'].update(showlegend=True, modebar_add=['v1hovermode'])
figure['layout']['xaxis1'].update(title=self.p1_time_label, showticklabels=True)
figure['layout']['yaxis1'].update(title="C/N0 (dB-Hz)")
- # Assign colors to each satellite.
- data_by_sv = GNSSSatelliteMessage.group_by_sv(data)
- svs = sorted(list(data_by_sv.keys()))
- color_by_sv = self._assign_colors(svs)
+ # Assign colors by PRN.
+ signal_hashes = np.unique(data.signal_data['signal_hash'])
+ prns = np.unique([get_prn(h) for h in signal_hashes])
+ color_by_prn = self._assign_colors(prns)
- # Plot each satellite.
- indices_by_system = defaultdict(list)
- for sv in svs:
- name = satellite_to_string(sv, short=False)
- system = get_system(sv)
- sv_data = data_by_sv[sv]
-
- text = ['P1: %.1f sec' % t for t in sv_data['p1_time']]
- time = sv_data['p1_time'] - float(self.t0)
- figure.add_trace(go.Scattergl(x=time, y=sv_data['cn0_dbhz'], text=text,
+ # Plot each signal.
+ indices_by_signal_type = defaultdict(list)
+ for signal_hash in signal_hashes:
+ signal = SignalID(signal_hash=signal_hash)
+ name = signal.to_string(short=False)
+
+ idx = data.signal_data['signal_hash'] == signal_hash
+ p1_time = data.signal_data['p1_time'][idx]
+ cn0_dbhz = data.signal_data['cn0_dbhz'][idx]
+
+ text = ['P1: %.1f sec' % t for t in p1_time]
+ time = p1_time - float(self.t0)
+ figure.add_trace(go.Scattergl(x=time, y=cn0_dbhz, text=text,
name=name, hoverlabel={'namelength': -1},
- mode='markers', marker={'color': color_by_sv[sv]}),
+ mode='markers', marker={'color': color_by_prn[signal.get_prn()]}),
1, 1)
- indices_by_system[system].append(len(figure.data) - 1)
+ indices_by_signal_type[signal.signal_type].append(len(figure.data) - 1)
# Add signal type selection buttons.
num_traces = len(figure.data)
- buttons = [dict(label='All', method='restyle', args=['visible', [True] * num_traces])]
- for system, indices in sorted(indices_by_system.items()):
+ buttons = [dict(label=f'All ({len(signal_hashes)})', method='restyle', args=['visible', [True] * num_traces])]
+ for signal_type, indices in sorted(indices_by_signal_type.items()):
if len(indices) == 0:
continue
visible = np.full((num_traces,), False)
visible[indices] = True
- buttons.append(dict(label=f'{str(system)} ({len(indices)})', method='restyle', args=['visible', visible]))
+ buttons.append(dict(label=f'{pretty_print_gnss_enum(signal_type)} ({len(indices)})', method='restyle',
+ args=['visible', visible]))
figure['layout']['updatemenus'] = [{
'type': 'buttons',
'direction': 'left',
@@ -1293,11 +1328,10 @@ def plot_gnss_azimuth_elevation(self):
"""!
@brief Plot GNSS azimuth/elevation angles.
"""
- result = self.reader.read(message_types=GNSSSatelliteMessage, **self.params)
- data = result[GNSSSatelliteMessage.MESSAGE_TYPE]
-
- if len(data.p1_time) == 0:
- self.logger.info('No satellite data available. Skipping azimuth/elevation plot.')
+ # Read the GNSS signal data.
+ data = self._get_gnss_signals_data()
+ if len(data.messages) == 0:
+ self.logger.info('No GNSS signal data available. Skipping azimuth/elevation time series plot.')
return
# Set up the figure.
@@ -1311,46 +1345,50 @@ def plot_gnss_azimuth_elevation(self):
figure['layout']['yaxis1'].update(title="Degrees")
figure['layout']['yaxis2'].update(title="Degrees")
- # Assign colors to each satellite.
- data_by_sv = GNSSSatelliteMessage.group_by_sv(data)
+ # Assign colors by PRN.
+ sv_hashes = np.unique(data.sv_data['sv_hash'])
+ prns = np.unique([get_prn(h) for h in sv_hashes])
+ color_by_prn = self._assign_colors(prns)
+
+ # Plot each satellite.
svs_by_system = defaultdict(set)
indices_by_system = defaultdict(list)
- svs = sorted(list(data_by_sv.keys()))
- color_by_sv = self._assign_colors(svs)
+ for sv_hash in sv_hashes:
+ sv_id = SatelliteID(sv_hash=sv_hash)
+ name = sv_id.to_string(short=False)
+ system = sv_id.get_satellite_type()
+ svs_by_system[system].add(sv_hash)
- for sv in svs:
- name = satellite_to_string(sv, short=False)
- system = get_system(sv)
- sv_data = data_by_sv[sv]
- svs_by_system[system].add(sv)
+ idx = data.sv_data['sv_hash'] == sv_hash
+ p1_time = data.sv_data['p1_time'][idx]
+ az_deg = data.sv_data['azimuth_deg'][idx]
+ el_deg = data.sv_data['elevation_deg'][idx]
- az_deg = sv_data['azimuth_deg']
- el_deg = sv_data['elevation_deg']
-
- time = sv_data['p1_time'] - float(self.t0)
+ time = p1_time - float(self.t0)
- # Plot the data. We set styles for coloring by SV.
+ # Plot the data.
+ color = color_by_prn[sv_id.get_prn()]
text = ["P1: %.3f sec" % (t + float(self.t0)) for t in time]
figure.add_trace(go.Scattergl(x=time, y=az_deg, text=text,
- name=name, hoverlabel={'namelength': -1},
- mode='markers',
- marker={'color': color_by_sv[sv], 'symbol': 'circle', 'size': 8},
- showlegend=True,
- legendgroup=name),
+ name=name, hoverlabel={'namelength': -1},
+ mode='markers',
+ marker={'color': color, 'symbol': 'circle', 'size': 8},
+ showlegend=True,
+ legendgroup=name),
1, 1)
indices_by_system[system].append(len(figure.data) - 1)
figure.add_trace(go.Scattergl(x=time, y=el_deg, text=text,
- name=name, hoverlabel={'namelength': -1},
- mode='markers',
- marker={'color': color_by_sv[sv], 'symbol': 'circle', 'size': 8},
- showlegend=False,
- legendgroup=name),
+ name=name, hoverlabel={'namelength': -1},
+ mode='markers',
+ marker={'color': color, 'symbol': 'circle', 'size': 8},
+ showlegend=False,
+ legendgroup=name),
2, 1)
indices_by_system[system].append(len(figure.data) - 1)
# Add signal type selection buttons.
num_traces = len(figure.data)
- buttons = [dict(label='All', method='restyle', args=['visible', [True] * num_traces])]
+ buttons = [dict(label=f'All ({len(sv_hashes)})', method='restyle', args=['visible', [True] * num_traces])]
for system, indices in sorted(indices_by_system.items()):
if len(indices) == 0:
continue
@@ -1374,122 +1412,241 @@ def plot_gnss_signal_status(self):
filename = 'gnss_signal_status'
figure_title = "GNSS Signal Status"
- # Read the satellite data.
- result = self.reader.read(message_types=[GNSSSatelliteMessage], **self.params)
- data = result[GNSSSatelliteMessage.MESSAGE_TYPE]
- is_legacy_message = True
-
- if len(data.p1_time) == 0:
- self.logger.info('No satellite data available. Skipping signal usage plot.')
+ # Read the GNSS signal data.
+ data = self._get_gnss_signals_data()
+ if len(data.messages) == 0:
+ self.logger.info('No GNSS signal data available. Skipping signal status plot.')
return
+ have_gnss_signals_message = not data.using_legacy_satellite_message
+
+ # Count the number of satellites/signals used in each epoch.
+ all_p1_time = data.p1_time
+
+ def _count_selected(selected_p1_times, return_nonzero_time=False):
+ selected_p1_time, p1_time_idx, count_per_time = np.unique(selected_p1_times, return_index=True,
+ return_counts=True)
+ count = np.full_like(all_p1_time, 0, dtype=int)
+ count[np.isin(all_p1_time, selected_p1_time)] = count_per_time
+ if return_nonzero_time:
+ return count, selected_p1_time, p1_time_idx
+ else:
+ return count
+
+ num_svs = _count_selected(data.sv_data["p1_time"])
+ num_signals = _count_selected(data.signal_data["p1_time"])
+
+ is_used_mask = (GNSSSignalInfo.STATUS_FLAG_USED_PR | GNSSSignalInfo.STATUS_FLAG_USED_DOPPLER |
+ GNSSSignalInfo.STATUS_FLAG_USED_CARRIER)
+ idx = (np.bitwise_and(data.signal_data['status_flags'], is_used_mask) != 0)
+ num_used_signals, used_p1_time, used_p1_time_idx = _count_selected(data.signal_data['p1_time'][idx],
+ return_nonzero_time=True)
+
+ used_signal_hashes = data.signal_data['signal_hash'][idx]
+ used_sv_hashes = np.array([get_satellite_hash(h) for h in used_signal_hashes])
+ used_sv_hashes_per_epoch = np.split(used_sv_hashes, used_p1_time_idx[1:])
+ num_used_svs_only = np.array([len(np.unique(svs)) for svs in used_sv_hashes_per_epoch])
+ num_used_svs = np.full_like(all_p1_time, 0, dtype=int)
+ num_used_svs[np.isin(data.p1_time, used_p1_time)] = num_used_svs_only
+
+ idx = (np.bitwise_and(data.signal_data['status_flags'],
+ GNSSSignalInfo.STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED) != 0)
+ num_fixed_signals = _count_selected(data.signal_data["p1_time"][idx])
# Setup the figure.
- colors = {'unused': 'black', 'pr': 'red', 'is_pivot': 'purple',
- 'float': 'darkgoldenrod', 'not_fixed': 'green', 'fixed_skipped': 'blue', 'fixed': 'orange'}
+ colors = {'unused': 'black', 'is_pivot': 'purple',
+ 'pr': 'red', 'pr_diff': 'deepskyblue',
+ 'float': 'green', 'fixed': 'orange'}
- # The legacy GNSSSatelliteMessage contains data per satellite, not per signal, and only includes in-use status.
- # It does not elaborate on how the signal was used.
- if is_legacy_message:
+ if have_gnss_signals_message:
title = '''\
-Satellite Status
-Black=Unused, Red=Used'''
- entry_type = 'Satellite'
+Signal Status
+Black=Unused, Red=Pseudorange, Light Blue=Differential Pseudorange
+Green=Float, Orange=Integer (Fixed)'''
else:
- # In practice, the signal status plot can be _VERY_ slow to generate for really long logs (multiple hours)
- # because plotly doesn't handle figures with lots of traces very efficiently. The legacy satellite status
- # plot doesn't seem to suffer nearly as much since A) it has fewer elements (# SVs vs # signals), and B) it
- # only supports at most 2 traces per element since it doesn't convey usage type.
- if self.truncate_data:
- _logger.warning('Skipping signal status plot for very long log. Rerun with --truncate=false to '
- 'generate this plot.')
- self._add_figure(name=filename, title=f'{figure_title} (Skipped - Long Log Detected)')
- return
-
+ # The legacy GNSSSatelliteMessage contains data per satellite, not per signal, and only includes in-use
+ # status. It does not elaborate on _how_ the signal was used for navigation.
title = '''\
-Signal Status
-Black=Unused, Red=Pseudorange, Pink=Pseudorange (Differential), Purple=Pivot (Differential)
-Gold=Float, Green=Integer (Not Fixed), Blue=Integer (Fixed, Float Solution Type), Orange=Integer (Fixed)'''
- entry_type = 'Signal'
+Satellite Status
+Black=Unused, Red=Used'''
figure = make_subplots(
rows=5, cols=1, print_grid=False, shared_xaxes=True,
subplot_titles=[title,
None, None, None,
- 'Satellite Count'],
+ 'Satellite/Signal Count'],
specs=[[{'rowspan': 4}],
[None],
[None],
[None],
[{}]])
- figure['layout'].update(showlegend=False, modebar_add=['v1hovermode'])
+ figure['layout'].update(showlegend=True, modebar_add=['v1hovermode'])
figure['layout']['xaxis1'].update(title=self.p1_time_label)
- figure['layout']['yaxis1'].update(title=entry_type)
- figure['layout']['yaxis2'].update(title=f"# {entry_type}s", rangemode='tozero')
+ figure['layout']['yaxis1'].update(title='Signal' if have_gnss_signals_message else 'Satellite')
+ figure['layout']['yaxis2'].update(title=f"# SVs/Signals", rangemode='tozero')
# Plot the signal counts.
time = data.p1_time - float(self.t0)
text = ["P1: %.3f sec" % (t + float(self.t0)) for t in time]
- figure.add_trace(go.Scattergl(x=time, y=data.num_svs, text=text,
- name=f'# {entry_type}s', hoverlabel={'namelength': -1},
+ figure.add_trace(go.Scattergl(x=time, y=num_svs, text=text,
+ name=f'# SVs', hoverlabel={'namelength': -1},
mode='lines', line={'color': 'black', 'dash': 'dash'}),
5, 1)
- figure.add_trace(go.Scattergl(x=time, y=data.num_used_svs, text=text,
- name=f'# Used {entry_type}s', hoverlabel={'namelength': -1},
+ if have_gnss_signals_message:
+ figure.add_trace(go.Scattergl(x=time, y=num_signals, text=text,
+ name=f'# Signals', hoverlabel={'namelength': -1},
+ mode='lines', line={'color': 'gray', 'dash': 'dash'}),
+ 5, 1)
+
+ figure.add_trace(go.Scattergl(x=time, y=num_used_svs, text=text,
+ name=f'# Used SVs', hoverlabel={'namelength': -1},
mode='lines', line={'color': 'green'}),
5, 1)
+ if have_gnss_signals_message:
+ figure.add_trace(go.Scattergl(x=time, y=num_used_signals, text=text,
+ name=f'# Used Signals', hoverlabel={'namelength': -1},
+ mode='lines', line={'color': 'red'}),
+ 5, 1)
+ figure.add_trace(go.Scattergl(x=time, y=num_fixed_signals, text=text,
+ name=f'# Fixed Signals', hoverlabel={'namelength': -1},
+ mode='lines', line={'color': 'orange'}),
+ 5, 1)
num_count_traces = len(figure.data)
- # Plot each satellite. Plot in reverse order so G01 is at the top of the Y axis.
- data_by_sv = GNSSSatelliteMessage.group_by_sv(data)
- svs = list(data_by_sv.keys())
- svs_by_system = defaultdict(set)
- indices_by_system = defaultdict(list)
- for i, sv in enumerate(svs[::-1]):
- sv = int(sv)
- system = get_system(sv)
- name = satellite_to_string(sv, short=True)
- svs_by_system[system].add(sv)
-
- sv_data = data_by_sv[sv]
- time = sv_data['p1_time'] - float(self.t0)
- is_used = np.bitwise_and(sv_data['flags'], SatelliteInfo.SATELLITE_USED).astype(bool)
+ # In practice, the signal status plot can be _VERY_ slow to generate for really long logs (multiple hours)
+ # because plotly doesn't handle figures with lots of traces very efficiently. If we think the log is very
+ # long, we'll skip this plot.
+ #
+ # The legacy GNSSSatelliteMessage status plot doesn't seem to suffer nearly as much since A) it has fewer
+ # elements (# SVs vs # signals), and B) it only supports at most 2 traces per element since it doesn't
+ # convey usage type.
+ if self.truncate_data:
+ _logger.warning('Skipping signal status plot for very long log. Rerun with --truncate=false to '
+ 'generate this plot.')
+ self._add_figure(name=filename, title=f'{figure_title} (Skipped - Long Log Detected)')
+ return
- idx = is_used
- if np.any(idx):
- text = ["P1: %.3f sec" % (t + float(self.t0)) for t in time[idx]]
- figure.add_trace(go.Scattergl(x=time[idx], y=[i] * np.sum(idx), text=text,
- name=name, hoverlabel={'namelength': -1},
- mode='markers',
- marker={'color': colors['pr'], 'symbol': 'circle', 'size': 8}),
- 1, 1)
- indices_by_system[system].append(len(figure.data) - 1)
+ if have_gnss_signals_message:
+ conditions = [
+ # Signal used for standalone pseudorange
+ {
+ 'cond': lambda status_flags, have_corrections: np.logical_and(
+ np.bitwise_and(status_flags, GNSSSignalInfo.STATUS_FLAG_USED_PR) != 0,
+ ~have_corrections),
+ 'marker': {'color': colors['pr'], 'symbol': 'circle', 'size': 8}
+ },
+ # Signal used for differential pseudorange (but not carrier phase)
+ {
+ 'cond': lambda status_flags, have_corrections: np.logical_and(
+ np.bitwise_and(status_flags, range_used_mask) == GNSSSignalInfo.STATUS_FLAG_USED_PR,
+ have_corrections),
+ 'marker': {'color': colors['pr_diff'], 'symbol': 'circle', 'size': 8}
+ },
+ # Signal used for float carrier phase
+ {
+ 'cond': lambda status_flags, have_corrections: np.logical_and(
+ np.bitwise_and(status_flags, cp_used_mask) == GNSSSignalInfo.STATUS_FLAG_USED_CARRIER,
+ have_corrections),
+ 'marker': {'color': colors['float'], 'symbol': 'circle', 'size': 8}
+ },
+ # Signal used for fixed carrier phase
+ {
+ 'cond': lambda status_flags, have_corrections: np.logical_and(
+ np.bitwise_and(status_flags, cp_used_mask) == cp_used_mask,
+ have_corrections),
+ 'marker': {'color': colors['fixed'], 'symbol': 'circle', 'size': 8}
+ },
+ # Signal not used
+ {
+ 'cond': lambda status_flags, _: np.bitwise_and(status_flags, is_used_mask) == 0,
+ 'marker': {'color': colors['unused'], 'symbol': 'x', 'size': 8}
+ },
+ ]
+
+ # At the moment, the signals message does not have a flag to indicate if RTK corrections are available. For
+ # now, we will say that they are available if _any_ signal in the epoch used carrier phase. If we use PR
+ # corrections but do not use carrier phase on any signals, it will display as uncorrected.
+ used_cp = np.bitwise_and(data.signal_data['status_flags'], GNSSSignalInfo.STATUS_FLAG_USED_CARRIER) != 0
+ _, idx, rev_idx = np.unique(data.signal_data['p1_time'], return_index=True, return_inverse=True)
+ used_cp_per_epoch = np.split(used_cp, idx[1:])
+ have_corrections_per_epoch = np.array([any(used) for used in used_cp_per_epoch], dtype=bool)
+ have_corrections = have_corrections_per_epoch[rev_idx]
+
+ range_used_mask = (GNSSSignalInfo.STATUS_FLAG_USED_PR | GNSSSignalInfo.STATUS_FLAG_USED_CARRIER)
+ cp_used_mask = (GNSSSignalInfo.STATUS_FLAG_USED_CARRIER |
+ GNSSSignalInfo.STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED)
+ else:
+ conditions = [
+ # Signal used
+ {
+ 'cond': lambda status_flags, _: np.bitwise_and(status_flags, is_used_mask) != 0,
+ 'marker': {'color': colors['pr'], 'symbol': 'circle', 'size': 8}
+ },
+ # Signal not used
+ {
+ 'cond': lambda status_flags, _: np.bitwise_and(status_flags, is_used_mask) == 0,
+ 'marker': {'color': colors['unused'], 'symbol': 'x', 'size': 8}
+ },
+ ]
+ have_corrections = None
+
+ # Plot each signal. Plot in reverse order so G01 is at the top of the Y axis.
+ signal_hashes = np.unique(data.signal_data['signal_hash'])
+ indices_by_signal_type = defaultdict(list)
+ signals_by_type = defaultdict(list)
+ tick_text = []
+ for signal_hash in signal_hashes[::-1]:
+ signal = SignalID(signal_hash=signal_hash)
+ name = signal.to_string(short=False)
+ signals_by_type[signal.signal_type].append(signal)
+
+ # Extract data for this signal.
+ idx = data.signal_data['signal_hash'] == signal_hash
+ p1_time = data.signal_data['p1_time'][idx]
+ cn0_dbhz = data.signal_data['cn0_dbhz'][idx]
+ status_flags = data.signal_data['status_flags'][idx]
+ signal_has_corrections = None if have_corrections is None else have_corrections[idx]
+ time = p1_time - float(self.t0)
- idx = ~is_used
- if np.any(idx):
- text = ["P1: %.3f sec" % (t + float(self.t0)) for t in time[idx]]
- figure.add_trace(go.Scattergl(x=time[idx], y=[i] * np.sum(idx), text=text,
- name=name + ' (Unused)', hoverlabel={'namelength': -1},
- mode='markers',
- marker={'color': colors['unused'], 'symbol': 'x', 'size': 8}),
- 1, 1)
- indices_by_system[system].append(len(figure.data) - 1)
+ # Find the satellite elevation for the times this signal was present.
+ sv_idx = data.sv_data['sv_hash'] == int(signal.get_satellite_id())
+ time_idx = np.isin(data.sv_data['p1_time'][sv_idx], p1_time)
+ elev_deg = data.sv_data['elevation_deg'][sv_idx][time_idx]
- tick_text = [satellite_to_string(s, short=True) for s in svs[::-1]]
- figure['layout']['yaxis1'].update(tickmode='array', tickvals=np.arange(0, len(svs)),
+ shown = False
+ y_offset = len(tick_text)
+ for cond in conditions:
+ idx = cond['cond'](status_flags, signal_has_corrections)
+ if np.any(idx):
+ figure.add_trace(go.Scattergl(x=time[idx], y=[y_offset] * np.sum(idx),
+ customdata=np.vstack((status_flags[idx],
+ cn0_dbhz[idx],
+ elev_deg[idx])),
+ name=name, hoverlabel={'namelength': -1},
+ showlegend=False, legendgroup=int(signal_hash),
+ mode='markers', marker=cond['marker']),
+ 1, 1)
+ indices_by_signal_type[signal.signal_type].append(len(figure.data) - 1)
+ shown = True
+
+ if shown:
+ tick_text.append(signal.to_string(short=True))
+
+ figure['layout']['yaxis1'].update(tickmode='array', tickvals=np.arange(0, len(tick_text)),
ticktext=tick_text, automargin=True)
# Add signal type selection buttons.
num_traces = len(figure.data)
- buttons = [dict(label='All', method='restyle', args=['visible', [True] * num_traces])]
- for system, indices in sorted(indices_by_system.items()):
+ num_signals = np.max(num_signals) if len(num_signals) > 0 else 0
+ buttons = [dict(label=f'All ({num_signals})', method='restyle', args=['visible', [True] * num_traces])]
+ for signal_type, indices in sorted(indices_by_signal_type.items()):
if len(indices) == 0:
continue
visible = np.full((num_traces,), False)
visible[:num_count_traces] = True
visible[indices] = True
- buttons.append(dict(label=f'{str(system)} ({len(svs_by_system[system])})', method='restyle',
- args=['visible', visible]))
+ buttons.append(dict(label=f'{pretty_print_gnss_enum(signal_type)} ({len(signals_by_type[signal_type])})',
+ method='restyle', args=['visible', visible]))
figure['layout']['updatemenus'] = [{
'type': 'buttons',
'direction': 'left',
@@ -1500,7 +1657,121 @@ def plot_gnss_signal_status(self):
'yanchor': 'top'
}]
- self._add_figure(name=filename, figure=figure, title=figure_title)
+ hover_js = f"""\
+function SetSignalStatusHover(point) {{
+ let status_flags = GetCustomData(point, 0);
+ let cn0_dbhz = GetCustomData(point, 1);
+ let elev_deg = GetCustomData(point, 2);
+
+ let tracking = [];
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_VALID_PR}) {{
+ tracking.push("PR");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_CARRIER_LOCKED}) {{
+ tracking.push("CP");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_VALID_DOPPLER}) {{
+ tracking.push("Doppler");
+ }}
+
+ let used = [];
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_USED_PR}) {{
+ used.push("PR");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_USED_CARRIER}) {{
+ used.push("CP");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_USED_DOPPLER}) {{
+ used.push("Doppler");
+ }}
+
+ let features = [];
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_HAS_EPHEM}) {{
+ features.push("Ephemeris");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_HAS_SBAS}) {{
+ features.push("SBAS");
+ }}
+ if (status_flags & {GNSSSignalInfo.STATUS_FLAG_HAS_RTK}) {{
+ features.push("RTK");
+ }}
+
+ let new_text = GetTimeText(point.x);
+ new_text += "
C/N0: " + cn0_dbhz.toFixed(2) + " dB-Hz";
+ new_text += "
Elevation: " + elev_deg.toFixed(1) + " deg";
+ new_text += "
Status mask: 0x" + status_flags.toString(16);
+ new_text += "
Available: " + tracking.join(", ");
+ new_text += "
Used: " + used.join(", ");
+ new_text += "
Features: " + features.join(", ");
+ new_text += "
Carrier: " + (status_flags & {GNSSSignalInfo.STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED} ?
+ "fixed" : "not fixed");
+ ChangeHoverText(point, new_text);
+}}
+
+figure.on('plotly_hover', function(data) {{
+ for (let i = 0; i < data.points.length; ++i) {{
+ let point = data.points[i];
+ if (point.curveNumber > {num_count_traces}) {{
+ SetSignalStatusHover(point);
+ }}
+ else {{
+ ChangeHoverText(point, GetTimeText(point.x));
+ }}
+ }}
+}});
+"""
+
+ self._add_figure(name=filename, figure=figure, title=figure_title, inject_js=hover_js)
+
+ def _get_gnss_signals_data(self):
+ # If we already have data cached, return it.
+ if self._gnss_signals_data is not None:
+ return self._gnss_signals_data
+
+ # See if we have GNSSSignalsMessages. If so, prefer those.
+ params = copy.deepcopy(self.params)
+ params['return_numpy'] = False
+
+ result = self.reader.read(message_types=[GNSSSignalsMessage], **params)
+ data = result[GNSSSignalsMessage.MESSAGE_TYPE]
+
+ # We store the result now, even if there were no GNSSSignalsMessage messages. That way if we also don't have any
+ # GNSSSatelliteMessage messages below, we'll have _something_ to return and we won't try to reload from disk on
+ # each call to this function.
+ self._gnss_signals_data = data
+ self._gnss_signals_data.using_legacy_satellite_message = False
+
+ # If we don't have any GNSSSignalsMessages, see if we have the legacy GNSSSatelliteMessage and fall back to
+ # that.
+ if len(data.messages) == 0:
+ # The legacy GNSSSatelliteMessage contains data per satellite, not per signal. The plotted C/N0 values will
+ # reflect the L1 signal, unless L1 is not being tracked.
+ result = self.reader.read(message_types=[GNSSSatelliteMessage], **params)
+ data = result[GNSSSatelliteMessage.MESSAGE_TYPE]
+
+ # Convert to GNSSSignalsMessages. Some of the fields, like signal type and tracking/usage status, will be
+ # approximated and may not be plotted.
+ #
+ # Note that we leave data.message_type as GNSSSatelliteMessage so the plotting functions can determine what
+ # information to display. However, we need to change data.message_class so MessageData calls the correct
+ # to_numpy() function.
+ if len(data.messages) > 0:
+ self.logger.warning('Using legacy GNSSSatelliteMessage to approximate per-signal information.')
+ data.messages = [m.to_gnss_signals_message() for m in data.messages]
+ data.message_type = GNSSSignalsMessage.MESSAGE_TYPE
+ data.message_class = GNSSSignalsMessage
+ self._gnss_signals_data = data
+ self._gnss_signals_data.using_legacy_satellite_message = True
+
+ self._gnss_signals_data.to_numpy()
+
+ return self._gnss_signals_data
+
+ def clear_gnss_signal_data_cache(self):
+ """!
+ @brief Clear cached GNSSSignalsMessage data to free memory when finished plotting.
+ """
+ self._gnss_signals_data = None
def plot_dop(self):
"""!
@@ -2807,7 +3078,7 @@ def _add_page(self, name, html_body, title=None):
self.plots[name] = {'title': title, 'path': path}
- def _add_figure(self, name, figure=None, title=None, config=None):
+ def _add_figure(self, name, figure=None, title=None, config=None, inject_js: str = None):
"""!
@brief Generate an HTML file for the specified figure.
@@ -2830,6 +3101,10 @@ def _add_figure(self, name, figure=None, title=None, config=None):
self.logger.info('Creating %s...' % path)
os.makedirs(os.path.dirname(path), exist_ok=True)
+
+ if inject_js is not None:
+ plotly.io.write_html = functools.partial(self.__write_html_and_inject_js, inject_js)
+
plotly.offline.plot(
figure,
output_type='file',
@@ -2839,8 +3114,36 @@ def _add_figure(self, name, figure=None, title=None, config=None):
show_link=False,
config=config)
+ if inject_js is not None:
+ plotly.io.write_html = Analyzer.__original_write_html
+
self.plots[name] = {'title': title, 'path': path if figure is not None else None}
+ # Support for injecting custom javascript into the generated plotly HTML file.
+ def __write_html_and_inject_js(self, inject_js, *args, **kwargs):
+ post_script = kwargs.get("post_script", None)
+ if post_script is None:
+ post_script = ""
+
+ # Create a global variable with the log's t0 timestamp.
+ post_script += f"""\
+var p1_t0_sec = {float(self.reader.t0)};
+var p1_time_axis_rel = {'true' if self.time_axis == 'relative' else 'false'}
+"""
+
+ # Inject common plotly data support functions (GetTimeText(), etc.).
+ script_dir = os.path.join(os.path.dirname(__file__))
+ with open(os.path.join(script_dir, 'plotly_data_support.js'), 'rt') as f:
+ post_script += f.read()
+
+ # Now inject the custom javascript.
+ post_script += inject_js
+
+ kwargs["post_script"] = post_script
+ return Analyzer.__original_write_html(*args, **kwargs)
+
+ __original_write_html = plotly.io.write_html
+
def _open_browser(self, filename):
try:
webbrowser.open("file:///" + os.path.abspath(filename))
@@ -3103,6 +3406,8 @@ def main(args=None):
analyzer.plot_gnss_signal_status()
analyzer.plot_gnss_skyplot()
analyzer.plot_gnss_azimuth_elevation()
+ analyzer.clear_gnss_signal_data_cache()
+
analyzer.plot_gnss_corrections_status()
analyzer.plot_dop()
diff --git a/python/fusion_engine_client/analysis/data_loader.py b/python/fusion_engine_client/analysis/data_loader.py
index d0201116..2f489893 100644
--- a/python/fusion_engine_client/analysis/data_loader.py
+++ b/python/fusion_engine_client/analysis/data_loader.py
@@ -89,20 +89,24 @@ def to_numpy(self, remove_nan_times: bool = True,
self.message_bytes = np.array(self.message_bytes, dtype=np.uint64)
self.message_index = np.array(self.message_index, dtype=int)
- if remove_nan_times and 'p1_time' in self.__dict__:
- is_nan = np.isnan(self.p1_time)
+ # Helper function for removing entries with NAN timestamps.
+ def _remove_nan_from_dict(data_dict):
+ if 'p1_time' not in data_dict:
+ return
+
+ is_nan = np.isnan(data_dict['p1_time'])
if np.any(is_nan):
keep_idx = ~is_nan
- for key, value in self.__dict__.items():
+ for key, value in data_dict.items():
if (key not in ('message_type', 'message_class', 'params', 'messages') and
isinstance(value, np.ndarray)):
- if key in self.__dict__.get('__metadata__', {}).get('not_time_dependent', []):
+ if key in data_dict.get('__metadata__', {}).get('not_time_dependent', []):
# Data is not time-dependent, even if it happens to have the same number of elements
# as the time vector.
pass
elif len(value.shape) == 1:
if len(value) == len(keep_idx):
- self.__dict__[key] = value[keep_idx]
+ data_dict[key] = value[keep_idx]
else:
# Field has a different length than the time vector. It is likely a
# non-time-varying element (e.g., a position std dev threshold).
@@ -113,14 +117,14 @@ def to_numpy(self, remove_nan_times: bool = True,
# along the columns.
if value.shape[1] == len(is_nan):
# Assuming second dimension (columns) is time.
- self.__dict__[key] = value[:, keep_idx]
+ data_dict[key] = value[:, keep_idx]
# Otherwise, check to see if the data is transposed as NxA.
elif value.shape[0] == len(is_nan):
# Assuming first dimension is time.
- self.__dict__[key] = value[keep_idx, :]
+ data_dict[key] = value[keep_idx, :]
elif value.shape[1] == len(is_nan):
# Assuming second dimension is time.
- self.__dict__[key] = value[:, keep_idx]
+ data_dict[key] = value[:, keep_idx]
else:
# Unrecognized data shape.
pass
@@ -128,6 +132,17 @@ def to_numpy(self, remove_nan_times: bool = True,
# Unrecognized data shape.
pass
+ # If requested, remove any entries with NAN P1 timestamps.
+ if remove_nan_times:
+ # Remove NANs from top-level numpy data.
+ _remove_nan_from_dict(self.__dict__)
+
+ # Special cast: also remove NANs from nested satellite/signal data elements.
+ if self.message_type == MessageType.GNSS_SIGNALS:
+ _remove_nan_from_dict(self.sv_data)
+ _remove_nan_from_dict(self.signal_data)
+
+
if not keep_messages:
self.messages = []
if not keep_message_bytes:
diff --git a/python/fusion_engine_client/analysis/plotly_data_support.js b/python/fusion_engine_client/analysis/plotly_data_support.js
new file mode 100644
index 00000000..39cb0aba
--- /dev/null
+++ b/python/fusion_engine_client/analysis/plotly_data_support.js
@@ -0,0 +1,28 @@
+var figure = document.getElementsByClassName("plotly-graph-div js-plotly-plot")[0];
+
+function GetTimeText(time_sec) {
+ if (p1_time_axis_rel) {
+ return `Rel: ${time_sec.toFixed(3)} sec (P1: ${(time_sec + p1_t0_sec).toFixed(3)} sec)`;
+ }
+ else {
+ return `Rel: ${(time_sec - p1_t0_sec).toFixed(3)} sec (P1: ${time_sec.toFixed(3)} sec)`;
+ }
+}
+
+function ChangeHoverText(point, new_text) {
+ // Note: Technically calling restyle() is more correct, however it can only restyle an entire trace, not just one
+ // point in a trace, and in practice it's very sluggish. Manually modifying fullData.text is much faster. Both options
+ // seem to have a small race condition and occasionally the text does not change before the hover div becomes visible.
+ // Nothing we can do about that right now.
+ // let text_array = point.data.text;
+ // text_array[point.pointNumber] = new_text;
+ // Plotly.restyle(d, {'text': text_array}, [point.curveNumber]);
+ point.fullData.text = new_text;
+}
+
+function GetCustomData(point, row) {
+ let customdata = point.data.customdata.hasOwnProperty("_inputArray") ?
+ point.data.customdata._inputArray :
+ point.data.customdata;
+ return customdata[row][point.pointNumber];
+}
diff --git a/python/fusion_engine_client/messages/defs.py b/python/fusion_engine_client/messages/defs.py
index c802fa42..7897aca8 100644
--- a/python/fusion_engine_client/messages/defs.py
+++ b/python/fusion_engine_client/messages/defs.py
@@ -93,6 +93,7 @@ class MessageType(IntEnum):
POSE_AUX = 10003
CALIBRATION_STATUS = 10004
RELATIVE_ENU_POSITION = 10005
+ GNSS_SIGNALS = 10006
# Device status messages.
SYSTEM_STATUS = 10500
diff --git a/python/fusion_engine_client/messages/signal_def_gen.py b/python/fusion_engine_client/messages/signal_def_gen.py
new file mode 100644
index 00000000..607dc1dc
--- /dev/null
+++ b/python/fusion_engine_client/messages/signal_def_gen.py
@@ -0,0 +1,622 @@
+#!/usr/bin/env python3
+
+from enum import IntEnum
+import inspect
+import os
+from pathlib import Path
+import re
+import subprocess
+import sys
+from textwrap import indent
+from typing import Callable, Iterable, NamedTuple, Optional, TypeVar
+
+if __package__ is None or __package__ == "":
+ root_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '../..'))
+ sys.path.insert(0, root_dir)
+ __package__ = os.path.dirname(os.path.relpath(__file__, root_dir)).replace('/', '.')
+
+from ..utils.argument_parser import ArgumentParser
+
+# Add the Python root directory (fusion-engine-client/python/) to the import search path to enable FusionEngine imports
+# if this application is being run directly out of the repository and is not installed as a pip package.
+ROOT_DIR = Path(__file__).absolute().parents[2]
+sys.path.insert(0, str(ROOT_DIR))
+
+# Text in source files to identify sections to update with generated code.
+CPP_CONSTANTS_START_TEXT = 'Start Autogenerated Constants'
+CPP_CONSTANTS_STOP_TEXT = 'Stop Autogenerated Constants'
+START_TEXT = 'Start Autogenerated Types'
+STOP_TEXT = 'Stop Autogenerated Types'
+
+CPP_SIGNAL_DEFS_PATH = ROOT_DIR.parent / 'src/point_one/fusion_engine/messages/signal_defs.h'
+PYTHON_SIGNAL_DEFS_PATH = ROOT_DIR / 'fusion_engine_client/messages/signal_defs.py'
+CPP_INDENT = ' '
+PYTHON_INDENT = ' '
+
+
+def update_generated_code_in_file(path: Path, generated_code: str, start_text: str = START_TEXT,
+ stop_text: str = STOP_TEXT):
+ '''!
+ Replace the text in the file at `path` between `start_text` and `stop_text` with `generated_code`.
+ '''
+ in_generated = False
+ current_file_contents = open(path).readlines()
+
+ with open(path, 'w') as fd:
+ for line in current_file_contents:
+ if start_text in line:
+ in_generated = True
+ fd.write(line)
+ fd.write(generated_code)
+ elif stop_text in line:
+ in_generated = False
+
+ if not in_generated:
+ fd.write(line)
+
+
+# This import is from the file with generated code. If a previous run generated invalid code, this import will fail with
+# whatever syntax error was introduced. This check will clear the generated code so hopefully re-running won't
+# experience the same error.
+try:
+ from fusion_engine_client.messages.signal_defs import (
+ BeiDouSignalName, GNSSComponent, FrequencyBand, GalileoSignalName,
+ GLONASSSignalName, GNSSSignalType, GPSSignalName, QZSSSignalName,
+ SatelliteType, SBASSignalName, SignalName, _get_gnss_enum_bit_packing,
+ _get_pretty_gnss_signal_type, _GNSSSignalPartType,
+ pretty_print_gnss_enum, to_signal_val)
+except:
+ print('Unable to load fusion_engine_client.messages.signal_defs. This may be a result of a previous code '
+ 'generation run producing code with syntax errors.')
+ print('Clearing generated code. Try rerunning.')
+ update_generated_code_in_file(PYTHON_SIGNAL_DEFS_PATH, PYTHON_INDENT + 'UNKNOWN = 0\n')
+ sys.exit(1)
+
+SIGNAL_PART_ENUM_LIST = [
+ SatelliteType,
+ FrequencyBand,
+ SignalName,
+ GNSSComponent,
+]
+
+ENUM_LIST = [
+ SatelliteType,
+ FrequencyBand,
+ GNSSComponent,
+ GPSSignalName,
+ GLONASSSignalName,
+ GalileoSignalName,
+ BeiDouSignalName,
+ SBASSignalName,
+ QZSSSignalName,
+ GNSSSignalType]
+
+############################# Common GNSSSignalType Generation #############################
+
+def get_generated_code_in_file(path: Path, start_text: str = START_TEXT,
+ stop_text: str = STOP_TEXT) -> str:
+ '''!
+ Return the text in the file at `path` between `start_text` and `stop_text`.
+ '''
+ current_file_contents = open(path).readlines()
+ generated_lines = []
+ in_generated = False
+ for line in current_file_contents:
+ if start_text in line:
+ in_generated = True
+ elif stop_text in line:
+ break
+ elif in_generated:
+ generated_lines.append(line)
+ return ''.join(generated_lines)
+
+
+class GNSSSignalTypeDefinition(NamedTuple):
+ satellite_type: SatelliteType
+ frequency_band: FrequencyBand
+ signal_name: SignalName
+ gnss_component: GNSSComponent = GNSSComponent.COMBINED
+
+
+# THIS IS THE CANONICAL DEFINITION OF GNSS SIGNAL TYPES.
+GNSS_SIGNAL_TYPE_DEFINITIONS = {
+ 'GPS_L1CA': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L1, GPSSignalName.L1CA),
+ 'GPS_L1P': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L1, GPSSignalName.L1P),
+ 'GPS_L1C': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L1, GPSSignalName.L1C),
+ 'GPS_L1C_D': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L1, GPSSignalName.L1C, GNSSComponent.DATA),
+ 'GPS_L1C_P': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L1, GPSSignalName.L1C, GNSSComponent.PILOT),
+ 'GPS_L2C': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L2, GPSSignalName.L2C),
+ 'GPS_L2C_M': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L2, GPSSignalName.L2C, GNSSComponent.DATA),
+ 'GPS_L2C_L': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L2, GPSSignalName.L2C, GNSSComponent.PILOT),
+ 'GPS_L2P': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L2, GPSSignalName.L2P),
+ 'GPS_L5': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L5, GPSSignalName.L5),
+ 'GPS_L5_I': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L5, GPSSignalName.L5, GNSSComponent.DATA),
+ 'GPS_L5_Q': GNSSSignalTypeDefinition(SatelliteType.GPS, FrequencyBand.L5, GPSSignalName.L5, GNSSComponent.PILOT),
+ 'GLONASS_L1CA': GNSSSignalTypeDefinition(SatelliteType.GLONASS, FrequencyBand.L1, GLONASSSignalName.L1CA),
+ 'GLONASS_L1P': GNSSSignalTypeDefinition(SatelliteType.GLONASS, FrequencyBand.L1, GLONASSSignalName.L1P),
+ 'GLONASS_L2CA': GNSSSignalTypeDefinition(SatelliteType.GLONASS, FrequencyBand.L2, GLONASSSignalName.L2CA),
+ 'GLONASS_L2P': GNSSSignalTypeDefinition(SatelliteType.GLONASS, FrequencyBand.L2, GLONASSSignalName.L2P),
+ 'GALILEO_E1A': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L1, GalileoSignalName.E1A),
+ 'GALILEO_E1BC': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L1, GalileoSignalName.E1BC),
+ 'GALILEO_E1B': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L1, GalileoSignalName.E1BC, GNSSComponent.DATA),
+ 'GALILEO_E1C': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L1, GalileoSignalName.E1BC, GNSSComponent.PILOT),
+ 'GALILEO_E5B': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L2, GalileoSignalName.E5B),
+ 'GALILEO_E5B_I': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L2, GalileoSignalName.E5B, GNSSComponent.DATA),
+ 'GALILEO_E5B_Q': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L2, GalileoSignalName.E5B, GNSSComponent.PILOT),
+ 'GALILEO_E5A': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L5, GalileoSignalName.E5A),
+ 'GALILEO_E5A_I': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L5, GalileoSignalName.E5A, GNSSComponent.DATA),
+ 'GALILEO_E5A_Q': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L5, GalileoSignalName.E5A, GNSSComponent.PILOT),
+ 'GALILEO_E6A': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L6, GalileoSignalName.E6A),
+ 'GALILEO_E6BC': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L6, GalileoSignalName.E6BC),
+ 'GALILEO_E6B': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L6, GalileoSignalName.E6BC, GNSSComponent.DATA),
+ 'GALILEO_E6C': GNSSSignalTypeDefinition(SatelliteType.GALILEO, FrequencyBand.L6, GalileoSignalName.E6BC, GNSSComponent.PILOT),
+ 'BEIDOU_B1I': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L1, BeiDouSignalName.B1I),
+ 'BEIDOU_B1C': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L1, BeiDouSignalName.B1C),
+ 'BEIDOU_B1C_D': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L1, BeiDouSignalName.B1C, GNSSComponent.DATA),
+ 'BEIDOU_B1C_P': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L1, BeiDouSignalName.B1C, GNSSComponent.PILOT),
+ 'BEIDOU_B2I': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L2, BeiDouSignalName.B2I),
+ 'BEIDOU_B2B': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L2, BeiDouSignalName.B2B),
+ 'BEIDOU_B2A': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L5, BeiDouSignalName.B2A),
+ 'BEIDOU_B2A_D': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L5, BeiDouSignalName.B2A, GNSSComponent.DATA),
+ 'BEIDOU_B2A_P': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L5, BeiDouSignalName.B2A, GNSSComponent.PILOT),
+ 'BEIDOU_B3I': GNSSSignalTypeDefinition(SatelliteType.BEIDOU, FrequencyBand.L6, BeiDouSignalName.B3I),
+ 'SBAS_L1CA': GNSSSignalTypeDefinition(SatelliteType.SBAS, FrequencyBand.L1, SBASSignalName.L1CA),
+ 'SBAS_L5': GNSSSignalTypeDefinition(SatelliteType.SBAS, FrequencyBand.L5, SBASSignalName.L5),
+ 'SBAS_L5_I': GNSSSignalTypeDefinition(SatelliteType.SBAS, FrequencyBand.L5, SBASSignalName.L5, GNSSComponent.DATA),
+ 'SBAS_L5_Q': GNSSSignalTypeDefinition(SatelliteType.SBAS, FrequencyBand.L5, SBASSignalName.L5, GNSSComponent.PILOT),
+ 'QZSS_L1CA': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L1, QZSSSignalName.L1CA),
+ 'QZSS_L1C': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L1, QZSSSignalName.L1C),
+ 'QZSS_L1C_D': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L1, QZSSSignalName.L1C, GNSSComponent.DATA),
+ 'QZSS_L1C_P': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L1, QZSSSignalName.L1C, GNSSComponent.PILOT),
+ 'QZSS_L2C': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L2C),
+ 'QZSS_L2C_M': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L2C, GNSSComponent.DATA),
+ 'QZSS_L2C_L': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L2C, GNSSComponent.PILOT),
+ 'QZSS_L5': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L5, QZSSSignalName.L5),
+ 'QZSS_L5_I': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L5, QZSSSignalName.L5, GNSSComponent.DATA),
+ 'QZSS_L5_Q': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L5, QZSSSignalName.L5, GNSSComponent.PILOT),
+ 'QZSS_L6': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L6),
+ 'QZSS_L6_M': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L6, GNSSComponent.DATA),
+ 'QZSS_L6_L': GNSSSignalTypeDefinition(SatelliteType.QZSS, FrequencyBand.L2, QZSSSignalName.L6, GNSSComponent.PILOT),
+}
+
+
+def gnss_signal_type_value_comment(name: str, definition: GNSSSignalTypeDefinition) -> str:
+ '''!
+ Get the comment string associated with a GNSSSignalType enum value.
+
+ For example:
+ GPS_L1CA -> "GPS L1 C/A: 4096 (0x1000)"
+ BEIDOU_B1C_D -> "BeiDou B1C-D (Data): 20485 (0x5005)"
+ '''
+ comment = _get_pretty_gnss_signal_type(
+ name,
+ definition.satellite_type,
+ definition.signal_name,
+ definition.gnss_component)
+ value = to_signal_val(*definition)
+ comment += f': {value} (0x{value:04X})'
+ return comment
+
+
+def find_nth(haystack: str, needle: str, n: int) -> int:
+ start = haystack.find(needle)
+ while start >= 0 and n > 1:
+ start = haystack.find(needle, start + len(needle))
+ n -= 1
+ return start
+
+
+def generate_gnss_signal_type_enum_values_source(comment_char: str, banner: Callable[[str], str],
+ value: Callable[[GNSSSignalTypeDefinition], str], use_commas=False) -> str:
+ '''!
+ Generate the body of the GNSSSignalType enum. Parameters are set to different values for generating either Python or
+ C++ code.
+ '''
+ comma_str = ',' if use_commas else ''
+ code = f'UNKNOWN = 0{comma_str}\n'
+
+ # The type ignores here are because our custom enum type doesn't report the right type when iterated over.
+ for sv_type in SatelliteType:
+ sv_matches = {k: v for k, v in GNSS_SIGNAL_TYPE_DEFINITIONS.items() if v.satellite_type == sv_type}
+ if len(sv_matches) > 0:
+ code += banner(pretty_print_gnss_enum(sv_type)) # type: ignore
+ for freq_band in FrequencyBand:
+ freq_matches = {k: v for k, v in sv_matches.items() if v.frequency_band == freq_band}
+ if len(freq_matches) > 0:
+ code += f'\n{(comment_char * 2)} {pretty_print_gnss_enum(freq_band)} Band\n\n' # type: ignore
+ for name, match in freq_matches.items():
+ code += f'{(comment_char * 3)} {gnss_signal_type_value_comment(name, match)}\n'
+ comma_str = ',' if use_commas else ''
+ declaration_str = f'{name} = {value(match)}{comma_str}\n'
+ # This is to decrease the line length by putting the arguments for the value onto multiple lines
+ # ```
+ # FOO(_1, _2, _3)
+ # vs
+ # FOO(_1, _2,
+ # _3)
+ param_start = declaration_str.index('(')
+ line_split = find_nth(declaration_str, ',', 2)
+ code += declaration_str[:line_split + 1] + '\n'
+ code += ' ' * param_start + declaration_str[line_split + 1:]
+
+ return code
+
+
+############################# Python Generation #############################
+
+
+def python_banner_str(msg: str) -> str:
+ '''!
+ Generate the string for a banner comment in Python.
+ '''
+ BANNER = '#' * 76
+ return f'\n{BANNER}\n## {msg}\n{BANNER}\n'
+
+
+def python_signal_enum_value_str(signal_def: GNSSSignalTypeDefinition) -> str:
+ '''!
+ Generate the string for the value assigned to a GNSSSignalType enum entry in Python.
+ '''
+ enum_names = tuple(e.name for e in signal_def)
+ FORMAT_STR = 'to_signal_val(SatelliteType.{}, FrequencyBand.{}, ' + \
+ type(signal_def.signal_name).__name__ + '.{}, GNSSComponent.{})'
+ return FORMAT_STR.format(*enum_names)
+
+
+############################# CPP Generation #############################
+
+def cpp_multiline_comment(contents: str) -> str:
+ '''!
+ Generate the string for a multiline C++ comment.
+ '''
+ lines = [(f' * {line}' if line else ' *') + '\n' for line in contents.split('\n')]
+ return '/**\n' + ''.join(lines) + ' */'
+
+
+def cpp_banner_str(msg: str) -> str:
+ '''!
+ Generate the string for a banner comment in C++.
+ '''
+ BANNER = '/' * 78
+ return f'\n{BANNER}\n// {msg}\n{BANNER}\n'
+
+
+def cpp_signal_enum_value_str(signal_def: GNSSSignalTypeDefinition) -> str:
+ '''!
+ Generate the string for the value assigned to a GNSSSignalType enum entry in C++.
+ '''
+ enum_names = tuple(e.name for e in signal_def)
+ FORMAT_STR = 'ToSignalVal(SatelliteType::{}, FrequencyBand::{}, ' + \
+ type(signal_def.signal_name).__name__ + '::{}, GNSSComponent::{})'
+ return FORMAT_STR.format(*enum_names)
+
+
+class EnumValue(NamedTuple):
+ comment: str
+ value: str
+
+
+_PYTHON_COMMENT_RE = re.compile(r'\s*#+\s*(.*)')
+_PYTHON_ENUM_VALUE_RE = re.compile(r'\s*([A-Z0-9_a-z]+) = (.+)')
+
+
+E = TypeVar('E', bound=IntEnum)
+
+
+def get_enum_values(enum_type: type[E]) -> Optional[dict[E, EnumValue]]:
+ '''!
+ Parse contents from Python enums to be used to generate C++ enums.
+ '''
+ source_lines = inspect.getsourcelines(enum_type)
+ comment = []
+ values: dict[E, EnumValue] = {}
+ enum_value_names = {e.name for e in enum_type}
+ for line in source_lines[0]:
+ comment_match = _PYTHON_COMMENT_RE.match(line)
+ if comment_match:
+ comment_line = comment_match.group(1)
+ # Skip empty lines at start of comment (used for doxygen formatting).
+ if len(comment) != 0 or len(comment_line.strip()) != 0:
+ comment.append(comment_match.group(1))
+ continue
+
+ value_match = _PYTHON_ENUM_VALUE_RE.match(line)
+ if value_match:
+ value_name = value_match.group(1)
+ value_def = value_match.group(2)
+ if value_name in enum_value_names:
+ values[enum_type[value_name]] = EnumValue('\n'.join(comment), value_def)
+ else:
+ print(f'Matched invalid enum value {value_name} while parsing {enum_type}')
+ return None
+
+ comment = []
+
+ for val in enum_type:
+ if val not in values:
+ print(f'Missed enum value {val.name} while parsing {enum_type}')
+ return None
+
+ return values
+
+
+def generate_cpp_enum_declaration(name: str, comment: Optional[str], int_type: str, values_code: str) -> str:
+ code = ''
+ if comment:
+ code += cpp_multiline_comment(comment) + '\n'
+ code += f'''\
+enum class {name} : {int_type} {{
+{indent(values_code, CPP_INDENT)}}};'''
+ return code
+
+
+def strip_python_comment(comment: Optional[str]) -> str:
+ '''!
+ Remove leading hashes and space from python comment.
+ If the comment is `None` return an empty string.
+ '''
+ if comment is None:
+ return ''
+ _PYTHON_COMMENT_PREFIX_RE = re.compile(r'^\s*#+ ?', re.MULTILINE)
+ return re.sub(_PYTHON_COMMENT_PREFIX_RE, '', comment).strip()
+
+
+def generate_cpp_enum_from_python(enum_type: type[IntEnum], int_type='uint8_t') -> str:
+ '''!
+ Generate a the full C++ enum source code from an existing Python enum.
+ '''
+ values = get_enum_values(enum_type)
+ assert values
+ comment = strip_python_comment(inspect.getcomments(enum_type))
+ values_code = ''
+ for key, value in values.items():
+ if value.comment:
+ values_code += ''.join(f'// {line.strip()}\n' for line in value.comment.split('\n'))
+ values_code += f'{key} = {value.value},\n'
+
+ return generate_cpp_enum_declaration(enum_type.__name__, comment, int_type, values_code)
+
+
+def generate_cpp_to_string(enum_type: type[_GNSSSignalPartType], pretty_values: dict[str, str]) -> str:
+ '''!
+ Generate the C++ source code for the enum `to_string`, `operator<<` and `ToPrettyString` functions.
+ '''
+ name = enum_type.__name__
+ to_string_cases = '\n\n'.join(f'''\
+case {name}::{k}:
+ return "{k}";''' for k in pretty_values.keys())
+
+ to_pretty_string_cases = '\n\n'.join(f'''\
+case {name}::{k}:
+ return "{p}";''' for k, p in pretty_values.items()) # type: ignore
+
+ return f'''\
+/**
+ * @brief Get a string representation of the @ref {name} enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string({name} type) {{
+ switch (type) {{
+{indent(to_string_cases, ' ')}
+ }}
+ return "INVALID";
+}}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString({name} type) {{ return to_string(type); }}
+
+/**
+ * @brief @ref {name} stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, {name} type) {{
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref {name}.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString({name} type) {{
+ switch (type) {{
+{indent(to_pretty_string_cases, ' ')}
+ }}
+ return "Invalid";
+}}'''
+
+
+def get_gnss_enum_cpp_name(enum_type: type[_GNSSSignalPartType]) -> str:
+ '''!
+ Convert a Python signal part type to a C++ constant prefix: `SatelliteType` -> "SATELLITE_TYPE".
+ '''
+ name = enum_type.__name__
+ # Reference: https://stackoverflow.com/a/29920015
+ def _camel_case_split(identifier):
+ matches = re.finditer('.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', identifier)
+ return [m.group(0) for m in matches]
+ cpp_name = ('_'.join(m.upper() for m in _camel_case_split(name)))
+ return cpp_name
+
+
+def generate_cpp_get_part(enum_type: type[_GNSSSignalPartType]) -> str:
+ '''!
+ Generate the C++ source code for getting the enum value of a component type of @ref GNSSSignalType. An example
+ function would be `GetSatelliteTypePart`.
+ '''
+ name = enum_type.__name__
+ # Note: All signal name enums (GPSSignalName, etc.) use the same SIGNAL_NAME_SHIFT/BITS with no leading name (GPS,
+ # etc.).
+ cpp_prefix = get_gnss_enum_cpp_name(SignalName if issubclass(enum_type, SignalName) else enum_type)
+ return f'''\
+/**
+ * @brief Extract the @ref {name} enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref {name} will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC {name} Get{name}(GNSSSignalType signal_type) {{
+ return static_cast<{name}>(
+ static_cast((static_cast(signal_type) >> {cpp_prefix}_SHIFT) & ((1 << {cpp_prefix}_BITS) - 1)));
+}}'''
+
+
+def generate_cpp_gnss_signal_type() -> str:
+ '''!
+ Generate the C++ source code for @ref GNSSSignalType. Unlike other enums, this is generated from @ref
+ GNSS_SIGNAL_TYPE_DEFINITIONS rather then from parsing the Python enum source code.
+ '''
+ # Turn formatting of to prevent formatter from modifying line breaks.
+ values_code = '// clang-format off\n'
+ values_code += generate_gnss_signal_type_enum_values_source(
+ '/', cpp_banner_str, cpp_signal_enum_value_str, use_commas=True)
+ values_code += '// clang-format on\n'
+ comment = strip_python_comment(inspect.getcomments(GNSSSignalType))
+ return generate_cpp_enum_declaration('GNSSSignalType', comment, 'uint16_t', values_code)
+
+
+def generate_cpp_constants_defs() -> str:
+ '''!
+ Generate the top constants portion C++ source code (shift and size constants).
+ '''
+ def _make_constants(enum_type: type[_GNSSSignalPartType]):
+ cpp_prefix = get_gnss_enum_cpp_name(enum_type)
+ bit_packing = _get_gnss_enum_bit_packing(enum_type) # type: ignore
+ return f'''\
+constexpr unsigned {cpp_prefix}_SHIFT = {bit_packing.bit_offset};
+constexpr unsigned {cpp_prefix}_BITS = {bit_packing.bit_len};
+'''
+ return ''.join(_make_constants(enum_type) for enum_type in SIGNAL_PART_ENUM_LIST)
+
+
+def generate_cpp_signal_defs() -> str:
+ '''!
+ Generate all the generated C++ source code. This includes enum definitions and helper functions.
+ '''
+ def _make_functions(enum_type: type[_GNSSSignalPartType]):
+ if enum_type == GNSSSignalType:
+ pretty_values = {
+ k: _get_pretty_gnss_signal_type(
+ k,
+ v.satellite_type,
+ v.signal_name,
+ v.gnss_component) for k,
+ v in GNSS_SIGNAL_TYPE_DEFINITIONS.items()}
+ pretty_values['UNKNOWN'] = 'Unknown'
+ return generate_cpp_gnss_signal_type() + '\n\n' + generate_cpp_to_string(enum_type, pretty_values) + '\n\n'
+ else:
+ pretty_values = {k.name: pretty_print_gnss_enum(k) for k in enum_type} # type: ignore
+ return generate_cpp_enum_from_python(enum_type) + '\n\n' \
+ + generate_cpp_to_string(enum_type, pretty_values) + '\n\n' \
+ + generate_cpp_get_part(enum_type) + '\n\n'
+
+ return ''.join(_make_functions(enum_type) for enum_type in ENUM_LIST)
+
+
+############################# Entry Point #############################
+
+def update_python_code(check_only=False) -> bool:
+ """!
+ @brief Generate source code contents for @ref GNSSSignalType.
+
+ If `not check_only` then replace the contents in fusion_engine_client/messages/signal_defs.py with the newly
+ generated values.
+
+ @param check_only If `True`, only return whether the generated contents differ from what's currently save and do not
+ update the file contents.
+
+ @return `True` if newly generated code differed from the previously generated code.
+ """
+ generated_python_code = '\n' + generate_gnss_signal_type_enum_values_source(
+ '#', python_banner_str, python_signal_enum_value_str) + '\n'
+ generated_python_code = indent(generated_python_code, PYTHON_INDENT)
+ differs = get_generated_code_in_file(PYTHON_SIGNAL_DEFS_PATH) != generated_python_code
+ if differs:
+ if check_only:
+ print('Python file out of date.')
+ else:
+ print('Updating generated Python code.')
+ update_generated_code_in_file(PYTHON_SIGNAL_DEFS_PATH, generated_python_code)
+ else:
+ print('Generated Python code matches current file contents.')
+ return differs
+
+
+def format_cpp_code(cpp_source: str) -> str:
+ result = subprocess.run(['clang-format', '-style=file'], cwd=os.path.dirname(os.path.abspath(__file__)),
+ input=cpp_source, capture_output=True, encoding='utf-8')
+ if result.returncode == 0:
+ return result.stdout
+ else:
+ raise RuntimeError(f'clang-format exited with code {result.returncode}: {result.stderr}')
+
+
+def update_cpp_code(check_only=False) -> bool:
+ """!
+ @brief Generate source code contents for @ref SatelliteType, @ref FrequencyBand, @ref GNSSComponent, @ref
+ GPSSignalName, @ref GLONASSSignalName, @ref BeiDouSignalName, @ref GalileoSignalName, @ref SBASSignalName,
+ @ref QZSSSignalName, and @ref GNSSSignalType.
+
+ If `not check_only` then replace the contents in src/point_one/fusion_engine/messages/signal_defs.h with the newly
+ generated values.
+
+ @param check_only If `True`, only return whether the generated contents differ from what's currently save and do not
+ update the file contents.
+
+ @return `True` if newly generated code differed from the previously generated code.
+ """
+ # The code we extract from the existing C++ file has been formatted with clang-format. We also need to format the
+ # generated code so the != check works, and so the updated C++ file is formatted correctly.
+ generated_constants_cpp_code = '\n' + format_cpp_code(generate_cpp_constants_defs()) + '\n'
+ current_constants_cpp_code = get_generated_code_in_file(CPP_SIGNAL_DEFS_PATH,
+ start_text=CPP_CONSTANTS_START_TEXT,
+ stop_text=CPP_CONSTANTS_STOP_TEXT)
+ constants_differs = generated_constants_cpp_code != current_constants_cpp_code
+
+ generated_signals_cpp_code = '\n' + format_cpp_code(generate_cpp_signal_defs()) + '\n'
+ current_signals_cpp_code = get_generated_code_in_file(CPP_SIGNAL_DEFS_PATH)
+ signals_differs = generated_signals_cpp_code != current_signals_cpp_code
+
+ differs = constants_differs or signals_differs
+ if differs:
+ if check_only:
+ print('C++ file out of date.')
+ else:
+ print('Updating generated C++ code.')
+ update_generated_code_in_file(CPP_SIGNAL_DEFS_PATH, generated_constants_cpp_code,
+ start_text=CPP_CONSTANTS_START_TEXT, stop_text=CPP_CONSTANTS_STOP_TEXT)
+ update_generated_code_in_file(CPP_SIGNAL_DEFS_PATH, generated_signals_cpp_code)
+ else:
+ print('Generated C++ code matches current file contents.')
+ return differs
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser(description="""\
+Update the auto-generated source code after making changes to the signal
+definitions in signal_defs.py.
+""")
+
+ parser.add_argument(
+ '--check-only', action='store_true',
+ help="Check if any of the source files are out of date but do not update them. The application will exit with "
+ "a non-zero exit code if one or more source files is outdated.")
+
+ options = parser.parse_args()
+
+ differs = False
+ differs |= update_python_code(check_only=options.check_only)
+ differs |= update_cpp_code(check_only=options.check_only)
+
+ if options.check_only and differs:
+ sys.exit(1)
diff --git a/python/fusion_engine_client/messages/signal_defs.py b/python/fusion_engine_client/messages/signal_defs.py
index 3544bc3b..3f64dc91 100644
--- a/python/fusion_engine_client/messages/signal_defs.py
+++ b/python/fusion_engine_client/messages/signal_defs.py
@@ -1,11 +1,23 @@
+import functools
import re
-from typing import List, Union
+from typing import NamedTuple, Optional, TypeAlias, TypeVar, Union
import numpy as np
from ..utils.enum_utils import IntEnum, enum_bitmask
+############################################################################
+## Signal Type Component Enums
+############################################################################
+
+##
+# @brief System/constellation type definitions.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 4 bits so no values above 15 are allowed.
class SatelliteType(IntEnum):
UNKNOWN = 0
GPS = 1
@@ -37,291 +49,987 @@ class SatelliteType(IntEnum):
@enum_bitmask(SatelliteType)
class SatelliteTypeMask:
- ALL = 0xFFFFFFFF
+ ALL = 0xFFFF
class SignalType(IntEnum):
UNKNOWN = 0
+## @brief GNSS frequency band definitions.
+#
+# A frequency band generally includes multiple GNSS carrier frequencies and
+# signal types, which can usually be captured by a single antenna element.
+# For example, an L1 antenna typically has sufficient bandwidth to capture
+# signals on the BeiDou B1I (1561.098 MHz), GPS L1 (1575.42 MHz), and GLONASS G1
+# (1602.0 MHz) frequencies.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 4 bits so no values about 15 are allowed.
class FrequencyBand(IntEnum):
- UNKNOWN = 0
- L1 = 1
- L2 = 2
- L5 = 5
- L6 = 6
+ ## ~L1 = 1561.098 MHz (B1) -> 1602.0 (G1)
+ # Includes: GPS L1, Galileo E1, BeiDou B1I (and B1C == L1), GLONASS G1
+ L1 = 0
+ ## ~L2 = 1202.025 MHz (G3) -> 1248.06 (G2)
+ ## Includes: GPS L2, Galileo E5b, BeiDou B2I, GLONASS G2 & G3
+ L2 = 1
+ ##
+ # ~L5 = 1176.45 MHz (L5)
+ # Includes: GPS L5, Galileo E5a, BeiDou B2a, IRNSS L5
+ L5 = 2
+ ## ~L6 = 1268.52 MHz (B3) -> 1278.75 MHz (L6)
+ # Includes: Galileo E6, BeiDou B3, QZSS L6
+ L6 = 3
+ ## ~(L1 - L2) = 296.67 MHz (QZSS L6) -> 373.395 MHz (G3)
+ L1_L2_WIDE_LANE = 4
+ ## ~(L1 - L5) = 398.97 MHz (L5)
+ L1_L5_WIDE_LANE = 5
+ ## S band 2.0 -> 4.0 GHz
+ # IRNSS S band is 2492.028 MHz
+ S = 6
@enum_bitmask(FrequencyBand)
class FrequencyBandMask:
- ALL = 0xFFFFFFFF
+ ALL = 0xFFFF
_SHORT_FORMAT = re.compile(r'([%s])(\d+)(?:\s+(\w+))?' % ''.join(SatelliteTypeCharReverse.keys()))
_LONG_FORMAT = re.compile(r'(\w+)(?:\s+(\w+))(?:\s+PRN\s+(\d+))?')
+##
+# @brief Groupings encapsulating the form of the signal broadcast by a
+# particular constellation.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class SignalName(IntEnum):
+ pass
+
+
+##
+# @brief The name of a signal from a GPS satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class GPSSignalName(SignalName):
+ L1CA = 0
+ L1P = 1
+ L1C = 2
+ L2C = 3
+ L2P = 4
+ L5 = 5
-def decode_signal_id(signal_id):
- """!
- @brief Convert a hashed signal ID to its components: system, signal type, and PRN.
- @param signal_id The signal hash, or a tuple containing enumeration values or
- string names.
+@enum_bitmask(GPSSignalName)
+class GPSSignalNameMask:
+ ALL = 0xFF
+
+##
+# @brief The name of a signal from a GLONASS satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class GLONASSSignalName(SignalName):
+ L1CA = 0
+ L1P = 1
+ L2CA = 2
+ L2P = 3
+
+
+@enum_bitmask(GPSSignalName)
+class GLONASSSignalNameMask:
+ ALL = 0xFF
+
+
+##
+# @brief The name of a signal from a Galileo satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class GalileoSignalName(SignalName):
+ E1A = 0
+ E1BC = 1
+ E5A = 2
+ E5B = 3
+ E6A = 4
+ E6BC = 5
+
+
+@enum_bitmask(GPSSignalName)
+class GalileoSignalNameMask:
+ ALL = 0xFF
+
+
+##
+# @brief The name of a signal from a BeiDou satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class BeiDouSignalName(SignalName):
+ B1I = 0
+ B1C = 1
+ B2I = 2
+ B2B = 3
+ B2A = 4
+ B3I = 5
+
+
+@enum_bitmask(GPSSignalName)
+class BeiDouSignalNameMask:
+ ALL = 0xFF
+
+
+##
+# @brief The name of a signal from a SBAS satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class SBASSignalName(SignalName):
+ L1CA = 0
+ L5 = 1
+
+
+@enum_bitmask(GPSSignalName)
+class BeiDouSignalNameMask:
+ ALL = 0xFF
+
+
+##
+# @brief The name of a signal from a QZSS satellite.
+#
+# For some purposes, this enum may be used as part of a bitmask. See
+# @ref gnss_enums_to_bitmasks.
+#
+# This needs to be packed into 3 bits so no values above 7 are allowed.
+class QZSSSignalName(SignalName):
+ L1CA = 0
+ L1C = 1
+ L2C = 2
+ L5 = 3
+ L6 = 4
+
+
+@enum_bitmask(GPSSignalName)
+class QZSSSignalNameMask:
+ ALL = 0xFF
+
+
+##
+# @brief The component being tracked for signals that have separate data and
+# pilot components.
+#
+# This needs to be packed into 2 bits so no values above 3 are allowed.
+class GNSSComponent(IntEnum):
+ COMBINED = 0
+ DATA = 1
+ PILOT = 2
+
+############################################################################
+## INTERNAL HELPER FUNCTIONS
+############################################################################
+
+
+class _BitPacking(NamedTuple):
+ bit_offset: int
+ bit_len: int
+
+
+## This defines the bit packing for the component values in @ref GNSSSignalType
+_GNSS_SIGNAL_TYPE_PARTS = {
+ SatelliteType: _BitPacking(12, 4),
+ # Reserved: _BitPacking(10, 2),
+ FrequencyBand: _BitPacking(6, 4),
+ # Reserved: _BitPacking(5, 1),
+ SignalName: _BitPacking(2, 3),
+ GNSSComponent: _BitPacking(0, 2),
+}
- @return A tuple containing the @ref SatelliteType, @ref SignalType, and PRN (@c int).
- """
- if isinstance(signal_id, (int, float)) or np.issubdtype(signal_id, np.integer):
- signal_id = int(signal_id)
+_SIGNAL_NAME_ENUM_MAP = {
+ SatelliteType.GPS: GPSSignalName,
+ SatelliteType.GLONASS: GLONASSSignalName,
+ SatelliteType.GALILEO: GalileoSignalName,
+ SatelliteType.BEIDOU: BeiDouSignalName,
+ SatelliteType.SBAS: SBASSignalName,
+ SatelliteType.QZSS: QZSSSignalName,
+}
- system = int(signal_id / 1000000)
- signal_id -= system * 1000000
+_GNSSSignalPartType: TypeAlias = SatelliteType | FrequencyBand | SignalName | GNSSComponent
- signal_type = int(signal_id / 1000)
- signal_id -= signal_type * 1000
- prn = signal_id
+def _get_gnss_enum_bit_packing(cls: type[_GNSSSignalPartType]) -> _BitPacking:
+ '''!
+ Get the bit packing for an enum component of @ref GNSSSignalType
+ '''
+ cls = SignalName if issubclass(cls, SignalName) else cls
+ return _GNSS_SIGNAL_TYPE_PARTS[cls]
- return SatelliteType(system), None, prn
- elif isinstance(signal_id, (tuple, list, set)):
- if len(signal_id) != 3:
- raise ValueError('Tuple must contain 3 elements: system, signal type, and PRN.')
- else:
- system, signal_type, prn = signal_id
- if isinstance(system, (int, float)) or np.issubdtype(system, np.integer):
- system = SatelliteType(int(system))
- elif isinstance(system, str):
- system = SatelliteType[system]
+_T = TypeVar('_T', SatelliteType, FrequencyBand, SignalName, GNSSComponent)
- # if isinstance(signal_type, (int, float)) or np.issubdtype(signal_type, np.integer):
- # signal_type = SignalType(int(signal_type))
- # elif isinstance(signal_type, str):
- # signal_type = SignalType[signal_type]
- return system, signal_type, prn
- else:
- raise ValueError('Unexpected input format (%s).' % type(signal_id))
+def _get_signal_part(signal: 'GNSSSignalType', cls: type[_T], raise_on_unrecognized: bool = True) -> _T:
+ '''!
+ Return the value for an enum component of @ref GNSSSignalType
+ @param signal The full signal type to get a component from.
+ @param cls The enum class to get the value for (e.g., @ref SatelliteType, @ref GalileoSignalName).
+ @param raise_on_unrecognized If `False`, insert a new enum element on unrecognized values. Otherwise, raise an
+ exception.
-def encode_signal_id(system, signal_type=None, prn=None):
- """!
- @param Encode a signal description into a numeric signal ID.
+ @return The component enum value.
+ '''
+ bit_packing = _get_gnss_enum_bit_packing(cls)
+ value = (int(signal) >> bit_packing.bit_offset) & ((1 << bit_packing.bit_len) - 1)
+ return cls(value, raise_on_unrecognized=raise_on_unrecognized)
- If @c signal and @c prn are @c None, @c system will be interpreted as one of the following options:
- -# A dictionary containing any of the following keys: `system`, `signal_type`, `prn`
- -# An object with `system`, `signal_type`, and `prn` member variables
- -# A tuple containing all three values, with system and signal type values stored as integers/enumeration values or
- string names
- @param system The @ref SatelliteType of the signal, or one of the options listed above.
- @param signal_type The @ref SignalType of the signal.
- @param prn The signal's PRN.
+def _shift_enum_value(value: _GNSSSignalPartType) -> int:
+ '''!
+ Get the integer value for a component of @ref GNSSSignalType shifted into the bits it will occupy in the @ref
+ GNSSSignalType value.
+s
+ @param value The value of the enum component.
- @return The encoded ID value.
- """
- if signal_type is None and prn is None:
- # If it's a tuple/list, extract the components from it.
- if isinstance(system, (tuple, list, set)):
- if len(system) == 3:
- system, signal_type, prn = system
- else:
- raise ValueError('Must specify a tuple containing system, signal type, and PRN.')
- # Similar for a dictionary or an object.
- elif isinstance(system, (object, dict)):
- signal_details = system
- if isinstance(signal_details, object):
- signal_details = signal_details.__dict__
-
- system = signal_details.get('system', SatelliteType.UNKNOWN)
- signal_type = signal_details.get('signal_type', SignalType.UNKNOWN)
- prn = signal_details.get('prn', 0)
- # If system is an integer, assume it's a complete signal ID and return it as is.
- elif isinstance(system, (int, float)) or np.issubdtype(system, np.integer):
- return int(system)
- else:
- raise ValueError('Unexpected input format.')
+ @return The shifted value.
+ '''
+ return int(value) << _get_gnss_enum_bit_packing(type(value)).bit_offset
- if system is None:
- system = int(SatelliteType.UNKNOWN)
- elif isinstance(system, str):
- system = int(SatelliteType[system])
- elif isinstance(system, SatelliteType):
- system = int(system)
- if signal_type is None:
- signal_type = int(SignalType.UNKNOWN)
- # elif isinstance(signal_type, str):
- # signal_type = int(SignalType[signal_type])
- # elif isinstance(signal_type, SignalType):
- # signal_type = int(signal_type)
+def _get_pretty_gnss_signal_type(name: str, sv_type: SatelliteType, signal_name: SignalName,
+ component: GNSSComponent, omit_component_hint: bool = False) -> str:
+ '''!
+ Get a human readable string to describe the components of a GNSSSignalType value.
+ '''
+ if sv_type is None:
+ sat_str = ''
+ else:
+ sat_str = pretty_print_gnss_enum(sv_type) + ' '
+ signal_str = pretty_print_gnss_enum(signal_name)
+ component_str = ''
+ if component != GNSSComponent.COMBINED:
+ if signal_str.endswith('B/C'):
+ end_char = 'B' if component == GNSSComponent.DATA else 'C'
+ signal_str = signal_str[:-3] + end_char
+ if name[-2] == '_' and name[-1] in ('D', 'P', 'I', 'Q', 'M', 'L'):
+ signal_str += '-' + name[-1]
+ if not omit_component_hint:
+ component_str = f' ({pretty_print_gnss_enum(component)})'
+ return sat_str + signal_str + component_str
+
+
+def to_signal_val(sv_type: SatelliteType, freq_band: FrequencyBand, signal_name: SignalName,
+ component=GNSSComponent.COMBINED) -> int:
+ '''!
+ Combine the component enums into the integer value used in @ref GNSSSignalType.
+
+ @note This is used in the definition of @ref GNSSSignalType. This means that this function and any functions it
+ references must be defined earlier in the file then @ref GNSSSignalType.
+ '''
+ return (_shift_enum_value(sv_type) | _shift_enum_value(freq_band) |
+ _shift_enum_value(signal_name) | _shift_enum_value(component))
+
+
+def to_signal_type(sv_type: SatelliteType, freq_band: FrequencyBand, signal_name: SignalName,
+ component=GNSSComponent.COMBINED) -> 'GNSSSignalType':
+ '''!
+ Combine the component enums into the @ref GNSSSignalType.
+
+ @warning This will throw an exception if this combination of values isn't defined in @ref GNSSSignalType.
+ '''
+ return GNSSSignalType(to_signal_val(sv_type, freq_band, signal_name, component))
+
+##
+# @brief Representation of the combination of GNSS constellation, signal type,
+# and component being tracked (pilot/data).
+#
+# This `enum` is organized as a bitmask, defined as follows:
+# ```
+# { SatelliteType (4b), Reserved (2b), FrequencyBand (4b), Reserved (1b),
+# *SignalName (3b), GNSSComponent (2b) }
+# ```
+#
+# The `*SignalName` mappings are specific to each @ref SatelliteType. For
+# example, use @ref GPSSignalName for GPS signal definitions.
+#
+# Each enumeration entry uniquely identifies an individual GNSS signal being
+# tracked by the GNSS receiver. For signals that have separate data and pilot
+# components, the entry indicates which component is being tracked.
+class GNSSSignalType(IntEnum):
+ ## > Start Autogenerated Types (See python/fusion_engine_client/messages/signal_def_gen.py)
- return system * 1000000 + signal_type * 1000 + prn
+ UNKNOWN = 0
+ ############################################################################
+ ## GPS
+ ############################################################################
+
+ ## L1 Band
+
+ ### GPS C/A: 4096 (0x1000)
+ GPS_L1CA = to_signal_val(SatelliteType.GPS, FrequencyBand.L1,
+ GPSSignalName.L1CA, GNSSComponent.COMBINED)
+ ### GPS L1 P(Y): 4100 (0x1004)
+ GPS_L1P = to_signal_val(SatelliteType.GPS, FrequencyBand.L1,
+ GPSSignalName.L1P, GNSSComponent.COMBINED)
+ ### GPS L1C: 4104 (0x1008)
+ GPS_L1C = to_signal_val(SatelliteType.GPS, FrequencyBand.L1,
+ GPSSignalName.L1C, GNSSComponent.COMBINED)
+ ### GPS L1C-D (Data): 4105 (0x1009)
+ GPS_L1C_D = to_signal_val(SatelliteType.GPS, FrequencyBand.L1,
+ GPSSignalName.L1C, GNSSComponent.DATA)
+ ### GPS L1C-P (Pilot): 4106 (0x100A)
+ GPS_L1C_P = to_signal_val(SatelliteType.GPS, FrequencyBand.L1,
+ GPSSignalName.L1C, GNSSComponent.PILOT)
+
+ ## L2 Band
+
+ ### GPS L2C: 4172 (0x104C)
+ GPS_L2C = to_signal_val(SatelliteType.GPS, FrequencyBand.L2,
+ GPSSignalName.L2C, GNSSComponent.COMBINED)
+ ### GPS L2C-M (Data): 4173 (0x104D)
+ GPS_L2C_M = to_signal_val(SatelliteType.GPS, FrequencyBand.L2,
+ GPSSignalName.L2C, GNSSComponent.DATA)
+ ### GPS L2C-L (Pilot): 4174 (0x104E)
+ GPS_L2C_L = to_signal_val(SatelliteType.GPS, FrequencyBand.L2,
+ GPSSignalName.L2C, GNSSComponent.PILOT)
+ ### GPS L2 P(Y): 4176 (0x1050)
+ GPS_L2P = to_signal_val(SatelliteType.GPS, FrequencyBand.L2,
+ GPSSignalName.L2P, GNSSComponent.COMBINED)
+
+ ## L5 Band
+
+ ### GPS L5: 4244 (0x1094)
+ GPS_L5 = to_signal_val(SatelliteType.GPS, FrequencyBand.L5,
+ GPSSignalName.L5, GNSSComponent.COMBINED)
+ ### GPS L5-I (Data): 4245 (0x1095)
+ GPS_L5_I = to_signal_val(SatelliteType.GPS, FrequencyBand.L5,
+ GPSSignalName.L5, GNSSComponent.DATA)
+ ### GPS L5-Q (Pilot): 4246 (0x1096)
+ GPS_L5_Q = to_signal_val(SatelliteType.GPS, FrequencyBand.L5,
+ GPSSignalName.L5, GNSSComponent.PILOT)
+
+ ############################################################################
+ ## GLONASS
+ ############################################################################
+
+ ## L1 Band
+
+ ### GLONASS L1 C/A: 8192 (0x2000)
+ GLONASS_L1CA = to_signal_val(SatelliteType.GLONASS, FrequencyBand.L1,
+ GLONASSSignalName.L1CA, GNSSComponent.COMBINED)
+ ### GLONASS L1P: 8196 (0x2004)
+ GLONASS_L1P = to_signal_val(SatelliteType.GLONASS, FrequencyBand.L1,
+ GLONASSSignalName.L1P, GNSSComponent.COMBINED)
+
+ ## L2 Band
+
+ ### GLONASS L2 C/A: 8264 (0x2048)
+ GLONASS_L2CA = to_signal_val(SatelliteType.GLONASS, FrequencyBand.L2,
+ GLONASSSignalName.L2CA, GNSSComponent.COMBINED)
+ ### GLONASS L2P: 8268 (0x204C)
+ GLONASS_L2P = to_signal_val(SatelliteType.GLONASS, FrequencyBand.L2,
+ GLONASSSignalName.L2P, GNSSComponent.COMBINED)
+
+ ############################################################################
+ ## Galileo
+ ############################################################################
+
+ ## L1 Band
+
+ ### Galileo E1-A: 16384 (0x4000)
+ GALILEO_E1A = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L1,
+ GalileoSignalName.E1A, GNSSComponent.COMBINED)
+ ### Galileo E1-B/C: 16388 (0x4004)
+ GALILEO_E1BC = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L1,
+ GalileoSignalName.E1BC, GNSSComponent.COMBINED)
+ ### Galileo E1-B (Data): 16389 (0x4005)
+ GALILEO_E1B = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L1,
+ GalileoSignalName.E1BC, GNSSComponent.DATA)
+ ### Galileo E1-C (Pilot): 16390 (0x4006)
+ GALILEO_E1C = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L1,
+ GalileoSignalName.E1BC, GNSSComponent.PILOT)
+
+ ## L2 Band
+
+ ### Galileo E5b: 16460 (0x404C)
+ GALILEO_E5B = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L2,
+ GalileoSignalName.E5B, GNSSComponent.COMBINED)
+ ### Galileo E5b-I (Data): 16461 (0x404D)
+ GALILEO_E5B_I = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L2,
+ GalileoSignalName.E5B, GNSSComponent.DATA)
+ ### Galileo E5b-Q (Pilot): 16462 (0x404E)
+ GALILEO_E5B_Q = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L2,
+ GalileoSignalName.E5B, GNSSComponent.PILOT)
+
+ ## L5 Band
+
+ ### Galileo E5a: 16520 (0x4088)
+ GALILEO_E5A = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L5,
+ GalileoSignalName.E5A, GNSSComponent.COMBINED)
+ ### Galileo E5a-I (Data): 16521 (0x4089)
+ GALILEO_E5A_I = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L5,
+ GalileoSignalName.E5A, GNSSComponent.DATA)
+ ### Galileo E5a-Q (Pilot): 16522 (0x408A)
+ GALILEO_E5A_Q = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L5,
+ GalileoSignalName.E5A, GNSSComponent.PILOT)
+
+ ## L6 Band
+
+ ### Galileo E6-A: 16592 (0x40D0)
+ GALILEO_E6A = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L6,
+ GalileoSignalName.E6A, GNSSComponent.COMBINED)
+ ### Galileo E6-B/C: 16596 (0x40D4)
+ GALILEO_E6BC = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L6,
+ GalileoSignalName.E6BC, GNSSComponent.COMBINED)
+ ### Galileo E6-B (Data): 16597 (0x40D5)
+ GALILEO_E6B = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L6,
+ GalileoSignalName.E6BC, GNSSComponent.DATA)
+ ### Galileo E6-C (Pilot): 16598 (0x40D6)
+ GALILEO_E6C = to_signal_val(SatelliteType.GALILEO, FrequencyBand.L6,
+ GalileoSignalName.E6BC, GNSSComponent.PILOT)
+
+ ############################################################################
+ ## BeiDou
+ ############################################################################
+
+ ## L1 Band
+
+ ### BeiDou B1I: 20480 (0x5000)
+ BEIDOU_B1I = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L1,
+ BeiDouSignalName.B1I, GNSSComponent.COMBINED)
+ ### BeiDou B1C: 20484 (0x5004)
+ BEIDOU_B1C = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L1,
+ BeiDouSignalName.B1C, GNSSComponent.COMBINED)
+ ### BeiDou B1C-D (Data): 20485 (0x5005)
+ BEIDOU_B1C_D = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L1,
+ BeiDouSignalName.B1C, GNSSComponent.DATA)
+ ### BeiDou B1C-P (Pilot): 20486 (0x5006)
+ BEIDOU_B1C_P = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L1,
+ BeiDouSignalName.B1C, GNSSComponent.PILOT)
+
+ ## L2 Band
+
+ ### BeiDou B2I: 20552 (0x5048)
+ BEIDOU_B2I = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L2,
+ BeiDouSignalName.B2I, GNSSComponent.COMBINED)
+ ### BeiDou B2b: 20556 (0x504C)
+ BEIDOU_B2B = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L2,
+ BeiDouSignalName.B2B, GNSSComponent.COMBINED)
+
+ ## L5 Band
+
+ ### BeiDou B2a: 20624 (0x5090)
+ BEIDOU_B2A = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L5,
+ BeiDouSignalName.B2A, GNSSComponent.COMBINED)
+ ### BeiDou B2a-D (Data): 20625 (0x5091)
+ BEIDOU_B2A_D = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L5,
+ BeiDouSignalName.B2A, GNSSComponent.DATA)
+ ### BeiDou B2a-P (Pilot): 20626 (0x5092)
+ BEIDOU_B2A_P = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L5,
+ BeiDouSignalName.B2A, GNSSComponent.PILOT)
+
+ ## L6 Band
+
+ ### BeiDou B3I: 20692 (0x50D4)
+ BEIDOU_B3I = to_signal_val(SatelliteType.BEIDOU, FrequencyBand.L6,
+ BeiDouSignalName.B3I, GNSSComponent.COMBINED)
+
+ ############################################################################
+ ## QZSS
+ ############################################################################
+
+ ## L1 Band
+
+ ### QZSS C/A: 24576 (0x6000)
+ QZSS_L1CA = to_signal_val(SatelliteType.QZSS, FrequencyBand.L1,
+ QZSSSignalName.L1CA, GNSSComponent.COMBINED)
+ ### QZSS L1C: 24580 (0x6004)
+ QZSS_L1C = to_signal_val(SatelliteType.QZSS, FrequencyBand.L1,
+ QZSSSignalName.L1C, GNSSComponent.COMBINED)
+ ### QZSS L1C-D (Data): 24581 (0x6005)
+ QZSS_L1C_D = to_signal_val(SatelliteType.QZSS, FrequencyBand.L1,
+ QZSSSignalName.L1C, GNSSComponent.DATA)
+ ### QZSS L1C-P (Pilot): 24582 (0x6006)
+ QZSS_L1C_P = to_signal_val(SatelliteType.QZSS, FrequencyBand.L1,
+ QZSSSignalName.L1C, GNSSComponent.PILOT)
+
+ ## L2 Band
+
+ ### QZSS L2C: 24648 (0x6048)
+ QZSS_L2C = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L2C, GNSSComponent.COMBINED)
+ ### QZSS L2C-M (Data): 24649 (0x6049)
+ QZSS_L2C_M = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L2C, GNSSComponent.DATA)
+ ### QZSS L2C-L (Pilot): 24650 (0x604A)
+ QZSS_L2C_L = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L2C, GNSSComponent.PILOT)
+ ### QZSS L6: 24656 (0x6050)
+ QZSS_L6 = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L6, GNSSComponent.COMBINED)
+ ### QZSS L6-M (Data): 24657 (0x6051)
+ QZSS_L6_M = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L6, GNSSComponent.DATA)
+ ### QZSS L6-L (Pilot): 24658 (0x6052)
+ QZSS_L6_L = to_signal_val(SatelliteType.QZSS, FrequencyBand.L2,
+ QZSSSignalName.L6, GNSSComponent.PILOT)
+
+ ## L5 Band
+
+ ### QZSS L5: 24716 (0x608C)
+ QZSS_L5 = to_signal_val(SatelliteType.QZSS, FrequencyBand.L5,
+ QZSSSignalName.L5, GNSSComponent.COMBINED)
+ ### QZSS L5-I (Data): 24717 (0x608D)
+ QZSS_L5_I = to_signal_val(SatelliteType.QZSS, FrequencyBand.L5,
+ QZSSSignalName.L5, GNSSComponent.DATA)
+ ### QZSS L5-Q (Pilot): 24718 (0x608E)
+ QZSS_L5_Q = to_signal_val(SatelliteType.QZSS, FrequencyBand.L5,
+ QZSSSignalName.L5, GNSSComponent.PILOT)
+
+ ############################################################################
+ ## SBAS
+ ############################################################################
+
+ ## L1 Band
+
+ ### SBAS C/A: 32768 (0x8000)
+ SBAS_L1CA = to_signal_val(SatelliteType.SBAS, FrequencyBand.L1,
+ SBASSignalName.L1CA, GNSSComponent.COMBINED)
+
+ ## L5 Band
+
+ ### SBAS L5: 32900 (0x8084)
+ SBAS_L5 = to_signal_val(SatelliteType.SBAS, FrequencyBand.L5,
+ SBASSignalName.L5, GNSSComponent.COMBINED)
+ ### SBAS L5-I (Data): 32901 (0x8085)
+ SBAS_L5_I = to_signal_val(SatelliteType.SBAS, FrequencyBand.L5,
+ SBASSignalName.L5, GNSSComponent.DATA)
+ ### SBAS L5-Q (Pilot): 32902 (0x8086)
+ SBAS_L5_Q = to_signal_val(SatelliteType.SBAS, FrequencyBand.L5,
+ SBASSignalName.L5, GNSSComponent.PILOT)
+
+ ## < Stop Autogenerated Types (See python/fusion_engine_client/messages/signal_def_gen.py)
+
+ def get_satellite_type(self) -> SatelliteType:
+ return _get_signal_part(self, SatelliteType, raise_on_unrecognized=False)
+
+ def get_frequency_band(self) -> FrequencyBand:
+ return _get_signal_part(self, FrequencyBand, raise_on_unrecognized=False)
+
+ def get_signal_name(self) -> SignalName:
+ system = self.get_satellite_type()
+ return _get_signal_part(self, _SIGNAL_NAME_ENUM_MAP[system], raise_on_unrecognized=False)
+
+ def get_gnss_component(self) -> GNSSComponent:
+ return _get_signal_part(self, GNSSComponent, raise_on_unrecognized=False)
+
+
+def pretty_print_gnss_enum(value: IntEnum, omit_satellite_type: bool = False, omit_component_hint: bool = False) -> str:
+ if isinstance(value, GNSSSignalType):
+ if value == GNSSSignalType.UNKNOWN:
+ return "Unknown Signal"
+ else:
+ return _get_pretty_gnss_signal_type(
+ value.name,
+ None if omit_satellite_type else value.get_satellite_type(),
+ value.get_signal_name(),
+ value.get_gnss_component(),
+ omit_component_hint)
+ elif isinstance(value, SatelliteType):
+ if SatelliteType.GALILEO == value:
+ return 'Galileo'
+ elif SatelliteType.BEIDOU == value:
+ return 'BeiDou'
+ elif isinstance(value, FrequencyBand):
+ if FrequencyBand.L1_L2_WIDE_LANE == value:
+ return 'L1-L2 Wide-Lane'
+ elif FrequencyBand.L1_L5_WIDE_LANE == value:
+ return 'L1-L5 Wide-Lane'
+ elif isinstance(value, GPSSignalName):
+ if GPSSignalName.L1P == value:
+ return 'L1 P(Y)'
+ elif GPSSignalName.L2P == value:
+ return 'L2 P(Y)'
+ elif isinstance(value, GalileoSignalName):
+ if GalileoSignalName.E5A == value:
+ return 'E5a'
+ elif GalileoSignalName.E5B == value:
+ return 'E5b'
+ elif value == GalileoSignalName.E1BC:
+ return 'E1-B/C'
+ elif value == GalileoSignalName.E6BC:
+ return 'E6-B/C'
+ elif value == GalileoSignalName.E1A:
+ return 'E1-A'
+ elif value == GalileoSignalName.E6A:
+ return 'E6-A'
+ elif isinstance(value, BeiDouSignalName):
+ if BeiDouSignalName.B2A == value:
+ return 'B2a'
+ elif BeiDouSignalName.B2B == value:
+ return 'B2b'
+ elif isinstance(value, GNSSComponent):
+ return value.name.title()
+
+ if value.name == 'L1CA' and value is not GLONASSSignalName.L1CA:
+ return 'C/A'
+ elif value.name.endswith('CA'):
+ return value.name[:-2] + ' C/A'
+ else:
+ return value.name
-def satellite_to_string(descriptor, short: bool = False):
- """!
- @brief Generate a human-readable string from a satellite descriptor.
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
- @param short If `True`, output a compact format string.
+INVALID_SV_HASH = 0x0FFF0000
+INVALID_SIGNAL_HASH = 0x00000000
- @return A string formatted as shown:
- - `short == True`: `Galileo PRN 5`
- - `short == False`: `E05`
- """
- system, _, prn = decode_signal_id(descriptor)
- if short:
- return '%s%02d' % (SatelliteTypeChar[system], prn)
- else:
- return '%s PRN %d' % (str(system), prn)
-
-
-def signal_from_string(string, return_encoded=False):
- m = _LONG_FORMAT.match(string)
- if m:
- try:
- system = SatelliteType[m.group(1)]
- except KeyError:
- raise ValueError("Unrecognized system '%s'." % m.group(1))
-
- if m.group(2):
- try:
- signal_type = SignalType[m.group(2)]
- except KeyError:
- raise ValueError("Unrecognized signal type '%s'." % m.group(2))
- else:
- signal_type = SignalType.UNKNOWN
+def get_satellite_type(hash: int) -> SatelliteType:
+ satellite_type = SatelliteType(hash >> 28, raise_on_unrecognized=False)
+ return satellite_type
- if m.group(3):
- prn = int(m.group(3))
- else:
- prn = 0
- else:
- m = _SHORT_FORMAT.match(string)
- if m:
- try:
- system = SatelliteTypeCharReverse[m.group(1)]
- except KeyError:
- raise ValueError("Unrecognized system character '%s'." % m.group(1))
-
- prn = int(m.group(2))
-
- if m.group(3):
- try:
- signal_type = SignalType[m.group(3)]
- except KeyError:
- raise ValueError("Unrecognized signal type '%s'." % m.group(3))
- else:
- signal_type = SignalType.UNKNOWN
- else:
- raise ValueError('Unrecognized string format.')
- if return_encoded:
- return encode_signal_id(system, signal_type, prn)
+def get_prn(hash: int) -> int:
+ prn = hash & 0xFFFF
+ return prn
+
+
+def get_signal_type(hash: int) -> GNSSSignalType:
+ signal_type_val = (hash >> 16) & 0xFFFF
+ if (signal_type_val & 0xFFF) == 0xFFF:
+ signal_type = GNSSSignalType.UNKNOWN
else:
- return system, signal_type, prn
+ signal_type = GNSSSignalType(signal_type_val, raise_on_unrecognized=False)
+ return signal_type
+
+def get_satellite_hash(signal_hash: int) -> int:
+ """!
+ @brief Get the satellite hash for a given signal hash.
+ """
+ sv_hash = signal_hash | 0x0FFF0000
+ return sv_hash
-def signal_to_string(descriptor, short: bool = False):
+def decode_signal_hash(signal_hash: int) -> tuple[SatelliteType, int, Optional[GNSSSignalType]]:
"""!
- @brief Generate a human-readable string from a signal descriptor.
+ @brief Decode an integer satellite/signal hash into its component parts: system, signal type, and PRN.
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
- @param short If `True`, output a compact format string.
+ Satellite/signal IDs are encoded as 32-bit integers as follows
+ ```
+ { SatelliteType (4b), 0xFFF, prn (16b) } # Satellite ID
+ { GNSSSignalType (16b), prn (16b) } # Signal ID
+ ```
- @return A string formatted as shown:
- - `short == True`: `Galileo E5a PRN 5`
- - `short == False`: `E05 E5a`
+ @param signal_hash An integer satellite/signal hash value.
+
+ @return A tuple containing the @ref SatelliteType, PRN (`int`), and a @ref GNSSSignalType if available. The
+ @ref GNSSSignalType will be `None` for satellite hashes.
"""
- system, signal_type, prn = decode_signal_id(descriptor)
+ prn = get_prn(signal_hash)
- if short:
- return '%s%02d %s' % (SatelliteTypeChar[system], prn, str(signal_type))
+ signal_type_val = (signal_hash >> 16) & 0xFFFF
+ if (signal_type_val & 0xFFF) == 0xFFF:
+ signal_type = None
+ satellite_type = get_satellite_type(signal_hash)
else:
- return '%s %s PRN %d' % (str(system), str(signal_type), prn)
+ signal_type = GNSSSignalType(signal_type_val, raise_on_unrecognized=False)
+ satellite_type = signal_type.get_satellite_type()
+ return satellite_type, prn, signal_type
-def signal_type_to_string(descriptor):
+def encode_signal_hash(signal_info: GNSSSignalType | SatelliteType, prn) -> int:
"""!
- @brief Generate a human-readable string from a signal type descriptor.
+ @brief Encode satellite/signal ID component parts into an integer hash.
+
+ See @ref decode_signal_hash() for details.
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
+ @param signal_info The @ref GNSSSignalType for a signal ID or the @ref SatelliteType for a satellite ID.
+ @param prn The satellite/signal's PRN.
- @return A string formatted like `Galileo E5a`.
+ @return The encoded integer value.
"""
- system, signal_type, _ = decode_signal_id(descriptor)
- return '%s %s' % (str(system), str(signal_type))
+ if isinstance(signal_info, GNSSSignalType):
+ signal_type_val = int(signal_info)
+ else:
+ signal_type_val = (int(signal_info) << 12) | 0xFFF
+ signal_hash = (signal_type_val << 16) | int(prn)
+ return signal_hash
-def get_system(descriptor):
+
+@functools.total_ordering
+class SatelliteID:
"""!
- @brief Get the satellite type (system) enumeration for a particular signal.
+ @brief Representation of a GNSS satellite (@ref SatelliteType + PRN).
+ """
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
+ def __init__(self, system: SatelliteType = None, prn: int = None, sv_hash: int = None):
+ """!
+ @brief Create a signal ID instance.
+
+ @param system The GNSS constellation to which this satellite belongs.
+ @param prn The satellite's PRN.
+ @param sv_hash A known integer satellite hash.
+ """
+ if sv_hash is not None:
+ if system is not None or prn is not None:
+ raise ValueError('You cannot specify both a signal hash and signal type/PRN.')
+
+ self.system = SatelliteType.UNKNOWN
+ self.prn = 0
+ self.decode_hash(sv_hash)
+ else:
+ if system is None:
+ if prn is None:
+ self.system = SatelliteType.UNKNOWN
+ self.prn = 0
+ else:
+ raise ValueError(f'GNSS system not specified.')
+ elif prn is None:
+ raise ValueError(f'PRN not specified.')
+ else:
+ self.system = system
+ self.prn = prn
- @return The @ref SatelliteType.
- """
- system, _, _ = decode_signal_id(descriptor)
- return system
+ self._hash = encode_signal_hash(signal_info=self.system, prn=self.prn)
+ def get_satellite_type(self) -> SatelliteType:
+ return self.system
-def get_prn(descriptor):
- """!
- @brief Get the PRN for a particular signal.
+ def get_prn(self) -> int:
+ return self.prn
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
+ def decode_hash(self, sv_hash: int):
+ self.system, self.prn, _ = decode_signal_hash(sv_hash)
+ self._hash = int(sv_hash)
- @return The PRN.
- """
- _, _, prn = decode_signal_id(descriptor)
- return prn
+ def encode_hash(self) -> int:
+ return self._hash
+ def to_string(self, short: bool = False) -> str:
+ """!
+ @brief Generate a human-readable string for this satellite.
-def get_satellite_id(descriptor):
- """!
- @brief Get a numeric ID for an individual satellite (can be used to represent all signals from a given SV).
+ @param short If `True`, output a compact format string.
- The signal type will be set to @c SignalType.UNKNOWN.
+ @return A string formatted as shown:
+ - `short == False`: `Galileo PRN 5`
+ - `short == True`: `E05`
+ """
+ if short:
+ if self.prn > 0:
+ return '%s%02d' % (SatelliteTypeChar[self.system], self.prn)
+ else:
+ return '%s??' % SatelliteTypeChar[self.system]
+ else:
+ if self.prn > 0:
+ return '%s PRN %d' % (str(self.system), self.prn)
+ elif self.system != SatelliteType.UNKNOWN:
+ return '%s PRN ?' % (str(self.system),)
+ else:
+ return 'Invalid Satellite'
+
+ def __lt__(self, other: Union['SatelliteID', int]):
+ self_hash = self.encode_hash()
+ other_hash = self.to_hash(other)
+ # Note: Invalid SV < valid SV.
+ if self_hash == INVALID_SV_HASH:
+ return other_hash != INVALID_SV_HASH
+ elif other_hash == INVALID_SV_HASH:
+ return False
+ else:
+ return self_hash < other_hash
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
+ def __eq__(self, other: Union['SatelliteID', int]):
+ self_hash = self.encode_hash()
+ other_hash = self.to_hash(other)
+ return self_hash == other_hash
- @return The resulting ID value.
- """
- system, _, prn = decode_signal_id(descriptor)
- return encode_signal_id(system, SignalType.UNKNOWN, prn)
+ def __int__(self):
+ return self._hash
+ def __hash__(self):
+ return self._hash
-def is_same_satellite(a, b):
- """!
- @brief Check if two signals originate from the same satellite.
+ def __str__(self):
+ return self.to_string(short=False)
- @param a The first signal descriptor.
- @param b The first signal descriptor.
- """
- return get_satellite_id(a) == get_satellite_id(b)
+ def __repr__(self):
+ return f"[{self.to_string(short=True)} (0x{self.encode_hash():08x})]"
+ @classmethod
+ def to_id(cls, id: Union['SatelliteID', int]) -> 'SatelliteID':
+ if isinstance(id, SatelliteID):
+ return id
+ else:
+ signal_id = SatelliteID()
+ signal_id.decode_hash(id)
+ return signal_id
+
+ @classmethod
+ def to_hash(cls, id: Union['SatelliteID', int]) -> int:
+ if isinstance(id, SatelliteID):
+ return id.encode_hash()
+ else:
+ return id
-def get_signal_type_id(descriptor):
+
+@functools.total_ordering
+class SignalID:
"""!
- @brief Get a numeric ID for a signal type within a given constellation (can be used to represent all signals of that
- type across all satellites).
+ @brief Representation of a GNSS signal (@ref SatelliteType, @ref GNSSSignalType, and PRN).
+ """
- The PRN will be set to 0.
+ def __init__(self, signal_type: GNSSSignalType = None, prn: int = None, signal_hash: int = None):
+ """!
+ @brief Create a signal ID instance.
+
+ @param signal_type The type of this signal.
+ @param prn The signal's PRN.
+ @param signal_hash A known integer signal hash.
+ """
+ if signal_hash is not None:
+ if signal_type is not None or prn is not None:
+ raise ValueError('You cannot specify both a signal hash and signal type/PRN.')
+
+ self.signal_type = None
+ self.prn = 0
+ self.decode_hash(signal_hash)
+ else:
+ if signal_type is None:
+ if prn is None:
+ self.signal_type = GNSSSignalType.UNKNOWN
+ self.prn = 0
+ else:
+ raise ValueError(f'Signal type not specified.')
+ elif prn is None:
+ raise ValueError(f'PRN not specified.')
+ else:
+ self.signal_type = signal_type
+ self.prn = prn
- @param descriptor A signal/satellite descriptor compatible with @ref decode_signal_id().
+ self._hash = encode_signal_hash(signal_info=self.signal_type, prn=self.prn)
- @return The resulting ID value.
- """
- system, descriptor, _ = decode_signal_id(descriptor)
- return encode_signal_id(system, descriptor, 0)
+ def get_satellite_id(self) -> SatelliteID:
+ """!
+ @brief Get the @ref SatelliteID corresponding with this signal.
+ @return The @ref SatelliteID for the satellite transmitting this signal.
+ """
+ return SatelliteID(system=self.signal_type.get_satellite_type(), prn=self.prn)
-def is_same_signal_type(a, b):
- """!
- @brief Check if two signals are of the same type (e.g., GPS C/A).
+ def get_satellite_type(self) -> SatelliteType:
+ return self.signal_type.get_satellite_type()
- @param a The first signal descriptor.
- @param b The first signal descriptor.
- """
- return get_signal_type_id(a) == get_signal_type_id(b)
+ def get_signal_type(self) -> GNSSSignalType:
+ return self.signal_type
+
+ def get_frequency_band(self) -> FrequencyBand:
+ return self.signal_type.get_frequency_band()
+
+ def get_signal_name(self) -> SignalName:
+ return self.signal_type.get_signal_name()
+
+ def get_gnss_component(self) -> GNSSComponent:
+ return self.signal_type.get_gnss_component()
+
+ def get_prn(self) -> int:
+ return self.prn
+
+ def decode_hash(self, signal_hash: int):
+ _, self.prn, self.signal_type = decode_signal_hash(signal_hash)
+ if self.signal_type is None:
+ raise ValueError(f'Signal type not known for signal hash 0x{signal_hash:08x}.')
+ self._hash = int(signal_hash)
+
+ def encode_hash(self) -> int:
+ return self._hash
+
+ def to_string(self, short: bool = False) -> str:
+ """!
+ @brief Generate a human-readable string for this signal.
+
+ @param short If `True`, output a compact format string.
+
+ @return A string formatted as shown:
+ - `short == False`: `Galileo E1-C PRN 5`
+ - `short == True`: `E05 E1-C`
+ """
+ if short:
+ if self.signal_type == GNSSSignalType.UNKNOWN:
+ signal_str = 'UNKNOWN'
+ else:
+ signal_str = pretty_print_gnss_enum(self.signal_type, omit_satellite_type=True,
+ omit_component_hint=True)
+ return '%s %s' % (self.get_satellite_id().to_string(short=True), signal_str)
+ else:
+ if self.prn > 0:
+ return '%s PRN %d' % (pretty_print_gnss_enum(self.signal_type), self.prn)
+ elif self.signal_type != GNSSSignalType.UNKNOWN:
+ return '%s PRN ?' % (pretty_print_gnss_enum(self.signal_type))
+ else:
+ return 'Invalid Signal'
+
+ def __lt__(self, other: Union['SignalID', int]):
+ self_hash = self.encode_hash()
+ other_hash = self.to_hash(other)
+ # Note: Invalid signals < valid signals.
+ if self_hash == INVALID_SIGNAL_HASH:
+ return other_hash != INVALID_SIGNAL_HASH
+ elif other_hash == INVALID_SIGNAL_HASH:
+ return False
+ else:
+ return self_hash < other_hash
+
+ def __eq__(self, other: Union['SignalID', int]):
+ self_hash = self.encode_hash()
+ other_hash = self.to_hash(other)
+ return self_hash == other_hash
+
+ def __int__(self):
+ return self._hash
+
+ def __hash__(self):
+ return self._hash
+
+ def __str__(self):
+ return self.to_string(short=False)
+
+ def __repr__(self):
+ return f"[{self.to_string(short=True)} (0x{self.encode_hash():08x})]"
+
+ def __getattr__(self, item):
+ # Aliases for convenience/consistency with SatelliteID.
+ if item == 'system':
+ return self.get_satellite_type()
+ elif item == 'sv':
+ return self.get_satellite_id()
+ else:
+ raise AttributeError(f"Unknown attribute '{item}'.")
+
+ @classmethod
+ def to_id(cls, id: Union['SignalID', int]) -> 'SignalID':
+ if isinstance(id, SignalID):
+ return id
+ else:
+ signal_id = SignalID()
+ signal_id.decode_hash(id)
+ return signal_id
+
+ @classmethod
+ def to_hash(cls, id: Union['SignalID', int]) -> int:
+ if isinstance(id, SignalID):
+ return id.encode_hash()
+ else:
+ return id
diff --git a/python/fusion_engine_client/messages/solution.py b/python/fusion_engine_client/messages/solution.py
index 292196f0..f9562aac 100644
--- a/python/fusion_engine_client/messages/solution.py
+++ b/python/fusion_engine_client/messages/solution.py
@@ -1,5 +1,6 @@
+from dataclasses import dataclass
import struct
-from typing import List, Sequence
+from typing import Dict, List, Sequence
from construct import (Struct, Float64l, Float32l, Int32ul, Int8ul, Padding, Array)
import numpy as np
@@ -428,6 +429,9 @@ def __init__(self):
## The C/N0 of the L1 signal present on this satellite (in dB-Hz).
self.cn0_dbhz = np.nan
+ def get_satellite_id(self) -> SatelliteID:
+ return SatelliteID(system=self.system, prn=self.prn)
+
def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True) -> (bytes, int):
if buffer is None:
buffer = bytearray(self.calcsize())
@@ -547,6 +551,71 @@ def __str__(self):
def calcsize(self) -> int:
return 2 * Timestamp.calcsize() + GNSSSatelliteMessage._SIZE + len(self.svs) * SatelliteInfo.calcsize()
+ def to_gnss_signals_message(self) -> 'GNSSSignalsMessage':
+ """!
+ @brief Convert this message to the newer @ref GNSSSignalsMessage format.
+
+ @note
+ The deprecated @ref GNSSSatelliteMessage does not store information about individual GNSS signals. This function
+ assumes that each satellite present in the message is being tracked on the L1 civilian signal on that
+ constellation.
+
+ @return A @ref GNSSSignalsMessage.
+ """
+ l1_signal_types_per_system = {
+ SatelliteType.GPS: GNSSSignalType.GPS_L1CA,
+ SatelliteType.GLONASS: GNSSSignalType.GLONASS_L1CA,
+ SatelliteType.GALILEO: GNSSSignalType.GALILEO_E1BC,
+ SatelliteType.BEIDOU: GNSSSignalType.BEIDOU_B1I,
+ SatelliteType.QZSS: GNSSSignalType.QZSS_L1CA,
+ SatelliteType.SBAS: GNSSSignalType.SBAS_L1CA,
+ }
+
+ result = GNSSSignalsMessage()
+ result.p1_time = self.p1_time
+ result.gps_time = self.gps_time
+ if self.gps_time:
+ gps_time_sec = float(self.gps_time)
+ result.gps_week = int(gps_time_sec / SECONDS_PER_WEEK)
+ result.gps_tow_ms = int(round((gps_time_sec - result.gps_week * SECONDS_PER_WEEK) * 1e3))
+
+ for sv_entry in self.svs:
+ sv = sv_entry.get_satellite_id()
+ signal_type = l1_signal_types_per_system.get(sv.get_satellite_type(), None)
+ if signal_type is None:
+ continue
+ signal = SignalID(signal_type=signal_type, prn=sv.get_prn())
+
+ is_used = (sv_entry.usage & SatelliteInfo.SATELLITE_USED) != 0
+ flags = 0
+ if is_used:
+ flags |= (GNSSSatelliteInfo.STATUS_FLAG_HAS_EPHEM | GNSSSatelliteInfo.STATUS_FLAG_IS_USED)
+ sv_info = GNSSSatelliteInfo(
+ system=sv.get_satellite_type(), prn=sv.get_prn(),
+ azimuth_deg=sv_entry.azimuth_deg, elevation_deg=sv_entry.elevation_deg,
+ status_flags=flags)
+ result.sat_info[sv] = sv_info
+
+ # This message doesn't indicate which measurements were used for a given satellite. We'll assume all of
+ # them, which may not be accurate.
+ #
+ # To avoid confusion, since we actually don't have tracking status, we'll assume that a signal has valid
+ # measurements if it is _present at all_. Otherwise, its tracking status could appear to change rapidly when
+ # plotted if the navigation engine simply decides not to use it for any reason. Since we do not know if we
+ # are fixed, we cannot accurately indicate "carrier ambiguity resolved", so we'll leave that out.
+ flags = (GNSSSignalInfo.STATUS_FLAG_VALID_PR | GNSSSignalInfo.STATUS_FLAG_VALID_DOPPLER |
+ GNSSSignalInfo.STATUS_FLAG_CARRIER_LOCKED)
+ if is_used:
+ flags |= (GNSSSignalInfo.STATUS_FLAG_USED_PR | GNSSSignalInfo.STATUS_FLAG_USED_DOPPLER |
+ GNSSSignalInfo.STATUS_FLAG_USED_CARRIER)
+ signal_info = GNSSSignalInfo(
+ signal_type=signal_type, prn=sv.get_prn(),
+ cn0_dbhz=sv_entry.cn0_dbhz,
+ status_flags=flags)
+ result.signal_info[signal] = signal_info
+
+ return result
+
@classmethod
def to_numpy(cls, messages):
return {
@@ -563,7 +632,8 @@ def group_by_sv(cls, input):
all_p1_time = flattened_data['p1_time']
all_gps_time = flattened_data['gps_time']
- all_sv_ids = np.array([encode_signal_id(entry) for entry in flattened_data['data']], dtype=int)
+ all_sv_ids = np.array([encode_signal_hash(entry.system, entry.prn)
+ for entry in flattened_data['data']], dtype=int)
all_azim_deg = np.array([entry.azimuth_deg for entry in flattened_data['data']])
all_elev_deg = np.array([entry.elevation_deg for entry in flattened_data['data']])
all_cn0_dbhz = np.array([entry.cn0_dbhz for entry in flattened_data['data']])
@@ -589,7 +659,8 @@ def group_by_time(cls, input):
all_p1_time = flattened_data['p1_time']
all_gps_time = flattened_data['gps_time']
- all_sv_ids = np.array([encode_signal_id(entry) for entry in flattened_data['data']], dtype=int)
+ all_sv_ids = np.array([encode_signal_hash(entry.system, entry.prn)
+ for entry in flattened_data['data']], dtype=int)
all_azim_deg = np.array([entry.azimuth_deg for entry in flattened_data['data']])
all_elev_deg = np.array([entry.elevation_deg for entry in flattened_data['data']])
all_cn0_dbhz = np.array([entry.cn0_dbhz for entry in flattened_data['data']])
@@ -624,6 +695,325 @@ def flatten(cls, input):
}
+# Breaking the declaration up allows specifying the class properties without affecting constructor or property
+# iteration.
+@dataclass
+class _GNSSSatelliteInfo:
+ system: SatelliteType = SatelliteType.UNKNOWN
+ elevation_deg: float = np.nan
+ azimuth_deg: float = np.nan
+ prn: int = 0
+ status_flags: int = 0
+
+
+class GNSSSatelliteInfo(_GNSSSatelliteInfo):
+ STATUS_FLAG_IS_USED = 0x01
+ STATUS_FLAG_IS_UNHEALTHY = 0x02
+ STATUS_FLAG_IS_NON_LINE_OF_SIGHT = 0x04
+ STATUS_FLAG_HAS_EPHEM = 0x10
+ STATUS_FLAG_HAS_SBAS = 0x20
+
+ _INVALID_AZIMUTH = 0xFFFF
+ _INVALID_ELEVATION = 0x7FFF
+
+ _STRUCT = struct.Struct(' SatelliteID:
+ return SatelliteID(system=self.system, prn=self.prn)
+
+ def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True) -> (bytes, int):
+ if buffer is None:
+ buffer = bytearray(self.calcsize())
+ offset = 0
+
+ initial_offset = offset
+
+ self._STRUCT.pack_into(
+ buffer, offset,
+ int(self.system),
+ self.prn,
+ self.status_flags,
+ self._INVALID_ELEVATION if np.isnan(self.elevation_deg) else int(np.round(self.elevation_deg * 100.0)),
+ self._INVALID_AZIMUTH if np.isnan(self.azimuth_deg) else int(np.round(self.azimuth_deg * 100.0)))
+ offset += self._STRUCT.size
+
+ if return_buffer:
+ return buffer
+ else:
+ return offset - initial_offset
+
+ def unpack(self, buffer: bytes, offset: int = 0, version: int = MessagePayload._UNSPECIFIED_VERSION) -> int:
+ initial_offset = offset
+
+ (system,
+ self.prn,
+ self.status_flags,
+ elev_int,
+ azim_int) = \
+ self._STRUCT.unpack_from(buffer=buffer, offset=offset)
+ offset += self._STRUCT.size
+
+ self.system = SatelliteType(system, raise_on_unrecognized=False)
+ self.elevation_deg = np.nan if elev_int == self._INVALID_ELEVATION else (elev_int * 0.01)
+ self.azimuth_deg = np.nan if azim_int == self._INVALID_AZIMUTH else (azim_int * 0.01)
+
+ return offset - initial_offset
+
+ @classmethod
+ def calcsize(cls) -> int:
+ return cls._STRUCT.size
+
+
+# Breaking the declaration up allows specifying the class properties without affecting constructor or property
+# iteration.
+@dataclass
+class _GNSSSignalInfo:
+ signal_type: GNSSSignalType = GNSSSignalType.UNKNOWN
+ prn: int = 0
+ cn0_dbhz: float = np.nan
+ status_flags: int = 0
+
+
+class GNSSSignalInfo(_GNSSSignalInfo):
+ STATUS_FLAG_USED_PR = 0x01
+ STATUS_FLAG_USED_DOPPLER = 0x02
+ STATUS_FLAG_USED_CARRIER = 0x04
+ STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED = 0x08
+
+ STATUS_FLAG_VALID_PR = 0x10
+ STATUS_FLAG_VALID_DOPPLER = 0x20
+ STATUS_FLAG_CARRIER_LOCKED = 0x40
+
+ STATUS_FLAG_HAS_RTK = 0x100
+ STATUS_FLAG_HAS_SBAS = 0x200
+ STATUS_FLAG_HAS_EPHEM = 0x400
+
+ _INVALID_CN0 = 0
+
+ _STRUCT = struct.Struct(' SignalID:
+ return SignalID(signal_type=self.signal_type, prn=self.prn)
+
+ def get_satellite_id(self) -> SatelliteID:
+ return SatelliteID(system=self.signal_type.get_satellite_type(), prn=self.prn)
+
+ def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True) -> (bytes, int):
+ if buffer is None:
+ buffer = bytearray(self.calcsize())
+ offset = 0
+
+ initial_offset = offset
+
+ self._STRUCT.pack_into(
+ buffer, offset,
+ int(self.signal_type),
+ self.prn,
+ self._INVALID_CN0 if np.isnan(self.cn0_dbhz) else int(np.round(self.cn0_dbhz / 0.25)),
+ self.status_flags)
+ offset += self._STRUCT.size
+
+ if return_buffer:
+ return buffer
+ else:
+ return offset - initial_offset
+
+ def unpack(self, buffer: bytes, offset: int = 0, version: int = MessagePayload._UNSPECIFIED_VERSION) -> int:
+ initial_offset = offset
+
+ (signal_type,
+ self.prn,
+ cn0_int,
+ self.status_flags) = \
+ self._STRUCT.unpack_from(buffer=buffer, offset=offset)
+ offset += self._STRUCT.size
+
+ self.signal_type = GNSSSignalType(signal_type, raise_on_unrecognized=False)
+ self.cn0_dbhz = np.nan if cn0_int == self._INVALID_CN0 else cn0_int * 0.25
+
+ return offset - initial_offset
+
+ @classmethod
+ def calcsize(cls) -> int:
+ return cls._STRUCT.size
+
+
+class GNSSSignalsMessage(MessagePayload):
+ """!
+ @brief Information about the individual GNSS satellites and signals used in the pose solution.
+ """
+ MESSAGE_TYPE = MessageType.GNSS_SIGNALS
+ MESSAGE_VERSION = 1
+
+ _INVALID_GPS_WEEK = 0xFFFF
+ _INVALID_GPS_TOW = 0xFFFFFFFF
+
+ _STRUCT = struct.Struct(' (bytes, int):
+ if buffer is None:
+ buffer = bytearray(self.calcsize())
+ offset = 0
+
+ initial_offset = offset
+
+ offset += self.p1_time.pack(buffer, offset, return_buffer=False)
+ offset += self.gps_time.pack(buffer, offset, return_buffer=False)
+
+ self._STRUCT.pack_into(
+ buffer, offset,
+ self._INVALID_GPS_TOW if self.gps_tow_ms is None else self.gps_tow_ms,
+ self._INVALID_GPS_WEEK if self.gps_week is None else self.gps_week,
+ len(self.signal_info),
+ len(self.sat_info))
+ offset += self._STRUCT.size
+
+ for info in self.sat_info.values():
+ offset += info.pack(buffer, offset, return_buffer=False)
+
+ for info in self.signal_info.values():
+ offset += info.pack(buffer, offset, return_buffer=False)
+
+ if return_buffer:
+ return buffer
+ else:
+ return offset - initial_offset
+
+ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessagePayload._UNSPECIFIED_VERSION) -> int:
+ # Legacy version 0 not supported.
+ if message_version == 0:
+ raise NotImplementedError('GNSSSignalsMessage version 0 not supported.')
+
+ initial_offset = offset
+
+ offset += self.p1_time.unpack(buffer, offset)
+ offset += self.gps_time.unpack(buffer, offset)
+
+ (gps_tow_ms_int,
+ gps_week_int,
+ num_signals,
+ num_satellites) = \
+ self._STRUCT.unpack_from(buffer=buffer, offset=offset)
+ offset += self._STRUCT.size
+
+ self.gps_tow_ms = None if gps_tow_ms_int == self._INVALID_GPS_TOW else gps_tow_ms_int
+ self.gps_week = None if gps_week_int == self._INVALID_GPS_WEEK else gps_week_int
+
+ sat_info_list = [GNSSSatelliteInfo() for _ in range(num_satellites)]
+ for info in sat_info_list:
+ offset += info.unpack(buffer, offset, version=message_version)
+
+ signal_info_list = [GNSSSignalInfo() for _ in range(num_signals)]
+ for info in signal_info_list:
+ offset += info.unpack(buffer, offset, version=message_version)
+
+ self.sat_info = {e.get_satellite_id(): e for e in sat_info_list}
+ self.signal_info = {e.get_signal_id(): e for e in signal_info_list}
+
+ return offset - initial_offset
+
+ def calcsize(self) -> int:
+ return ((2 * Timestamp.calcsize()) + GNSSSignalsMessage._STRUCT.size +
+ (len(self.sat_info) * GNSSSatelliteInfo.calcsize()) +
+ (len(self.signal_info) * GNSSSignalInfo.calcsize()))
+
+ def __repr__(self):
+ result = super().__repr__()[:-1]
+ result += f', num_svs={len(self.sat_info)}, num_signals={len(self.signal_info)}]'
+ return result
+
+ def __str__(self):
+ string = 'GNSS Signals Message @ %s\n' % str(self.p1_time)
+ string += ' %d SVs:' % len(self.sat_info)
+
+ def _signal_usage_str(status_flags: int) -> str:
+ if status_flags & GNSSSignalInfo.STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED:
+ return 'Fixed'
+ elif status_flags & GNSSSignalInfo.STATUS_FLAG_USED_CARRIER:
+ return 'Float'
+ elif status_flags & GNSSSignalInfo.STATUS_FLAG_USED_PR:
+ return 'PR'
+ else:
+ return "No"
+
+ def _signal_tracking_str(status_flags: int) -> str:
+ if status_flags & GNSSSignalInfo.STATUS_FLAG_CARRIER_LOCKED:
+ return 'PR,CP'
+ elif status_flags & GNSSSignalInfo.STATUS_FLAG_VALID_PR:
+ return 'PR'
+ else:
+ return 'No'
+
+ for sv, info in self.sat_info.items():
+ string += '\n'
+ string += ' %s:\n' % str(sv)
+ string += ' Used in solution: %s\n' % \
+ ('yes' if (info.status_flags | info.STATUS_FLAG_IS_USED) else 'no')
+ string += ' Az/el: %.1f, %.1f deg' % (info.azimuth_deg, info.elevation_deg)
+ string += '\n %d signals:' % len(self.signal_info)
+ for signal, info in self.signal_info.items():
+ string += '\n'
+ string += ' %s:\n' % str(signal)
+ string += ' C/N0: %.1f dB-Hz\n' % (info.cn0_dbhz,)
+ string += ' Available: %s\n' % (_signal_tracking_str(info.status_flags),)
+ string += ' Used: %s' % (_signal_usage_str(info.status_flags),)
+ return string
+
+ @classmethod
+ def to_numpy(cls, messages: List['GNSSSignalsMessage']):
+ flat_sv_info = [(m, sv, info) for m in messages for sv, info in m.sat_info.items()]
+ sv_ids = np.array([e[1] for e in flat_sv_info], dtype=SatelliteID)
+ sv_data = {
+ 'p1_time': np.array([float(e[0].p1_time) for e in flat_sv_info]),
+ 'gps_time': np.array([float(e[0].gps_time) for e in flat_sv_info]),
+ 'gps_week': np.array([(e[0].gps_week if e[0].gps_week is not None else -1) for e in flat_sv_info],
+ dtype=int),
+ 'gps_tow_sec': np.array([(e[0].gps_tow_ms * 1e-3 if e[0].gps_tow_ms is not None else np.nan)
+ for e in flat_sv_info]),
+ 'sv': sv_ids,
+ 'sv_hash': sv_ids.astype(int),
+ 'azimuth_deg': np.array([e[2].azimuth_deg for e in flat_sv_info]),
+ 'elevation_deg': np.array([e[2].elevation_deg for e in flat_sv_info]),
+ 'status_flags': np.array([e[2].status_flags for e in flat_sv_info], dtype=int),
+ }
+
+ flat_sig_info = [(m, signal, info) for m in messages for signal, info in m.signal_info.items()]
+ signal_ids = np.array([e[1] for e in flat_sig_info], dtype=SignalID)
+ signal_data = {
+ 'p1_time': np.array([float(e[0].p1_time) for e in flat_sig_info]),
+ 'gps_time': np.array([float(e[0].gps_time) for e in flat_sig_info]),
+ 'gps_week': np.array([(e[0].gps_week if e[0].gps_week is not None else -1) for e in flat_sig_info],
+ dtype=int),
+ 'gps_tow_sec': np.array([(e[0].gps_tow_ms * 1e-3 if e[0].gps_tow_ms is not None else np.nan)
+ for e in flat_sig_info]),
+ 'signal': signal_ids,
+ 'signal_hash': signal_ids.astype(int),
+ 'cn0_dbhz': np.array([e[2].cn0_dbhz for e in flat_sig_info]),
+ 'status_flags': np.array([e[2].status_flags for e in flat_sig_info], dtype=int),
+ }
+
+ return {
+ 'p1_time': np.array([float(m.p1_time) for m in messages]),
+ 'gps_time': np.array([float(m.gps_time) for m in messages]),
+ 'gps_week': np.array([(m.gps_week if m.gps_week is not None else -1) for m in messages],
+ dtype=int),
+ 'gps_tow_sec': np.array([(m.gps_tow_ms * 1e-3 if m.gps_tow_ms is not None else np.nan)
+ for m in messages]),
+ 'num_svs': np.array([len(m.sat_info) for m in messages], dtype=int),
+ 'num_signals': np.array([len(m.signal_info) for m in messages], dtype=int),
+ 'sv_data': sv_data,
+ 'signal_data': signal_data,
+ }
+
+
class CalibrationStage(IntEnum):
"""!
@brief The stages of the device calibration process.
diff --git a/python/fusion_engine_client/parsers/decoder.py b/python/fusion_engine_client/parsers/decoder.py
index b1e588e4..5359691f 100644
--- a/python/fusion_engine_client/parsers/decoder.py
+++ b/python/fusion_engine_client/parsers/decoder.py
@@ -257,23 +257,28 @@ def on_data(self, data: Union[bytes, int]) -> List[Union[MessageTuple, MessageWi
# Get the class for the received message type and deserialize the message payload. If cls is not None, it is
# a child of @ref MessagePayload that maps to the received @ref MessageType.
cls = message_type_to_class.get(self._header.message_type, None)
+ contents = None
if cls is not None:
contents = cls()
try:
contents.unpack(buffer=self._buffer, offset=MessageHeader.calcsize())
_logger.debug('Decoded FusionEngine message %s.', repr(contents))
+ except NotImplementedError as e:
+ print_func = _logger.warning if self._warn_on_unrecognized else _logger.debug
+ print_func('Error deserializing message %s payload: %s', self._header.get_type_string(), e)
except Exception as e:
# unpack() may fail if the payload length in the header differs from the length expected by the
# class, the payload contains an illegal value, etc.
_logger.error('Error deserializing message %s payload: %s', self._header.get_type_string(), e)
- contents = bytes(self._buffer[MessageHeader.calcsize():self._msg_len])
# If cls is None, we don't have a class for the message type. Return a copy of the payload bytes.
else:
- contents = bytes(self._buffer[MessageHeader.calcsize():self._msg_len])
print_func = _logger.warning if self._warn_on_unrecognized else _logger.debug
print_func('Decoded unknown FusionEngine message. [type=%d, payload_size=%d B]',
self._header.message_type, self._header.payload_size_bytes)
+ if contents is None:
+ contents = bytes(self._buffer[MessageHeader.calcsize():self._msg_len])
+
# Store the result.
result = [self._header, contents]
if self._return_bytes:
diff --git a/python/fusion_engine_client/parsers/fast_indexer.py b/python/fusion_engine_client/parsers/fast_indexer.py
index 929018a9..db984ee0 100644
--- a/python/fusion_engine_client/parsers/fast_indexer.py
+++ b/python/fusion_engine_client/parsers/fast_indexer.py
@@ -113,7 +113,7 @@ def _search_blocks_for_fe(input_path: str, thread_idx: int, block_starts: List[i
payload.unpack(buffer=data, offset=i +
MessageHeader.calcsize(), message_version=header.message_version)
p1_time = payload.get_p1_time()
- except BaseException:
+ except Exception:
pass
# Convert the Timestamp to an integer.
p1_time_raw = Timestamp._INVALID if math.isnan(p1_time.seconds) else int(p1_time.seconds)
@@ -123,7 +123,7 @@ def _search_blocks_for_fe(input_path: str, thread_idx: int, block_starts: List[i
f'file_offset={absolute_offset} B, p1_time={p1_time}',
depth=3)
raw_list.append((p1_time_raw, int(header.message_type), absolute_offset, header.get_message_size()))
- except BaseException:
+ except Exception:
pass
_logger.trace(f'Thread {thread_idx}: {num_syncs} sync with {len(raw_list)} valid FE.')
# Return the index data for this section of the file.
diff --git a/python/fusion_engine_client/parsers/mixed_log_reader.py b/python/fusion_engine_client/parsers/mixed_log_reader.py
index 08e09427..8efbb2b0 100644
--- a/python/fusion_engine_client/parsers/mixed_log_reader.py
+++ b/python/fusion_engine_client/parsers/mixed_log_reader.py
@@ -329,6 +329,9 @@ def _read_next(self, require_p1_time=False, require_system_time=False, force_eof
try:
payload = cls()
payload.unpack(buffer=payload_bytes, offset=0, message_version=header.message_version)
+ except NotImplementedError as e:
+ self.logger.debug("Error %s message: %s" % (header.get_type_string(), str(e)))
+ payload = None
except Exception as e:
self.logger.error("Error parsing %s message: %s" % (header.get_type_string(), str(e)))
payload = None
diff --git a/src/point_one/fusion_engine/messages/defs.h b/src/point_one/fusion_engine/messages/defs.h
index 4db7396b..caecde81 100644
--- a/src/point_one/fusion_engine/messages/defs.h
+++ b/src/point_one/fusion_engine/messages/defs.h
@@ -41,6 +41,7 @@ enum class MessageType : uint16_t {
POSE_AUX = 10003, ///< @ref PoseAuxMessage
CALIBRATION_STATUS = 10004, ///< @ref CalibrationStatusMessage
RELATIVE_ENU_POSITION = 10005, ///< @ref RelativeENUPositionMessage
+ GNSS_SIGNALS = 10006, ///< @ref GNSSSignalsMessage
// Device status messages.
SYSTEM_STATUS = 10500, ///< @ref SystemStatusMessage
@@ -146,6 +147,9 @@ P1_CONSTEXPR_FUNC const char* to_string(MessageType type) {
case MessageType::RELATIVE_ENU_POSITION:
return "Relative ENU Position";
+ case MessageType::GNSS_SIGNALS:
+ return "GNSS Signals";
+
// Device status messages.
case MessageType::SYSTEM_STATUS:
return "System Status";
@@ -340,6 +344,7 @@ P1_CONSTEXPR_FUNC bool IsCommand(MessageType message_type) {
case MessageType::POSE_AUX:
case MessageType::CALIBRATION_STATUS:
case MessageType::RELATIVE_ENU_POSITION:
+ case MessageType::GNSS_SIGNALS:
case MessageType::SYSTEM_STATUS:
case MessageType::IMU_OUTPUT:
case MessageType::DEPRECATED_RAW_HEADING_OUTPUT:
diff --git a/src/point_one/fusion_engine/messages/signal_defs.h b/src/point_one/fusion_engine/messages/signal_defs.h
index 32d89fbf..fa5a4210 100644
--- a/src/point_one/fusion_engine/messages/signal_defs.h
+++ b/src/point_one/fusion_engine/messages/signal_defs.h
@@ -12,88 +12,1701 @@ namespace point_one {
namespace fusion_engine {
namespace messages {
-////////////////////////////////////////////////////////////////////////////////
-// SatelliteType
-////////////////////////////////////////////////////////////////////////////////
+// Enum forward declarations.
+enum class SatelliteType : uint8_t;
+enum class FrequencyBand : uint8_t;
+enum class GNSSComponent : uint8_t;
+enum class GPSSignalName : uint8_t;
+enum class GLONASSSignalName : uint8_t;
+enum class BeiDouSignalName : uint8_t;
+enum class GalileoSignalName : uint8_t;
+enum class SBASSignalName : uint8_t;
+enum class QZSSSignalName : uint8_t;
+enum class GNSSSignalType : uint16_t;
+
+// > Start Autogenerated Constants (See python/fusion_engine_client/messages/signal_def_gen.py)
+
+constexpr unsigned SATELLITE_TYPE_SHIFT = 12;
+constexpr unsigned SATELLITE_TYPE_BITS = 4;
+constexpr unsigned FREQUENCY_BAND_SHIFT = 6;
+constexpr unsigned FREQUENCY_BAND_BITS = 4;
+constexpr unsigned SIGNAL_NAME_SHIFT = 2;
+constexpr unsigned SIGNAL_NAME_BITS = 3;
+constexpr unsigned GNSS_COMPONENT_SHIFT = 0;
+constexpr unsigned GNSS_COMPONENT_BITS = 2;
+
+// < Stop Autogenerated Constants (See python/fusion_engine_client/messages/signal_def_gen.py)
+
+/**
+ * @brief Compile time check for whether a type is one of the SignalName enums.
+ *
+ * @tparam T The type to check.
+ *
+ * @return `true`, if T is a SignalName enum. Return `false` otherwise.
+ */
+template
+P1_CONSTEXPR_FUNC bool IsSignalNameType() {
+ return std::is_same::value ||
+ std::is_same::value ||
+ std::is_same::value ||
+ std::is_same::value ||
+ std::is_same::value ||
+ std::is_same::value;
+}
+
+/**
+ * @defgroup gnss_enum_types Enums and helper functions for describing GNSS
+ * satellites and their signals.
+ * @ingroup enum_definitions
+ *
+ * @section gnss_enums_to_bitmasks
+ * The GNSS signal enumerations can be used in bitmasks, such as listing
+ * enabled/disabled signal types. In this case, the bit mask corresponds with
+ * the enum value as follows: `mask = (1 << bit value)`
+ *
+ * For example, for GPS signal types, to enable L1 C/A (enum value of 0) and L5
+ * (enum value of 5) signals, a mask of 0x21 would be used, calculated as shown:
+ * 0x21 (0b0010 0001) = 0x1 (0b0000 0001) | 0x20 (0b0010 0000)
+ *
+ * @{
+ */
+
+/**
+ * @brief Create a @ref GNSSSignalType from the enums that it's made up from.
+ *
+ * @tparam SignalName The type of the `SignalName` (see @ref IsSignalNameType())
+ * that is included in this signal type.
+ * @param sv_type The constellation of this signal.
+ * @param freq_band The frequency band of this signal.
+ * @param signal_name The type of signal for the constellation. For example, if
+ * the `sv_type` is @ref SatelliteType::GPS, than this will specify which
+ * of the @ref GPSSignalName this signal is.
+ * @param component The components of this signal.
+ *
+ * @return A @ref GNSSSignalType value made from combining the parameters.
+ */
+// Note: This function intentionally does not use P1_CONSTEXPR_FUNC. It must be
+// constexpr to be used within the GNSSSignalType enum definition below.
+// Instead, where multi-line constexpr functions are not supported (i.e., before
+// C++14), the static_assert is disabled to allow the function to compile.
+template
+constexpr uint16_t ToSignalVal(SatelliteType sv_type, FrequencyBand freq_band,
+ SignalName signal_name,
+ GNSSComponent component) {
+#if P1_HAVE_MULTILINE_CONSTEXPR_FUNC
+ static_assert(IsSignalNameType(),
+ "signal_name must be one of the *SignalName types.");
+#endif // P1_HAVE_MULTILINE_CONSTEXPR_FUNC
+ return static_cast(sv_type) << SATELLITE_TYPE_SHIFT |
+ static_cast(freq_band) << FREQUENCY_BAND_SHIFT |
+ static_cast(signal_name) << SIGNAL_NAME_SHIFT |
+ static_cast(component) << GNSS_COMPONENT_SHIFT;
+}
+
+/**
+ * @copydoc ToSignalVal()
+ *
+ * @warning If the combination of values is not defined in @ref GNSSSignalType,
+ * the returned value will not be valid.
+ */
+template
+P1_CONSTEXPR_FUNC GNSSSignalType ToSignalType(SatelliteType sv_type,
+ FrequencyBand freq_band,
+ SignalName signal_name,
+ GNSSComponent component) {
+ return static_cast(
+ ToSignalVal(sv_type, freq_band, signal_name, component));
+}
+
+// > Start Autogenerated Types (See python/fusion_engine_client/messages/signal_def_gen.py)
+
+/**
+ * @brief System/constellation type definitions.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 4 bits so no values above 15 are allowed.
+ */
+enum class SatelliteType : uint8_t {
+ UNKNOWN = 0,
+ GPS = 1,
+ GLONASS = 2,
+ LEO = 3,
+ GALILEO = 4,
+ BEIDOU = 5,
+ QZSS = 6,
+ MIXED = 7,
+ SBAS = 8,
+ IRNSS = 9,
+};
+
+/**
+ * @brief Get a string representation of the @ref SatelliteType enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(SatelliteType type) {
+ switch (type) {
+ case SatelliteType::UNKNOWN:
+ return "UNKNOWN";
+
+ case SatelliteType::GPS:
+ return "GPS";
+
+ case SatelliteType::GLONASS:
+ return "GLONASS";
+
+ case SatelliteType::LEO:
+ return "LEO";
+
+ case SatelliteType::GALILEO:
+ return "GALILEO";
+
+ case SatelliteType::BEIDOU:
+ return "BEIDOU";
+
+ case SatelliteType::QZSS:
+ return "QZSS";
+
+ case SatelliteType::MIXED:
+ return "MIXED";
+
+ case SatelliteType::SBAS:
+ return "SBAS";
+
+ case SatelliteType::IRNSS:
+ return "IRNSS";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(SatelliteType type) { return to_string(type); }
+
+/**
+ * @brief @ref SatelliteType stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, SatelliteType type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref SatelliteType.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(SatelliteType type) {
+ switch (type) {
+ case SatelliteType::UNKNOWN:
+ return "UNKNOWN";
+
+ case SatelliteType::GPS:
+ return "GPS";
+
+ case SatelliteType::GLONASS:
+ return "GLONASS";
+
+ case SatelliteType::LEO:
+ return "LEO";
+
+ case SatelliteType::GALILEO:
+ return "Galileo";
+
+ case SatelliteType::BEIDOU:
+ return "BeiDou";
+
+ case SatelliteType::QZSS:
+ return "QZSS";
+
+ case SatelliteType::MIXED:
+ return "MIXED";
+
+ case SatelliteType::SBAS:
+ return "SBAS";
+
+ case SatelliteType::IRNSS:
+ return "IRNSS";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref SatelliteType enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref SatelliteType will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC SatelliteType GetSatelliteType(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SATELLITE_TYPE_SHIFT) &
+ ((1 << SATELLITE_TYPE_BITS) - 1)));
+}
+
+/**
+ * @brief GNSS frequency band definitions.
+ *
+ * A frequency band generally includes multiple GNSS carrier frequencies and
+ * signal types, which can usually be captured by a single antenna element.
+ * For example, an L1 antenna typically has sufficient bandwidth to capture
+ * signals on the BeiDou B1I (1561.098 MHz), GPS L1 (1575.42 MHz), and GLONASS G1
+ * (1602.0 MHz) frequencies.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 4 bits so no values about 15 are allowed.
+ */
+enum class FrequencyBand : uint8_t {
+ // ~L1 = 1561.098 MHz (B1) -> 1602.0 (G1)
+ // Includes: GPS L1, Galileo E1, BeiDou B1I (and B1C == L1), GLONASS G1
+ L1 = 0,
+ // ~L2 = 1202.025 MHz (G3) -> 1248.06 (G2)
+ // Includes: GPS L2, Galileo E5b, BeiDou B2I, GLONASS G2 & G3
+ L2 = 1,
+ // ~L5 = 1176.45 MHz (L5)
+ // Includes: GPS L5, Galileo E5a, BeiDou B2a, IRNSS L5
+ L5 = 2,
+ // ~L6 = 1268.52 MHz (B3) -> 1278.75 MHz (L6)
+ // Includes: Galileo E6, BeiDou B3, QZSS L6
+ L6 = 3,
+ // ~(L1 - L2) = 296.67 MHz (QZSS L6) -> 373.395 MHz (G3)
+ L1_L2_WIDE_LANE = 4,
+ // ~(L1 - L5) = 398.97 MHz (L5)
+ L1_L5_WIDE_LANE = 5,
+ // S band 2.0 -> 4.0 GHz
+ // IRNSS S band is 2492.028 MHz
+ S = 6,
+};
+
+/**
+ * @brief Get a string representation of the @ref FrequencyBand enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(FrequencyBand type) {
+ switch (type) {
+ case FrequencyBand::L1:
+ return "L1";
+
+ case FrequencyBand::L2:
+ return "L2";
+
+ case FrequencyBand::L5:
+ return "L5";
+
+ case FrequencyBand::L6:
+ return "L6";
+
+ case FrequencyBand::L1_L2_WIDE_LANE:
+ return "L1_L2_WIDE_LANE";
+
+ case FrequencyBand::L1_L5_WIDE_LANE:
+ return "L1_L5_WIDE_LANE";
+
+ case FrequencyBand::S:
+ return "S";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(FrequencyBand type) { return to_string(type); }
+
+/**
+ * @brief @ref FrequencyBand stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, FrequencyBand type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref FrequencyBand.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(FrequencyBand type) {
+ switch (type) {
+ case FrequencyBand::L1:
+ return "L1";
+
+ case FrequencyBand::L2:
+ return "L2";
+
+ case FrequencyBand::L5:
+ return "L5";
+
+ case FrequencyBand::L6:
+ return "L6";
+
+ case FrequencyBand::L1_L2_WIDE_LANE:
+ return "L1-L2 Wide-Lane";
+
+ case FrequencyBand::L1_L5_WIDE_LANE:
+ return "L1-L5 Wide-Lane";
+
+ case FrequencyBand::S:
+ return "S";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref FrequencyBand enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref FrequencyBand will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC FrequencyBand GetFrequencyBand(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> FREQUENCY_BAND_SHIFT) &
+ ((1 << FREQUENCY_BAND_BITS) - 1)));
+}
+
+/**
+ * @brief The component being tracked for signals that have separate data and
+ * pilot components.
+ *
+ * This needs to be packed into 2 bits so no values above 3 are allowed.
+ */
+enum class GNSSComponent : uint8_t {
+ COMBINED = 0,
+ DATA = 1,
+ PILOT = 2,
+};
+
+/**
+ * @brief Get a string representation of the @ref GNSSComponent enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(GNSSComponent type) {
+ switch (type) {
+ case GNSSComponent::COMBINED:
+ return "COMBINED";
+
+ case GNSSComponent::DATA:
+ return "DATA";
+
+ case GNSSComponent::PILOT:
+ return "PILOT";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(GNSSComponent type) { return to_string(type); }
+
+/**
+ * @brief @ref GNSSComponent stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, GNSSComponent type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref GNSSComponent.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(GNSSComponent type) {
+ switch (type) {
+ case GNSSComponent::COMBINED:
+ return "Combined";
+
+ case GNSSComponent::DATA:
+ return "Data";
+
+ case GNSSComponent::PILOT:
+ return "Pilot";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref GNSSComponent enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref GNSSComponent will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC GNSSComponent GetGNSSComponent(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> GNSS_COMPONENT_SHIFT) &
+ ((1 << GNSS_COMPONENT_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a GPS satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class GPSSignalName : uint8_t {
+ L1CA = 0,
+ L1P = 1,
+ L1C = 2,
+ L2C = 3,
+ L2P = 4,
+ L5 = 5,
+};
+
+/**
+ * @brief Get a string representation of the @ref GPSSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(GPSSignalName type) {
+ switch (type) {
+ case GPSSignalName::L1CA:
+ return "L1CA";
+
+ case GPSSignalName::L1P:
+ return "L1P";
+
+ case GPSSignalName::L1C:
+ return "L1C";
+
+ case GPSSignalName::L2C:
+ return "L2C";
+
+ case GPSSignalName::L2P:
+ return "L2P";
+
+ case GPSSignalName::L5:
+ return "L5";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(GPSSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref GPSSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, GPSSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref GPSSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(GPSSignalName type) {
+ switch (type) {
+ case GPSSignalName::L1CA:
+ return "C/A";
+
+ case GPSSignalName::L1P:
+ return "L1 P(Y)";
+
+ case GPSSignalName::L1C:
+ return "L1C";
+
+ case GPSSignalName::L2C:
+ return "L2C";
+
+ case GPSSignalName::L2P:
+ return "L2 P(Y)";
+
+ case GPSSignalName::L5:
+ return "L5";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref GPSSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref GPSSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC GPSSignalName GetGPSSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a GLONASS satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class GLONASSSignalName : uint8_t {
+ L1CA = 0,
+ L1P = 1,
+ L2CA = 2,
+ L2P = 3,
+};
+
+/**
+ * @brief Get a string representation of the @ref GLONASSSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(GLONASSSignalName type) {
+ switch (type) {
+ case GLONASSSignalName::L1CA:
+ return "L1CA";
+
+ case GLONASSSignalName::L1P:
+ return "L1P";
+
+ case GLONASSSignalName::L2CA:
+ return "L2CA";
+
+ case GLONASSSignalName::L2P:
+ return "L2P";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(GLONASSSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref GLONASSSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, GLONASSSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref GLONASSSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(GLONASSSignalName type) {
+ switch (type) {
+ case GLONASSSignalName::L1CA:
+ return "L1 C/A";
+
+ case GLONASSSignalName::L1P:
+ return "L1P";
+
+ case GLONASSSignalName::L2CA:
+ return "L2 C/A";
+
+ case GLONASSSignalName::L2P:
+ return "L2P";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref GLONASSSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref GLONASSSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC GLONASSSignalName
+GetGLONASSSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a Galileo satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class GalileoSignalName : uint8_t {
+ E1A = 0,
+ E1BC = 1,
+ E5A = 2,
+ E5B = 3,
+ E6A = 4,
+ E6BC = 5,
+};
+
+/**
+ * @brief Get a string representation of the @ref GalileoSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(GalileoSignalName type) {
+ switch (type) {
+ case GalileoSignalName::E1A:
+ return "E1A";
+
+ case GalileoSignalName::E1BC:
+ return "E1BC";
+
+ case GalileoSignalName::E5A:
+ return "E5A";
+
+ case GalileoSignalName::E5B:
+ return "E5B";
+
+ case GalileoSignalName::E6A:
+ return "E6A";
+
+ case GalileoSignalName::E6BC:
+ return "E6BC";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(GalileoSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref GalileoSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, GalileoSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref GalileoSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(GalileoSignalName type) {
+ switch (type) {
+ case GalileoSignalName::E1A:
+ return "E1-A";
+
+ case GalileoSignalName::E1BC:
+ return "E1-B/C";
+
+ case GalileoSignalName::E5A:
+ return "E5a";
+
+ case GalileoSignalName::E5B:
+ return "E5b";
+
+ case GalileoSignalName::E6A:
+ return "E6-A";
+
+ case GalileoSignalName::E6BC:
+ return "E6-B/C";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref GalileoSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref GalileoSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC GalileoSignalName
+GetGalileoSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a BeiDou satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class BeiDouSignalName : uint8_t {
+ B1I = 0,
+ B1C = 1,
+ B2I = 2,
+ B2B = 3,
+ B2A = 4,
+ B3I = 5,
+};
+
+/**
+ * @brief Get a string representation of the @ref BeiDouSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(BeiDouSignalName type) {
+ switch (type) {
+ case BeiDouSignalName::B1I:
+ return "B1I";
+
+ case BeiDouSignalName::B1C:
+ return "B1C";
+
+ case BeiDouSignalName::B2I:
+ return "B2I";
+
+ case BeiDouSignalName::B2B:
+ return "B2B";
+
+ case BeiDouSignalName::B2A:
+ return "B2A";
+
+ case BeiDouSignalName::B3I:
+ return "B3I";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(BeiDouSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref BeiDouSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, BeiDouSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref BeiDouSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(BeiDouSignalName type) {
+ switch (type) {
+ case BeiDouSignalName::B1I:
+ return "B1I";
+
+ case BeiDouSignalName::B1C:
+ return "B1C";
+
+ case BeiDouSignalName::B2I:
+ return "B2I";
+
+ case BeiDouSignalName::B2B:
+ return "B2b";
+
+ case BeiDouSignalName::B2A:
+ return "B2a";
+
+ case BeiDouSignalName::B3I:
+ return "B3I";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref BeiDouSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref BeiDouSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC BeiDouSignalName
+GetBeiDouSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a SBAS satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class SBASSignalName : uint8_t {
+ L1CA = 0,
+ L5 = 1,
+};
+
+/**
+ * @brief Get a string representation of the @ref SBASSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(SBASSignalName type) {
+ switch (type) {
+ case SBASSignalName::L1CA:
+ return "L1CA";
+
+ case SBASSignalName::L5:
+ return "L5";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(SBASSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref SBASSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, SBASSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref SBASSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(SBASSignalName type) {
+ switch (type) {
+ case SBASSignalName::L1CA:
+ return "C/A";
+
+ case SBASSignalName::L5:
+ return "L5";
+ }
+ return "Invalid";
+}
+
+/**
+ * @brief Extract the @ref SBASSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref SBASSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
+ */
+P1_CONSTEXPR_FUNC SBASSignalName GetSBASSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
+
+/**
+ * @brief The name of a signal from a QZSS satellite.
+ *
+ * For some purposes, this enum may be used as part of a bitmask. See
+ * @ref gnss_enums_to_bitmasks.
+ *
+ * This needs to be packed into 3 bits so no values above 7 are allowed.
+ */
+enum class QZSSSignalName : uint8_t {
+ L1CA = 0,
+ L1C = 1,
+ L2C = 2,
+ L5 = 3,
+ L6 = 4,
+};
+
+/**
+ * @brief Get a string representation of the @ref QZSSSignalName enum value.
+ *
+ * @param type The enum to get the string name for.
+ *
+ * @return The corresponding string name.
+ */
+P1_CONSTEXPR_FUNC const char* to_string(QZSSSignalName type) {
+ switch (type) {
+ case QZSSSignalName::L1CA:
+ return "L1CA";
+
+ case QZSSSignalName::L1C:
+ return "L1C";
+
+ case QZSSSignalName::L2C:
+ return "L2C";
+
+ case QZSSSignalName::L5:
+ return "L5";
+
+ case QZSSSignalName::L6:
+ return "L6";
+ }
+ return "INVALID";
+}
+
+/**
+ * @copydoc to_string()
+ */
+inline const char* ToString(QZSSSignalName type) { return to_string(type); }
+
+/**
+ * @brief @ref QZSSSignalName stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, QZSSSignalName type) {
+ stream << to_string(type) << " (" << (int)type << ")";
+ return stream;
+}
+
+/**
+ * @brief Get a human-friendly string for the specified @ref QZSSSignalName.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(QZSSSignalName type) {
+ switch (type) {
+ case QZSSSignalName::L1CA:
+ return "C/A";
+
+ case QZSSSignalName::L1C:
+ return "L1C";
+
+ case QZSSSignalName::L2C:
+ return "L2C";
+
+ case QZSSSignalName::L5:
+ return "L5";
+
+ case QZSSSignalName::L6:
+ return "L6";
+ }
+ return "Invalid";
+}
/**
- * @name GNSS Constellation (System) Definitions
- * @{
+ * @brief Extract the @ref QZSSSignalName enum value from a
+ * @ref GNSSSignalType.
+ *
+ * @param signal_type The signal type from which the @ref QZSSSignalName will
+ * be extracted.
+ *
+ * @return The corresponding enum value.
*/
+P1_CONSTEXPR_FUNC QZSSSignalName GetQZSSSignalName(GNSSSignalType signal_type) {
+ return static_cast(static_cast(
+ (static_cast(signal_type) >> SIGNAL_NAME_SHIFT) &
+ ((1 << SIGNAL_NAME_BITS) - 1)));
+}
/**
- * @brief System/constellation type definitions.
+ * @brief Representation of the combination of GNSS constellation, signal type,
+ * and component being tracked (pilot/data).
+ *
+ * This `enum` is organized as a bitmask, defined as follows:
+ * ```
+ * { SatelliteType (4b), Reserved (2b), FrequencyBand (4b), Reserved (1b),
+ * *SignalName (3b), GNSSComponent (2b) }
+ * ```
+ *
+ * The `*SignalName` mappings are specific to each @ref SatelliteType. For
+ * example, use @ref GPSSignalName for GPS signal definitions.
+ *
+ * Each enumeration entry uniquely identifies an individual GNSS signal being
+ * tracked by the GNSS receiver. For signals that have separate data and pilot
+ * components, the entry indicates which component is being tracked.
*/
-enum class SatelliteType : uint8_t {
+enum class GNSSSignalType : uint16_t {
+ // clang-format off
UNKNOWN = 0,
- GPS = 1,
- GLONASS = 2,
- LEO = 3,
- GALILEO = 4,
- BEIDOU = 5,
- QZSS = 6,
- MIXED = 7,
- SBAS = 8,
- IRNSS = 9,
- MAX_VALUE = IRNSS,
+
+ //////////////////////////////////////////////////////////////////////////////
+ // GPS
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// GPS C/A: 4096 (0x1000)
+ GPS_L1CA = ToSignalVal(SatelliteType::GPS, FrequencyBand::L1,
+ GPSSignalName::L1CA, GNSSComponent::COMBINED),
+ /// GPS L1 P(Y): 4100 (0x1004)
+ GPS_L1P = ToSignalVal(SatelliteType::GPS, FrequencyBand::L1,
+ GPSSignalName::L1P, GNSSComponent::COMBINED),
+ /// GPS L1C: 4104 (0x1008)
+ GPS_L1C = ToSignalVal(SatelliteType::GPS, FrequencyBand::L1,
+ GPSSignalName::L1C, GNSSComponent::COMBINED),
+ /// GPS L1C-D (Data): 4105 (0x1009)
+ GPS_L1C_D = ToSignalVal(SatelliteType::GPS, FrequencyBand::L1,
+ GPSSignalName::L1C, GNSSComponent::DATA),
+ /// GPS L1C-P (Pilot): 4106 (0x100A)
+ GPS_L1C_P = ToSignalVal(SatelliteType::GPS, FrequencyBand::L1,
+ GPSSignalName::L1C, GNSSComponent::PILOT),
+
+ // L2 Band
+
+ /// GPS L2C: 4172 (0x104C)
+ GPS_L2C = ToSignalVal(SatelliteType::GPS, FrequencyBand::L2,
+ GPSSignalName::L2C, GNSSComponent::COMBINED),
+ /// GPS L2C-M (Data): 4173 (0x104D)
+ GPS_L2C_M = ToSignalVal(SatelliteType::GPS, FrequencyBand::L2,
+ GPSSignalName::L2C, GNSSComponent::DATA),
+ /// GPS L2C-L (Pilot): 4174 (0x104E)
+ GPS_L2C_L = ToSignalVal(SatelliteType::GPS, FrequencyBand::L2,
+ GPSSignalName::L2C, GNSSComponent::PILOT),
+ /// GPS L2 P(Y): 4176 (0x1050)
+ GPS_L2P = ToSignalVal(SatelliteType::GPS, FrequencyBand::L2,
+ GPSSignalName::L2P, GNSSComponent::COMBINED),
+
+ // L5 Band
+
+ /// GPS L5: 4244 (0x1094)
+ GPS_L5 = ToSignalVal(SatelliteType::GPS, FrequencyBand::L5,
+ GPSSignalName::L5, GNSSComponent::COMBINED),
+ /// GPS L5-I (Data): 4245 (0x1095)
+ GPS_L5_I = ToSignalVal(SatelliteType::GPS, FrequencyBand::L5,
+ GPSSignalName::L5, GNSSComponent::DATA),
+ /// GPS L5-Q (Pilot): 4246 (0x1096)
+ GPS_L5_Q = ToSignalVal(SatelliteType::GPS, FrequencyBand::L5,
+ GPSSignalName::L5, GNSSComponent::PILOT),
+
+ //////////////////////////////////////////////////////////////////////////////
+ // GLONASS
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// GLONASS L1 C/A: 8192 (0x2000)
+ GLONASS_L1CA = ToSignalVal(SatelliteType::GLONASS, FrequencyBand::L1,
+ GLONASSSignalName::L1CA, GNSSComponent::COMBINED),
+ /// GLONASS L1P: 8196 (0x2004)
+ GLONASS_L1P = ToSignalVal(SatelliteType::GLONASS, FrequencyBand::L1,
+ GLONASSSignalName::L1P, GNSSComponent::COMBINED),
+
+ // L2 Band
+
+ /// GLONASS L2 C/A: 8264 (0x2048)
+ GLONASS_L2CA = ToSignalVal(SatelliteType::GLONASS, FrequencyBand::L2,
+ GLONASSSignalName::L2CA, GNSSComponent::COMBINED),
+ /// GLONASS L2P: 8268 (0x204C)
+ GLONASS_L2P = ToSignalVal(SatelliteType::GLONASS, FrequencyBand::L2,
+ GLONASSSignalName::L2P, GNSSComponent::COMBINED),
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Galileo
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// Galileo E1-A: 16384 (0x4000)
+ GALILEO_E1A = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L1,
+ GalileoSignalName::E1A, GNSSComponent::COMBINED),
+ /// Galileo E1-B/C: 16388 (0x4004)
+ GALILEO_E1BC = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L1,
+ GalileoSignalName::E1BC, GNSSComponent::COMBINED),
+ /// Galileo E1-B (Data): 16389 (0x4005)
+ GALILEO_E1B = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L1,
+ GalileoSignalName::E1BC, GNSSComponent::DATA),
+ /// Galileo E1-C (Pilot): 16390 (0x4006)
+ GALILEO_E1C = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L1,
+ GalileoSignalName::E1BC, GNSSComponent::PILOT),
+
+ // L2 Band
+
+ /// Galileo E5b: 16460 (0x404C)
+ GALILEO_E5B = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L2,
+ GalileoSignalName::E5B, GNSSComponent::COMBINED),
+ /// Galileo E5b-I (Data): 16461 (0x404D)
+ GALILEO_E5B_I = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L2,
+ GalileoSignalName::E5B, GNSSComponent::DATA),
+ /// Galileo E5b-Q (Pilot): 16462 (0x404E)
+ GALILEO_E5B_Q = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L2,
+ GalileoSignalName::E5B, GNSSComponent::PILOT),
+
+ // L5 Band
+
+ /// Galileo E5a: 16520 (0x4088)
+ GALILEO_E5A = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L5,
+ GalileoSignalName::E5A, GNSSComponent::COMBINED),
+ /// Galileo E5a-I (Data): 16521 (0x4089)
+ GALILEO_E5A_I = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L5,
+ GalileoSignalName::E5A, GNSSComponent::DATA),
+ /// Galileo E5a-Q (Pilot): 16522 (0x408A)
+ GALILEO_E5A_Q = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L5,
+ GalileoSignalName::E5A, GNSSComponent::PILOT),
+
+ // L6 Band
+
+ /// Galileo E6-A: 16592 (0x40D0)
+ GALILEO_E6A = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L6,
+ GalileoSignalName::E6A, GNSSComponent::COMBINED),
+ /// Galileo E6-B/C: 16596 (0x40D4)
+ GALILEO_E6BC = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L6,
+ GalileoSignalName::E6BC, GNSSComponent::COMBINED),
+ /// Galileo E6-B (Data): 16597 (0x40D5)
+ GALILEO_E6B = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L6,
+ GalileoSignalName::E6BC, GNSSComponent::DATA),
+ /// Galileo E6-C (Pilot): 16598 (0x40D6)
+ GALILEO_E6C = ToSignalVal(SatelliteType::GALILEO, FrequencyBand::L6,
+ GalileoSignalName::E6BC, GNSSComponent::PILOT),
+
+ //////////////////////////////////////////////////////////////////////////////
+ // BeiDou
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// BeiDou B1I: 20480 (0x5000)
+ BEIDOU_B1I = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L1,
+ BeiDouSignalName::B1I, GNSSComponent::COMBINED),
+ /// BeiDou B1C: 20484 (0x5004)
+ BEIDOU_B1C = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L1,
+ BeiDouSignalName::B1C, GNSSComponent::COMBINED),
+ /// BeiDou B1C-D (Data): 20485 (0x5005)
+ BEIDOU_B1C_D = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L1,
+ BeiDouSignalName::B1C, GNSSComponent::DATA),
+ /// BeiDou B1C-P (Pilot): 20486 (0x5006)
+ BEIDOU_B1C_P = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L1,
+ BeiDouSignalName::B1C, GNSSComponent::PILOT),
+
+ // L2 Band
+
+ /// BeiDou B2I: 20552 (0x5048)
+ BEIDOU_B2I = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L2,
+ BeiDouSignalName::B2I, GNSSComponent::COMBINED),
+ /// BeiDou B2b: 20556 (0x504C)
+ BEIDOU_B2B = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L2,
+ BeiDouSignalName::B2B, GNSSComponent::COMBINED),
+
+ // L5 Band
+
+ /// BeiDou B2a: 20624 (0x5090)
+ BEIDOU_B2A = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L5,
+ BeiDouSignalName::B2A, GNSSComponent::COMBINED),
+ /// BeiDou B2a-D (Data): 20625 (0x5091)
+ BEIDOU_B2A_D = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L5,
+ BeiDouSignalName::B2A, GNSSComponent::DATA),
+ /// BeiDou B2a-P (Pilot): 20626 (0x5092)
+ BEIDOU_B2A_P = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L5,
+ BeiDouSignalName::B2A, GNSSComponent::PILOT),
+
+ // L6 Band
+
+ /// BeiDou B3I: 20692 (0x50D4)
+ BEIDOU_B3I = ToSignalVal(SatelliteType::BEIDOU, FrequencyBand::L6,
+ BeiDouSignalName::B3I, GNSSComponent::COMBINED),
+
+ //////////////////////////////////////////////////////////////////////////////
+ // QZSS
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// QZSS C/A: 24576 (0x6000)
+ QZSS_L1CA = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L1,
+ QZSSSignalName::L1CA, GNSSComponent::COMBINED),
+ /// QZSS L1C: 24580 (0x6004)
+ QZSS_L1C = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L1,
+ QZSSSignalName::L1C, GNSSComponent::COMBINED),
+ /// QZSS L1C-D (Data): 24581 (0x6005)
+ QZSS_L1C_D = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L1,
+ QZSSSignalName::L1C, GNSSComponent::DATA),
+ /// QZSS L1C-P (Pilot): 24582 (0x6006)
+ QZSS_L1C_P = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L1,
+ QZSSSignalName::L1C, GNSSComponent::PILOT),
+
+ // L2 Band
+
+ /// QZSS L2C: 24648 (0x6048)
+ QZSS_L2C = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L2C, GNSSComponent::COMBINED),
+ /// QZSS L2C-M (Data): 24649 (0x6049)
+ QZSS_L2C_M = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L2C, GNSSComponent::DATA),
+ /// QZSS L2C-L (Pilot): 24650 (0x604A)
+ QZSS_L2C_L = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L2C, GNSSComponent::PILOT),
+ /// QZSS L6: 24656 (0x6050)
+ QZSS_L6 = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L6, GNSSComponent::COMBINED),
+ /// QZSS L6-M (Data): 24657 (0x6051)
+ QZSS_L6_M = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L6, GNSSComponent::DATA),
+ /// QZSS L6-L (Pilot): 24658 (0x6052)
+ QZSS_L6_L = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L2,
+ QZSSSignalName::L6, GNSSComponent::PILOT),
+
+ // L5 Band
+
+ /// QZSS L5: 24716 (0x608C)
+ QZSS_L5 = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L5,
+ QZSSSignalName::L5, GNSSComponent::COMBINED),
+ /// QZSS L5-I (Data): 24717 (0x608D)
+ QZSS_L5_I = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L5,
+ QZSSSignalName::L5, GNSSComponent::DATA),
+ /// QZSS L5-Q (Pilot): 24718 (0x608E)
+ QZSS_L5_Q = ToSignalVal(SatelliteType::QZSS, FrequencyBand::L5,
+ QZSSSignalName::L5, GNSSComponent::PILOT),
+
+ //////////////////////////////////////////////////////////////////////////////
+ // SBAS
+ //////////////////////////////////////////////////////////////////////////////
+
+ // L1 Band
+
+ /// SBAS C/A: 32768 (0x8000)
+ SBAS_L1CA = ToSignalVal(SatelliteType::SBAS, FrequencyBand::L1,
+ SBASSignalName::L1CA, GNSSComponent::COMBINED),
+
+ // L5 Band
+
+ /// SBAS L5: 32900 (0x8084)
+ SBAS_L5 = ToSignalVal(SatelliteType::SBAS, FrequencyBand::L5,
+ SBASSignalName::L5, GNSSComponent::COMBINED),
+ /// SBAS L5-I (Data): 32901 (0x8085)
+ SBAS_L5_I = ToSignalVal(SatelliteType::SBAS, FrequencyBand::L5,
+ SBASSignalName::L5, GNSSComponent::DATA),
+ /// SBAS L5-Q (Pilot): 32902 (0x8086)
+ SBAS_L5_Q = ToSignalVal(SatelliteType::SBAS, FrequencyBand::L5,
+ SBASSignalName::L5, GNSSComponent::PILOT),
+ // clang-format on
};
/**
- * @brief Get a human-friendly string name for the specified @ref SatelliteType
- * (GNSS constellation).
- * @ingroup enum_definitions
+ * @brief Get a string representation of the @ref GNSSSignalType enum value.
*
- * @param type The desired satellite type.
+ * @param type The enum to get the string name for.
*
* @return The corresponding string name.
*/
-P1_CONSTEXPR_FUNC const char* to_string(SatelliteType type) {
+P1_CONSTEXPR_FUNC const char* to_string(GNSSSignalType type) {
switch (type) {
- case SatelliteType::UNKNOWN:
- return "Unknown";
+ case GNSSSignalType::GPS_L1CA:
+ return "GPS_L1CA";
- case SatelliteType::GPS:
- return "GPS";
+ case GNSSSignalType::GPS_L1P:
+ return "GPS_L1P";
- case SatelliteType::GLONASS:
- return "GLONASS";
+ case GNSSSignalType::GPS_L1C:
+ return "GPS_L1C";
- case SatelliteType::LEO:
- return "LEO";
+ case GNSSSignalType::GPS_L1C_D:
+ return "GPS_L1C_D";
- case SatelliteType::GALILEO:
- return "Galileo";
+ case GNSSSignalType::GPS_L1C_P:
+ return "GPS_L1C_P";
- case SatelliteType::BEIDOU:
- return "BeiDou";
+ case GNSSSignalType::GPS_L2C:
+ return "GPS_L2C";
- case SatelliteType::QZSS:
- return "QZSS";
+ case GNSSSignalType::GPS_L2C_M:
+ return "GPS_L2C_M";
- case SatelliteType::MIXED:
- return "Mixed";
+ case GNSSSignalType::GPS_L2C_L:
+ return "GPS_L2C_L";
- case SatelliteType::SBAS:
- return "SBAS";
+ case GNSSSignalType::GPS_L2P:
+ return "GPS_L2P";
- case SatelliteType::IRNSS:
- return "IRNSS";
+ case GNSSSignalType::GPS_L5:
+ return "GPS_L5";
+
+ case GNSSSignalType::GPS_L5_I:
+ return "GPS_L5_I";
+
+ case GNSSSignalType::GPS_L5_Q:
+ return "GPS_L5_Q";
+
+ case GNSSSignalType::GLONASS_L1CA:
+ return "GLONASS_L1CA";
+
+ case GNSSSignalType::GLONASS_L1P:
+ return "GLONASS_L1P";
+
+ case GNSSSignalType::GLONASS_L2CA:
+ return "GLONASS_L2CA";
+
+ case GNSSSignalType::GLONASS_L2P:
+ return "GLONASS_L2P";
+
+ case GNSSSignalType::GALILEO_E1A:
+ return "GALILEO_E1A";
+
+ case GNSSSignalType::GALILEO_E1BC:
+ return "GALILEO_E1BC";
+
+ case GNSSSignalType::GALILEO_E1B:
+ return "GALILEO_E1B";
+
+ case GNSSSignalType::GALILEO_E1C:
+ return "GALILEO_E1C";
+
+ case GNSSSignalType::GALILEO_E5B:
+ return "GALILEO_E5B";
+
+ case GNSSSignalType::GALILEO_E5B_I:
+ return "GALILEO_E5B_I";
+
+ case GNSSSignalType::GALILEO_E5B_Q:
+ return "GALILEO_E5B_Q";
+
+ case GNSSSignalType::GALILEO_E5A:
+ return "GALILEO_E5A";
+
+ case GNSSSignalType::GALILEO_E5A_I:
+ return "GALILEO_E5A_I";
+
+ case GNSSSignalType::GALILEO_E5A_Q:
+ return "GALILEO_E5A_Q";
+
+ case GNSSSignalType::GALILEO_E6A:
+ return "GALILEO_E6A";
+
+ case GNSSSignalType::GALILEO_E6BC:
+ return "GALILEO_E6BC";
+
+ case GNSSSignalType::GALILEO_E6B:
+ return "GALILEO_E6B";
+
+ case GNSSSignalType::GALILEO_E6C:
+ return "GALILEO_E6C";
+
+ case GNSSSignalType::BEIDOU_B1I:
+ return "BEIDOU_B1I";
+
+ case GNSSSignalType::BEIDOU_B1C:
+ return "BEIDOU_B1C";
+
+ case GNSSSignalType::BEIDOU_B1C_D:
+ return "BEIDOU_B1C_D";
+
+ case GNSSSignalType::BEIDOU_B1C_P:
+ return "BEIDOU_B1C_P";
+
+ case GNSSSignalType::BEIDOU_B2I:
+ return "BEIDOU_B2I";
+
+ case GNSSSignalType::BEIDOU_B2B:
+ return "BEIDOU_B2B";
+
+ case GNSSSignalType::BEIDOU_B2A:
+ return "BEIDOU_B2A";
+
+ case GNSSSignalType::BEIDOU_B2A_D:
+ return "BEIDOU_B2A_D";
+
+ case GNSSSignalType::BEIDOU_B2A_P:
+ return "BEIDOU_B2A_P";
+
+ case GNSSSignalType::BEIDOU_B3I:
+ return "BEIDOU_B3I";
+
+ case GNSSSignalType::SBAS_L1CA:
+ return "SBAS_L1CA";
+
+ case GNSSSignalType::SBAS_L5:
+ return "SBAS_L5";
+
+ case GNSSSignalType::SBAS_L5_I:
+ return "SBAS_L5_I";
+
+ case GNSSSignalType::SBAS_L5_Q:
+ return "SBAS_L5_Q";
+
+ case GNSSSignalType::QZSS_L1CA:
+ return "QZSS_L1CA";
+
+ case GNSSSignalType::QZSS_L1C:
+ return "QZSS_L1C";
+
+ case GNSSSignalType::QZSS_L1C_D:
+ return "QZSS_L1C_D";
+
+ case GNSSSignalType::QZSS_L1C_P:
+ return "QZSS_L1C_P";
+
+ case GNSSSignalType::QZSS_L2C:
+ return "QZSS_L2C";
+
+ case GNSSSignalType::QZSS_L2C_M:
+ return "QZSS_L2C_M";
+
+ case GNSSSignalType::QZSS_L2C_L:
+ return "QZSS_L2C_L";
+
+ case GNSSSignalType::QZSS_L5:
+ return "QZSS_L5";
+
+ case GNSSSignalType::QZSS_L5_I:
+ return "QZSS_L5_I";
+
+ case GNSSSignalType::QZSS_L5_Q:
+ return "QZSS_L5_Q";
- default:
- return "Invalid System";
+ case GNSSSignalType::QZSS_L6:
+ return "QZSS_L6";
+
+ case GNSSSignalType::QZSS_L6_M:
+ return "QZSS_L6_M";
+
+ case GNSSSignalType::QZSS_L6_L:
+ return "QZSS_L6_L";
+
+ case GNSSSignalType::UNKNOWN:
+ return "UNKNOWN";
}
+ return "INVALID";
}
/**
- * @brief @ref SatelliteType stream operator.
- * @ingroup enum_definitions
+ * @copydoc to_string()
*/
-inline p1_ostream& operator<<(p1_ostream& stream, SatelliteType type) {
+inline const char* ToString(GNSSSignalType type) { return to_string(type); }
+
+/**
+ * @brief @ref GNSSSignalType stream operator.
+ */
+inline p1_ostream& operator<<(p1_ostream& stream, GNSSSignalType type) {
stream << to_string(type) << " (" << (int)type << ")";
return stream;
}
-/** @} */
+/**
+ * @brief Get a human-friendly string for the specified @ref GNSSSignalType.
+ *
+ * @param type The enum to get the string for.
+ *
+ * @return The corresponding string.
+ */
+P1_CONSTEXPR_FUNC const char* ToPrettyString(GNSSSignalType type) {
+ switch (type) {
+ case GNSSSignalType::GPS_L1CA:
+ return "GPS C/A";
+
+ case GNSSSignalType::GPS_L1P:
+ return "GPS L1 P(Y)";
+
+ case GNSSSignalType::GPS_L1C:
+ return "GPS L1C";
+
+ case GNSSSignalType::GPS_L1C_D:
+ return "GPS L1C-D (Data)";
+
+ case GNSSSignalType::GPS_L1C_P:
+ return "GPS L1C-P (Pilot)";
+
+ case GNSSSignalType::GPS_L2C:
+ return "GPS L2C";
+
+ case GNSSSignalType::GPS_L2C_M:
+ return "GPS L2C-M (Data)";
+
+ case GNSSSignalType::GPS_L2C_L:
+ return "GPS L2C-L (Pilot)";
+
+ case GNSSSignalType::GPS_L2P:
+ return "GPS L2 P(Y)";
+
+ case GNSSSignalType::GPS_L5:
+ return "GPS L5";
+
+ case GNSSSignalType::GPS_L5_I:
+ return "GPS L5-I (Data)";
+
+ case GNSSSignalType::GPS_L5_Q:
+ return "GPS L5-Q (Pilot)";
+
+ case GNSSSignalType::GLONASS_L1CA:
+ return "GLONASS L1 C/A";
+
+ case GNSSSignalType::GLONASS_L1P:
+ return "GLONASS L1P";
+
+ case GNSSSignalType::GLONASS_L2CA:
+ return "GLONASS L2 C/A";
+
+ case GNSSSignalType::GLONASS_L2P:
+ return "GLONASS L2P";
+
+ case GNSSSignalType::GALILEO_E1A:
+ return "Galileo E1-A";
+
+ case GNSSSignalType::GALILEO_E1BC:
+ return "Galileo E1-B/C";
+
+ case GNSSSignalType::GALILEO_E1B:
+ return "Galileo E1-B (Data)";
+
+ case GNSSSignalType::GALILEO_E1C:
+ return "Galileo E1-C (Pilot)";
+
+ case GNSSSignalType::GALILEO_E5B:
+ return "Galileo E5b";
+
+ case GNSSSignalType::GALILEO_E5B_I:
+ return "Galileo E5b-I (Data)";
+
+ case GNSSSignalType::GALILEO_E5B_Q:
+ return "Galileo E5b-Q (Pilot)";
+
+ case GNSSSignalType::GALILEO_E5A:
+ return "Galileo E5a";
+
+ case GNSSSignalType::GALILEO_E5A_I:
+ return "Galileo E5a-I (Data)";
+
+ case GNSSSignalType::GALILEO_E5A_Q:
+ return "Galileo E5a-Q (Pilot)";
+
+ case GNSSSignalType::GALILEO_E6A:
+ return "Galileo E6-A";
+
+ case GNSSSignalType::GALILEO_E6BC:
+ return "Galileo E6-B/C";
+
+ case GNSSSignalType::GALILEO_E6B:
+ return "Galileo E6-B (Data)";
+
+ case GNSSSignalType::GALILEO_E6C:
+ return "Galileo E6-C (Pilot)";
+
+ case GNSSSignalType::BEIDOU_B1I:
+ return "BeiDou B1I";
+
+ case GNSSSignalType::BEIDOU_B1C:
+ return "BeiDou B1C";
+
+ case GNSSSignalType::BEIDOU_B1C_D:
+ return "BeiDou B1C-D (Data)";
+
+ case GNSSSignalType::BEIDOU_B1C_P:
+ return "BeiDou B1C-P (Pilot)";
+
+ case GNSSSignalType::BEIDOU_B2I:
+ return "BeiDou B2I";
+
+ case GNSSSignalType::BEIDOU_B2B:
+ return "BeiDou B2b";
+
+ case GNSSSignalType::BEIDOU_B2A:
+ return "BeiDou B2a";
+
+ case GNSSSignalType::BEIDOU_B2A_D:
+ return "BeiDou B2a-D (Data)";
+
+ case GNSSSignalType::BEIDOU_B2A_P:
+ return "BeiDou B2a-P (Pilot)";
+
+ case GNSSSignalType::BEIDOU_B3I:
+ return "BeiDou B3I";
+
+ case GNSSSignalType::SBAS_L1CA:
+ return "SBAS C/A";
+
+ case GNSSSignalType::SBAS_L5:
+ return "SBAS L5";
+
+ case GNSSSignalType::SBAS_L5_I:
+ return "SBAS L5-I (Data)";
+
+ case GNSSSignalType::SBAS_L5_Q:
+ return "SBAS L5-Q (Pilot)";
+
+ case GNSSSignalType::QZSS_L1CA:
+ return "QZSS C/A";
+
+ case GNSSSignalType::QZSS_L1C:
+ return "QZSS L1C";
+
+ case GNSSSignalType::QZSS_L1C_D:
+ return "QZSS L1C-D (Data)";
+
+ case GNSSSignalType::QZSS_L1C_P:
+ return "QZSS L1C-P (Pilot)";
+
+ case GNSSSignalType::QZSS_L2C:
+ return "QZSS L2C";
+
+ case GNSSSignalType::QZSS_L2C_M:
+ return "QZSS L2C-M (Data)";
+
+ case GNSSSignalType::QZSS_L2C_L:
+ return "QZSS L2C-L (Pilot)";
+
+ case GNSSSignalType::QZSS_L5:
+ return "QZSS L5";
+
+ case GNSSSignalType::QZSS_L5_I:
+ return "QZSS L5-I (Data)";
+
+ case GNSSSignalType::QZSS_L5_Q:
+ return "QZSS L5-Q (Pilot)";
+
+ case GNSSSignalType::QZSS_L6:
+ return "QZSS L6";
+
+ case GNSSSignalType::QZSS_L6_M:
+ return "QZSS L6-M (Data)";
+
+ case GNSSSignalType::QZSS_L6_L:
+ return "QZSS L6-L (Pilot)";
+
+ case GNSSSignalType::UNKNOWN:
+ return "Unknown";
+ }
+ return "Invalid";
+}
+
+// < Stop Autogenerated Types (See python/fusion_engine_client/messages/signal_def_gen.py)
/**
* @defgroup sat_type_masks @ref SatelliteType Bitmask Support
@@ -160,7 +1773,7 @@ static constexpr uint32_t SATELLITE_TYPE_MASK_ALL = 0xFFFFFFFF;
*
* @return The corresponding bitmask.
*/
-constexpr uint32_t ToBitMask(SatelliteType type) {
+P1_CONSTEXPR_FUNC uint32_t ToBitMask(SatelliteType type) {
return (1U << (static_cast(type)));
}
@@ -189,92 +1802,12 @@ constexpr uint32_t ToBitMask(SatelliteType type) {
* @return The corresponding bitmask.
*/
template
-constexpr uint32_t ToBitMask(SatelliteType first, Args... others) {
+P1_CONSTEXPR_FUNC uint32_t ToBitMask(SatelliteType first, Args... others) {
return ToBitMask(first) | ToBitMask(others...);
}
/** @} */
-////////////////////////////////////////////////////////////////////////////////
-// FrequencyBand
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * @name GNSS Constellation (System) Definitions
- * @{
- */
-
-/**
- * @brief GNSS frequency band definitions.
- */
-enum class FrequencyBand : uint8_t {
- UNKNOWN = 0,
- /**
- * L1 band = 1561.098 MHz (BeiDou B1) -> 1602.0 (GLONASS G1)
- * Includes: GPS/QZSS L1, Galileo E1 (same as GPS L1), BeiDou B1I and B1C
- * (same as GPS L1), GLONASS G1
- */
- L1 = 1,
- /**
- * L2 band = 1202.025 MHz (G3) -> 1248.06 (G2)
- * Includes: GPS L2, Galileo E5b, BeiDou B2I (same as Galileo E5b),
- * GLONASS G2 & G3
- */
- L2 = 2,
- /**
- * L5 band = 1176.45 MHz (L5)
- * Includes: GPS/QZSS L5, Galileo E5a, BeiDou B2a, IRNSS L5
- */
- L5 = 5,
- /**
- * L2 band = 1262.52 MHz (B3) -> 1278.75 (QZSS L6)
- * Includes: Galileo E6, BeiDou B3, QZSS L6
- */
- L6 = 6,
- MAX_VALUE = L6,
-};
-
-/**
- * @brief Get a human-friendly string name for the specified @ref FrequencyBand.
- * @ingroup enum_definitions
- *
- * @param type The desired frequency band.
- *
- * @return The corresponding string name.
- */
-P1_CONSTEXPR_FUNC const char* to_string(FrequencyBand type) {
- switch (type) {
- case FrequencyBand::UNKNOWN:
- return "Unknown";
-
- case FrequencyBand::L1:
- return "L1";
-
- case FrequencyBand::L2:
- return "L2";
-
- case FrequencyBand::L5:
- return "L5";
-
- case FrequencyBand::L6:
- return "L6";
-
- default:
- return "Invalid Frequency Band";
- }
-}
-
-/**
- * @brief @ref FrequencyBand stream operator.
- * @ingroup enum_definitions
- */
-inline p1_ostream& operator<<(p1_ostream& stream, FrequencyBand type) {
- stream << to_string(type) << " (" << (int)type << ")";
- return stream;
-}
-
-/** @} */
-
/**
* @defgroup freq_band_masks @ref FrequencyBand Bitmask Support
* @ingroup config_types
@@ -328,7 +1861,7 @@ static constexpr uint32_t FREQUENCY_BAND_MASK_ALL = 0xFFFFFFFF;
*
* @return The corresponding bitmask.
*/
-constexpr uint32_t ToBitMask(FrequencyBand type) {
+P1_CONSTEXPR_FUNC uint32_t ToBitMask(FrequencyBand type) {
return (1U << (static_cast(type)));
}
@@ -355,12 +1888,14 @@ constexpr uint32_t ToBitMask(FrequencyBand type) {
* @return The corresponding bitmask.
*/
template
-constexpr uint32_t ToBitMask(FrequencyBand first, Args... others) {
+P1_CONSTEXPR_FUNC uint32_t ToBitMask(FrequencyBand first, Args... others) {
return ToBitMask(first) | ToBitMask(others...);
}
/** @} */
+/** @} */
+
} // namespace messages
} // namespace fusion_engine
} // namespace point_one
diff --git a/src/point_one/fusion_engine/messages/solution.h b/src/point_one/fusion_engine/messages/solution.h
index 33a0f87a..5156e346 100644
--- a/src/point_one/fusion_engine/messages/solution.h
+++ b/src/point_one/fusion_engine/messages/solution.h
@@ -290,6 +290,10 @@ struct P1_ALIGNAS(4) GNSSInfoMessage : public MessagePayload {
* (@ref MessageType::GNSS_SATELLITE, version 1.0).
* @ingroup solution_messages
*
+ * @deprecated This message is deprecated in favor of the @ref
+ * GNSSSignalsMessage that gives more information on both the
+ * tracked satellites and signals.
+ *
* This message is followed by `N` @ref SatelliteInfo objects, where `N` is
* equal to @ref num_satellites. For example, a message with two satellites
* would be serialized as:
@@ -369,6 +373,162 @@ struct P1_ALIGNAS(4) SatelliteInfo {
float elevation_deg = NAN;
};
+/**
+ * @brief Information about the individual GNSS satellites and signals used in
+ * the @ref PoseMessage and @ref GNSSInfoMessage with the corresponding
+ * timestamp (@ref MessageType::GNSS_SIGNALS, version 1.1).
+ * @ingroup solution_messages
+ *
+ * This message is followed by `N` @ref GNSSSatelliteInfo objects, where `N` is
+ * equal to @ref num_satellites.
+ *
+ * After the satellite data objects, there will be a section of `S` @ref
+ * GNSSSignalInfo objects, where `S` is equal to `num_signals`.
+ *
+ * For example:
+ * - A message with two satellites where the the first had one signal and the
+ * second had two.
+ *
+ * ```
+ * num_satellites=2
+ * num_signals=3
+ *
+ * The data structure of the serialized message:
+ * {MessageHeader, GNSSSignalsMessage, GNSSSatelliteInfo, GNSSSatelliteInfo,
+ * GNSSSignalInfo, GNSSSignalInfo, GNSSSignalInfo}
+ * ```
+ */
+struct P1_ALIGNAS(4) GNSSSignalsMessage : public MessagePayload {
+ static constexpr MessageType MESSAGE_TYPE = MessageType::GNSS_SIGNALS;
+ static constexpr uint8_t MESSAGE_VERSION = 1;
+
+ static constexpr uint16_t INVALID_GPS_WEEK = 0xFFFF;
+ static constexpr uint32_t INVALID_GPS_TOW = 0xFFFFFFFF;
+
+ /** The time of the message, in P1 time (beginning at power-on). */
+ Timestamp p1_time;
+
+ /**
+ * The precise GPS time of the message, if available, referenced to 1980/1/6.
+ */
+ Timestamp gps_time;
+
+ /** The approximate GPS time of week in milliseconds. */
+ uint32_t gps_tow_ms = INVALID_GPS_TOW;
+
+ /** The GPS week number. */
+ uint16_t gps_week = INVALID_GPS_WEEK;
+
+ /** The number of GNSS signals reported in this message. */
+ uint16_t num_signals = 0;
+
+ /** The number satellites reported in this message. */
+ uint8_t num_satellites = 0;
+
+ uint8_t reserved[7] = {0};
+};
+
+/**
+ * @brief Information about an individual satellite (see @ref
+ * GNSSSignalsMessage).
+ */
+struct P1_ALIGNAS(4) GNSSSatelliteInfo {
+ /**
+ * @defgroup gnss_sv_status_flags Bit definitions for the satellite status
+ * bitmask (@ref GNSSSatelliteInfo::status_flags).
+ * @{
+ */
+ static constexpr uint8_t STATUS_FLAG_IS_USED = 0x01;
+ static constexpr uint8_t STATUS_FLAG_IS_UNHEALTHY = 0x02;
+ static constexpr uint8_t STATUS_FLAG_IS_NON_LINE_OF_SIGHT = 0x04;
+ static constexpr uint8_t STATUS_FLAG_HAS_EPHEM = 0x10;
+ static constexpr uint8_t STATUS_FLAG_HAS_SBAS = 0x20;
+ /** @} */
+
+ static constexpr uint16_t INVALID_AZIMUTH = 0xFFFF;
+ static constexpr int16_t INVALID_ELEVATION = 0x7FFF;
+
+ /** The GNSS system to which this satellite belongs. */
+ SatelliteType system = SatelliteType::UNKNOWN;
+
+ /** The satellite's PRN (or slot number for GLONASS). */
+ uint8_t prn = 0;
+
+ /**
+ * A bitmask specifying how this satellite was used and what information was
+ * available for it.
+ */
+ uint8_t status_flags = 0;
+
+ uint8_t reserved[1] = {0};
+
+ /** The elevation of the satellite [-90, 90] (in 0.01 degrees). */
+ int16_t elevation_cdeg = INVALID_ELEVATION;
+
+ /**
+ * The azimuth of the satellite [0,359] (in 0.01 degrees). 0 is north, and
+ * azimuth increases in a clockwise direction.
+ */
+ uint16_t azimuth_cdeg = INVALID_AZIMUTH;
+};
+static_assert(sizeof(GNSSSatelliteInfo) == 8,
+ "GNSSSatelliteInfo does not match expected packed size.");
+
+/**
+ * @brief Information about an individual GNSS signal (see @ref
+ * GNSSSignalsMessage).
+ */
+struct P1_ALIGNAS(4) GNSSSignalInfo {
+ /**
+ * @defgroup gnss_signal_status_flags Bit definitions for the signal status
+ * bitmask (@ref GNSSSignalInfo::status_flags).
+ * @{
+ */
+ static constexpr uint16_t STATUS_FLAG_USED_PR = 0x01;
+ static constexpr uint16_t STATUS_FLAG_USED_DOPPLER = 0x02;
+ static constexpr uint16_t STATUS_FLAG_USED_CARRIER = 0x04;
+ static constexpr uint16_t STATUS_FLAG_CARRIER_AMBIGUITY_RESOLVED = 0x08;
+
+ static constexpr uint16_t STATUS_FLAG_VALID_PR = 0x10;
+ static constexpr uint16_t STATUS_FLAG_VALID_DOPPLER = 0x20;
+ static constexpr uint16_t STATUS_FLAG_CARRIER_LOCKED = 0x40;
+
+ static constexpr uint16_t STATUS_FLAG_HAS_RTK = 0x100;
+ static constexpr uint16_t STATUS_FLAG_HAS_SBAS = 0x200;
+ static constexpr uint16_t STATUS_FLAG_HAS_EPHEM = 0x400;
+ /** @} */
+
+ static constexpr uint8_t INVALID_CN0 = 0;
+
+ /** The type of signal being reported. */
+ GNSSSignalType signal_type = GNSSSignalType::UNKNOWN;
+
+ /**
+ * The PRN (or slot number for GLONASS) of the satellite that generated this
+ * signal.
+ */
+ uint8_t prn = 0;
+
+ /**
+ * The carrier-to-noise density ratio (C/N0) this signal.
+ *
+ * Stored in units of 0.25 dB-Hz: `cn0_dbhz = cn0 * 0.25`. Set to 0 if
+ * invalid. The range of this field is 0.25-63.75 dB-Hz. Values outside of
+ * this range will be clipped to the min/max values.
+ */
+ uint8_t cn0 = INVALID_CN0;
+
+ /**
+ * A bitmask specifying how this signal was used and what information was
+ * available for it.
+ */
+ uint16_t status_flags = 0;
+
+ uint8_t reserved[2] = {0};
+};
+static_assert(sizeof(GNSSSignalInfo) == 8,
+ "GNSSSignalInfo does not match expected packed size.");
+
/**
* @brief The stages of the device calibration process.
* @ingroup solution_messages