Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b17b4ac1d | ||
|
|
977f7b709e | ||
|
|
a2838b06a2 | ||
|
|
c6cd661676 | ||
|
|
bf08c7ebc0 | ||
|
|
c7470acc1c |
+16
-8
@@ -22,15 +22,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
build-essential pkg-config cmake \
|
build-essential pkg-config cmake clang libclang-dev \
|
||||||
libopus-dev libasound2-dev libsoapysdr-dev
|
libopus-dev libasound2-dev libsoapysdr-dev
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
run: |
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
if ! command -v rustup >/dev/null 2>&1; then
|
if ! command -v rustup >/dev/null 2>&1; then
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
| sh -s -- -y --profile minimal
|
| sh -s -- -y --profile minimal
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
||||||
fi
|
fi
|
||||||
rustup toolchain install stable --profile minimal \
|
rustup toolchain install stable --profile minimal \
|
||||||
--component rustfmt --component clippy
|
--component rustfmt --component clippy
|
||||||
@@ -47,10 +47,14 @@ jobs:
|
|||||||
restore-keys: cargo-${{ runner.os }}-
|
restore-keys: cargo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: rustfmt
|
- name: rustfmt
|
||||||
run: cargo fmt --all -- --check
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
- name: clippy
|
- name: clippy
|
||||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -61,15 +65,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
build-essential pkg-config cmake \
|
build-essential pkg-config cmake clang libclang-dev \
|
||||||
libopus-dev libasound2-dev libsoapysdr-dev
|
libopus-dev libasound2-dev libsoapysdr-dev
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
run: |
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
if ! command -v rustup >/dev/null 2>&1; then
|
if ! command -v rustup >/dev/null 2>&1; then
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
| sh -s -- -y --profile minimal
|
| sh -s -- -y --profile minimal
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
||||||
fi
|
fi
|
||||||
rustup toolchain install stable --profile minimal
|
rustup toolchain install stable --profile minimal
|
||||||
rustup default stable
|
rustup default stable
|
||||||
@@ -85,10 +89,14 @@ jobs:
|
|||||||
restore-keys: cargo-${{ runner.os }}-
|
restore-keys: cargo-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --workspace --all-targets --locked
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
cargo build --workspace --all-targets --locked
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --workspace --locked
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
cargo test --workspace --locked
|
||||||
|
|
||||||
reuse:
|
reuse:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ fn parse_frame(frame: RawFrame, channel: &str) -> Option<AisMessage> {
|
|||||||
|
|
||||||
let message_type = get_uint(&bits, 0, 6)? as u8;
|
let message_type = get_uint(&bits, 0, 6)? as u8;
|
||||||
let repeat = get_uint(&bits, 6, 2)? as u8;
|
let repeat = get_uint(&bits, 6, 2)? as u8;
|
||||||
let mmsi = get_uint(&bits, 8, 30)? as u32;
|
let mmsi = get_uint(&bits, 8, 30)?;
|
||||||
|
|
||||||
let mut msg = AisMessage {
|
let mut msg = AisMessage {
|
||||||
rig_id: None,
|
rig_id: None,
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ mod tests {
|
|||||||
for (i, &ch) in b"N0CALL".iter().enumerate() {
|
for (i, &ch) in b"N0CALL".iter().enumerate() {
|
||||||
addr[i] = ch << 1;
|
addr[i] = ch << 1;
|
||||||
}
|
}
|
||||||
addr[6] = (0 << 1) | 1; // SSID=0, last=true
|
addr[6] = 1; // SSID=0, last=true
|
||||||
|
|
||||||
let decoded = decode_ax25_address(&addr, 0);
|
let decoded = decode_ax25_address(&addr, 0);
|
||||||
assert_eq!(decoded.call, "N0CALL");
|
assert_eq!(decoded.call, "N0CALL");
|
||||||
@@ -652,7 +652,7 @@ mod tests {
|
|||||||
for (i, &ch) in b"SP2SJG".iter().enumerate() {
|
for (i, &ch) in b"SP2SJG".iter().enumerate() {
|
||||||
addr[i] = ch << 1;
|
addr[i] = ch << 1;
|
||||||
}
|
}
|
||||||
addr[6] = (5 << 1) | 0; // SSID=5, last=false
|
addr[6] = 5 << 1; // SSID=5, last=false
|
||||||
|
|
||||||
let decoded = decode_ax25_address(&addr, 0);
|
let decoded = decode_ax25_address(&addr, 0);
|
||||||
assert_eq!(decoded.call, "SP2SJG");
|
assert_eq!(decoded.call, "SP2SJG");
|
||||||
@@ -667,7 +667,7 @@ mod tests {
|
|||||||
for (i, &ch) in b"W1AW ".iter().enumerate() {
|
for (i, &ch) in b"W1AW ".iter().enumerate() {
|
||||||
addr[i] = ch << 1;
|
addr[i] = ch << 1;
|
||||||
}
|
}
|
||||||
addr[6] = (0 << 1) | 1;
|
addr[6] = 1;
|
||||||
|
|
||||||
let decoded = decode_ax25_address(&addr, 0);
|
let decoded = decode_ax25_address(&addr, 0);
|
||||||
assert_eq!(decoded.call, "W1AW");
|
assert_eq!(decoded.call, "W1AW");
|
||||||
@@ -691,7 +691,7 @@ mod tests {
|
|||||||
for &ch in src_bytes.as_bytes().iter().take(6) {
|
for &ch in src_bytes.as_bytes().iter().take(6) {
|
||||||
frame.push(ch << 1);
|
frame.push(ch << 1);
|
||||||
}
|
}
|
||||||
frame.push((0 << 1) | 1); // SSID=0, last=true
|
frame.push(1); // SSID=0, last=true
|
||||||
// Control + PID
|
// Control + PID
|
||||||
frame.push(0x03); // UI frame
|
frame.push(0x03); // UI frame
|
||||||
frame.push(0xF0); // No layer-3 protocol
|
frame.push(0xF0); // No layer-3 protocol
|
||||||
|
|||||||
@@ -160,8 +160,7 @@ impl CallsignHashTable {
|
|||||||
let mut idx = start_idx;
|
let mut idx = start_idx;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match &self.entries[idx] {
|
let entry = self.entries[idx].as_ref()?;
|
||||||
Some(entry) => {
|
|
||||||
let stored = (entry.hash & HASH22_MASK) >> shift;
|
let stored = (entry.hash & HASH22_MASK) >> shift;
|
||||||
if stored == target {
|
if stored == target {
|
||||||
return Some(entry.callsign.clone());
|
return Some(entry.callsign.clone());
|
||||||
@@ -171,9 +170,6 @@ impl CallsignHashTable {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => return None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Age all entries and remove those older than `max_age`.
|
/// Age all entries and remove those older than `max_age`.
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ pub(crate) fn ldpc_check(codeword: &[u8; FTX_LDPC_N]) -> i32 {
|
|||||||
for m in 0..FTX_LDPC_M {
|
for m in 0..FTX_LDPC_M {
|
||||||
let mut x: u8 = 0;
|
let mut x: u8 = 0;
|
||||||
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
|
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
|
||||||
for i in 0..num_rows {
|
for &nm in FTX_LDPC_NM[m].iter().take(num_rows) {
|
||||||
x ^= codeword[FTX_LDPC_NM[m][i] as usize - 1];
|
x ^= codeword[nm as usize - 1];
|
||||||
}
|
}
|
||||||
if x != 0 {
|
if x != 0 {
|
||||||
errors += 1;
|
errors += 1;
|
||||||
@@ -81,11 +81,11 @@ pub fn ldpc_decode(
|
|||||||
for j in 0..FTX_LDPC_M {
|
for j in 0..FTX_LDPC_M {
|
||||||
let num_rows = FTX_LDPC_NUM_ROWS[j] as usize;
|
let num_rows = FTX_LDPC_NUM_ROWS[j] as usize;
|
||||||
let m_row = j * FTX_LDPC_N;
|
let m_row = j * FTX_LDPC_N;
|
||||||
for ii1 in 0..num_rows {
|
for &nm1 in FTX_LDPC_NM[j].iter().take(num_rows) {
|
||||||
let i1 = FTX_LDPC_NM[j][ii1] as usize - 1;
|
let i1 = nm1 as usize - 1;
|
||||||
let mut a = 1.0f32;
|
let mut a = 1.0f32;
|
||||||
for ii2 in 0..num_rows {
|
for &nm2 in FTX_LDPC_NM[j].iter().take(num_rows) {
|
||||||
let i2 = FTX_LDPC_NM[j][ii2] as usize - 1;
|
let i2 = nm2 as usize - 1;
|
||||||
if i2 != i1 {
|
if i2 != i1 {
|
||||||
a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32);
|
a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32);
|
||||||
}
|
}
|
||||||
@@ -97,8 +97,8 @@ pub fn ldpc_decode(
|
|||||||
// Hard decisions
|
// Hard decisions
|
||||||
for i in 0..FTX_LDPC_N {
|
for i in 0..FTX_LDPC_N {
|
||||||
let mut l = codeword[i];
|
let mut l = codeword[i];
|
||||||
for j in 0..3 {
|
for &mn in FTX_LDPC_MN[i].iter().take(3) {
|
||||||
l += e_matrix[(FTX_LDPC_MN[i][j] as usize - 1) * FTX_LDPC_N + i];
|
l += e_matrix[(mn as usize - 1) * FTX_LDPC_N + i];
|
||||||
}
|
}
|
||||||
plain[i] = if l > 0.0 { 1 } else { 0 };
|
plain[i] = if l > 0.0 { 1 } else { 0 };
|
||||||
}
|
}
|
||||||
@@ -113,12 +113,12 @@ pub fn ldpc_decode(
|
|||||||
|
|
||||||
// Update m[][] from e[][]
|
// Update m[][] from e[][]
|
||||||
for i in 0..FTX_LDPC_N {
|
for i in 0..FTX_LDPC_N {
|
||||||
for ji1 in 0..3 {
|
for (ji1, &mn1) in FTX_LDPC_MN[i].iter().enumerate().take(3) {
|
||||||
let j1 = FTX_LDPC_MN[i][ji1] as usize - 1;
|
let j1 = mn1 as usize - 1;
|
||||||
let mut l = codeword[i];
|
let mut l = codeword[i];
|
||||||
for ji2 in 0..3 {
|
for (ji2, &mn2) in FTX_LDPC_MN[i].iter().enumerate().take(3) {
|
||||||
if ji1 != ji2 {
|
if ji1 != ji2 {
|
||||||
let j2 = FTX_LDPC_MN[i][ji2] as usize - 1;
|
let j2 = mn2 as usize - 1;
|
||||||
l += e_matrix[j2 * FTX_LDPC_N + i];
|
l += e_matrix[j2 * FTX_LDPC_N + i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,9 @@ pub fn charn(mut c: i32, table: CharTable) -> char {
|
|||||||
return EXTRAS[c as usize];
|
return EXTRAS[c as usize];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CharTable::AlphanumSpaceSlash => {
|
CharTable::AlphanumSpaceSlash if c == 0 => {
|
||||||
if c == 0 {
|
|
||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +114,9 @@ pub fn nchar(c: char, table: CharTable) -> Option<i32> {
|
|||||||
'?' => return Some(n + 4),
|
'?' => return Some(n + 4),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
CharTable::AlphanumSpaceSlash => {
|
CharTable::AlphanumSpaceSlash if c == '/' => {
|
||||||
if c == '/' {
|
|
||||||
return Some(n);
|
return Some(n);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -632,33 +632,18 @@ impl Candidate {
|
|||||||
}
|
}
|
||||||
let segment = usize::from((block_b & 0x0003) as u8);
|
let segment = usize::from((block_b & 0x0003) as u8);
|
||||||
let di = ((block_b >> 2) & 0x1) != 0;
|
let di = ((block_b >> 2) & 0x1) != 0;
|
||||||
match segment {
|
let di_flag = Some(di);
|
||||||
0 => {
|
let slot = match segment {
|
||||||
if self.state.dynamic_pty != Some(di) {
|
0 => &mut self.state.dynamic_pty,
|
||||||
self.state.dynamic_pty = Some(di);
|
1 => &mut self.state.compressed,
|
||||||
|
2 => &mut self.state.artificial_head,
|
||||||
|
3 => &mut self.state.stereo,
|
||||||
|
_ => unreachable!("segment is masked to two bits"),
|
||||||
|
};
|
||||||
|
if *slot != di_flag {
|
||||||
|
*slot = di_flag;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
if self.state.compressed != Some(di) {
|
|
||||||
self.state.compressed = Some(di);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
if self.state.artificial_head != Some(di) {
|
|
||||||
self.state.artificial_head = Some(di);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
if self.state.stereo != Some(di) {
|
|
||||||
self.state.stereo = Some(di);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let [b0, b1] = block_d.to_be_bytes();
|
let [b0, b1] = block_d.to_be_bytes();
|
||||||
self.ps_bytes[segment * 2] = sanitize_text_byte(b0);
|
self.ps_bytes[segment * 2] = sanitize_text_byte(b0);
|
||||||
self.ps_bytes[segment * 2 + 1] = sanitize_text_byte(b1);
|
self.ps_bytes[segment * 2 + 1] = sanitize_text_byte(b1);
|
||||||
@@ -1458,9 +1443,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BPSK modulate onto the 57 kHz subcarrier.
|
// BPSK modulate onto the 57 kHz subcarrier.
|
||||||
for t in 0..n {
|
for (t, sample) in shaped.iter_mut().enumerate().take(n) {
|
||||||
let phase = TAU * RDS_SUBCARRIER_HZ * t as f32 / sample_rate;
|
let phase = TAU * RDS_SUBCARRIER_HZ * t as f32 / sample_rate;
|
||||||
shaped[t] *= phase.cos();
|
*sample *= phase.cos();
|
||||||
}
|
}
|
||||||
shaped
|
shaped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,9 +134,7 @@ mod tests {
|
|||||||
.flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1))
|
.flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1))
|
||||||
.collect();
|
.collect();
|
||||||
// Append wrong CRC
|
// Append wrong CRC
|
||||||
for _ in 0..16 {
|
bits.resize(bits.len() + 16, 0);
|
||||||
bits.push(0);
|
|
||||||
}
|
|
||||||
assert!(!check_crc16(&bits));
|
assert!(!check_crc16(&bits));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,8 +346,8 @@ mod tests {
|
|||||||
write_bits(&mut bits, 12, 32, 123456); // source_id
|
write_bits(&mut bits, 12, 32, 123456); // source_id
|
||||||
write_bits(&mut bits, 44, 11, 20); // data_count = 20
|
write_bits(&mut bits, 44, 11, 20); // data_count = 20
|
||||||
// Fill some payload
|
// Fill some payload
|
||||||
for i in 55..75 {
|
for (i, bit) in bits.iter_mut().enumerate().take(75).skip(55) {
|
||||||
bits[i] = (i % 2) as u8;
|
*bit = (i % 2) as u8;
|
||||||
}
|
}
|
||||||
append_crc(&mut bits);
|
append_crc(&mut bits);
|
||||||
|
|
||||||
|
|||||||
@@ -374,8 +374,8 @@ mod tests {
|
|||||||
let (y, m, d, h, mi, _) = unix_to_utc(1775055000);
|
let (y, m, d, h, mi, _) = unix_to_utc(1775055000);
|
||||||
assert_eq!(y, 2026);
|
assert_eq!(y, 2026);
|
||||||
// Just verify reasonable values without asserting exact date.
|
// Just verify reasonable values without asserting exact date.
|
||||||
assert!(m >= 1 && m <= 12);
|
assert!((1..=12).contains(&m));
|
||||||
assert!(d >= 1 && d <= 31);
|
assert!((1..=31).contains(&d));
|
||||||
assert!(h < 24);
|
assert!(h < 24);
|
||||||
assert!(mi < 60);
|
assert!(mi < 60);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,10 +161,8 @@ mod tests {
|
|||||||
|
|
||||||
for line_idx in 0..20 {
|
for line_idx in 0..20 {
|
||||||
let mut line = vec![1.0f32; spl];
|
let mut line = vec![1.0f32; spl];
|
||||||
for j in pulse_start..pulse_start + pw {
|
for slot in line.iter_mut().skip(pulse_start).take(pw) {
|
||||||
if j < spl {
|
*slot = 0.0;
|
||||||
line[j] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let result = det.process(&line);
|
let result = det.process(&line);
|
||||||
if let Some(offset) = result {
|
if let Some(offset) = result {
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ mod tests {
|
|||||||
let c4 = idx27(b'T');
|
let c4 = idx27(b'T');
|
||||||
let c5 = idx27(b' ');
|
let c5 = idx27(b' ');
|
||||||
let n1 = ((c0 * 36 + c1) * 10 + c2) * 27u32.pow(3) + c3 * 27u32.pow(2) + c4 * 27 + c5;
|
let n1 = ((c0 * 36 + c1) * 10 + c2) * 27u32.pow(3) + c3 * 27u32.pow(2) + c4 * 27 + c5;
|
||||||
let m1 = (179 - 10 * 5 - 2) * 180 + 10 * 13 + 0; // FN20
|
let m1 = (179 - 10 * 5 - 2) * 180 + 10 * 13; // FN20 (final term is 0)
|
||||||
let power_code = 37u32;
|
let power_code = 37u32;
|
||||||
|
|
||||||
let mut input_bits = [0u8; NBITS];
|
let mut input_bits = [0u8; NBITS];
|
||||||
@@ -530,8 +530,8 @@ mod tests {
|
|||||||
fn interleave_deinterleave_roundtrip() {
|
fn interleave_deinterleave_roundtrip() {
|
||||||
// Create a sequence of distinguishable values
|
// Create a sequence of distinguishable values
|
||||||
let mut original = [0u8; NSYMS];
|
let mut original = [0u8; NSYMS];
|
||||||
for i in 0..NSYMS {
|
for (i, slot) in original.iter_mut().enumerate() {
|
||||||
original[i] = (i % 256) as u8;
|
*slot = (i % 256) as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
let interleaved = interleave(&original);
|
let interleaved = interleave(&original);
|
||||||
|
|||||||
@@ -356,59 +356,6 @@ async fn run_single_rig_audio_client(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{resolve_audio_addr, AudioConnectConfig};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_audio_addr_prefers_fixed_url() {
|
|
||||||
let mut rig_connect = HashMap::new();
|
|
||||||
rig_connect.insert(
|
|
||||||
"home-hf".to_string(),
|
|
||||||
AudioConnectConfig::fixed("audio.example.com:4700".to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let addr = resolve_audio_addr(
|
|
||||||
"home-hf",
|
|
||||||
Some(4531),
|
|
||||||
&rig_connect,
|
|
||||||
&AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531),
|
|
||||||
);
|
|
||||||
assert_eq!(addr, "audio.example.com:4700");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_audio_addr_uses_advertised_port_with_remote_host() {
|
|
||||||
let mut rig_connect = HashMap::new();
|
|
||||||
rig_connect.insert(
|
|
||||||
"home-hf".to_string(),
|
|
||||||
AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531),
|
|
||||||
);
|
|
||||||
|
|
||||||
let addr = resolve_audio_addr(
|
|
||||||
"home-hf",
|
|
||||||
Some(4600),
|
|
||||||
&rig_connect,
|
|
||||||
&AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531),
|
|
||||||
);
|
|
||||||
assert_eq!(addr, "control.example.com:4600");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_audio_addr_falls_back_to_default_port() {
|
|
||||||
let rig_connect = HashMap::new();
|
|
||||||
|
|
||||||
let addr = resolve_audio_addr(
|
|
||||||
"home-hf",
|
|
||||||
None,
|
|
||||||
&rig_connect,
|
|
||||||
&AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531),
|
|
||||||
);
|
|
||||||
assert_eq!(addr, "fallback.example.com:4531");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a single TCP connection for one rig. Similar to `handle_audio_connection`
|
/// Handle a single TCP connection for one rig. Similar to `handle_audio_connection`
|
||||||
/// but publishes to per-rig channels directly and mirrors to global when selected.
|
/// but publishes to per-rig channels directly and mirrors to global when selected.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -767,3 +714,56 @@ async fn handle_single_rig_connection(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{resolve_audio_addr, AudioConnectConfig};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_audio_addr_prefers_fixed_url() {
|
||||||
|
let mut rig_connect = HashMap::new();
|
||||||
|
rig_connect.insert(
|
||||||
|
"home-hf".to_string(),
|
||||||
|
AudioConnectConfig::fixed("audio.example.com:4700".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let addr = resolve_audio_addr(
|
||||||
|
"home-hf",
|
||||||
|
Some(4531),
|
||||||
|
&rig_connect,
|
||||||
|
&AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531),
|
||||||
|
);
|
||||||
|
assert_eq!(addr, "audio.example.com:4700");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_audio_addr_uses_advertised_port_with_remote_host() {
|
||||||
|
let mut rig_connect = HashMap::new();
|
||||||
|
rig_connect.insert(
|
||||||
|
"home-hf".to_string(),
|
||||||
|
AudioConnectConfig::from_host_port("control.example.com".to_string(), 4531),
|
||||||
|
);
|
||||||
|
|
||||||
|
let addr = resolve_audio_addr(
|
||||||
|
"home-hf",
|
||||||
|
Some(4600),
|
||||||
|
&rig_connect,
|
||||||
|
&AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531),
|
||||||
|
);
|
||||||
|
assert_eq!(addr, "control.example.com:4600");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_audio_addr_falls_back_to_default_port() {
|
||||||
|
let rig_connect = HashMap::new();
|
||||||
|
|
||||||
|
let addr = resolve_audio_addr(
|
||||||
|
"home-hf",
|
||||||
|
None,
|
||||||
|
&rig_connect,
|
||||||
|
&AudioConnectConfig::from_host_port("fallback.example.com".to_string(), 4531),
|
||||||
|
);
|
||||||
|
assert_eq!(addr, "fallback.example.com:4531");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1110,8 +1110,8 @@ url = "remote.example.com:4530"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_rejects_duplicate_remote_names() {
|
fn test_validate_rejects_duplicate_remote_names() {
|
||||||
let mut config = ClientConfig::default();
|
let config = ClientConfig {
|
||||||
config.remotes = vec![
|
remotes: vec![
|
||||||
RemoteEntry {
|
RemoteEntry {
|
||||||
name: "dup".to_string(),
|
name: "dup".to_string(),
|
||||||
url: "a:4530".to_string(),
|
url: "a:4530".to_string(),
|
||||||
@@ -1126,20 +1126,24 @@ url = "remote.example.com:4530"
|
|||||||
auth: RemoteAuthConfig::default(),
|
auth: RemoteAuthConfig::default(),
|
||||||
poll_interval_ms: 750,
|
poll_interval_ms: 750,
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(config.validate().unwrap_err().contains("duplicate name"));
|
assert!(config.validate().unwrap_err().contains("duplicate name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_rejects_empty_remote_name() {
|
fn test_validate_rejects_empty_remote_name() {
|
||||||
let mut config = ClientConfig::default();
|
let config = ClientConfig {
|
||||||
config.remotes = vec![RemoteEntry {
|
remotes: vec![RemoteEntry {
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
url: "a:4530".to_string(),
|
url: "a:4530".to_string(),
|
||||||
rig_id: None,
|
rig_id: None,
|
||||||
auth: RemoteAuthConfig::default(),
|
auth: RemoteAuthConfig::default(),
|
||||||
poll_interval_ms: 750,
|
poll_interval_ms: 750,
|
||||||
}];
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(config
|
assert!(config
|
||||||
.validate()
|
.validate()
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
@@ -1148,14 +1152,16 @@ url = "remote.example.com:4530"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_rejects_empty_remote_url() {
|
fn test_validate_rejects_empty_remote_url() {
|
||||||
let mut config = ClientConfig::default();
|
let config = ClientConfig {
|
||||||
config.remotes = vec![RemoteEntry {
|
remotes: vec![RemoteEntry {
|
||||||
name: "hf".to_string(),
|
name: "hf".to_string(),
|
||||||
url: " ".to_string(),
|
url: " ".to_string(),
|
||||||
rig_id: None,
|
rig_id: None,
|
||||||
auth: RemoteAuthConfig::default(),
|
auth: RemoteAuthConfig::default(),
|
||||||
poll_interval_ms: 750,
|
poll_interval_ms: 750,
|
||||||
}];
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(config
|
assert!(config
|
||||||
.validate()
|
.validate()
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
@@ -1164,14 +1170,16 @@ url = "remote.example.com:4530"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_rejects_zero_remote_poll_interval() {
|
fn test_validate_rejects_zero_remote_poll_interval() {
|
||||||
let mut config = ClientConfig::default();
|
let config = ClientConfig {
|
||||||
config.remotes = vec![RemoteEntry {
|
remotes: vec![RemoteEntry {
|
||||||
name: "hf".to_string(),
|
name: "hf".to_string(),
|
||||||
url: "a:4530".to_string(),
|
url: "a:4530".to_string(),
|
||||||
rig_id: None,
|
rig_id: None,
|
||||||
auth: RemoteAuthConfig::default(),
|
auth: RemoteAuthConfig::default(),
|
||||||
poll_interval_ms: 0,
|
poll_interval_ms: 0,
|
||||||
}];
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(config
|
assert!(config
|
||||||
.validate()
|
.validate()
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
|
|||||||
@@ -1678,7 +1678,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn global_target_for_snapshot_skips_other_server_selection() {
|
fn global_target_for_snapshot_skips_other_server_selection() {
|
||||||
let snapshot = sample_snapshot();
|
let snapshot = sample_snapshot();
|
||||||
let rigs = vec![RigEntry {
|
let rigs = [RigEntry {
|
||||||
rig_id: "hf".to_string(),
|
rig_id: "hf".to_string(),
|
||||||
display_name: Some("Gdansk HF".to_string()),
|
display_name: Some("Gdansk HF".to_string()),
|
||||||
state: snapshot,
|
state: snapshot,
|
||||||
|
|||||||
@@ -1596,7 +1596,7 @@ mod tests {
|
|||||||
let t = i as f32 / fs;
|
let t = i as f32 / fs;
|
||||||
adj_phase += adj_mod_index * adj_composite[i];
|
adj_phase += adj_mod_index * adj_composite[i];
|
||||||
let adj = Complex::from_polar(0.5, adj_phase + TAU * adj_freq_offset * t);
|
let adj = Complex::from_polar(0.5, adj_phase + TAU * adj_freq_offset * t);
|
||||||
*s = *s + adj;
|
*s += adj;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut decoder = WfmStereoDecoder::new(
|
let mut decoder = WfmStereoDecoder::new(
|
||||||
@@ -1673,7 +1673,7 @@ mod tests {
|
|||||||
// Mix at 70 % of the main signal's amplitude — strong enough to
|
// Mix at 70 % of the main signal's amplitude — strong enough to
|
||||||
// overcome the FM capture effect and visibly degrade pilot coherence.
|
// overcome the FM capture effect and visibly degrade pilot coherence.
|
||||||
for (s, intf) in iq.iter_mut().zip(intf_iq.iter()) {
|
for (s, intf) in iq.iter_mut().zip(intf_iq.iter()) {
|
||||||
*s = *s + intf * 0.7;
|
*s += intf * 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut decoder = WfmStereoDecoder::new(
|
let mut decoder = WfmStereoDecoder::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user