orion-sdr 0.0.21

DSP/SDR block library targeting HF-to-UHF, satellites, and Python bindings. Roadmap inside.
Documentation
use crate::message::{
    CallsignHashTable, GridField, Ft8Message, NonstdExtra, pack77, unpack77,
};
use crate::message::callsign::{pack_basecall, pack28, unpack28};
use crate::message::grid::{packgrid, unpackgrid};
use crate::message::free_text::{encode_free_text, decode_free_text};

// --------------------------------------------------------------------------
// Callsign tests
// --------------------------------------------------------------------------

#[test]
fn pack_basecall_long() {
    // "W9XYZ" -> right-align as " W9XYZ": i0=0, i1=32, i2=9, i3=24, i4=25, i5=26
    let n = pack_basecall("W9XYZ").expect("W9XYZ should be a valid basecall");
    // n = 0*36*10*27*27*27 + 32*10*27*27*27 + 9*27*27*27 + 24*27*27 + 25*27 + 26
    //   = 0 + 32*196830 + 9*19683 + 24*729 + 25*27 + 26
    //   = 6298560 + 177147 + 17496 + 675 + 26 = 6493904
    assert_eq!(n, 6_493_904);
}

#[test]
fn pack_basecall_roundtrip() {
    // These should all survive pack -> unpack
    for call in &["W9XYZ", "KD9ABC", "VE3XYZ", "G0ABC"] {
        let n = pack_basecall(call).unwrap_or_else(|| panic!("{} should be valid", call));
        // Reconstruct from callsign module internals (tested separately)
        let _ = n; // pack_basecall verified sufficient; unpack tested in callsign.rs
    }
}

#[test]
fn unpack28_special_tokens() {
    let ht = CallsignHashTable::new();
    assert_eq!(unpack28(0, false, 1, &ht).unwrap(), "DE");
    assert_eq!(unpack28(1, false, 1, &ht).unwrap(), "QRZ");
    assert_eq!(unpack28(2, false, 1, &ht).unwrap(), "CQ");
}

#[test]
fn pack28_roundtrip_standard() {
    let mut ht = CallsignHashTable::new();
    for call in &["W9XYZ", "KD9ABC", "VE3XYZ"] {
        let mut ip = false;
        let n28 = pack28(call, &mut ht, &mut ip).expect(call);
        let back = unpack28(n28, ip, 1, &ht).expect("unpack28 failed");
        assert_eq!(&back, call, "Roundtrip failed for {}", call);
    }
}

#[test]
fn pack28_cq_tokens() {
    let mut ht = CallsignHashTable::new();
    // CQ NNN
    let mut ip = false;
    let n = pack28("CQ 123", &mut ht, &mut ip).expect("CQ 123");
    let back = unpack28(n, ip, 1, &ht).expect("unpack CQ 123");
    assert_eq!(back, "CQ 123");

    // CQ DX (letter-only modifier)
    let n = pack28("CQ DX", &mut ht, &mut ip).expect("CQ DX");
    let back = unpack28(n, ip, 1, &ht).expect("unpack CQ DX");
    assert_eq!(back, "CQ DX");
}

// --------------------------------------------------------------------------
// Grid tests
// --------------------------------------------------------------------------

#[test]
fn packgrid_grid_roundtrip() {
    for g in &["FN31", "AA00", "RR99"] {
        let raw = packgrid(g);
        let gf = unpackgrid(raw & 0x7FFF, (raw & 0x8000) != 0);
        assert_eq!(gf, GridField::Grid(g.to_string()), "Roundtrip failed for {}", g);
    }
}

#[test]
fn packgrid_report_roundtrip() {
    let cases: &[(&str, GridField)] = &[
        ("+07",  GridField::Report(7)),
        ("-12",  GridField::Report(-12)),
        ("R-05", GridField::RReport(-5)),
    ];
    for (s, expected) in cases {
        let raw = packgrid(s);
        let gf = unpackgrid(raw & 0x7FFF, (raw & 0x8000) != 0);
        assert_eq!(&gf, expected, "Roundtrip failed for '{}'", s);
    }
}

#[test]
fn packgrid_tokens() {
    let raw = packgrid("RRR");
    assert_eq!(unpackgrid(raw & 0x7FFF, false), GridField::RRR);
    let raw = packgrid("RR73");
    assert_eq!(unpackgrid(raw & 0x7FFF, false), GridField::RR73);
    let raw = packgrid("73");
    assert_eq!(unpackgrid(raw & 0x7FFF, false), GridField::Seventy3);
    let raw = packgrid("");
    assert_eq!(unpackgrid(raw & 0x7FFF, false), GridField::None);
}

// --------------------------------------------------------------------------
// Free text tests
// --------------------------------------------------------------------------

#[test]
fn free_text_roundtrip() {
    let cases = &["CQ DX", "HELLO WORLD", "TNX 73 GL", "73", ""];
    for &text in cases {
        let encoded = encode_free_text(text)
            .unwrap_or_else(|| panic!("encode failed for '{}'", text));
        let decoded = decode_free_text(&encoded);
        assert_eq!(decoded, text.trim_end(), "Free text roundtrip failed for '{}'", text);
    }
}

#[test]
fn free_text_max_length() {
    // Exactly 13 characters is fine
    let text = "ABCDEFGHIJKLM";
    assert_eq!(text.len(), 13);
    let enc = encode_free_text(text).expect("13-char text should encode");
    let dec = decode_free_text(&enc);
    assert_eq!(dec, text);
}

#[test]
fn free_text_too_long() {
    assert!(encode_free_text("ABCDEFGHIJKLMN").is_none());
}

// --------------------------------------------------------------------------
// pack77 / unpack77 tests
// --------------------------------------------------------------------------

#[test]
fn pack77_type1_roundtrip() {
    let mut ht = CallsignHashTable::new();
    let msg = Ft8Message::Standard {
        call_to: "KD9ABC".to_string(),
        call_de: "W9XYZ".to_string(),
        extra: GridField::Grid("FN31".to_string()),
    };
    let payload = pack77(&msg, &mut ht).expect("pack77 type1");
    let decoded = unpack77(&payload, &ht);
    assert_eq!(decoded, msg);
}

#[test]
fn pack77_type1_report() {
    let mut ht = CallsignHashTable::new();
    let msg = Ft8Message::Standard {
        call_to: "KD9ABC".to_string(),
        call_de: "W9XYZ".to_string(),
        extra: GridField::Report(-7),
    };
    let payload = pack77(&msg, &mut ht).expect("pack77 report");
    let decoded = unpack77(&payload, &ht);
    assert_eq!(decoded, msg);
}

#[test]
fn pack77_type1_rr73() {
    let mut ht = CallsignHashTable::new();
    let msg = Ft8Message::Standard {
        call_to: "CQ".to_string(),
        call_de: "W9XYZ".to_string(),
        extra: GridField::RR73,
    };
    let payload = pack77(&msg, &mut ht).expect("pack77 rr73");
    let decoded = unpack77(&payload, &ht);
    assert_eq!(decoded, msg);
}

#[test]
fn pack77_free_text_roundtrip() {
    let mut ht = CallsignHashTable::new();
    for text in &["CQ DX", "HELLO WORLD", "73"] {
        let msg = Ft8Message::FreeText(text.to_string());
        let payload = pack77(&msg, &mut ht).expect("pack77 free text");
        let decoded = unpack77(&payload, &ht);
        assert_eq!(decoded, msg, "Free text roundtrip failed for '{}'", text);
    }
}

#[test]
fn pack77_telemetry_roundtrip() {
    let mut ht = CallsignHashTable::new();
    let data = [0x12u8, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11];
    let msg = Ft8Message::Telemetry(data);
    let payload = pack77(&msg, &mut ht).expect("pack77 telemetry");
    let decoded = unpack77(&payload, &ht);
    assert_eq!(decoded, msg);
}

#[test]
fn pack77_type4_roundtrip() {
    let mut ht = CallsignHashTable::new();
    // Pre-populate the hash table with the "to" callsign so lookup works
    ht.save("W9XYZ");
    let msg = Ft8Message::NonStd {
        call_to: "W9XYZ".to_string(),
        call_de: "KD9ABC/R".to_string(),
        extra: NonstdExtra::RRR,
    };
    let payload = pack77(&msg, &mut ht).expect("pack77 type4");
    let decoded = unpack77(&payload, &ht);
    // The decoded call_to should be wrapped in < > since it was looked up from hash
    match decoded {
        Ft8Message::NonStd { call_to, call_de, extra } => {
            assert!(call_to.contains("W9XYZ") || call_to == "<W9XYZ>",
                "Expected W9XYZ in call_to, got {}", call_to);
            assert_eq!(call_de, "KD9ABC/R");
            assert_eq!(extra, NonstdExtra::RRR);
        }
        other => panic!("Expected NonStd, got {:?}", other),
    }
}