mirror of
https://github.com/gnss-sdr/gnss-sdr
synced 2026-07-02 09:28:50 +00:00
322 lines
11 KiB
Python
Executable File
322 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
dll_pll_veml_plot_sample.py
|
|
|
|
Reads GNSS-SDR DLL/PLL VEML tracking dump binary files and plots internal
|
|
tracking variables.
|
|
|
|
File format:
|
|
{input_path}/{file_prefix}{channel}.dat
|
|
|
|
-----------------------------------------------------------------------------
|
|
|
|
GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
|
This file is part of GNSS-SDR.
|
|
|
|
Copyright (C) 2022 (see AUTHORS file for a list of contributors)
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
-----------------------------------------------------------------------------
|
|
"""
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
from lib.dll_pll_veml_read_tracking_dump import dll_pll_veml_read_tracking_dump
|
|
from lib.dump_filename import resolve_dump_prefix
|
|
from lib.gnss_sdr_conf import (
|
|
ChannelDump,
|
|
ConfigError,
|
|
SIGNAL_TYPES,
|
|
add_conf_argument,
|
|
load_gnss_sdr_conf,
|
|
signal_pretty_name,
|
|
)
|
|
from lib.plot_format import add_output_format_argument, apply_publication_style
|
|
from lib.plotVEMLTracking import plotVEMLTracking
|
|
|
|
DEFAULT_FILE_PREFIX = "track_ch"
|
|
DEFAULT_SAMPLING_FREQUENCY = 3000000.0
|
|
DEFAULT_CHANNELS = 5
|
|
DEFAULT_FIRST_CHANNEL = 0
|
|
DEFAULT_SIGNAL_TYPE = "1C"
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Plot GNSS-SDR DLL/PLL VEML tracking dumps."
|
|
)
|
|
add_conf_argument(parser)
|
|
parser.add_argument(
|
|
"-i",
|
|
"--input-path",
|
|
type=Path,
|
|
default=Path("."),
|
|
help="Directory containing tracking .dat dumps (default: .).",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--fig-path",
|
|
type=Path,
|
|
default=Path("plots/dll-pll-veml-tracking"),
|
|
help="Directory where plots are saved.",
|
|
)
|
|
parser.add_argument(
|
|
"--file-prefix",
|
|
default=None,
|
|
help="GNSS-SDR Tracking.dump_filename value. May include a directory "
|
|
"and extension; the matching <prefix><channel>.dat files are read, "
|
|
"resolved against --input-path. Defaults to the configured Tracking "
|
|
"dump filename or filenames from --conf, or track_ch.",
|
|
)
|
|
parser.add_argument(
|
|
"--sampling-frequency",
|
|
type=float,
|
|
default=None,
|
|
help="Signal sampling frequency in Hz. Defaults to "
|
|
"GNSS-SDR.internal_fs_sps from --conf, or 3000000.0.",
|
|
)
|
|
parser.add_argument(
|
|
"--channels",
|
|
type=int,
|
|
default=None,
|
|
help="Number of channels to read. Defaults to all selected channels "
|
|
"from --conf, or 5.",
|
|
)
|
|
parser.add_argument(
|
|
"--first-channel",
|
|
type=int,
|
|
default=None,
|
|
help="First channel number in the dump filenames. Defaults to the "
|
|
"first selected absolute channel from --conf, or 0.",
|
|
)
|
|
parser.add_argument(
|
|
"--signal-type",
|
|
type=str.upper,
|
|
choices=sorted(SIGNAL_TYPES),
|
|
default=None,
|
|
metavar="CODE",
|
|
help="GNSS-SDR signal code used to restrict channel ranges and dump "
|
|
"filenames from --conf (e.g. 1C, 5X, 7X, E6, J1). Omitted with a "
|
|
"multi-signal --conf means all configured signals.",
|
|
)
|
|
parser.add_argument(
|
|
"--plot-last-outputs",
|
|
type=int,
|
|
default=0,
|
|
help="Only plot the last N outputs; 0 plots all outputs.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-doppler",
|
|
dest="plot_doppler",
|
|
action="store_false",
|
|
help="Do not generate the extra Doppler-only plots.",
|
|
)
|
|
parser.add_argument(
|
|
"--show",
|
|
action="store_true",
|
|
help="Display figures interactively after saving them.",
|
|
)
|
|
add_output_format_argument(parser)
|
|
parser.set_defaults(plot_doppler=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
apply_conf_defaults(args)
|
|
except ConfigError as exc:
|
|
parser.error(str(exc))
|
|
return args
|
|
|
|
|
|
def apply_conf_defaults(args):
|
|
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
|
signals = []
|
|
explicit_range = args.channels is not None or args.first_channel is not None
|
|
|
|
if conf is not None:
|
|
signals = conf.select_signals(args.signal_type)
|
|
if args.signal_type is not None:
|
|
args.signal_type = signals[0].signal
|
|
|
|
if args.file_prefix is None:
|
|
if conf is None or (len(signals) == 1 and explicit_range):
|
|
signal = signals[0] if signals else None
|
|
args.file_prefix = (
|
|
signal.tracking_dump_filename
|
|
if signal is not None and signal.tracking_dump_filename
|
|
else DEFAULT_FILE_PREFIX
|
|
)
|
|
if args.sampling_frequency is None:
|
|
if conf is not None:
|
|
if conf.internal_fs_sps is None:
|
|
raise ConfigError(
|
|
"GNSS-SDR.internal_fs_sps is required to infer "
|
|
"--sampling-frequency."
|
|
)
|
|
args.sampling_frequency = conf.internal_fs_sps
|
|
else:
|
|
args.sampling_frequency = DEFAULT_SAMPLING_FREQUENCY
|
|
|
|
if conf is not None and not explicit_range:
|
|
args.channel_plan = conf.channel_dump_plan(
|
|
signals=signals,
|
|
dump_filename_attr="tracking_dump_filename",
|
|
default_file_prefix=DEFAULT_FILE_PREFIX,
|
|
override_file_prefix=args.file_prefix,
|
|
)
|
|
args.channels = len(args.channel_plan)
|
|
args.first_channel = args.channel_plan[0].channel
|
|
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
|
if len(signals) == 1:
|
|
args.file_prefix = args.channel_plan[0].file_prefix
|
|
return
|
|
|
|
if args.channels is None:
|
|
args.channels = signals[0].count if signals else DEFAULT_CHANNELS
|
|
|
|
if args.first_channel is None:
|
|
args.first_channel = (
|
|
signals[0].first_channel if signals else DEFAULT_FIRST_CHANNEL
|
|
)
|
|
|
|
if args.file_prefix is None:
|
|
args.file_prefix = DEFAULT_FILE_PREFIX
|
|
|
|
manual_signal = args.signal_type or (
|
|
signals[0].signal if signals else DEFAULT_SIGNAL_TYPE
|
|
)
|
|
args.channel_plan = [
|
|
ChannelDump(
|
|
channel=channel,
|
|
signal=manual_signal,
|
|
file_prefix=args.file_prefix,
|
|
)
|
|
for channel in range(args.first_channel, args.first_channel + args.channels)
|
|
]
|
|
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
|
|
|
|
|
def read_tracking_dumps(args):
|
|
dumps = []
|
|
for entry in args.channel_plan:
|
|
directory, base = resolve_dump_prefix(entry.file_prefix, args.input_path)
|
|
channel = entry.channel
|
|
tracking_log_path = directory / f"{base}{channel}.dat"
|
|
if not tracking_log_path.exists():
|
|
print(f"Skipping channel {channel}: missing {tracking_log_path}")
|
|
continue
|
|
tracking = dll_pll_veml_read_tracking_dump(tracking_log_path)
|
|
if not tracking["PRN"]:
|
|
print(f"Skipping channel {channel}: no samples in {tracking_log_path}")
|
|
continue
|
|
tracking["_channel"] = channel
|
|
tracking["_signal_type"] = entry.signal
|
|
dumps.append(tracking)
|
|
return dumps
|
|
|
|
|
|
def prn_label(prns):
|
|
prns = dict.fromkeys(int(prn) for prn in prns if int(prn))
|
|
return "/".join(str(prn) for prn in prns) or "unknown"
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
args.fig_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
apply_publication_style()
|
|
|
|
gnss_tracking = read_tracking_dumps(args)
|
|
if not gnss_tracking:
|
|
raise SystemExit("Error: no channel dumps found to plot.")
|
|
|
|
plotted_channel_ids = [tracking["_channel"] for tracking in gnss_tracking]
|
|
track_results = []
|
|
settings = {
|
|
"numberOfChannels": len(gnss_tracking),
|
|
"firstChannel": plotted_channel_ids[0],
|
|
"channelIds": plotted_channel_ids,
|
|
"fig_path": args.fig_path,
|
|
"show": args.show,
|
|
"output_format": args.output_format,
|
|
}
|
|
|
|
for index, tracking in enumerate(gnss_tracking, start=1):
|
|
if 0 < args.plot_last_outputs < len(tracking["code_freq_hz"]):
|
|
start_sample = len(tracking["code_freq_hz"]) - args.plot_last_outputs
|
|
else:
|
|
start_sample = 0
|
|
|
|
track_result = {
|
|
"status": "T",
|
|
"codeFreq": np.copy(tracking["code_freq_hz"][start_sample:]),
|
|
"carrFreq": np.copy(tracking["carrier_doppler_hz"][start_sample:]),
|
|
"dllDiscr": np.copy(tracking["code_error"][start_sample:]),
|
|
"dllDiscrFilt": np.copy(tracking["code_nco"][start_sample:]),
|
|
"pllDiscr": np.copy(tracking["carr_error"][start_sample:]),
|
|
"pllDiscrFilt": np.copy(tracking["carr_nco"][start_sample:]),
|
|
"I_P": np.copy(tracking["P"][start_sample:]),
|
|
"Q_P": np.zeros(len(tracking["P"][start_sample:])),
|
|
"I_VE": np.copy(tracking["VE"][start_sample:]),
|
|
"I_E": np.copy(tracking["E"][start_sample:]),
|
|
"I_L": np.copy(tracking["L"][start_sample:]),
|
|
"I_VL": np.copy(tracking["VL"][start_sample:]),
|
|
"Q_VE": np.zeros(len(tracking["VE"][start_sample:])),
|
|
"Q_E": np.zeros(len(tracking["E"][start_sample:])),
|
|
"Q_L": np.zeros(len(tracking["L"][start_sample:])),
|
|
"Q_VL": np.zeros(len(tracking["VL"][start_sample:])),
|
|
"data_I": np.copy(tracking["prompt_I"][start_sample:]),
|
|
"data_Q": np.copy(tracking["prompt_Q"][start_sample:]),
|
|
"PRN": np.copy(tracking["PRN"][start_sample:]),
|
|
"CNo": np.copy(tracking["CN0_SNV_dB_Hz"][start_sample:]),
|
|
"prn_start_time_s": (
|
|
np.copy(tracking["PRN_start_sample"][start_sample:])
|
|
/ args.sampling_frequency
|
|
),
|
|
}
|
|
signal_name = signal_pretty_name(
|
|
tracking.get("_signal_type") or DEFAULT_SIGNAL_TYPE
|
|
)
|
|
track_result["signalName"] = signal_name
|
|
track_result["prnLabel"] = prn_label(track_result["PRN"])
|
|
track_result["plotTitle"] = (
|
|
f"{signal_name} PRN {track_result['prnLabel']}"
|
|
)
|
|
track_results.append(track_result)
|
|
|
|
plotVEMLTracking(index, track_results, settings)
|
|
|
|
if args.plot_doppler:
|
|
channel = tracking["_channel"]
|
|
plt.figure()
|
|
plt.plot(
|
|
track_result["prn_start_time_s"],
|
|
[x / 1000 for x in tracking["carrier_doppler_hz"][start_sample:]],
|
|
)
|
|
plt.xlabel("Time (s)")
|
|
plt.ylabel("Doppler (kHz)")
|
|
plt.title(
|
|
f"Doppler frequency - {track_result['plotTitle']} "
|
|
f"(channel {channel})"
|
|
)
|
|
plt.savefig(
|
|
args.fig_path
|
|
/ f"Doppler_freq_ch_{channel}.{args.output_format}"
|
|
)
|
|
if not args.show:
|
|
plt.close()
|
|
|
|
# Show all saved figures with a single plt.show() to avoid the repeated
|
|
# show()/close() cycle that can crash interactive backends on macOS.
|
|
if args.show:
|
|
plt.show()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except OSError as exc:
|
|
raise SystemExit(f"Error: {exc}")
|