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_TEXT: &str = include_str!("../../../data/galileo-os-sis-icd/C7_E1B.txt");
pub const E1C_TABLE_TEXT: &str = include_str!("../../../data/galileo-os-sis-icd/C8_E1C.txt");
#[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 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())
}
}
pub fn e1b_table() -> &'static MemoryCodeTable {
static T: OnceLock<MemoryCodeTable> = OnceLock::new();
T.get_or_init(|| {
MemoryCodeTable::parse(E1B_TABLE_TEXT, E1_CODE_LEN).expect("committed E1-B table parses")
})
}
pub fn e1c_table() -> &'static MemoryCodeTable {
static T: OnceLock<MemoryCodeTable> = OnceLock::new();
T.get_or_init(|| {
MemoryCodeTable::parse(E1C_TABLE_TEXT, E1_CODE_LEN).expect("committed E1-C table parses")
})
}
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 prn_range_is_enforced() {
assert!(e1b(0).is_err());
assert!(e1c(51).is_err());
assert!(e5a_i(51).is_err());
assert!(e5a_q(0).is_err());
}
}