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