[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
|
// Number of top frequency candidates to try full decode on
|
||||||
const MAX_FREQ_CANDIDATES: usize = 8;
|
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].
|
/// WSPR sync vector (162 bits). symbol = sync[i] + 2*data[i].
|
||||||
/// The LSB of each received symbol should match this pattern.
|
/// The LSB of each received symbol should match this pattern.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
@@ -123,7 +128,10 @@ impl WsprDecoder {
|
|||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
let mut seen_messages = std::collections::HashSet::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 start = (EXPECTED_SIGNAL_START_SAMPLES as isize + dt_samples) as usize;
|
||||||
let signal = &samples[start..start + WSPR_SIGNAL_SAMPLES];
|
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 = (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;
|
let power_dbm = power_code as i32;
|
||||||
if !(0..=60).contains(&power_dbm) {
|
if !VALID_POWER.contains(&power_dbm) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +185,23 @@ fn unpack_message(bits: &[u8; NBITS]) -> Option<String> {
|
|||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.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()) {
|
if callsign.len() < 3 || !callsign.chars().any(|c| c.is_alphabetic()) {
|
||||||
return None;
|
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.
|
// Decode Maidenhead grid from M1.
|
||||||
// M1 = (179 - 10*loc1 - loc3)*180 + 10*loc2 + loc4
|
// M1 = (179 - 10*loc1 - loc3)*180 + 10*loc2 + loc4
|
||||||
|
|||||||
Reference in New Issue
Block a user