use yodel::SampleRate;
use yodel::aprs::{Addressee, AprsPacket, Message, MessageContent};
use yodel::ax25::Address;
use yodel::tnc::{DefaultTncReceiver, MAX_FRAME_BYTES, TncConfig, TncReceiver, TncTransmitter};
pub const MYCALL: &[u8] = b"N0CALL";
pub const MYCALL_SSID: u8 = 0;
pub const REPLY_TEXT: &[u8] = b"QSL - automated yodel station";
pub const TOCALL: &[u8] = b"APRS";
pub const SAMPLE_RATE_HZ: u32 = 48_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponsePlan {
pub to: Vec<u8>,
pub ack_id: Option<Vec<u8>>,
pub reply_text: Vec<u8>,
}
#[must_use]
pub fn decide(mycall: &[u8], src: &Address, info: &[u8]) -> Option<ResponsePlan> {
let AprsPacket::Message(msg) = AprsPacket::parse(info).ok()? else {
return None;
};
if msg.addressee.as_bytes() != mycall {
return None;
}
let (ack_id, _text) = match msg.content {
MessageContent::Text { text, id } => (id.map(<[u8]>::to_vec), text),
MessageContent::Ack { .. } | MessageContent::Reject { .. } => return None,
};
Some(ResponsePlan {
to: addr_text(src),
ack_id,
reply_text: REPLY_TEXT.to_vec(),
})
}
pub fn build_responses(plan: &ResponsePlan) -> Result<Vec<Vec<u8>>, yodel::aprs::AprsError> {
let addressee = Addressee::new(&plan.to)?;
let mut out = Vec::new();
let mut buf = [0u8; 128];
if let Some(id) = &plan.ack_id {
let ack = Message {
addressee,
content: MessageContent::Ack { id },
};
let len = ack.build(&mut buf)?;
out.push(buf[..len].to_vec());
}
let reply = Message {
addressee,
content: MessageContent::Text {
text: &plan.reply_text,
id: None, },
};
let len = reply.build(&mut buf)?;
out.push(buf[..len].to_vec());
Ok(out)
}
fn addr_text(addr: &Address) -> Vec<u8> {
let mut out = addr.callsign.as_bytes().to_vec();
let ssid = addr.ssid.value();
if ssid != 0 {
out.push(b'-');
out.extend_from_slice(ssid.to_string().as_bytes());
}
out
}
const INPUT_HELP: &str = "\
input: a 16-bit mono integer PCM WAV, 8000-48000 Hz, containing an APRS
message addressed to MYCALL (see the constant in this file).
make one that actually triggers a reply:
cargo run --features cli -- encode --out msg.wav \\
--from N0CALL-7 --to APRS \\
message --to-call MYCALL --text 'ping' --id 42
or just something to decode (no reply expected):
cargo run --example encode_wav --features tnc,wav
cargo run --features cli -- gen --out test.wav --count 10 --snr 10";
fn main() {
if let Err(e) = run() {
eprintln!("{e}");
std::process::exit(2);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args()
.nth(1)
.ok_or_else(|| format!("usage: trigger_reply <input.wav>\n\n{INPUT_HELP}"))?;
let mut reader = hound::WavReader::open(&path).map_err(|e| match e {
hound::Error::IoError(io) if io.kind() == std::io::ErrorKind::NotFound => {
format!("cannot open {path}: no such file\n\n{INPUT_HELP}")
}
hound::Error::FormatError(_) => format!("{path} is not a WAV file ({e})\n\n{INPUT_HELP}"),
other => format!("cannot open {path}: {other}"),
})?;
let spec = reader.spec();
if spec.channels != 1
|| spec.bits_per_sample != 16
|| spec.sample_format != hound::SampleFormat::Int
{
return Err(format!(
"{path} is {}-channel {}-bit at {} Hz; need 1-channel 16-bit integer\n\n{INPUT_HELP}",
spec.channels, spec.bits_per_sample, spec.sample_rate
)
.into());
}
let rate = SampleRate::new(spec.sample_rate)?;
let mut rx: DefaultTncReceiver = TncReceiver::new(TncConfig::bell_202(rate)?)?;
let mut plans = Vec::new();
for sample in reader.samples::<i16>() {
if let Some(frame) = rx.push_i16(sample?)
&& let Some(plan) = decide(MYCALL, &frame.src(), frame.info())
{
println!(
"triggered by {}: ack_id {:?}",
String::from_utf8_lossy(&plan.to),
plan.ack_id.as_deref().map(String::from_utf8_lossy)
);
plans.push(plan);
}
}
if plans.is_empty() {
println!(
"no messages addressed to {} heard",
String::from_utf8_lossy(MYCALL)
);
return Ok(());
}
let out_rate = SampleRate::new(SAMPLE_RATE_HZ)?;
let tx = TncTransmitter::new(TncConfig::bell_202(out_rate)?);
let spec = hound::WavSpec {
channels: 1,
sample_rate: SAMPLE_RATE_HZ,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer = hound::WavWriter::create("reply.wav", spec)?;
let src = Address::new(MYCALL, MYCALL_SSID)?;
let dest = Address::new(TOCALL, 0)?;
let mut frames = 0usize;
for plan in &plans {
for info in build_responses(plan)? {
let mut frame_buf = [0u8; MAX_FRAME_BYTES];
let len = tx.build_frame_raw(dest, src, &[], &info, &mut frame_buf)?;
for sample in tx.frame_samples_i16(&frame_buf[..len]) {
writer.write_sample(sample)?;
}
frames += 1;
}
}
writer.finalize()?;
println!("wrote reply.wav: {frames} response frame(s) at {SAMPLE_RATE_HZ} Hz");
Ok(())
}