use std::path::PathBuf;
use std::sync::OnceLock;
use super::tables::{CS100, E5A_ROWS};
use super::{bipolar, hex_to_bits, Modulation, SignalCode, SignalError};
use crate::iq::DataModulation;
pub const E1_HZ: f64 = 1_575_420_000.0;
pub const E5A_HZ: f64 = 1_176_450_000.0;
pub const E1_CHIP_RATE_HZ: f64 = 1_023_000.0;
pub const E1_CODE_LEN: usize = 4092;
pub const E5A_CHIP_RATE_HZ: f64 = 10_230_000.0;
pub const E5A_CODE_LEN: usize = 10_230;
pub const CODES_PER_COMPONENT: u16 = 50;
pub const CS25: &str = "380AD90";
pub const CS20: &str = "842E9";
pub const E5A_TAPS_REG1_OCTAL: u16 = 0o40503;
pub const E5A_TAPS_REG2_OCTAL: u16 = 0o50661;
pub const E1B_TABLE_ENV: &str = "KSHANA_GALILEO_E1B";
pub const E1C_TABLE_ENV: &str = "KSHANA_GALILEO_E1C";
#[derive(Clone, Debug, PartialEq)]
pub struct MemoryCodeTable {
codes: Vec<(u16, Vec<u8>)>,
}
impl MemoryCodeTable {
pub fn parse(text: &str, len: usize) -> Result<Self, SignalError> {
let mut codes = Vec::new();
for (ln, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (label, hex) = line
.split_once(';')
.ok_or_else(|| SignalError::Table(format!("line {}: no ';'", ln + 1)))?;
let number: u16 = label
.rsplit('_')
.next()
.and_then(|n| n.parse().ok())
.ok_or_else(|| {
SignalError::Table(format!("line {}: no code number in {label:?}", ln + 1))
})?;
if codes.iter().any(|(n, _)| *n == number) {
return Err(SignalError::Table(format!("code {number} appears twice")));
}
codes.push((number, hex_to_bits(hex, len)?));
}
Ok(Self { codes })
}
pub fn from_path(path: &std::path::Path, len: usize) -> Result<Self, SignalError> {
let text = std::fs::read_to_string(path)
.map_err(|e| SignalError::Table(format!("{}: {e}", path.display())))?;
Self::parse(&text, len)
}
pub fn numbers(&self) -> Vec<u16> {
self.codes.iter().map(|(n, _)| *n).collect()
}
pub fn bits(&self, number: u16) -> Option<&[u8]> {
self.codes
.iter()
.find(|(n, _)| *n == number)
.map(|(_, b)| b.as_slice())
}
}
fn load_user_table(env: &str, file: &str) -> Result<MemoryCodeTable, SignalError> {
let path = std::env::var_os(env).map(PathBuf::from).unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("data/galileo-os-sis-icd")
.join(file)
});
let text = std::fs::read_to_string(&path).map_err(|e| {
SignalError::Table(format!(
"the Galileo E1 memory codes are not bundled (the ICD's terms allow \
redistribution only for non-commercial purposes; see LICENSING.md). Supply your \
copy of ICD Annex C ({file}) with {env}=<path>, or run \
scripts/fetch_galileo_e1_codes.sh in a source checkout ({}: {e})",
path.display()
))
})?;
let table = MemoryCodeTable::parse(&text, E1_CODE_LEN)?;
let n = table.numbers().len();
if n != usize::from(CODES_PER_COMPONENT) {
return Err(SignalError::Table(format!(
"{}: {n} codes, the ICD defines {CODES_PER_COMPONENT}",
path.display()
)));
}
Ok(table)
}
pub fn e1b_table() -> Result<&'static MemoryCodeTable, SignalError> {
static T: OnceLock<Result<MemoryCodeTable, SignalError>> = OnceLock::new();
T.get_or_init(|| load_user_table(E1B_TABLE_ENV, "C7_E1B.txt"))
.as_ref()
.map_err(Clone::clone)
}
pub fn e1c_table() -> Result<&'static MemoryCodeTable, SignalError> {
static T: OnceLock<Result<MemoryCodeTable, SignalError>> = OnceLock::new();
T.get_or_init(|| load_user_table(E1C_TABLE_ENV, "C8_E1C.txt"))
.as_ref()
.map_err(Clone::clone)
}
pub fn e1b_from_table(table: &MemoryCodeTable, prn: u16) -> Result<SignalCode, SignalError> {
let bits = table.bits(prn).ok_or(SignalError::UnknownPrn {
signal: "Galileo E1-B",
prn,
})?;
Ok(SignalCode::new(
format!("Galileo E1-B PRN {prn}"),
E1_CHIP_RATE_HZ,
E1_HZ,
bipolar(bits),
vec![],
Modulation::Cboc { plus: true },
)
.with_data_modulation(DataModulation::Symbols { symbol_s: 0.004 }))
}
pub fn e1c_from_table(table: &MemoryCodeTable, prn: u16) -> Result<SignalCode, SignalError> {
let bits = table.bits(prn).ok_or(SignalError::UnknownPrn {
signal: "Galileo E1-C",
prn,
})?;
Ok(SignalCode::new(
format!("Galileo E1-C PRN {prn}"),
E1_CHIP_RATE_HZ,
E1_HZ,
bipolar(bits),
bipolar(&hex_to_bits(CS25, 25)?),
Modulation::Cboc { plus: false },
)
.with_data_modulation(DataModulation::Pilot))
}
pub fn e1b(prn: u16) -> Result<SignalCode, SignalError> {
e1b_from_table(e1b_table()?, prn)
}
pub fn e1c(prn: u16) -> Result<SignalCode, SignalError> {
e1c_from_table(e1c_table()?, prn)
}
pub fn e5_primary_bits(
taps1_octal: u16,
taps2_octal: u16,
start2_octal: u16,
len: usize,
) -> Vec<u8> {
const R: usize = 14;
let taps = |w: u16| -> [u8; R] {
let mut a = [0u8; R];
for (j, t) in a.iter_mut().enumerate() {
*t = ((w >> (j + 1)) & 1) as u8;
}
a
};
let (a1, a2) = (taps(taps1_octal), taps(taps2_octal));
let mut c1 = [1u8; R];
let mut c2 = [0u8; R];
for (j, c) in c2.iter_mut().enumerate() {
*c = ((start2_octal >> j) & 1) as u8;
}
let step = |c: &mut [u8; R], a: &[u8; R]| {
let fb = c.iter().zip(a).fold(0u8, |acc, (&x, &t)| acc ^ (x & t));
c.copy_within(0..R - 1, 1);
c[0] = fb;
};
let mut out = Vec::with_capacity(len);
for _ in 0..len {
out.push(c1[R - 1] ^ c2[R - 1]);
step(&mut c1, &a1);
step(&mut c2, &a2);
}
out
}
fn e5a_row(signal: &'static str, prn: u16) -> Result<&'static super::tables::E5aRow, SignalError> {
(prn as usize)
.checked_sub(1)
.and_then(|i| E5A_ROWS.get(i))
.ok_or(SignalError::UnknownPrn { signal, prn })
}
pub fn e5a_i(prn: u16) -> Result<SignalCode, SignalError> {
let row = e5a_row("Galileo E5a-I", prn)?;
let bits = e5_primary_bits(
E5A_TAPS_REG1_OCTAL,
E5A_TAPS_REG2_OCTAL,
row.i_start,
E5A_CODE_LEN,
);
Ok(SignalCode::new(
format!("Galileo E5a-I PRN {prn}"),
E5A_CHIP_RATE_HZ,
E5A_HZ,
bipolar(&bits),
bipolar(&hex_to_bits(CS20, 20)?),
Modulation::Bpsk,
)
.with_data_modulation(DataModulation::Symbols { symbol_s: 0.02 }))
}
pub fn e5a_q(prn: u16) -> Result<SignalCode, SignalError> {
let row = e5a_row("Galileo E5a-Q", prn)?;
let bits = e5_primary_bits(
E5A_TAPS_REG1_OCTAL,
E5A_TAPS_REG2_OCTAL,
row.q_start,
E5A_CODE_LEN,
);
Ok(SignalCode::new(
format!("Galileo E5a-Q PRN {prn}"),
E5A_CHIP_RATE_HZ,
E5A_HZ,
bipolar(&bits),
bipolar(&hex_to_bits(CS100[prn as usize - 1], 100)?),
Modulation::Bpsk,
)
.with_data_modulation(DataModulation::Pilot))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_parser_rejects_malformed_input() {
assert!(MemoryCodeTable::parse("X_1;F", 4).is_ok());
assert!(MemoryCodeTable::parse("X_1 F", 4).is_err());
assert!(MemoryCodeTable::parse("X_a;F", 4).is_err());
assert!(MemoryCodeTable::parse("X_1;F\nX_1;0", 4).is_err());
assert!(MemoryCodeTable::parse("X_1;FF", 4).is_err());
}
#[test]
fn user_table_loader_builds_a_code() {
let t = MemoryCodeTable::parse("E1B__Code_No_07;A5\r\n", 8).unwrap();
let c = e1b_from_table(&t, 7).unwrap();
assert_eq!(c.primary_bits(), vec![1, 0, 1, 0, 0, 1, 0, 1]);
assert!(e1b_from_table(&t, 1).is_err());
}
#[test]
fn user_table_file_loads_from_a_path() {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let uniq = SEQ.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("kshana-e1-{}-{uniq}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("user_e1b.txt");
std::fs::write(&path, "E1B__Code_No_07;A5\r\n").unwrap();
let t = MemoryCodeTable::from_path(&path, 8).unwrap();
assert_eq!(t.numbers(), vec![7]);
assert!(MemoryCodeTable::from_path(&dir.join("absent.txt"), 8).is_err());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn prn_range_is_enforced() {
assert!(e1b(0).is_err());
let e = load_user_table("KSHANA_TEST_UNSET_E1_TABLE", "no-such-table.txt").unwrap_err();
assert!(
matches!(&e, SignalError::Table(m) if m.contains("not bundled")),
"{e:?}"
);
assert!(e1c(51).is_err());
assert!(e5a_i(51).is_err());
assert!(e5a_q(0).is_err());
}
}