[fix](trx-wspr): reduce false positives with stricter validation
Restrict accepted power levels to the 19 valid WSPR values instead of any 0-60. Require a digit at position 1 or 2 of the trimmed callsign per the WSPR encoding rules. Skip candidates whose sync correlation score falls below a minimum threshold before attempting Fano decode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -25,6 +25,11 @@ const DT_SEARCH_STEP_SAMPLES: isize = (WSPR_SAMPLE_RATE as isize) / 2;
|
||||
// Number of top frequency candidates to try full decode on
|
||||
const MAX_FREQ_CANDIDATES: usize = 8;
|
||||
|
||||
// Minimum sync correlation score to attempt a full decode. Candidates below
|
||||
// this threshold are almost certainly noise and skipping them avoids expensive
|
||||
// Fano decode attempts that would produce false positives.
|
||||
const MIN_SYNC_SCORE: f32 = 10.0;
|
||||
|
||||
/// WSPR sync vector (162 bits). symbol = sync[i] + 2*data[i].
|
||||
/// The LSB of each received symbol should match this pattern.
|
||||
#[rustfmt::skip]
|
||||
@@ -123,7 +128,10 @@ impl WsprDecoder {
|
||||
let mut results = Vec::new();
|
||||
let mut seen_messages = std::collections::HashSet::new();
|
||||
|
||||
for &(freq, dt_samples, _) in candidates.iter().take(MAX_FREQ_CANDIDATES) {
|
||||
for &(freq, dt_samples, score) in candidates.iter().take(MAX_FREQ_CANDIDATES) {
|
||||
if score < MIN_SYNC_SCORE {
|
||||
break; // candidates are sorted by score, no point continuing
|
||||
}
|
||||
let start = (EXPECTED_SIGNAL_START_SAMPLES as isize + dt_samples) as usize;
|
||||
let signal = &samples[start..start + WSPR_SIGNAL_SAMPLES];
|
||||
|
||||
|
||||
@@ -141,9 +141,12 @@ fn unpack_message(bits: &[u8; NBITS]) -> Option<String> {
|
||||
power_code = (power_code << 1) | b as u32;
|
||||
}
|
||||
|
||||
// power_code is the raw dBm value; valid WSPR levels are 0–60 dBm.
|
||||
// WSPR only permits specific power levels (dBm).
|
||||
const VALID_POWER: [i32; 19] = [
|
||||
0, 3, 7, 10, 13, 17, 20, 23, 27, 30, 33, 37, 40, 43, 47, 50, 53, 57, 60,
|
||||
];
|
||||
let power_dbm = power_code as i32;
|
||||
if !(0..=60).contains(&power_dbm) {
|
||||
if !VALID_POWER.contains(&power_dbm) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -182,9 +185,23 @@ fn unpack_message(bits: &[u8; NBITS]) -> Option<String> {
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
// WSPR callsigns: after trimming, the digit (from position 2 of the
|
||||
// 6-char padded form) must appear at index 1 or 2. The callsign must
|
||||
// also contain at least one letter and be at least 3 characters long.
|
||||
if callsign.len() < 3 || !callsign.chars().any(|c| c.is_alphabetic()) {
|
||||
return None;
|
||||
}
|
||||
let has_digit_at_1_or_2 = callsign
|
||||
.chars()
|
||||
.nth(1)
|
||||
.is_some_and(|c| c.is_ascii_digit())
|
||||
|| callsign
|
||||
.chars()
|
||||
.nth(2)
|
||||
.is_some_and(|c| c.is_ascii_digit());
|
||||
if !has_digit_at_1_or_2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Decode Maidenhead grid from M1.
|
||||
// M1 = (179 - 10*loc1 - loc3)*180 + 10*loc2 + loc4
|
||||
|
||||
Reference in New Issue
Block a user