From 54c8e2cf2bf39dec2ccf9f30b6c7f7a4632aa55c Mon Sep 17 00:00:00 2001 From: siebc <226531417+siebc@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:49:37 +0000 Subject: [PATCH 1/3] refactor!: unify nav-data field names across signals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #60 and extends it with two further inconsistencies found while auditing all nav-data structs against IS-GPS-200/705/800 and the Galileo OS SIS ICD v2.2. Klobuchar ionospheric coefficients (#60): - GPSL5IData, GPSL1C_DData: α0…α3/β0…β3 -> α_0…α_3/β_0…β_3 Almanac SV-clock coefficients (#60): - GPSL1CAAlmanac: af0/af1 -> a_f0/a_f1 SV health (D): - GPSL1CAData: svhealth -> sv_health, matching the sibling sv_health_sf4_25/ sv_health_sf5_25 fields; IS-GPS-200 labels all of them identically "SV Health". Rate of inclination (E): - GPSL5IData, GPSL1C_DData: i_0_dot -> i_dot. IS-GPS-200/705/800 name this IDOT (an absolute rate, not a delta), matching LNAV/Galileo i_dot; also aligns with PositionVelocityTime.jl's generic data.i_dot access. BREAKING CHANGE: renamed public struct fields on GPSL1CAData, GPSL1CAAlmanac, GPSL5IData and GPSL1C_DData. Downstream code reading the old names must update. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gps/l1c_d.jl | 78 +++++++++++++++++++++++------------------------ src/gps/l1ca.jl | 34 ++++++++++----------- src/gps/l5.jl | 78 +++++++++++++++++++++++------------------------ test/gps_l1c_d.jl | 16 +++++----- test/gps_l5i.jl | 18 +++++------ test/gpsl1.jl | 6 ++-- 6 files changed, 115 insertions(+), 115 deletions(-) diff --git a/src/gps/l1c_d.jl b/src/gps/l1c_d.jl index 1942c21..1bde4b1 100644 --- a/src/gps/l1c_d.jl +++ b/src/gps/l1c_d.jl @@ -309,7 +309,7 @@ first decoded. - `Ω_0::Float64`: Reference right ascension angle (rad). - `i_0::Float64`: Inclination angle at reference time (rad). - `ΔΩ_dot::Float64`: Rate of right ascension difference (rad/s). - - `i_0_dot::Float64`: Rate of inclination angle (rad/s). + - `i_dot::Float64`: Rate of inclination angle (rad/s). - `C_is::Float64`, `C_ic::Float64`: Sine/cosine inclination harmonic corrections (rad). - `C_rs::Float64`, `C_rc::Float64`: Sine/cosine orbit-radius harmonic corrections (m). - `C_us::Float64`, `C_uc::Float64`: Sine/cosine argument-of-latitude harmonic corrections (rad). @@ -336,7 +336,7 @@ CRC-valid SF3 page regardless of whether its page format is parsed. - `t_ot::Int64`: UTC reference time of week (s). - `WN_ot,WN_LSF::Int64`: UTC and leap-second reference week numbers. - `DN::Int64`: leap-second reference day number (1-7). - - `α0,α1,α2,α3,β0,β1,β2,β3::Float64`: Klobuchar ionospheric coefficients. + - `α_0,α_1,α_2,α_3,β_0,β_1,β_2,β_3::Float64`: Klobuchar ionospheric coefficients. - `ISC_L1CA,ISC_L2C,ISC_L5I5,ISC_L5Q5::Float64`: inter-signal corrections (s). ## Page 2 — GGTO + EOP @@ -391,7 +391,7 @@ Base.@kwdef struct GPSL1C_DData <: AbstractGNSSData Ω_0::Union{Nothing,Float64} = nothing i_0::Union{Nothing,Float64} = nothing ΔΩ_dot::Union{Nothing,Float64} = nothing - i_0_dot::Union{Nothing,Float64} = nothing + i_dot::Union{Nothing,Float64} = nothing C_is::Union{Nothing,Float64} = nothing C_ic::Union{Nothing,Float64} = nothing C_rs::Union{Nothing,Float64} = nothing @@ -417,14 +417,14 @@ Base.@kwdef struct GPSL1C_DData <: AbstractGNSSData WN_LSF::Union{Nothing,Int64} = nothing DN::Union{Nothing,Int64} = nothing Δt_LSF::Union{Nothing,Int64} = nothing - α0::Union{Nothing,Float64} = nothing - α1::Union{Nothing,Float64} = nothing - α2::Union{Nothing,Float64} = nothing - α3::Union{Nothing,Float64} = nothing - β0::Union{Nothing,Float64} = nothing - β1::Union{Nothing,Float64} = nothing - β2::Union{Nothing,Float64} = nothing - β3::Union{Nothing,Float64} = nothing + α_0::Union{Nothing,Float64} = nothing + α_1::Union{Nothing,Float64} = nothing + α_2::Union{Nothing,Float64} = nothing + α_3::Union{Nothing,Float64} = nothing + β_0::Union{Nothing,Float64} = nothing + β_1::Union{Nothing,Float64} = nothing + β_2::Union{Nothing,Float64} = nothing + β_3::Union{Nothing,Float64} = nothing ISC_L1CA::Union{Nothing,Float64} = nothing ISC_L2C::Union{Nothing,Float64} = nothing ISC_L5I5::Union{Nothing,Float64} = nothing @@ -481,7 +481,7 @@ function GPSL1C_DData( Ω_0 = data.Ω_0, i_0 = data.i_0, ΔΩ_dot = data.ΔΩ_dot, - i_0_dot = data.i_0_dot, + i_dot = data.i_dot, C_is = data.C_is, C_ic = data.C_ic, C_rs = data.C_rs, @@ -504,14 +504,14 @@ function GPSL1C_DData( WN_LSF = data.WN_LSF, DN = data.DN, Δt_LSF = data.Δt_LSF, - α0 = data.α0, - α1 = data.α1, - α2 = data.α2, - α3 = data.α3, - β0 = data.β0, - β1 = data.β1, - β2 = data.β2, - β3 = data.β3, + α_0 = data.α_0, + α_1 = data.α_1, + α_2 = data.α_2, + α_3 = data.α_3, + β_0 = data.β_0, + β_1 = data.β_1, + β_2 = data.β_2, + β_3 = data.β_3, ISC_L1CA = data.ISC_L1CA, ISC_L2C = data.ISC_L2C, ISC_L5I5 = data.ISC_L5I5, @@ -556,7 +556,7 @@ function GPSL1C_DData( Ω_0, i_0, ΔΩ_dot, - i_0_dot, + i_dot, C_is, C_ic, C_rs, @@ -579,14 +579,14 @@ function GPSL1C_DData( WN_LSF, DN, Δt_LSF, - α0, - α1, - α2, - α3, - β0, - β1, - β2, - β3, + α_0, + α_1, + α_2, + α_3, + β_0, + β_1, + β_2, + β_3, ISC_L1CA, ISC_L2C, ISC_L5I5, @@ -935,7 +935,7 @@ function decode_subframe2(state::GNSSDecoderState{<:GPSL1C_DData}, sf2_symbols) Ω_0 = get_twos_complement_num(word, word_length, 240, 33) * 2.0^-32 * PI i_0 = get_twos_complement_num(word, word_length, 273, 33) * 2.0^-32 * PI ΔΩ_dot = get_twos_complement_num(word, word_length, 306, 17) * 2.0^-44 * PI - i_0_dot = get_twos_complement_num(word, word_length, 323, 15) * 2.0^-44 * PI + i_dot = get_twos_complement_num(word, word_length, 323, 15) * 2.0^-44 * PI C_is = get_twos_complement_num(word, word_length, 338, 16) * 2.0^-30 C_ic = get_twos_complement_num(word, word_length, 354, 16) * 2.0^-30 C_rs = get_twos_complement_num(word, word_length, 370, 24) * 2.0^-8 @@ -970,7 +970,7 @@ function decode_subframe2(state::GNSSDecoderState{<:GPSL1C_DData}, sf2_symbols) Ω_0, i_0, ΔΩ_dot, - i_0_dot, + i_dot, C_is, C_ic, C_rs, @@ -1062,14 +1062,14 @@ function parse_sf3_page1(raw::GPSL1C_DData, word::UInt288, PI::Float64) Δt_LSF = get_twos_complement_num(word, word_length, 105, 8), # Klobuchar ionospheric coefficients (IS-GPS-200 Table 20-X; all 8-bit # two's-complement, scaled in seconds / seconds-per-semicircle^n). - α0 = get_twos_complement_num(word, word_length, 113, 8) * 2.0^-30, - α1 = get_twos_complement_num(word, word_length, 121, 8) * 2.0^-27, - α2 = get_twos_complement_num(word, word_length, 129, 8) * 2.0^-24, - α3 = get_twos_complement_num(word, word_length, 137, 8) * 2.0^-24, - β0 = get_twos_complement_num(word, word_length, 145, 8) * 2.0^11, - β1 = get_twos_complement_num(word, word_length, 153, 8) * 2.0^14, - β2 = get_twos_complement_num(word, word_length, 161, 8) * 2.0^16, - β3 = get_twos_complement_num(word, word_length, 169, 8) * 2.0^16, + α_0 = get_twos_complement_num(word, word_length, 113, 8) * 2.0^-30, + α_1 = get_twos_complement_num(word, word_length, 121, 8) * 2.0^-27, + α_2 = get_twos_complement_num(word, word_length, 129, 8) * 2.0^-24, + α_3 = get_twos_complement_num(word, word_length, 137, 8) * 2.0^-24, + β_0 = get_twos_complement_num(word, word_length, 145, 8) * 2.0^11, + β_1 = get_twos_complement_num(word, word_length, 153, 8) * 2.0^14, + β_2 = get_twos_complement_num(word, word_length, 161, 8) * 2.0^16, + β_3 = get_twos_complement_num(word, word_length, 169, 8) * 2.0^16, # Inter-signal corrections (Fig 3.5-2; 13-bit two's complement, 2^-35 s). ISC_L1CA = get_twos_complement_num(word, word_length, 177, 13) * 2.0^-35, ISC_L2C = get_twos_complement_num(word, word_length, 190, 13) * 2.0^-35, diff --git a/src/gps/l1ca.jl b/src/gps/l1ca.jl index cc543a8..9ef5c9c 100755 --- a/src/gps/l1ca.jl +++ b/src/gps/l1ca.jl @@ -61,8 +61,8 @@ which the decoder stores in `δi`. - `Ω_0::Float64`: Longitude of ascending node at weekly epoch (rad) - `ω::Float64`: Argument of perigee (rad) - `M_0::Float64`: Mean anomaly at reference time (rad) - - `af0::Float64`: SV clock bias correction coefficient (seconds) - - `af1::Float64`: SV clock drift correction coefficient (s/s) + - `a_f0::Float64`: SV clock bias correction coefficient (seconds) + - `a_f1::Float64`: SV clock drift correction coefficient (s/s) # Reference @@ -78,8 +78,8 @@ Base.@kwdef struct GPSL1CAAlmanac Ω_0::Union{Nothing,Float64} = nothing ω::Union{Nothing,Float64} = nothing M_0::Union{Nothing,Float64} = nothing - af0::Union{Nothing,Float64} = nothing - af1::Union{Nothing,Float64} = nothing + a_f0::Union{Nothing,Float64} = nothing + a_f1::Union{Nothing,Float64} = nothing end """ @@ -103,7 +103,7 @@ subframes 1, 2, and 3 of the GPS LNAV message. All parameters conform to IS-GPS- - `trans_week::Int64`: GPS week number (modulo 1024) - `codeonl2::Int64`: Code on L2 channel (0=invalid, 1=P-code, 2=C/A-code, 3=invalid) - `ura::Float64`: User Range Accuracy (meters), derived from URA index - - `svhealth::String`: 6-bit satellite health status ("000000" = healthy) + - `sv_health::String`: 6-bit satellite health status ("000000" = healthy) - `IODC::String`: Issue of Data, Clock (10-bit binary string) - `l2pcode::Bool`: L2 P-code data flag (1=LNAV OFF on P-code) - `T_GD::Float64`: L1-L2 group delay correction (seconds) @@ -152,7 +152,7 @@ Base.@kwdef struct GPSL1CAData <: AbstractGNSSData trans_week::Union{Nothing,Int64} = nothing codeonl2::Union{Nothing,Int64} = nothing ura::Union{Nothing,Float64} = nothing - svhealth::Union{Nothing,String} = nothing + sv_health::Union{Nothing,String} = nothing IODC::Union{Nothing,String} = nothing l2pcode::Union{Nothing,Bool} = nothing T_GD::Union{Nothing,Float64} = nothing @@ -230,7 +230,7 @@ function GPSL1CAData( trans_week = data.trans_week, codeonl2 = data.codeonl2, ura = data.ura, - svhealth = data.svhealth, + sv_health = data.sv_health, IODC = data.IODC, l2pcode = data.l2pcode, T_GD = data.T_GD, @@ -290,7 +290,7 @@ function GPSL1CAData( trans_week, codeonl2, ura, - svhealth, + sv_health, IODC, l2pcode, T_GD, @@ -409,7 +409,7 @@ function is_subframe1_decoded(data::GPSL1CAData) !isnothing(data.trans_week) && !isnothing(data.codeonl2) && !isnothing(data.ura) && - !isnothing(data.svhealth) && + !isnothing(data.sv_health) && !isnothing(data.IODC) && !isnothing(data.l2pcode) && !isnothing(data.T_GD) && @@ -731,12 +731,12 @@ function decode_syncro_sequence(state::GNSSDecoderState{<:GPSL1CAData}, buffer) end # Satellite Health - svhealth = bitstring(get_bits(word3, 30, 17, 6))[(end-5):end] + sv_health = bitstring(get_bits(word3, 30, 17, 6))[(end-5):end] if get_bit(word3, 30, 17) - @warn "Bad LNAV Data, SV-Health critical", svhealth + @warn "Bad LNAV Data, SV-Health critical", sv_health end - GPSL1CAData(state.raw_data; trans_week, codeonl2, ura, svhealth) + GPSL1CAData(state.raw_data; trans_week, codeonl2, ura, sv_health) end state = can_decode_two_words(state, buffer, 3, 8) do word3, word8, state @@ -1270,8 +1270,8 @@ function decode_almanac_page(state::GNSSDecoderState{<:GPSL1CAData}, buffer, sv_ Ω_0 = alm_Ω_0, ω = alm_ω, M_0 = alm_M_0, - af0 = alm_af0, - af1 = alm_af1, + a_f0 = alm_af0, + a_f1 = alm_af1, ) almanac = something(state.raw_data.almanac, Dictionary{Int64,GPSL1CAAlmanac}()) @@ -1405,13 +1405,13 @@ $(TYPEDSIGNATURES) Check if the GPS satellite is healthy and usable for positioning. -Examines the 6-bit satellite health field (`svhealth`) from subframe 1. A satellite +Examines the 6-bit satellite health field (`sv_health`) from subframe 1. A satellite is considered healthy only if all health bits are zero (`"000000"`). !!! warning This function requires that subframe 1 has been successfully decoded. - Check that `state.data.svhealth` is not `nothing` before relying on this result. + Check that `state.data.sv_health` is not `nothing` before relying on this result. # Arguments @@ -1437,5 +1437,5 @@ end - [`decode`](@ref): Decode navigation data """ function is_sat_healthy(state::GNSSDecoderState{<:GPSL1CAData}) - state.data.svhealth == "000000" + state.data.sv_health == "000000" end diff --git a/src/gps/l5.jl b/src/gps/l5.jl index 8687697..39af868 100644 --- a/src/gps/l5.jl +++ b/src/gps/l5.jl @@ -328,7 +328,7 @@ all `Union{Nothing,…}` until first decoded. - `Ω_0::Float64`: Reference right ascension angle (rad). - `i_0::Float64`: Inclination angle at reference time (rad). - `ΔΩ_dot::Float64`: Rate of right ascension difference (rad/s). - - `i_0_dot::Float64`: Rate of inclination angle (rad/s). + - `i_dot::Float64`: Rate of inclination angle (rad/s). - `C_is::Float64`, `C_ic::Float64`: Sine/cosine inclination harmonic corrections (rad). - `C_rs::Float64`, `C_rc::Float64`: Sine/cosine orbit-radius harmonic corrections (m). - `C_us::Float64`, `C_uc::Float64`: Sine/cosine argument-of-latitude harmonic corrections (rad). @@ -343,7 +343,7 @@ all `Union{Nothing,…}` until first decoded. - `T_GD::Float64`: L1/L2 P(Y) inter-signal correction (seconds). - `ISC_L1CA,ISC_L2C,ISC_L5I5,ISC_L5Q5::Float64`: inter-signal corrections (s). - - `α0,α1,α2,α3,β0,β1,β2,β3::Float64`: Klobuchar ionospheric coefficients. + - `α_0,α_1,α_2,α_3,β_0,β_1,β_2,β_3::Float64`: Klobuchar ionospheric coefficients. - `WN_op::Int64`: Data predict week number (mod 256). # EOP (message type 32, Table 20-VII) @@ -410,7 +410,7 @@ Base.@kwdef struct GPSL5IData <: AbstractGNSSData Ω_0::Union{Nothing,Float64} = nothing i_0::Union{Nothing,Float64} = nothing ΔΩ_dot::Union{Nothing,Float64} = nothing - i_0_dot::Union{Nothing,Float64} = nothing + i_dot::Union{Nothing,Float64} = nothing C_is::Union{Nothing,Float64} = nothing C_ic::Union{Nothing,Float64} = nothing C_rs::Union{Nothing,Float64} = nothing @@ -430,14 +430,14 @@ Base.@kwdef struct GPSL5IData <: AbstractGNSSData ISC_L2C::Union{Nothing,Float64} = nothing ISC_L5I5::Union{Nothing,Float64} = nothing ISC_L5Q5::Union{Nothing,Float64} = nothing - α0::Union{Nothing,Float64} = nothing - α1::Union{Nothing,Float64} = nothing - α2::Union{Nothing,Float64} = nothing - α3::Union{Nothing,Float64} = nothing - β0::Union{Nothing,Float64} = nothing - β1::Union{Nothing,Float64} = nothing - β2::Union{Nothing,Float64} = nothing - β3::Union{Nothing,Float64} = nothing + α_0::Union{Nothing,Float64} = nothing + α_1::Union{Nothing,Float64} = nothing + α_2::Union{Nothing,Float64} = nothing + α_3::Union{Nothing,Float64} = nothing + β_0::Union{Nothing,Float64} = nothing + β_1::Union{Nothing,Float64} = nothing + β_2::Union{Nothing,Float64} = nothing + β_3::Union{Nothing,Float64} = nothing WN_op::Union{Nothing,Int64} = nothing t_EOP::Union{Nothing,Int64} = nothing @@ -507,7 +507,7 @@ function GPSL5IData( Ω_0 = data.Ω_0, i_0 = data.i_0, ΔΩ_dot = data.ΔΩ_dot, - i_0_dot = data.i_0_dot, + i_dot = data.i_dot, C_is = data.C_is, C_ic = data.C_ic, C_rs = data.C_rs, @@ -525,14 +525,14 @@ function GPSL5IData( ISC_L2C = data.ISC_L2C, ISC_L5I5 = data.ISC_L5I5, ISC_L5Q5 = data.ISC_L5Q5, - α0 = data.α0, - α1 = data.α1, - α2 = data.α2, - α3 = data.α3, - β0 = data.β0, - β1 = data.β1, - β2 = data.β2, - β3 = data.β3, + α_0 = data.α_0, + α_1 = data.α_1, + α_2 = data.α_2, + α_3 = data.α_3, + β_0 = data.β_0, + β_1 = data.β_1, + β_2 = data.β_2, + β_3 = data.β_3, WN_op = data.WN_op, t_EOP = data.t_EOP, PM_X = data.PM_X, @@ -590,7 +590,7 @@ function GPSL5IData( Ω_0, i_0, ΔΩ_dot, - i_0_dot, + i_dot, C_is, C_ic, C_rs, @@ -608,14 +608,14 @@ function GPSL5IData( ISC_L2C, ISC_L5I5, ISC_L5Q5, - α0, - α1, - α2, - α3, - β0, - β1, - β2, - β3, + α_0, + α_1, + α_2, + α_3, + β_0, + β_1, + β_2, + β_3, WN_op, t_EOP, PM_X, @@ -731,7 +731,7 @@ function is_ephemeris_decoded(data::GPSL5IData) !isnothing(data.Ω_0) && !isnothing(data.i_0) && !isnothing(data.ΔΩ_dot) && - !isnothing(data.i_0_dot) && + !isnothing(data.i_dot) && !isnothing(data.C_is) && !isnothing(data.C_ic) && !isnothing(data.C_rs) && @@ -1158,7 +1158,7 @@ function parse_mt11(raw::GPSL5IData, word::UInt320, PI::Float64) Ω_0 = get_twos_complement_num(word, word_length, 50, 33) * 2.0^-32 * PI, i_0 = get_twos_complement_num(word, word_length, 83, 33) * 2.0^-32 * PI, ΔΩ_dot = get_twos_complement_num(word, word_length, 116, 17) * 2.0^-44 * PI, - i_0_dot = get_twos_complement_num(word, word_length, 133, 15) * 2.0^-44 * PI, + i_dot = get_twos_complement_num(word, word_length, 133, 15) * 2.0^-44 * PI, C_is = get_twos_complement_num(word, word_length, 148, 16) * 2.0^-30, C_ic = get_twos_complement_num(word, word_length, 164, 16) * 2.0^-30, C_rs = get_twos_complement_num(word, word_length, 180, 24) * 2.0^-8, @@ -1199,14 +1199,14 @@ function parse_mt30(raw::GPSL5IData, word::UInt320) ISC_L2C = get_twos_complement_num(word, word_length, 154, 13) * 2.0^-35, ISC_L5I5 = get_twos_complement_num(word, word_length, 167, 13) * 2.0^-35, ISC_L5Q5 = get_twos_complement_num(word, word_length, 180, 13) * 2.0^-35, - α0 = get_twos_complement_num(word, word_length, 193, 8) * 2.0^-30, - α1 = get_twos_complement_num(word, word_length, 201, 8) * 2.0^-27, - α2 = get_twos_complement_num(word, word_length, 209, 8) * 2.0^-24, - α3 = get_twos_complement_num(word, word_length, 217, 8) * 2.0^-24, - β0 = get_twos_complement_num(word, word_length, 225, 8) * 2.0^11, - β1 = get_twos_complement_num(word, word_length, 233, 8) * 2.0^14, - β2 = get_twos_complement_num(word, word_length, 241, 8) * 2.0^16, - β3 = get_twos_complement_num(word, word_length, 249, 8) * 2.0^16, + α_0 = get_twos_complement_num(word, word_length, 193, 8) * 2.0^-30, + α_1 = get_twos_complement_num(word, word_length, 201, 8) * 2.0^-27, + α_2 = get_twos_complement_num(word, word_length, 209, 8) * 2.0^-24, + α_3 = get_twos_complement_num(word, word_length, 217, 8) * 2.0^-24, + β_0 = get_twos_complement_num(word, word_length, 225, 8) * 2.0^11, + β_1 = get_twos_complement_num(word, word_length, 233, 8) * 2.0^14, + β_2 = get_twos_complement_num(word, word_length, 241, 8) * 2.0^16, + β_3 = get_twos_complement_num(word, word_length, 249, 8) * 2.0^16, WN_op = Int64(get_bits(word, word_length, 257, 8)), ) end diff --git a/test/gps_l1c_d.jl b/test/gps_l1c_d.jl index 59b6c8f..1cbca7d 100644 --- a/test/gps_l1c_d.jl +++ b/test/gps_l1c_d.jl @@ -384,14 +384,14 @@ end @test d.WN_LSF == 2201 @test d.DN == 6 @test d.Δt_LSF == 19 - @test d.α0 ≈ 12 * 2.0^-30 - @test d.α1 ≈ -3 * 2.0^-27 - @test d.α2 ≈ 4 * 2.0^-24 - @test d.α3 ≈ -1 * 2.0^-24 - @test d.β0 ≈ 7 * 2.0^11 - @test d.β1 ≈ -2 * 2.0^14 - @test d.β2 ≈ 5 * 2.0^16 - @test d.β3 ≈ -4 * 2.0^16 + @test d.α_0 ≈ 12 * 2.0^-30 + @test d.α_1 ≈ -3 * 2.0^-27 + @test d.α_2 ≈ 4 * 2.0^-24 + @test d.α_3 ≈ -1 * 2.0^-24 + @test d.β_0 ≈ 7 * 2.0^11 + @test d.β_1 ≈ -2 * 2.0^14 + @test d.β_2 ≈ 5 * 2.0^16 + @test d.β_3 ≈ -4 * 2.0^16 @test d.ISC_L1CA ≈ 9 * 2.0^-35 @test d.ISC_L2C ≈ -9 * 2.0^-35 @test d.ISC_L5I5 ≈ 11 * 2.0^-35 diff --git a/test/gps_l5i.jl b/test/gps_l5i.jl index aa6c6d8..3f56f50 100644 --- a/test/gps_l5i.jl +++ b/test/gps_l5i.jl @@ -259,7 +259,7 @@ end @test d.Ω_0 ≈ 0.51429651258513331 * π @test d.i_0 ≈ 0.34116418519988656 * π @test d.ΔΩ_dot ≈ 3.7252334550430533e-9 * π - @test d.i_0_dot ≈ 9.9987573776161298e-11 * π + @test d.i_dot ≈ 9.9987573776161298e-11 * π @test d.C_is ≈ 4.0000304579734802e-6 @test d.C_ic ≈ 2.9997900128364563e-6 @test d.C_rs ≈ 6.0 @@ -282,14 +282,14 @@ end @test d.ISC_L2C ≈ 0.0 @test d.ISC_L5I5 ≈ 0.0 @test d.ISC_L5Q5 ≈ 0.0 - @test d.α0 ≈ 4.6566128730773926e-9 - @test d.α1 ≈ 1.4901161193847656e-8 - @test d.α2 ≈ -5.9604644775390625e-8 - @test d.α3 ≈ -5.9604644775390625e-8 - @test d.β0 ≈ 79872.0 - @test d.β1 ≈ 65536.0 - @test d.β2 ≈ -65536.0 - @test d.β3 ≈ -393216.0 + @test d.α_0 ≈ 4.6566128730773926e-9 + @test d.α_1 ≈ 1.4901161193847656e-8 + @test d.α_2 ≈ -5.9604644775390625e-8 + @test d.α_3 ≈ -5.9604644775390625e-8 + @test d.β_0 ≈ 79872.0 + @test d.β_1 ≈ 65536.0 + @test d.β_2 ≈ -65536.0 + @test d.β_3 ≈ -393216.0 @test d.WN_op == 58 # Message type 32 — EOP. Spirent's reference decoder predates diff --git a/test/gpsl1.jl b/test/gpsl1.jl index 6487a0e..6dfcbbd 100644 --- a/test/gpsl1.jl +++ b/test/gpsl1.jl @@ -254,8 +254,8 @@ end Ω_0 = Ω_0_values[prn], ω = ω_values[prn], M_0 = M_0_values[prn], - af0 = af0_values[prn], - af1 = af1_values[prn], + a_f0 = af0_values[prn], + a_f1 = af1_values[prn], ) for prn in prns ], ) @@ -268,7 +268,7 @@ end trans_week = 58, codeonl2 = 1, ura = 2.0, - svhealth = "000000", + sv_health = "000000", IODC = "0000000001", l2pcode = false, T_GD = 9.313225746154785e-10, From 9c09665af8cfa3b5b968396bf8185652475d5eab Mon Sep 17 00:00:00 2001 From: siebc <226531417+siebc@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:06:58 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(l1c=5Fd)!:=20correct=20CNAV-2=20EOP=20?= =?UTF-8?q?=CE=94UT=5FGPS=20per=20IS-GPS-800J=20Table=203.5-5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The L1C-D subframe-3 page-2 EOP field was decoded as ΔUT1 = UT1−UTC with scale 2⁻²⁴, reflecting a pre-Rev-G interpretation of the ICD. IS-GPS-800J Table 3.5-5 defines the 31-bit field as ΔUT_GPS = UT1−GPS (UT1−GPST) with scale 2⁻²³ (the rate keeps scale 2⁻²⁵). This brings L1C-D in line with the latest ICD and with the L5 CNAV MT32 decoder, which already uses ΔUT_GPS. - Rename struct/parser field ΔUT1/ΔUT1_dot → ΔUT_GPS/ΔUT_GPS_dot. - Fix scale factor 2⁻²⁴ → 2⁻²³ for the value; rate unchanged. - Update docstring/comments and the SF3 page-2 test expectation. Fixes #61. BREAKING CHANGE: the GPSL1C_DData fields ΔUT1/ΔUT1_dot are renamed to ΔUT_GPS/ΔUT_GPS_dot, and the decoded ΔUT_GPS value is now twice the previous (incorrect) value due to the 2⁻²⁴ → 2⁻²³ scale-factor fix. Downstream code reading data.ΔUT1 must be updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gps/l1c_d.jl | 20 ++++++++++---------- test/gps_l1c_d.jl | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/gps/l1c_d.jl b/src/gps/l1c_d.jl index 1bde4b1..31f694f 100644 --- a/src/gps/l1c_d.jl +++ b/src/gps/l1c_d.jl @@ -348,7 +348,7 @@ CRC-valid SF3 page regardless of whether its page format is parsed. "GGTO ID" by IRN-IS-800J-003.) - `t_EOP::Int64`: EOP reference time of week (s). - `PM_X,PM_X_dot,PM_Y,PM_Y_dot::Float64`: polar-motion values/rates. - - `ΔUT1,ΔUT1_dot::Float64`: UT1-UTC difference and rate. + - `ΔUT_GPS,ΔUT_GPS_dot::Float64`: UT1-GPS (UT1−GPST) difference and rate. ## Pages 3/4/5 — keyed dictionaries (`nothing` until first decoded) @@ -442,8 +442,8 @@ Base.@kwdef struct GPSL1C_DData <: AbstractGNSSData PM_X_dot::Union{Nothing,Float64} = nothing PM_Y::Union{Nothing,Float64} = nothing PM_Y_dot::Union{Nothing,Float64} = nothing - ΔUT1::Union{Nothing,Float64} = nothing - ΔUT1_dot::Union{Nothing,Float64} = nothing + ΔUT_GPS::Union{Nothing,Float64} = nothing + ΔUT_GPS_dot::Union{Nothing,Float64} = nothing # --- Subframe 3, pages 3/4/5: per-SV keyed dictionaries --- reduced_almanacs::Union{Nothing,Dictionary{Int,GPSL1C_DReducedAlmanac}} = nothing @@ -527,8 +527,8 @@ function GPSL1C_DData( PM_X_dot = data.PM_X_dot, PM_Y = data.PM_Y, PM_Y_dot = data.PM_Y_dot, - ΔUT1 = data.ΔUT1, - ΔUT1_dot = data.ΔUT1_dot, + ΔUT_GPS = data.ΔUT_GPS, + ΔUT_GPS_dot = data.ΔUT_GPS_dot, reduced_almanacs = data.reduced_almanacs, midi_almanacs = data.midi_almanacs, differential_corrections = data.differential_corrections, @@ -602,8 +602,8 @@ function GPSL1C_DData( PM_X_dot, PM_Y, PM_Y_dot, - ΔUT1, - ΔUT1_dot, + ΔUT_GPS, + ΔUT_GPS_dot, reduced_almanacs, midi_almanacs, differential_corrections, @@ -1095,14 +1095,14 @@ function parse_sf3_page2(raw::GPSL1C_DData, word::UInt288, PI::Float64) # EOP (Table 3.5-5). All fields are contiguous in the info block; Figure # 3.5-3 only *draws* PM_X across its 100-bit row boundary — the 2 MSBs end # row 1 (bits 99-100) and the 19 LSBs begin row 2 (bits 101-119) — so the - # plain 21-bit read at bit 99 is correct (likewise ΔUT1 spans the next row). + # plain 21-bit read at bit 99 is correct (likewise ΔUT_GPS spans the next row). t_EOP = Int(get_bits(word, word_length, 83, 16)) * 2^4, PM_X = get_twos_complement_num(word, word_length, 99, 21) * 2.0^-20, PM_X_dot = get_twos_complement_num(word, word_length, 120, 15) * 2.0^-21, PM_Y = get_twos_complement_num(word, word_length, 135, 21) * 2.0^-20, PM_Y_dot = get_twos_complement_num(word, word_length, 156, 15) * 2.0^-21, - ΔUT1 = get_twos_complement_num(word, word_length, 171, 31) * 2.0^-24, - ΔUT1_dot = get_twos_complement_num(word, word_length, 202, 19) * 2.0^-25, + ΔUT_GPS = get_twos_complement_num(word, word_length, 171, 31) * 2.0^-23, + ΔUT_GPS_dot = get_twos_complement_num(word, word_length, 202, 19) * 2.0^-25, ) end diff --git a/test/gps_l1c_d.jl b/test/gps_l1c_d.jl index 1cbca7d..3ec361b 100644 --- a/test/gps_l1c_d.jl +++ b/test/gps_l1c_d.jl @@ -412,8 +412,8 @@ end _setbits!(b, 120, 15, -55) # PM_X_dot _setbits!(b, 135, 21, -4321) # PM_Y _setbits!(b, 156, 15, 77) # PM_Y_dot - _setbits!(b, 171, 31, 100000) # ΔUT1 - _setbits!(b, 202, 19, -250) # ΔUT1_dot + _setbits!(b, 171, 31, 100000) # ΔUT_GPS + _setbits!(b, 202, 19, -250) # ΔUT_GPS_dot end) d = st.data @test d.GGTO_ID == 1 @@ -427,8 +427,8 @@ end @test d.PM_X_dot ≈ -55 * 2.0^-21 @test d.PM_Y ≈ -4321 * 2.0^-20 @test d.PM_Y_dot ≈ 77 * 2.0^-21 - @test d.ΔUT1 ≈ 100000 * 2.0^-24 - @test d.ΔUT1_dot ≈ -250 * 2.0^-25 + @test d.ΔUT_GPS ≈ 100000 * 2.0^-23 + @test d.ΔUT_GPS_dot ≈ -250 * 2.0^-25 end @testset "SF3 page 3 — reduced almanac (multi-packet)" begin From 26b432c270c4dcedbabea05272bef7c28dc0d97d Mon Sep 17 00:00:00 2001 From: siebc <226531417+siebc@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:53:23 +0000 Subject: [PATCH 3/3] test(gpsl1c): anchor SF3 page-2 EOP to Spirent L1C reference capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SF3 page-2 EOP assertions previously checked `raw * `, which is tautological for scale factors — it cannot detect a wrong scale (this is how the earlier 2⁻²⁴ ΔUT_GPS bug went unnoticed). Re-anchor the EOP fields to Spirent's L1C reference capture: feed the raw integers Spirent actually transmitted and assert the physical values its own reference decoder reported. The expected values now come from an independent decoder, so a scale regression fails the test. Verified: reverting the decoder to 2⁻²⁴ makes the new ΔUT_GPS assertion fail. ΔUT_GPS is asserted as 2× Spirent's printed Delta_UT1, since Spirent uses the pre-Rev-G UT1−UTC scale 2⁻²⁴ while IS-GPS-800J Table 3.5-5 defines ΔUT_GPS = UT1−GPS at 2⁻²³ (matching the L5 MT32 golden test). GGTO stays synthetic — the capture carries no GGTO data — to retain its scale-factor coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/gps_l1c_d.jl | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/test/gps_l1c_d.jl b/test/gps_l1c_d.jl index 3ec361b..7964bda 100644 --- a/test/gps_l1c_d.jl +++ b/test/gps_l1c_d.jl @@ -400,20 +400,31 @@ end end @testset "SF3 page 2 — GGTO + EOP" begin + # The EOP fields are anchored to Spirent's GSS L1C reference capture. + # The raw integers fed in below are exactly what Spirent transmitted; the + # assertions are the physical values Spirent's *own* reference decoder + # reported. + # + # Spirent prints the 31-bit field as Delta_UT1 = UT1−UTC with the + # pre-Rev-G scale 2⁻²⁴ (-0.25315952301025391 s). IS-GPS-800J Table 3.5-5 + # redefines it as ΔUT_GPS = UT1−GPS with scale 2⁻²³ — exactly twice that. st = decode_with_sf3(build_sf3_page(7, 2) do b - _setbits!(b, 15, 3, 1) # GGTO_ID = Galileo - _setbits!(b, 18, 16, 50) # tGGTO (scale 2^4) - _setbits!(b, 34, 13, 2100) # WNGGTO - _setbits!(b, 47, 16, 800) # A0GGTO - _setbits!(b, 63, 13, -200) # A1GGTO - _setbits!(b, 76, 7, 2) # A2GGTO - _setbits!(b, 83, 16, 60) # tEOP (scale 2^4) - _setbits!(b, 99, 21, 1234) # PM_X - _setbits!(b, 120, 15, -55) # PM_X_dot - _setbits!(b, 135, 21, -4321) # PM_Y - _setbits!(b, 156, 15, 77) # PM_Y_dot - _setbits!(b, 171, 31, 100000) # ΔUT_GPS - _setbits!(b, 202, 19, -250) # ΔUT_GPS_dot + # GGTO — synthetic non-zero values (the capture carries no GGTO data: + # GNSS ID = 0, all coefficients 0), kept to exercise the GGTO scaling. + _setbits!(b, 15, 3, 1) # GGTO_ID = Galileo + _setbits!(b, 18, 16, 50) # tGGTO (scale 2^4) + _setbits!(b, 34, 13, 2100) # WNGGTO + _setbits!(b, 47, 16, 800) # A0GGTO + _setbits!(b, 63, 13, -200) # A1GGTO + _setbits!(b, 76, 7, 2) # A2GGTO + # EOP — raw values as transmitted in the Spirent capture (message 2). + _setbits!(b, 83, 16, 16200) # tEOP -> 259200 s + _setbits!(b, 99, 21, 111281) # PM_X -> 0.10612583160400391 + _setbits!(b, 120, 15, 2779) # PM_X_dot -> 0.0013251304626464844 + _setbits!(b, 135, 21, 467600) # PM_Y -> 0.4459381103515625 + _setbits!(b, 156, 15, 843) # PM_Y_dot -> 0.00040197372436523438 + _setbits!(b, 171, 31, -4247312) # ΔUT_GPS raw -> Spirent UT1−UTC @2⁻²⁴ + _setbits!(b, 202, 19, -15502) # ΔUT_GPS_dot -> -0.00046199560165405273 end) d = st.data @test d.GGTO_ID == 1 @@ -422,13 +433,16 @@ end @test d.A0_GGTO ≈ 800 * 2.0^-35 @test d.A1_GGTO ≈ -200 * 2.0^-51 @test d.A2_GGTO ≈ 2 * 2.0^-68 - @test d.t_EOP == 60 * 16 - @test d.PM_X ≈ 1234 * 2.0^-20 - @test d.PM_X_dot ≈ -55 * 2.0^-21 - @test d.PM_Y ≈ -4321 * 2.0^-20 - @test d.PM_Y_dot ≈ 77 * 2.0^-21 - @test d.ΔUT_GPS ≈ 100000 * 2.0^-23 - @test d.ΔUT_GPS_dot ≈ -250 * 2.0^-25 + # EOP — Spirent reference-decoder values (independent of our scale LSBs). + @test d.t_EOP == 259200 + @test d.PM_X ≈ 0.10612583160400391 + @test d.PM_X_dot ≈ 0.0013251304626464844 + @test d.PM_Y ≈ 0.4459381103515625 + @test d.PM_Y_dot ≈ 0.00040197372436523438 + # Rev-J ΔUT_GPS = 2 × Spirent's UT1−UTC (2⁻²³ vs Spirent's 2⁻²⁴); a + # regression to 2⁻²⁴ would reproduce Spirent's value and FAIL here. + @test d.ΔUT_GPS ≈ 2 * -0.25315952301025391 + @test d.ΔUT_GPS_dot ≈ -0.00046199560165405273 end @testset "SF3 page 3 — reduced almanac (multi-packet)" begin