8 Commits

Author SHA1 Message Date
f4a9078dd2 Radio ptt enable 2025-10-29 11:28:53 +01:00
e7448a2e65 Re-enable 2025-10-21 17:31:29 +02:00
6caa6c7d82 GFSK Weirdness with bandpass at baseband 2025-10-21 17:23:50 +02:00
1b03b13e3c idk 2025-10-20 22:48:55 +02:00
b4650361b4 Set ip and remove low sound 2025-10-20 22:36:06 +02:00
97ba2ea3e2 Some elementary network code 2025-10-20 22:21:31 +02:00
ce9b36fa41 1200 bauds ? 2025-10-20 21:40:26 +02:00
1a13c7b372 Switched to gfsk 2025-10-20 21:25:34 +02:00
9 changed files with 791 additions and 171 deletions

625
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,3 +11,4 @@ hound = "3.5.1"
plotters = "0.3.7"
rand = "0.9.2"
tokio = { version = "1.47.1", features = ["full", "macros", "net", "sync", "time"] }
tun-tap = "0.1.4"

12
run_net.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
echo "running as $1"
cargo b --release
sudo setcap cap_net_admin+eip target/release/rdsp
./target/release/rdsp 0 > /dev/ttyACM0 &
sleep .5
sudo ip a a $1/24 dev radio0
sudo ip -6 addr flush radio0
sudo ip link set radio0 up
sudo ip -6 addr flush radio0

View File

@ -1,3 +0,0 @@
#!/bin/sh
ffmpeg -re -i audio/modulated.wav -f s16le -acodec pcm_s16le udp://127.0.0.1:8080

View File

@ -1,43 +1,44 @@
// Utilities for impulse response design
pub mod design
{
pub mod design {
use crate::complex::Complex32;
use crate::fft::FFT;
use crate::windows::{self, Window};
use crate::complex::Complex32;
// Completely stolen from sdrpp dsp code
pub fn estimate_fir_length(transition_width: f32, sample_rate: f32) -> f32 {
3.8 * sample_rate / transition_width
}
///Designs a impulse response from a desired transfer function using windowing technique
pub fn ir_from_transfer_function(transfer_function: &[Complex32], ir_length: usize, window: Window) -> Vec<Complex32>
{
pub fn ir_from_transfer_function(
transfer_function: &[Complex32],
window: Window,
) -> Vec<Complex32> {
let tf_len = transfer_function.len();
let mut ifft = FFT::new_inv(tf_len);
// Compute ideal convolution kernel/impulse response
ifft.execute(transfer_function);
// Shorten and window
let mut ir = vec![];
for i in 0..ir_length
{
for i in 0..tf_len {
// Get value within ifft result (centering/trimming)
let k = (tf_len - (ir_length / 2) + i) % tf_len;
let k = (tf_len - (tf_len / 2) + i) % tf_len;
// Windowing
ir.push(
ifft.get_output()[k] * window(i as f32 / ir_length as f32) / tf_len as f32
);
ir.push(ifft.get_output()[k] * window(i as f32 / tf_len as f32) / tf_len as f32);
}
ir
}
pub fn frequency_response(impulse_response: &[Complex32]) -> Vec<Complex32>
{
pub fn frequency_response(impulse_response: &[Complex32]) -> Vec<Complex32> {
let len = impulse_response.len();
let mut fft = FFT::new(len, windows::rectangular);
// Recenter impulse response
let mut centered_ir = vec![];
for i in 0..len
{
let mut centered_ir = vec![];
for i in 0..len {
let k = (len / 2) + i;
centered_ir.push(impulse_response[k % len]);
}

View File

@ -1,15 +1,13 @@
use std::f32::consts::PI;
use crate::{complex::Complex32, filtering::fir::FIRFilter, math::map, nco::Nco, windows};
use std::f32::consts::PI;
pub struct IQSampler
{
pub struct IQSampler {
local_oscillator: Nco,
low_pass_i: FIRFilter,
low_pass_q: FIRFilter,
}
impl IQSampler
{
impl IQSampler {
pub fn new(center_freq: f32) -> Self {
// Design a lowpass filter that cuts off at the center freq
// Estimate FIR length :
@ -17,14 +15,17 @@ impl IQSampler
// Ideal transfer function :
let mut transfer_function = vec![Complex32::zero(); fir_length];
let bin_id = map(center_freq, 0., PI, 0., transfer_function.len() as f32 / 2.).floor() as usize;
for i in 0..bin_id
{
let bin_id =
map(center_freq, 0., PI, 0., transfer_function.len() as f32 / 2.).floor() as usize;
for i in 0..bin_id {
transfer_function[i] = Complex32::new(1., 0.);
transfer_function[fir_length - 1 - i] = Complex32::new(1., 0.);
}
let ir = crate::filtering::impulse_response::design::ir_from_transfer_function(&transfer_function, fir_length, windows::blackmann);
let ir = crate::filtering::impulse_response::design::ir_from_transfer_function(
&transfer_function,
windows::blackmann,
);
let mut low_pass_i = FIRFilter::new(&ir);
let mut low_pass_q = FIRFilter::new(&ir);
low_pass_i.normalize_dc();
@ -33,12 +34,11 @@ impl IQSampler
IQSampler {
local_oscillator: Nco::new(center_freq),
low_pass_i,
low_pass_q
low_pass_q,
}
}
pub fn sample(&mut self, input_sample: f32) -> Complex32
{
pub fn sample(&mut self, input_sample: f32) -> Complex32 {
let i_mixed = self.local_oscillator.cexp().re * input_sample;
let q_mixed = self.local_oscillator.cexp().im * input_sample;
self.local_oscillator.step();
@ -46,8 +46,7 @@ impl IQSampler
// TODO: Could use one filter for both I and Q
Complex32::new(
self.low_pass_i.next(Complex32::new(i_mixed, 0.)).re,
self.low_pass_q.next(Complex32::new(q_mixed, 0.)).re
self.low_pass_q.next(Complex32::new(q_mixed, 0.)).re,
) * 2.
}
}

View File

@ -20,6 +20,7 @@ use std::{
cell::{Cell, RefCell},
collections::VecDeque,
env::{self, args},
f32::consts::PI,
fmt::Display,
fs::File,
io::{BufWriter, Read, Sink, Write, stdout},
@ -36,12 +37,21 @@ use tokio::{
sync::mpsc::{UnboundedSender, error::TryRecvError, unbounded_channel},
time::timeout,
};
use tun_tap::Iface;
use crate::{
bfsk::BFSKMod,
complex::Complex32,
filtering::{dc_block::DCBlocker, fir::FIRFilter},
filtering::{
dc_block::DCBlocker,
fir::FIRFilter,
impulse_response::{
self,
design::{estimate_fir_length, frequency_response},
},
},
iq::IQSampler,
math::map,
nco::Nco,
squelch::Squelch,
ted::elg::ELGate,
@ -51,7 +61,7 @@ use eframe::egui::{self, CentralPanel, Color32, RichText};
use tokio::sync::RwLock;
use tokio::sync::mpsc::{Receiver, Sender, channel};
const BAUD_RATE: u32 = 1000;
const BAUD_RATE: u32 = 1200;
const SAMPLE_RATE: u32 = 48000;
// Modulation parameters
@ -108,71 +118,75 @@ impl SampleSender for WavSampleSender {
}
struct FSKReceiver {
pos_correllator: FIRFilter,
neg_correllator: FIRFilter,
eye_sender: Sender<Vec<f32>>,
matched_lowpass: FIRFilter,
phase_lowpass: FIRFilter,
baseband_filter: FIRFilter,
elg: ELGate,
dc_block: DCBlocker,
last_byte: u8,
frame_constructor: FrameConstructor,
bit_count: Option<u32>,
last_sample: Complex32,
}
impl FSKReceiver {
fn new(eye_sender: Sender<Vec<f32>>) -> Self {
let samples_per_symbol = (SAMPLE_RATE as f32) / (BAUD_RATE as f32);
let correllator_length = samples_per_symbol as usize;
let mut pos_nco = Nco::new(hz_to_rad_per_sample(DEVIATION, SAMPLE_RATE as f32));
let mut neg_nco = Nco::new(hz_to_rad_per_sample(-DEVIATION, SAMPLE_RATE as f32));
let pos_ir = (0..correllator_length).map(|i| {
pos_nco.step();
pos_nco.cexp() * windows::blackmann(i as f32 / correllator_length as f32)
});
let neg_ir = (0..correllator_length).map(|i| {
neg_nco.step();
neg_nco.cexp() * windows::blackmann(i as f32 / correllator_length as f32)
});
let mut pos_correllator = FIRFilter::new(&pos_ir.collect::<Vec<_>>());
let mut neg_correllator = FIRFilter::new(&neg_ir.collect::<Vec<_>>());
pos_correllator.normalize_freq(hz_to_rad_per_sample(DEVIATION, SAMPLE_RATE as f32));
neg_correllator.normalize_freq(hz_to_rad_per_sample(-DEVIATION, SAMPLE_RATE as f32));
let mut matched_lowpass = FIRFilter::new(&vec![
Complex32::new(1., 0.);
samples_per_symbol as usize / 2
]);
matched_lowpass.normalize_freq(hz_to_rad_per_sample(DEVIATION, SAMPLE_RATE as f32));
let mut phase_lowpass =
FIRFilter::new(&vec![Complex32::new(1., 0.); samples_per_symbol as usize]);
phase_lowpass.normalize_dc();
//let mut dc_block = DCBlocker::new(0.999);
let mut dc_block = DCBlocker::new(1.);
//let mut dc_block = DCBlocker::new(1.);
let loop_i = 0.03;
let loop_p = 0.1;
let mut loop_ir = vec![Complex32::new(loop_i, 0.); samples_per_symbol as usize / 2];
let mut loop_ir = vec![Complex32::new(loop_i, 0.); samples_per_symbol as usize / 4];
loop_ir.push(Complex32::new(loop_p, 0.));
let mut elg = ELGate::new(samples_per_symbol, FIRFilter::new(&loop_ir));
let elg = ELGate::new(samples_per_symbol, FIRFilter::new(&loop_ir));
// Baseband filter
let bbf_length = estimate_fir_length(100., SAMPLE_RATE as f32).floor() as usize;
let mut frequency_response = vec![Complex32::zero(); bbf_length].into_boxed_slice();
let cutoff_bin = map(
hz_to_rad_per_sample(DEVIATION + 300., SAMPLE_RATE as f32),
0.,
2. * PI,
0.,
bbf_length as f32,
)
.floor() as usize;
// Design transfer function
for i in 0..cutoff_bin {
frequency_response[i] = Complex32::new(1., 0.);
frequency_response[bbf_length - 1 - i] = Complex32::new(1., 0.);
}
Self {
//iq_sampler: IQSampler::new(hz_to_rad_per_sample(CENTER_FREQ, SAMPLE_RATE as f32)),
pos_correllator,
neg_correllator,
matched_lowpass,
dc_block,
phase_lowpass,
baseband_filter: FIRFilter::new(&impulse_response::design::ir_from_transfer_function(
&frequency_response,
windows::blackmann,
)),
elg,
last_byte: 0x00u8,
frame_constructor: FrameConstructor::new(),
bit_count: None,
eye_sender,
last_sample: Complex32::new(1., 0.),
}
}
async fn receive(&mut self, iq: Complex32) -> Result<Option<Frame>, FrameConstructionError> {
// Frame reconstruction
let matched =
self.matched_lowpass.next_real(self.dc_block.next_real(
self.pos_correllator.next(iq).mag() - self.neg_correllator.next(iq).mag(),
));
if let Some((bit_sample, eye)) = self.elg.next_eye(matched) {
let filtered_bb = self.baseband_filter.next(iq);
// let dphi = self
// .phase_lowpass
// .next_real((self.last_sample * filtered_bb.conj()).arg());
let dphi = (self.last_sample * filtered_bb.conj()).arg();
self.last_sample = filtered_bb;
if let Some((bit_sample, eye)) = self.elg.next_eye(dphi) {
let _ = self.eye_sender.send(eye).await;
self.last_byte >>= 1;
self.last_byte |= ((bit_sample > 0.) as u8) << 7;
@ -193,7 +207,7 @@ impl FSKReceiver {
let frame_opt = self.frame_constructor.add_byte(self.last_byte);
self.bit_count = Some(0);
//print!("{}", last_byte as char);
print!(".{:x}.", self.last_byte);
eprint!(".{:x}.", self.last_byte);
let _ = std::io::stdout().flush();
return frame_opt;
}
@ -252,7 +266,7 @@ impl Transceiver {
let receiving = Arc::new(RwLock::new(false));
tokio::spawn(async move {
let mut squelch = Squelch::new(200, 0.1);
let mut squelch = Squelch::new(200, 0.5);
let mut iq_sampler =
IQSampler::new(hz_to_rad_per_sample(CENTER_FREQ, SAMPLE_RATE as f32));
@ -274,8 +288,14 @@ impl Transceiver {
{
match recv.as_mut().unwrap().receive(iq).await
{
Ok(Some(Frame::Data(_))) => {println!("GOT DATA"); send_ack = true; recv = None; state_tx.try_send(TransceiverState::EOT);},
Ok(Some(Frame::Ack)) => {current_message = None; recv = None; state_tx.try_send(TransceiverState::EOT);},
Ok(Some(Frame::Data(dat))) => {
eprintln!("GOT DATA");
let _ = rx_stream_sender.try_send(dat);
send_ack = false;
recv = None;
state_tx.try_send(TransceiverState::EOT);
},
//Ok(Some(Frame::Ack)) => {current_message = None; recv = None; state_tx.try_send(TransceiverState::EOT);},
Err(()) => {recv = None;},
_ => {}
}
@ -295,15 +315,15 @@ impl Transceiver {
current_message = Some((tx_stream_receiver).recv().await.unwrap());
}
state_tx.try_send(TransceiverState::Listening);
tokio::time::sleep(Duration::from_millis(500 * rand::random_range(1..6))).await;
tokio::time::sleep(Duration::from_millis(50 * rand::random_range(1..10))).await;
current_message.as_ref().unwrap()
} =>
{
state_tx.try_send(TransceiverState::Sending);
println!("Sending message");
eprintln!("Sending message");
Self::transmit(Frame::Data(message.clone()), &mut sample_sender).await;
//current_message = None;
println!("Sent message");
current_message = None;
eprintln!("Sent message");
state_tx.try_send(TransceiverState::Waiting);
}
};
@ -321,19 +341,47 @@ impl Transceiver {
pub async fn transmit(frame: Frame, samples_sender: &mut Sender<Vec<f32>>) {
let bytes = frame.bytes();
let mut bit_stream = bytes.iter().flat_map(|x| byte_to_bits(*x));
let modulator = BFSKMod::new(
(SAMPLE_RATE as f32 / BAUD_RATE as f32).round() as u32,
hz_to_rad_per_sample(DEVIATION, SAMPLE_RATE as f32),
&mut bit_stream,
);
let data = bytes
.iter()
.flat_map(|x| byte_to_bits(*x))
.collect::<Vec<_>>();
let up_lo = Nco::new(hz_to_rad_per_sample(CENTER_FREQ, SAMPLE_RATE as f32));
let mut samples = vec![];
for (m, up) in modulator.zip(up_lo) {
let sample = m * up;
samples.push(sample.re);
let sample_per_symbols = SAMPLE_RATE / BAUD_RATE;
let bitstream = (0..(bytes.len() * 8 * sample_per_symbols as usize)).map(|i| {
if data[i / sample_per_symbols as usize] {
1.
} else {
-1.
}
});
// Synthesise impulse response
let mut impulse_response =
vec![Complex32::zero(); sample_per_symbols as usize].into_boxed_slice();
for (i, x) in impulse_response.iter_mut().enumerate() {
*x = Complex32::new(
windows::gaussian(0.3, i as f32 / sample_per_symbols as f32),
0.,
);
}
let mut gaussian_filter = FIRFilter::new(&impulse_response);
gaussian_filter.normalize_dc();
let filtered_bitstream = bitstream.map(|x| gaussian_filter.next_real(x));
let mut nco = Nco::new(0.);
let mut lo = Nco::new(hz_to_rad_per_sample(CENTER_FREQ, SAMPLE_RATE as f32));
// Generate passband
let samples = filtered_bitstream
.map(|f| {
nco.set_frequency(hz_to_rad_per_sample(f * DEVIATION, SAMPLE_RATE as f32));
nco.step_n(1);
lo.step_n(1);
(nco.cexp() * lo.cexp()).re
})
.collect::<Vec<_>>();
let len = samples.len();
samples_sender.send(samples).await.unwrap();
tokio::time::sleep(Duration::from_secs_f32(len as f32 / SAMPLE_RATE as f32)).await;
@ -365,7 +413,7 @@ impl FrameConstructor {
pub fn add_byte(&mut self, byte: u8) -> Result<Option<Frame>, FrameConstructionError> {
if self.frame.is_empty() && byte != 0xC4 && byte != 0x4C && !self.started {
println!("Wrong type {:x}", byte);
eprintln!("Wrong type {:x}", byte);
self.started = true;
return Err(());
}
@ -400,7 +448,7 @@ impl FrameConstructor {
)));
}
println!("Checksum failed");
eprintln!("Checksum failed");
return Err(());
}
@ -453,6 +501,10 @@ impl Frame {
#[tokio::main]
async fn main() {
// Read instance
eprintln!(
"fir length: {}",
impulse_response::design::estimate_fir_length(1000., 48000.)
);
let id = std::env::args().collect::<Vec<_>>()[1]
.parse::<u32>()
.expect("NO INPUT ID");
@ -489,9 +541,13 @@ struct EguiApp {
eyes: VecDeque<Vec<f32>>,
current_state: TransceiverState,
iface: Iface,
}
impl EguiApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let iface = Iface::new("radio%d", tun_tap::Mode::Tap).unwrap();
iface.set_non_blocking().unwrap();
let (up_sender, mut up_receiver) = channel::<Vec<f32>>(16);
let (down_sender, down_receiver) = channel::<f32>(1024);
@ -499,7 +555,7 @@ impl EguiApp {
let instance_id = unsafe { INSTANCE_ID };
tokio::task::spawn(async move {
println!("Waiting for connection ...");
eprintln!("Waiting for connection ...");
// let socket = Arc::new(
// UdpSocket::bind(format!("0.0.0.0:{}", 9000 + instance_id))
@ -554,6 +610,8 @@ impl EguiApp {
let progression = Arc::new(AtomicU64::new(0));
let (finished_tx, mut finished_rx) = channel::<()>(16);
print!("o");
stdout().flush().unwrap();
let send_stream = device
.build_output_stream(
&config,
@ -568,8 +626,7 @@ impl EguiApp {
*d = stream[progression
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
as usize]
* 0.1; // TODO
as usize];
}
},
move |err| {
@ -580,6 +637,8 @@ impl EguiApp {
.unwrap();
send_stream.play().unwrap();
let _ = finished_rx.recv().await;
print!("c");
stdout().flush().unwrap();
}
});
@ -588,6 +647,7 @@ impl EguiApp {
eyes: VecDeque::new(),
current_state: TransceiverState::Waiting,
iface,
}
}
}
@ -597,6 +657,20 @@ impl eframe::App for EguiApp {
egui::CentralPanel::default().show(ctx, |ui| {
let max_eyes = 100;
// INTERFACE
let mut frame = [0u8; 2000];
while let Ok(length) = self.iface.recv(&mut frame) {
//break;
let _ = self
.transceiver
.get_sender()
.try_send(Vec::from(&frame[0..length]));
}
while let Ok(frame) = self.transceiver.try_recv() {
let _ = self.iface.send(frame.as_slice());
}
while let Ok(eye) = self.transceiver.try_recv_eye() {
self.eyes.push_back(eye);
}
@ -608,17 +682,17 @@ impl eframe::App for EguiApp {
}
ui.horizontal(|ui| {
if ui.button("Start").clicked() {
let snd = self.transceiver.get_sender();
let data = (0..rand::random_range(50..250))
.map(|_| rand::random::<char>() as u8)
.collect::<Vec<_>>();
tokio::spawn(async move {
let _ = snd.send(data).await;
});
}
// if ui.button("Start").clicked() {
// let snd = self.transceiver.get_sender();
// let data = (0..rand::random_range(50..250))
// .map(|_| rand::random::<char>() as u8)
// .collect::<Vec<_>>();
//
// tokio::spawn(async move {
// let _ = snd.send(data).await;
// });
// }
//
ui.label(
RichText::new(format!("{:?}", self.current_state))
.size(35.)

View File

@ -1,10 +1,11 @@
use std::collections::VecDeque;
use crate::filtering::fir::FIRFilter;
use std::collections::VecDeque;
// Crued Early late gate timing error detector
pub struct ELGate {
samples_per_symbol: f32,
buffer: VecDeque<f32>, // Store baseband, matched filtered samples,
eye_buffer: VecDeque<f32>,
loop_filter: FIRFilter,
delta: f32,
@ -18,6 +19,7 @@ impl ELGate {
samples_per_symbol,
loop_filter,
buffer: VecDeque::with_capacity(2 * samples_per_symbol.ceil() as usize),
eye_buffer: VecDeque::with_capacity(2 * samples_per_symbol.ceil() as usize),
delta: 0.5,
next_sample: samples_per_symbol,
current_position: 0.,
@ -30,8 +32,26 @@ impl ELGate {
pub fn next_eye(&mut self, sample: f32) -> Option<(f32, Vec<f32>)> {
self.buffer.push_front(sample);
self.eye_buffer.push_front(sample);
self.current_position += 1.;
if self.current_position >= self.next_sample {
// Eye stuff
let mut eye = Vec::new();
if self.eye_buffer.len() >= (self.samples_per_symbol / 2.) as usize {
let start_index = (self.samples_per_symbol / 2.) as usize;
let end_index = (start_index * 3).min(self.eye_buffer.len());
eye = self
.eye_buffer
.range(start_index..)
.copied()
.collect::<Vec<_>>();
while self.eye_buffer.len() > start_index {
self.eye_buffer.pop_back();
}
}
// Sample center, early late
let early_id = (self.samples_per_symbol / 2. + self.samples_per_symbol * self.delta)
.floor()
@ -63,4 +83,3 @@ impl ELGate {
}
}
}

View File

@ -10,18 +10,20 @@ pub fn bartlett(t: f32) -> f32 {
if t < 0.5 { 2. * t } else { 2. - 2. * t }
}
pub fn hann(t: f32) -> f32
{
pub fn hann(t: f32) -> f32 {
0.5 - 0.5 * (2. * PI * t).cos()
}
pub fn hamming(t: f32) -> f32
{
pub fn hamming(t: f32) -> f32 {
0.54 - 0.46 * (2. * PI * t).cos()
}
pub fn blackmann(t: f32) -> f32
{
pub fn blackmann(t: f32) -> f32 {
let x = 2. * PI * t;
0.45 - 0.5 * x.cos() + 0.08 * (2. * x).cos()
}
pub fn gaussian(sigma: f32, t: f32) -> f32 {
let sq = (t - 0.5) / sigma;
(-sq * sq).exp()
}