Tracking State

The tracking state nests as TrackState → SignalGroup → TrackedSat → TrackedSignal:

  • TrackState: a NamedTuple of SignalGroups and the Doppler estimator.
  • SignalGroup: satellites with the same signal tuple, and so one concrete TrackedSat type (the hot loop's type stability), on one RF band and antenna array.
  • TrackedSat: the carrier/code Doppler and phase its signals share, a tuple of TrackedSignals, and the Doppler-estimator state.
  • TrackedSignal: correlator, post-correlation filter, C/N₀ estimator, bit buffer and integration progress.

Estimator-driver signal

The first signal of each group's tuple is the estimator-driver signal, the one the Doppler estimator updates the shared carrier and code Doppler from. The conventional estimators (ConventionalPLLAndDLL, ConventionalAssistedPLLAndDLL) run their discriminators on it and take its default loop bandwidths (a carrier loop pulling in at 50 Hz and narrowing to 18 Hz, 1 Hz code). A custom AbstractDopplerEstimator may use any signal.

Bit sync, the post-correlation filter and the C/N₀ estimator run per signal, so a multi-signal satellite has one C/N₀ per signal; see CN0 Estimator and NoCN0Estimator, the per-signal opt-out.

Choosing a TrackState constructor

The right constructor depends on when you know which satellites you'll track.

One-shot scripts: acquire once, then track

The Acquisition extension's constructors derive signals, loop bandwidths and satellites from AcquisitionResults:

using Acquisition  # loads the extension; required for TrackState(acq...)

# Single satellite
acq = acquire(GPSL1CA(), data, sampling_frequency, 7)
track_state = TrackState(acq)

# Many satellites, one signal
acqs = acquire(GPSL1CA(), data, sampling_frequency, 1:32)
track_state = TrackState(filter(is_detected, acqs))

# Many satellites, multiple signals
acqs = vcat(
    acquire(GPSL1CA(),    data, sampling_frequency, 1:32),
    acquire(GalileoE1B(), data, sampling_frequency, 1:36),
)
track_state = TrackState(filter(is_detected, acqs);
    signals = (gps = (GPSL1CA(),), gal = (GalileoE1B(),)),
)

add_satellite! can later add satellites only for the signals those acqs declared, as the groups are frozen at construction. To track more signals, build empty and populate instead.

Real-time / repeating loops: build empty, then populate

A receiver that re-acquires periodically builds an empty TrackState once with TrackState(; signal = ...) or TrackState(; signals = (...)) and adds and removes satellites with add_satellite! and remove_satellite!:

# Build once
track_state = TrackState(;
    signals = (gps = (GPSL1CA(),), gal = (GalileoE1B(),)),
)

# In your acquisition loop
while running
    acqs = acquire(GPSL1CA(), latest_chunk, sampling_frequency, candidate_prns)
    track_state = add_satellite!(track_state, filter(is_detected, acqs))   # routes each acq to the matching group
    track_state = track(latest_chunk, track_state, sampling_frequency)
end

The TrackState's concrete type stays fixed as satellites come and go. signal = GPSL1CA() is short for signals = (default = (GPSL1CA(),),); with one group, add_satellite! may omit group =.

Power-user: pre-built TrackedSats

To change the correlator or post-correlation filter type, build the TrackedSats yourself, pass them to TrackState(signal, sats), and add more with add_satellite!(track_state, group, sat). The single-signal constructor:

TrackedSat(
    signal,                # e.g. GPSL1CA()
    prn::Int,
    code_phase,
    carrier_doppler;
    # all kwargs below are optional and have signal-derived defaults
    doppler_estimator     = ConventionalAssistedPLLAndDLL(...),
    num_ants              = NumAnts(1),
    correlator            = get_default_correlator(signal, num_ants),
    carrier_phase         = 0.0,
    code_doppler          = carrier_doppler * get_code_center_frequency_ratio(signal),
    num_prompts_for_cn0_estimation = 100,
    cn0_estimator         = TrackingLoops.default_cn0_estimator(signal, num_prompts_for_cn0_estimation),
    post_corr_filter      = DefaultPostCorrFilter(),
)

cn0_estimator takes any AbstractCN0Estimator; as a type parameter of TrackedSignal, unlike the correlator and filter, it may change the satellite's type (see CN0 Estimator).

An example with a narrower correlator, a trivial mean-of-antennas beamformer and a larger C/N₀ buffer. A filter declares its combining weights rather than combining the taps itself, so the C/N₀ path can reduce the measured noise covariance through the same weights:

julia> using Tracking, TrackingLoops, GNSSSignals, StaticArrays

julia> using Tracking: Hz

julia> using TrackingLoops: NumAnts, AbstractPostCorrFilter

julia> # Trivial beamformer — averages across antenna elements
       struct MyBeamformer <: AbstractPostCorrFilter end

julia> TrackingLoops.update(f::MyBeamformer, prompt) = f;

julia> TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{1}) = 1.0 + 0.0im;

julia> TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{M}) where {M} =
           SVector{M,ComplexF64}(ntuple(_ -> 1 / M + 0.0im, M));

julia> sat = TrackedSat(GPSL1CA(), 1, 50.0, 1000.0Hz;
           num_ants                       = NumAnts(4),
           correlator                     = EarlyPromptLateCorrelator(
               num_ants = NumAnts(4),
               preferred_early_late_to_prompt_code_shift = 0.1,
           ),
           post_corr_filter               = MyBeamformer(),
           num_prompts_for_cn0_estimation = 200,
       );

julia> track_state = TrackState(GPSL1CA(), sat);

julia> get_num_ants(track_state, 1)
4

julia> get_correlator(track_state, 1).preferred_early_late_to_prompt_code_shift
0.1

For a multi-signal satellite, customize individual TrackedSignals with the kwarg-update constructor TrackedSat(sat; signals = (...)).

Tracking.TrackState — Type

The tracking state: the satellites of every signal group and the Doppler estimator.

groups is a NamedTuple of SignalGroups.

noise_estimators is a NamedTuple of AbstractNoiseEstimators keyed by signal id (GNSSSignals.get_signal_id), since the post-correlation floor depends on the despreading modulation. Only signals whose C/N₀ estimator reads a noise density (see requires_noise_density) get one, so the others pay nothing. They average in place.

noise_descriptor is per-call scratch: a heap cell where the correlate step parks the chunk's noise descriptors, so a threaded backend reaches them through one pointer (see _park_noise_items!). Shared by every derived TrackState.

sample_offsets counts, per band (keyed by get_band_id), the samples earlier track! calls consumed: the origin of the current measurement on a sample grid that runs on across calls. The estimate phase adds it to a record's index, so an estimator with per-satellite state of its own (TrackingLoops' VectorPLLAndDLL) sees every satellite on one time grid. The in-place mutators share it; track, merge_sats, add_satellite and remove_satellite give their result a copy.

A fresh TrackState starts every band at 0. To carry such an estimator over into a rebuilt TrackState (e.g. to add a band), pass the old state's offsets as the sample_offsets keyword, or the estimator receives records from its past. A band the old state did not track starts at elapsed_time * sampling_frequency samples of the new band.

source

Adding satellites

add_satellite! builds a TrackedSat with the default correlator and post-correlation filter from the acquisition handoff (prn, code_phase, carrier_doppler, optionally code_doppler and carrier_phase). Adding a PRN again overwrites it, as merge_sats does.

Multi-satellite tracking

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(; signal = GPSL1CA());

julia> track_state = add_satellite!(track_state; prn = 1,  code_phase = 50.0,  carrier_doppler = 1000.0Hz);

julia> track_state = add_satellite!(track_state; prn = 5,  code_phase = 120.0, carrier_doppler = -500.0Hz);

julia> track_state = add_satellite!(track_state; prn = 17, code_phase = 890.0, carrier_doppler = 2000.0Hz);

julia> get_carrier_doppler(track_state, 5)
-500.0 Hz

julia> get_code_phase(track_state, 17)
890.0

Multi-system tracking (different signals on different sats)

Satellites with different signals go into different named groups:

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(;
           signals = (
               gps     = (GPSL1CA(),),
               galileo = (GalileoE1B(),),
           ),
       );

julia> track_state = add_satellite!(track_state; prn = 1,  group = :gps,     code_phase = 50.0,  carrier_doppler = 1000.0Hz);

julia> track_state = add_satellite!(track_state; prn = 11, group = :galileo, code_phase = 200.0, carrier_doppler = -300.0Hz);

julia> get_carrier_doppler(track_state, :gps, 1)
1000.0 Hz

julia> get_carrier_doppler(track_state, :galileo, 11)
-300.0 Hz

Multi-signal tracking (one satellite, several signals)

A modern GPS satellite transmits L1 C/A, L1C-D and L1C-P on one carrier; all three can be tracked together, sharing the carrier downconvert:

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(;
           signals = (
               modern_gps = (GPSL1C_P(), GPSL1C_D(), GPSL1CA()),
           ),
       );

julia> track_state = add_satellite!(track_state;
           prn = 11, group = :modern_gps,
           code_phase = 0.0, carrier_doppler = 1234.0Hz,
       );

julia> get_carrier_doppler(track_state, :modern_gps, 11)
1234.0 Hz

Put a pilot first where there is one: without data bits the PLL can integrate longer. The data signals still decode their bits, each with its own bit_buffer.

Signals with different code periods (L1 C/A 1 ms, L1C-P 10 ms) each complete records on their own boundaries; the conventional estimators update the Doppler from the driver's records (see Chunked Doppler updates).

Phased-array tracking

Pass a Matrix measurement (rows = samples, columns = antennas) and declare the antenna count:

julia> using Tracking, TrackingLoops, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(;
           signal = GPSL1CA(),
           num_ants = NumAnts(4),
       );

julia> track_state = add_satellite!(track_state; prn = 1, code_phase = 50.0, carrier_doppler = 1000.0Hz);

julia> get_num_ants(track_state, 1)
4

By default the last antenna drives the discriminators. For a beamformer, build the TrackedSat with a custom post_corr_filter, whose get_weights are applied to the correlator and to the measured noise covariance alike, so the C/N₀ stays correct:

julia> using Tracking, TrackingLoops, GNSSSignals, StaticArrays

julia> using Tracking: Hz

julia> using TrackingLoops: NumAnts, AbstractPostCorrFilter

julia> # Same trivial mean-of-antennas filter as the power-user example above
       struct MyBeamformer <: AbstractPostCorrFilter end

julia> TrackingLoops.update(f::MyBeamformer, prompt) = f;

julia> TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{1}) = 1.0 + 0.0im;

julia> TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{M}) where {M} =
           SVector{M,ComplexF64}(ntuple(_ -> 1 / M + 0.0im, M));

julia> sat = TrackedSat(GPSL1CA(), 1, 50.0, 1000.0Hz;
                        num_ants = NumAnts(4),
                        post_corr_filter = MyBeamformer());

julia> track_state = TrackState(GPSL1CA(), sat);

julia> get_num_ants(track_state, 1)
4

Acquisition handoff

With using Acquisition, add_satellite! / add_satellite accept AcquisitionResults (or a vector of them, each routed independently). Without group =, an acq goes to the group whose longest-primary-code signal matches acq.system.

using Acquisition  # loads the extension

# Single acq
ts = add_satellite!(ts, acq)                       # auto-route
ts = add_satellite!(ts, acq; group = :legacy_gps)  # explicit group, asserts match

# Vector of acqs (mixed constellations OK)
ts = add_satellite!(ts, filter(is_detected, acqs))

Only the longest code's phase is unambiguous for the whole group, so hand over an L1C-P acq (not L1 C/A) for (GPSL1C_P(), GPSL1C_D(), GPSL1CA()).

Removing satellites

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(; signal = GPSL1CA());

julia> track_state = add_satellite!(track_state; prn = 1,  code_phase = 50.0,  carrier_doppler = 1000.0Hz);

julia> track_state = add_satellite!(track_state; prn = 23, code_phase = 500.0, carrier_doppler = 1500.0Hz);

julia> track_state = remove_satellite!(track_state; prn = 1);

julia> haskey(get_sat_states(track_state, :default), 23)
true

julia> haskey(get_sat_states(track_state, :default), 1)
false
Tracking.add_satellite! — Function
add_satellite!(track_state; prn, group, kwargs...)

Add (or replace, by prn) a satellite in track_state in place. Builds a TrackedSat for the group with the default correlator and post-corr filter from the acquisition handoff (prn, code_phase, code_doppler, carrier_phase, carrier_doppler), its estimator state from init_estimator_state.

group may be omitted on a single-group TrackState; otherwise an ArgumentError names the available groups.

Keep using the returned TrackState: an estimator whose update_estimator_on_handoff returns a rebuilt estimator can only hand it back there (TrackState is immutable). Estimators that update in place, the conventional ones included, return the same track_state.

track_state = TrackState(; signals = (modern_gps = (GPSL1C_P(), GPSL1C_D(), GPSL1CA()),))
add_satellite!(
    track_state;
    prn = 11,
    group = :modern_gps,
    code_phase = 0.0,
    carrier_doppler = 1234.0Hz,
)

For non-default correlator or post-corr-filter types, build the TrackState from such TrackedSats and add more with the add_satellite!(track_state, group, sat) overload.

source
add_satellite!(track_state, group, sat)

In-place add (or replace) with a pre-built TrackedSat. Its type must match the group's slot type fixed at TrackState construction, so non-default correlator or post-corr-filter types need a state built from such sats. Keep using the return value, as for the keyword form.

source
Tracking.remove_satellite! — Function
remove_satellite!(track_state; prn, group)

Remove a satellite from track_state in place; a KeyError if the group has no such prn.

remove_satellite!(track_state; prn = 11, group = :modern_gps)

group may be omitted as for add_satellite!. Returns track_state.

source
Tracking.merge_sats — Function
merge_sats(track_state, group_idx, tracked_sats)

Merge already-built TrackedSats into the group_idx group of track_state, returning a new TrackState (the input is left unchanged). tracked_sats may be a single TrackedSat, a Vector, or a Dictionary keyed by PRN; existing PRNs in the group are overwritten.

Each sat's type must match the group's slot type, including the doppler_estimator_state the configured estimator produces. The estimator's update_estimator_on_handoff hook is called once with the incoming sats.

For a single-group TrackState the group_idx may be omitted.

source

Multi-band tracking

Satellites broadcast on several RF bands (GPS L1 and L5, Galileo E1 and E5a), which arrive from separate front ends, generally at different sample rates. Each group of a multi-band TrackState sits on one band. Two bands give near-independent observations of one path: for ionosphere-free combinations, wider code bandwidth, and cross-band aiding, as the L1/L5 Doppler ratio is that of the carrier frequencies.

Tracking.jl provides per-band groups, measurement routing and an estimate phase that sees every band's records at once; for joint cross-band estimation see docs/plans/2026-05-15-multi-band-tracking-design.md.

Declaring bands

A group's band is get_band(signals[1]):

julia> using Tracking, GNSSSignals

julia> track_state = TrackState(;
           signals = (
               legacy_gps_l1 = (GPSL1CA(),),
               modern_gps_l1 = (GPSL1C_P(), GPSL1C_D(), GPSL1CA()),
               galileo       = (GalileoE1B(),),
               gps_l5        = (GPSL5I(),),
           ),
       );

julia> keys(track_state.groups)
(:legacy_gps_l1, :modern_gps_l1, :galileo, :gps_l5)

Three groups on L1 (GPS L1 and Galileo E1 share the carrier), one on L5.

Tracking against multiple measurements

Pass one BandMeasurement per band, in a NamedTuple keyed by GNSSSignals.get_band_id:

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> using GNSSSignals: gen_code, get_code_frequency, get_code_center_frequency_ratio

julia> function make_signal(sys, prn, carrier_doppler, num_samples, fs)
           code_freq = carrier_doppler * get_code_center_frequency_ratio(sys) + get_code_frequency(sys)
           range = 0:num_samples-1
           cis.(2π .* carrier_doppler .* range ./ fs) .*
               gen_code(num_samples, sys, prn, fs, code_freq, 0.0)
       end;

julia> track_state = TrackState(;
           signals = (legacy_gps_l1 = (GPSL1CA(),), gps_l5 = (GPSL5I(),)),
       );

julia> track_state = add_satellite!(track_state; prn = 1, group = :legacy_gps_l1, code_phase = 0.0, carrier_doppler = 200Hz);

julia> track_state = add_satellite!(track_state; prn = 1, group = :gps_l5, code_phase = 0.0, carrier_doppler = -150Hz);

julia> buf_l1 = make_signal(GPSL1CA(),  1, 200Hz,  4000,  4e6Hz);  # 1 ms at 4 MHz

julia> buf_l5 = make_signal(GPSL5I(),   1, -150Hz, 25000, 25e6Hz); # 1 ms at 25 MHz

julia> track!((L1 = BandMeasurement(buf_l1, 4e6Hz),
               L5 = BandMeasurement(buf_l5, 25e6Hz)), track_state);

julia> get_carrier_doppler(track_state, :legacy_gps_l1, 1)
200.00000359633913 Hz

get_band_id is nameof the band type, so user-defined bands have keys too. All measurements must cover exactly the same duration: 4000 samples at 4 MHz and 25000 at 25 MHz are both 1 ms; 25001 at 25 MHz is rejected.

Per-band antenna counts

Pass SignalGroups as the entries to give each group its own num_ants:

julia> using Tracking, TrackingLoops, GNSSSignals

julia> using TrackingLoops: NumAnts

julia> track_state = TrackState(;
           signals = (
               legacy_gps_l1 = SignalGroup((GPSL1CA(),); num_ants = NumAnts(2)),
               gps_l5        = SignalGroup((GPSL5I(),);  num_ants = NumAnts(1)),
           ),
       );

julia> track_state.groups[:legacy_gps_l1].num_ants
NumAnts{2}()

julia> track_state.groups[:gps_l5].num_ants
NumAnts{1}()

Groups on one band share a front end and must declare the same num_ants; the constructor throws otherwise.

A single-band TrackState can still be tracked with a bare buffer, track!(buf, state, fs; intermediate_frequency).

SignalGroup

Tracking.SignalGroup — Type

A group of satellites tracking the same tuple of signals, on one RF band and antenna array: the unit of type stability (one concrete TrackedSat type). Groups may share a band (e.g. (GPSL1CA(),) and (GalileoE1B(),) on L1()); the band routes the right measurement to the group.

Fields:

  • band: an AbstractGNSSSignal Band instance (L1(), L5(), …)
  • satellites: Dictionary{Int, <:TrackedSat} keyed by PRN
  • signals: the signal-instance tuple (e.g. (GPSL1C_P(), GPSL1C_D(), GPSL1CA()))
  • num_ants: the antenna count for this group's band
source

Band routing

band_keys lists the band ids a TrackState expects as measurement keys.

Tracking.band_keys — Function
band_keys(track_state)

The distinct band ids (GNSSSignals.get_band_id) of track_state's groups, in first-encounter order: the keys a multi-band measurements NamedTuple must use.

ts = TrackState(; signals = (legacy_gps = (GPSL1CA(),), gps_l5 = (GPSL5I(),)))
band_keys(ts) == (:L1, :L5)
source

Addressing satellites and signals

track_state.groups[:legacy_gps].satellites reaches a group directly. The accessors take the group key as an argument; their argument count says where the lookup stops:

FormMeaning
f(track_state)Single-group, single-sat — folds via only(...) at each level.
f(track_state, prn)Single-group, multi-sat — picks the sat by PRN.
f(track_state, group, prn)Multi-group — picks the sat in the named group.
f(track_state, group, prn, sig)Per-signal — picks one TrackedSignal within a multi-signal sat.

The trailing sig selector is either:

  • an Integer index into signals (1 is the estimator-driver signal), or
  • a signal type like GPSL1CA (not GPSL1CA()), which must match exactly one signal.

What you can read

Sat-level — shared across all signals on the sat (no sig selector):

AccessorReturns
get_prnPRN number.
get_num_antsNumber of antenna elements.
get_code_phaseShared code phase (wraps at max_code_length).
get_code_dopplerShared code Doppler.
get_carrier_phaseShared carrier phase in radians.
get_carrier_phase_correctionWhat the estimator adds to get_carrier_phase for the carrier-phase measurement (vector loop only; takes the band's sampling frequency).
get_carrier_dopplerShared carrier Doppler.
get_signal_start_sampleIndex of the next sample to integrate.

Per-signal — pass sig (index or type) on multi-signal sats:

AccessorReturns
get_correlatorThe working correlator (in-flight accumulator).
get_last_fully_integrated_correlatorCorrelator value from the last completed integration.
get_last_fully_integrated_filtered_promptFiltered prompt value from the last completed integration.
get_last_fully_integrated_num_code_blocksPrimary-code block count of the last completed integration.
get_last_fully_integrated_integration_timeIntegration time T of the last completed integration; the post-integration SNR is C/N₀ × T.
get_post_corr_filterThe post-correlation filter.
get_cn0_estimatorThe CN0 estimator.
estimate_cn0CN0 estimate in dB-Hz.
get_bit_buffer / get_soft_bits / get_num_bitsBit buffer, decoded soft bits (sign = the hard bit), bit count.
get_integrated_samplesNumber of samples accumulated into the current integration so far.
has_bit_or_secondary_code_been_foundtrue once bit/secondary-code synchronization has been achieved.

The per-signal form always names the group, :default on a single-group TrackState: estimate_cn0(track_state, :default, 11, GPSL1C_P).

julia> using Tracking, GNSSSignals

julia> using Tracking: Hz

julia> track_state = TrackState(;
           signals = (modern_gps = (GPSL1C_P(), GPSL1C_D(), GPSL1CA()),),
       );

julia> track_state = add_satellite!(track_state; prn = 11, group = :modern_gps,
                                   code_phase = 0.0, carrier_doppler = 1234.0Hz);

julia> get_carrier_doppler(track_state, :modern_gps, 11)  # sat-level: same for all signals
1234.0 Hz

julia> get_num_bits(track_state, :modern_gps, 11, 1)  # per-signal by index (estimator-driver signal)
0

julia> get_num_bits(track_state, :modern_gps, 11, GPSL1CA)  # per-signal by type
0

Group accessors

Tracking.SatelliteDicts — Type

A tuple or named tuple of per-group satellite dictionaries, each a Dictionary{I, <:TrackedSat} keyed by PRN.

source
Tracking.get_sat_states — Function
get_sat_states(satellites, group_idx)

Get the per-group satellite dictionary for a specific signal-group index.

source
Tracking.get_signal — Function
get_signal(track_state, group_idx)

Return the first (driver) signal of the given group, also for a group without satellites. Resolves at compile time for a literal group_idx.

For a single-group TrackState the index can be omitted.

source
Tracking.get_carrier_phase_correction — Function
get_carrier_phase_correction(
    track_state,
    group_idx,
    sat_identifier,
    sampling_frequency
)

The carrier phase (radians, not wrapped) the Doppler estimator adds to the replica's get_carrier_phase for its estimate of the incoming carrier phase, the carrier-phase measurement (RINEX L); 0.0 for the scalar loops and for a satellite out of the vector loop.

In TrackingLoops' VectorPLLAndDLL loop the navigation filter sets the carrier NCO, and the residual PLL follows the carrier without steering it, so the replica drifts off the carrier by whole cycles; this is the residual PLL's prediction at the end of the latest track/track! call, every cycle counted (see TrackingLoops.carrier_phase_correction, which also names where it steps). sampling_frequency is the band's. The group can be omitted on a single-group TrackState.

The whole cycles only count against a replica phase that counts them too: get_carrier_phase wraps, so a RINEX host builds L from its own accumulated replica phase (e.g. the integrated carrier Doppler) plus this.

Only valid right after track/track!: a host feeding records through append_correlator_output! calls TrackingLoops.carrier_phase_correction with its own NCO words and sample instead.

source

TrackedSat

Tracking.TrackedSat — Type

The state of one tracked satellite: the carrier/code Doppler and phase shared by its signals, the per-signal state in signals, and the Doppler-estimator state in doppler_estimator_state.

signals[1] is the estimator-driver signal. The conventional estimators (ConventionalPLLAndDLL, ConventionalAssistedPLLAndDLL) run their discriminators on it, at its integration boundaries; the other signals update their own filters, C/N₀ and bit buffers on theirs. A custom AbstractDopplerEstimator may use any signal.

The shared code_phase wraps at max_code_length.

source

The sat-level accessors also take a TrackedSat:

TrackedSignal

Tracking.TrackedSignal — Type

Per-signal tracking state, one per signal tracked on a satellite: the correlator, the integration progress and the per-record loop state, a TrackingLoops SignalLoopState (post-corr filter, C/N₀ estimator, bit buffer, and what the last record left for the next). The carrier/code Doppler and phase are shared and live on the TrackedSat.

source
Tracking.get_last_fully_integrated_integration_time — Method
get_last_fully_integrated_integration_time(t)

The integration time T of the most recently completed record (its primary-code blocks times one code period), which estimate_cn0 divides by. The record's post-integration SNR is C/N₀ · T.

Not to be confused with get_integrated_samples, the record in progress.

source

The per-signal accessors also take a TrackedSignal, as do get_signal(tsig), the signal instance, and get_filtered_prompts(tsig), the filtered prompts of the most recent track call.

Bit and secondary-code sync, and how it widens the code-phase wrap, have their own page: Bit and Secondary-Code Sync. Most users only need has_bit_or_secondary_code_been_found to gate get_soft_bits.

Handing over to PositionVelocityTime

With the navigation data GNSSDecoder.jl decodes from their soft bits, tracked satellites become the measurements PositionVelocityTime.calc_pvt solves. Pass a NamedTuple of per-group PRN → GNSSDecoderState mappings:

using PositionVelocityTime: PositionVelocityTime, calc_pvt
pvt = calc_pvt(PositionVelocityTime.signal_groups(track_state, decoders))
PositionVelocityTime.signal_groups — Method
PositionVelocityTime.signal_groups(track_state::TrackState, decoders)
    -> NamedTuple of PositionVelocityTime.SignalGroup

One epoch's measurements from a whole TrackState for PositionVelocityTime.calc_pvt. decoders has the group keys of track_state.groups, each a PRN → GNSSDecoderState mapping; both levels of key are kept. Each group ranges on its first signal (see PositionVelocityTime.SignalGroup(::Tracking.SignalGroup, decoders)). Inferable.

source
PositionVelocityTime.SignalGroup — Method
PositionVelocityTime.SignalGroup(group::Tracking.SignalGroup, decoders;
                                 signal = first(group.signals))

One Tracking.SignalGroup's measurements for an epoch, each satellite paired with its decoded navigation data.

decoders maps the group's PRNs to GNSSDecoderStates (a Dictionary, or any getindex/haskey mapping); the result keeps those keys. Satellites without a decoder are skipped, as tracking starts long before the message is complete.

signal is the ranging signal, by default the group's first (the estimator driver, and GNSSReceiver's RANGING_SIGNAL_INDEX = 1).

source
PositionVelocityTime.SatelliteState — Method
PositionVelocityTime.SatelliteState(decoder::GNSSDecoderState, system, sat_state::TrackedSat)

One tracked satellite's code phase, carrier Doppler and carrier phase (from get_sat_state) paired with its decoded navigation data.

source