orion-sdr 0.0.60

Composable SDR/DSP block library targeting HF-to-EHF: analog and single-carrier digital modes, FT8/FT4, PSK31, OFDM/COFDM, and DVB-T/NB-DVB-T, with Python bindings.
Documentation
// Copyright (c) 2026 G & R Associates LLC
// SPDX-License-Identifier: MIT OR Apache-2.0

const MAXGRID4: u16 = 32_400;

/// The "extra" field in a standard FT8 message.
#[derive(Debug, Clone, PartialEq)]
pub enum GridField {
    Grid(String), // 4-char Maidenhead, e.g. "FN31"
    Report(i8),   // signal report in dB, e.g. +7 or -12
    RReport(i8),  // same with "R" prefix
    RRR,
    RR73,
    Seventy3, // "73"
    None,
}

/// Pack the extra/grid field into a u16.
/// The high bit (bit 15) is the `ir` flag (set for R-prefixed reports).
/// The low 15 bits are `igrid4` sent in the payload.
pub fn packgrid(extra: &str) -> u16 {
    if extra.is_empty() {
        return MAXGRID4 + 1; // No extra field
    }
    match extra {
        "RRR" => return MAXGRID4 + 2,
        "RR73" => return MAXGRID4 + 3,
        "73" => return MAXGRID4 + 4,
        _ => {}
    }

    let bytes = extra.as_bytes();

    // 4-char Maidenhead grid
    if bytes.len() == 4
        && bytes[0] >= b'A'
        && bytes[0] <= b'R'
        && bytes[1] >= b'A'
        && bytes[1] <= b'R'
        && bytes[2].is_ascii_digit()
        && bytes[3].is_ascii_digit()
    {
        let igrid4 = (bytes[0] - b'A') as u16 * 18 * 10 * 10
            + (bytes[1] - b'A') as u16 * 10 * 10
            + (bytes[2] - b'0') as u16 * 10
            + (bytes[3] - b'0') as u16;
        return igrid4;
    }

    // Signal report: optional 'R' prefix, then +/- and digits
    if bytes[0] == b'R' && bytes.len() >= 2 {
        let dd = dd_to_int(&extra[1..]);
        let irpt = (35i32 + dd as i32) as u16;
        (MAXGRID4 + irpt) | 0x8000 // ir = 1
    } else {
        let dd = dd_to_int(extra);
        let irpt = (35i32 + dd as i32) as u16;
        MAXGRID4 + irpt // ir = 0
    }
}

fn dd_to_int(s: &str) -> i8 {
    let bytes = s.as_bytes();
    if bytes.is_empty() {
        return 0;
    }
    let (neg, start) = match bytes[0] {
        b'-' => (true, 1),
        b'+' => (false, 1),
        _ => (false, 0),
    };
    let mut val: i8 = 0;
    for &b in &bytes[start..] {
        if b.is_ascii_digit() {
            val = val.wrapping_mul(10).wrapping_add((b - b'0') as i8);
        } else {
            break;
        }
    }
    if neg { -val } else { val }
}

/// Unpack the grid/extra field.
/// `igrid4` is the 15-bit value from the payload; `ir` is the high bit (bit 15).
pub fn unpackgrid(igrid4: u16, ir: bool) -> GridField {
    if igrid4 <= MAXGRID4 {
        // 4-char Maidenhead grid
        let mut n = igrid4;
        let d3 = (n % 10) as u8;
        n /= 10;
        let d2 = (n % 10) as u8;
        n /= 10;
        let c1 = (n % 18) as u8;
        n /= 18;
        let c0 = (n % 18) as u8;
        let grid = format!(
            "{}{}{}{}",
            (b'A' + c0) as char,
            (b'A' + c1) as char,
            (b'0' + d2) as char,
            (b'0' + d3) as char,
        );
        if ir {
            // Grid with R prefix in unpack corresponds to R-prefixed report, but
            // per ft8_lib, ir=1 on a grid means "R " prepended
            return GridField::Grid(format!("R {}", grid));
        }
        return GridField::Grid(grid);
    }

    let irpt = (igrid4 - MAXGRID4) as i32;
    match irpt {
        1 => GridField::None,
        2 => GridField::RRR,
        3 => GridField::RR73,
        4 => GridField::Seventy3,
        _ => {
            let dd = (irpt - 35) as i8;
            if ir {
                GridField::RReport(dd)
            } else {
                GridField::Report(dd)
            }
        }
    }
}

/// Convert a `GridField` back to its string representation for display.
pub fn gridfield_to_str(gf: &GridField) -> String {
    match gf {
        GridField::Grid(s) => s.clone(),
        GridField::Report(n) => format!("{:+03}", n),
        GridField::RReport(n) => format!("R{:+03}", n),
        GridField::RRR => "RRR".to_string(),
        GridField::RR73 => "RR73".to_string(),
        GridField::Seventy3 => "73".to_string(),
        GridField::None => String::new(),
    }
}

/// Convert a `GridField` to (igrid4_15bits, ir) for packing.
pub fn gridfield_to_pack(gf: &GridField) -> (u16, bool) {
    let raw = match gf {
        GridField::Grid(s) => packgrid(s),
        GridField::Report(n) => packgrid(&format!("{:+03}", n)),
        GridField::RReport(n) => packgrid(&format!("R{:+03}", n)),
        GridField::RRR => packgrid("RRR"),
        GridField::RR73 => packgrid("RR73"),
        GridField::Seventy3 => packgrid("73"),
        GridField::None => packgrid(""),
    };
    let ir = (raw & 0x8000) != 0;
    let igrid4 = raw & 0x7FFF;
    (igrid4, ir)
}