├── .gitignore ├── Cargo.toml ├── README.md ├── flake.lock ├── flake.nix └── src ├── bin ├── ampkt.rs ├── recorder.rs ├── rx.rs ├── test_frame.rs └── tx.rs ├── carrier_sync.rs ├── clock_sync.rs ├── frame.rs ├── lib.rs ├── qam.rs ├── sym.rs ├── sym_sync.rs └── tap.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ampkt" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.68" 10 | async-io = "1.12.0" 11 | clap = { version = "4.1.6", features = ["derive"] } 12 | futuresdr = { version = "0.0.27", features = ["soapy"] } 13 | soapysdr = "0.3.2" 14 | tun-tap = "0.1.3" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMPKT - Amateur radio packet explorer 2 | 3 | This is a project to facilitate experimentation with connecting two Linux 4 | machine's network stacks via the amateur radio bands. 5 | 6 | ## Required Hardware 7 | 8 | You will need two full-duplex SDRs, one for each machine. They will also need to 9 | be compatible with SoapySDR. 10 | 11 | Hardware that is known to work: 12 | - USRP N210 13 | - BladeRF 14 | - LimeSDR 15 | 16 | 17 | ## Getting Started 18 | 19 | 1. Build the project: 20 | 21 | ```console 22 | cargo build --release 23 | ``` 24 | 25 | 2. Connect your SDR hardware and start the `ampkt` binary (note that you will 26 | probably need to run with `sudo` so that the `tap` interface can be 27 | created): 28 | 29 | ```console 30 | sudo ./target/release/ampkt "driver=bladerf" 433000000 435000000 31 | ``` 32 | 33 | This will start ampkt using bladerf hardware, Txing on 433MHz and Rxing on 34 | 435MHz. 35 | 36 | 3. On a second machine do the same, ensuring that you swap the frequencies. 37 | 38 | 4. On both machines, bring up the tap interface and assign an IP address: 39 | 40 | ```console 41 | sudo ifconfig tap0 up 42 | sudo ip add 10.0.0.1/24 dev tap0 43 | ``` 44 | 45 | Making sure you use a different IP address on the other machine. 46 | 47 | 5. At this point you should have working comms. You can check running `ping`and 48 | seeing whether you get a reply from the other machine 49 | 50 | ```console 51 | ping 10.0.0.2 52 | ``` 53 | 54 | 55 | ## Technical Information 56 | 57 | A Linux [TAP](https://www.kernel.org/doc/html/v5.8/networking/tuntap.html) 58 | interface is created. This interface is used as the sink for the RX path and the 59 | source for the TX path. 60 | 61 | ### Tx Path 62 | 63 | Each incoming packet that is read from the tap interface is converted into a 64 | frame. The frame consists of a sync header (repeated twice) a frame size (as 65 | u16) followed by the packet data. We then convert the frame to a stream of 66 | symbols. We use QPSK modulation which means we have two bits per symbol. Each 67 | 2-bit nibble is converted into a symbol using the following map: 68 | 69 | ``` 70 | A => 0b00 71 | B => 0b01 72 | C => 0b10 73 | D => 0b11 74 | ``` 75 | 76 | We then module the symbol stream into QPSK with the following constellation: 77 | 78 | ``` 79 | Im 80 | | 81 | B | A 82 | | 83 | | 84 | -------------> re 85 | | 86 | | 87 | D | C 88 | | 89 | ``` 90 | 91 | The output of the QPSK modulator is then sent to the SDR for Tx. 92 | 93 | ### Rx Path 94 | 95 | Samples from the SDR are first sent into the clock sync block. This block 96 | decimates the incoming stream by selection of particular samples from the input 97 | stream. The number of samples that are 'skipped' during selection is shifted by 98 | an error function which attempts to pick the sample at the peak of a symbol. 99 | 100 | The decimated stream is then sent into the carrier sync block. This attempts to 101 | compensate for any difference in clocks between the SDRs by 'de-reotating' the 102 | constellation. When this block has 'locked' the output should be stable samples 103 | in each quadrent of the constellation plot. 104 | 105 | Next, the samples passed through the QPSK demodulator which converts the samples 106 | into a stream of symbols. 107 | 108 | The symbol stream is then passed through a frame decoder. This block attempts to 109 | create the original packet of data from a stream of symbols. We use a SYNC 110 | header of 16-bytes (repeated twice) to resolve the phase ambiguity. Then the 111 | computed difference in phase is applied to all incoming symbols to decode the 112 | frame. 113 | 114 | Finally the frames are written to the TAP interface for injection into the Linux 115 | kernel network stack. 116 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1671096816, 9 | "narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=", 10 | "owner": "nix-community", 11 | "repo": "naersk", 12 | "rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "nix-community", 17 | "ref": "master", 18 | "repo": "naersk", 19 | "type": "github" 20 | } 21 | }, 22 | "nixpkgs": { 23 | "locked": { 24 | "lastModified": 1674746945, 25 | "narHash": "sha256-xJ5XrXWPHDArSAJtJVBGRQzHu3ESI13zMNjXbnRaAjY=", 26 | "path": "/nix/store/z8zabgrqz3bjdjdr64lvi61xgj38h78c-source", 27 | "rev": "e39a949aaa9e4fc652b1619b56e59584e1fc305b", 28 | "type": "path" 29 | }, 30 | "original": { 31 | "id": "nixpkgs", 32 | "type": "indirect" 33 | } 34 | }, 35 | "nixpkgs_2": { 36 | "locked": { 37 | "lastModified": 1675153841, 38 | "narHash": "sha256-EWvU3DLq+4dbJiukfhS7r6sWZyJikgXn6kNl7eHljW8=", 39 | "owner": "NixOS", 40 | "repo": "nixpkgs", 41 | "rev": "ea692c2ad1afd6384e171eabef4f0887d2b882d3", 42 | "type": "github" 43 | }, 44 | "original": { 45 | "owner": "NixOS", 46 | "ref": "nixpkgs-unstable", 47 | "repo": "nixpkgs", 48 | "type": "github" 49 | } 50 | }, 51 | "root": { 52 | "inputs": { 53 | "naersk": "naersk", 54 | "nixpkgs": "nixpkgs_2", 55 | "utils": "utils" 56 | } 57 | }, 58 | "utils": { 59 | "locked": { 60 | "lastModified": 1667395993, 61 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 62 | "owner": "numtide", 63 | "repo": "flake-utils", 64 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 65 | "type": "github" 66 | }, 67 | "original": { 68 | "owner": "numtide", 69 | "repo": "flake-utils", 70 | "type": "github" 71 | } 72 | } 73 | }, 74 | "root": "root", 75 | "version": 7 76 | } 77 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | naersk.url = "github:nix-community/naersk/master"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, utils, naersk }: 9 | utils.lib.eachDefaultSystem (system: 10 | let 11 | pkgs = import nixpkgs { inherit system; }; 12 | naersk-lib = pkgs.callPackage naersk { }; 13 | in 14 | { 15 | defaultPackage = naersk-lib.buildPackage ./.; 16 | devShell = with pkgs; mkShell { 17 | nativeBuildInputs = [ rustPlatform.bindgenHook ]; 18 | buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy soapysdr-with-plugins pkg-config libclang alsa-lib libbladeRF ]; 19 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 20 | }; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/bin/ampkt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use clap::Parser; 3 | use futuresdr::{ 4 | blocks::{SoapySinkBuilder, SoapySourceBuilder}, 5 | macros::connect, 6 | runtime::{Flowgraph, Runtime}, 7 | }; 8 | 9 | use ampkt::{ 10 | carrier_sync::CarrierSync, 11 | clock_sync::ClockSync, 12 | frame::{FrameDecoder, FrameEncoder}, 13 | qam::{QamDemod, QamMod}, 14 | tap::Tap, 15 | }; 16 | 17 | #[derive(Parser)] 18 | struct Args { 19 | #[clap(short,long)] 20 | tx_gain: f64, 21 | #[clap(short,long)] 22 | rx_gain: f64, 23 | soapy_device: String, 24 | tx_freq: f64, 25 | rx_freq: f64, 26 | } 27 | 28 | const SAMP_RATE: f64 = 800_000.0; 29 | 30 | fn main() -> Result<()> { 31 | let mut fg = Flowgraph::new(); 32 | 33 | let args = Args::parse(); 34 | 35 | let tap = Tap::new("tap%d", tun_tap::Mode::Tap)?; 36 | 37 | // TX Blocks. 38 | let frame_encoder = FrameEncoder::new(); 39 | 40 | let qam_mod = QamMod::new(10); 41 | 42 | let tx_dev = 43 | soapysdr::Device::new(args.soapy_device.as_str()).context("Could not find SDR device")?; 44 | let tx_soapy_dev = SoapySinkBuilder::new() 45 | .device(tx_dev) 46 | .sample_rate(SAMP_RATE) 47 | .freq(args.tx_freq) 48 | .gain(args.tx_gain) 49 | .build(); 50 | 51 | // RX Blocks. 52 | let rx_dev = 53 | soapysdr::Device::new(args.soapy_device.as_str()).context("Could not find SDR device")?; 54 | 55 | let rx_soapy_dev = SoapySourceBuilder::new() 56 | .device(rx_dev) 57 | .sample_rate(SAMP_RATE) 58 | .freq(args.rx_freq) 59 | .gain(args.rx_gain) 60 | .build(); 61 | 62 | let clock_sync = ClockSync::new(10, 20.0); 63 | 64 | let carrier_sync = CarrierSync::new(); 65 | 66 | let qam_demod = QamDemod::new(); 67 | 68 | let frame_decoder = FrameDecoder::new(); 69 | 70 | connect!(fg, 71 | // TX Path 72 | tap | frame_encoder > qam_mod > tx_soapy_dev; 73 | // RX Path 74 | rx_soapy_dev > clock_sync > carrier_sync > qam_demod > frame_decoder | tap); 75 | 76 | Runtime::new().run(fg)?; 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/bin/recorder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use futuresdr::{ 3 | blocks::{FileSink, SoapySourceBuilder}, 4 | macros::connect, 5 | num_complex::Complex32, 6 | runtime::{Flowgraph, Runtime}, 7 | }; 8 | 9 | fn main() -> Result<()> { 10 | let mut fg = Flowgraph::new(); 11 | 12 | let dev = soapysdr::Device::new("driver=bladerf").context("Could not find SDR device")?; 13 | 14 | let soapy_src = SoapySourceBuilder::new() 15 | .device(dev) 16 | .sample_rate(800_000.0) 17 | .freq(433_000_000.0) 18 | .gain(60.0) 19 | .build(); 20 | 21 | let file_sink = FileSink::::new("out.cf32"); 22 | 23 | connect!(fg, soapy_src > file_sink); 24 | 25 | Runtime::new().run(fg)?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/bin/rx.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::Parser; 5 | use futuresdr::{ 6 | blocks::{FileSink, FileSource, MessagePipe, SoapySourceBuilder}, 7 | futures::{channel::mpsc, executor::block_on, StreamExt}, 8 | macros::connect, 9 | num_complex::Complex32, 10 | runtime::{Flowgraph, Pmt, Runtime}, 11 | }; 12 | 13 | use ampkt::{carrier_sync::CarrierSync, clock_sync::ClockSync, frame::FrameDecoder, qam::QamDemod}; 14 | 15 | #[derive(Parser)] 16 | struct Args { 17 | #[command(subcommand)] 18 | cmd: RxType, 19 | } 20 | 21 | #[derive(clap::Subcommand)] 22 | enum RxType { 23 | File { 24 | path: PathBuf, 25 | }, 26 | SDR { 27 | soapy_device: String, 28 | rx_freq: f64, 29 | #[clap(short, long)] 30 | gain: f64, 31 | }, 32 | } 33 | 34 | fn main() -> Result<()> { 35 | let args = Args::parse(); 36 | 37 | let mut fg = Flowgraph::new(); 38 | 39 | let src = match args.cmd { 40 | RxType::File { path } => FileSource::::new(path.to_string_lossy(), false), 41 | RxType::SDR { 42 | soapy_device, 43 | rx_freq, 44 | gain, 45 | } => { 46 | let dev = soapysdr::Device::new(soapy_device.as_str()) 47 | .context("Could not find SDR device")?; 48 | SoapySourceBuilder::new() 49 | .device(dev) 50 | .sample_rate(800_000.0) 51 | .freq(rx_freq) 52 | .gain(gain) 53 | .build() 54 | } 55 | }; 56 | 57 | let clock_sync = ClockSync::new(10, 20.0); 58 | 59 | let carrier_sync = CarrierSync::new(); 60 | 61 | let qam_demod = QamDemod::new(); 62 | 63 | let frame_decoder = FrameDecoder::new(); 64 | 65 | let (tx, mut rx) = mpsc::channel::(100); 66 | 67 | let message_sink = MessagePipe::new(tx); 68 | 69 | let raw_signal_sink = FileSink::::new("raw.cf32"); 70 | let clock_sync_sink = FileSink::::new("clock_sync.cf32"); 71 | let carrier_sync_sink = FileSink::::new("carrier_sync.cf32"); 72 | 73 | connect!(fg, src > clock_sync > carrier_sync > qam_demod > frame_decoder | message_sink; 74 | src > raw_signal_sink; 75 | carrier_sync > carrier_sync_sink; 76 | clock_sync > clock_sync_sink); 77 | 78 | let rt = Runtime::new(); 79 | let (_fg, _handle) = block_on(rt.start(fg)); 80 | 81 | rt.block_on(async move { 82 | while let Some(x) = rx.next().await { 83 | match x { 84 | Pmt::Blob(frame) => println!("RX'd frame: {frame:X?}"), 85 | Pmt::Null => break, 86 | _ => eprintln!("Unexpected message type from qam demot"), 87 | } 88 | } 89 | }); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/test_frame.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::Parser; 5 | use futuresdr::{ 6 | blocks::{MessageSource, SoapySinkBuilder}, 7 | macros::connect, 8 | runtime::{Flowgraph, Runtime}, 9 | }; 10 | 11 | use ampkt::{frame::FrameEncoder, qam::QamMod}; 12 | 13 | #[derive(Parser)] 14 | struct Args { 15 | soapy_device: String, 16 | tx_freq: f64, 17 | } 18 | 19 | fn main() -> Result<()> { 20 | let args = Args::parse(); 21 | 22 | let mut fg = Flowgraph::new(); 23 | 24 | let test_frame = MessageSource::new( 25 | futuresdr::runtime::Pmt::Blob([0xde, 0xad, 0xbe, 0xef].to_vec()), 26 | Duration::from_secs(0), 27 | None, 28 | ); 29 | 30 | let frame_encoder = FrameEncoder::new(); 31 | 32 | let qam_mod = QamMod::new(10); 33 | 34 | let dev = 35 | soapysdr::Device::new(args.soapy_device.as_str()).context("Could not find SDR device")?; 36 | 37 | let soapy_sink = SoapySinkBuilder::new() 38 | .device(dev) 39 | .sample_rate(800_000.0) 40 | .freq(args.tx_freq) 41 | .build(); 42 | 43 | connect!(fg, test_frame | frame_encoder > qam_mod > soapy_sink); 44 | 45 | Runtime::new().run(fg)?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/bin/tx.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use futuresdr::{ 3 | blocks::SoapySinkBuilder, 4 | macros::connect, 5 | runtime::{Flowgraph, Runtime}, 6 | }; 7 | 8 | use ampkt::{frame::FrameEncoder, qam::QamMod, tap::Tap}; 9 | 10 | fn main() -> Result<()> { 11 | let mut fg = Flowgraph::new(); 12 | 13 | let tap = Tap::new("tap%d", tun_tap::Mode::Tap).unwrap(); 14 | 15 | let frame_encoder = FrameEncoder::new(); 16 | 17 | let qam_mod = QamMod::new(10); 18 | 19 | let dev = soapysdr::Device::new("driver=uhd,addr=192.168.50.2") 20 | .context("Could not find SDR device")?; 21 | 22 | let soapy_sink = SoapySinkBuilder::new() 23 | .device(dev) 24 | .sample_rate(800_000.0) 25 | .freq(433_000_000.0) 26 | .gain(0.0) 27 | .build(); 28 | 29 | connect!(fg, tap | frame_encoder > qam_mod > soapy_sink); 30 | 31 | Runtime::new().run(fg)?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /src/carrier_sync.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | 3 | use futuresdr::{blocks::Apply, num_complex::Complex32, runtime::Block}; 4 | 5 | pub struct CarrierSync; 6 | 7 | impl CarrierSync { 8 | fn threshold(x: f32) -> f32 { 9 | if x > 0.0 { 10 | 1.0 11 | } else { 12 | -1.0 13 | } 14 | } 15 | 16 | fn calc_error(s: Complex32) -> f32 { 17 | (Self::threshold(s.im) * s.re) - (Self::threshold(s.re) * s.im) 18 | } 19 | 20 | pub fn new() -> Block { 21 | let mut phase_comp: f32 = 0.0; 22 | 23 | Apply::new(move |x: &Complex32| { 24 | let (mag, mut phase) = x.to_polar(); 25 | 26 | phase += phase_comp; 27 | 28 | let ret = Complex32::from_polar(mag, phase); 29 | phase_comp += Self::calc_error(ret); 30 | 31 | while phase_comp > (2.0 * PI) { 32 | phase_comp -= 2.0 * PI; 33 | } 34 | 35 | while phase_comp < (-2.0 * PI) { 36 | phase_comp += 2.0 * PI; 37 | } 38 | 39 | ret 40 | }) 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use anyhow::Result; 47 | use futuresdr::num_complex::Complex32; 48 | 49 | use super::CarrierSync; 50 | 51 | #[test] 52 | fn error_fn() -> Result<()> { 53 | assert_eq!(CarrierSync::calc_error(Complex32::new(1.0, 1.0)), 0.0); 54 | assert_eq!(CarrierSync::calc_error(Complex32::new(-1.0, 1.0)), 0.0); 55 | assert_eq!(CarrierSync::calc_error(Complex32::new(1.0, -1.0)), 0.0); 56 | assert_eq!(CarrierSync::calc_error(Complex32::new(-1.0, -1.0)), 0.0); 57 | 58 | assert!(CarrierSync::calc_error(Complex32::new(1.0, 1.1)) < 0.0); 59 | assert!(CarrierSync::calc_error(Complex32::new(1.0, 0.9)) > 0.0); 60 | 61 | assert_eq!( 62 | CarrierSync::calc_error(Complex32::new(1.0, 1.1)).abs(), 63 | CarrierSync::calc_error(Complex32::new(1.0, 0.9)).abs() 64 | ); 65 | Ok(()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/clock_sync.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futuresdr::{ 3 | async_trait::async_trait, 4 | num_complex::Complex32, 5 | runtime::{ 6 | Block, BlockMeta, BlockMetaBuilder, Kernel, MessageIo, MessageIoBuilder, StreamIo, 7 | StreamIoBuilder, WorkIo, 8 | }, 9 | }; 10 | 11 | pub struct ClockSync { 12 | skip: i32, 13 | n: i32, 14 | err_gain: f32, 15 | window: Vec, 16 | } 17 | 18 | impl ClockSync { 19 | pub fn new(sps: u32, err_gain: f32) -> Block { 20 | assert!(sps > 3); 21 | 22 | Block::new( 23 | BlockMetaBuilder::new("ClockSync").build(), 24 | StreamIoBuilder::new() 25 | .add_input("in", std::mem::size_of::()) 26 | .add_output("out", std::mem::size_of::()) 27 | .build(), 28 | MessageIoBuilder::new().build(), 29 | ClockSync { 30 | skip: sps as i32, 31 | n: sps as i32, 32 | err_gain, 33 | window: Vec::with_capacity(3), 34 | }, 35 | ) 36 | } 37 | 38 | fn calc_error(&mut self) -> f32 { 39 | let max_delta = self.skip as f32 / 2.0; 40 | 41 | let x0 = self.window[0].re; 42 | let x1 = self.window[1].re; 43 | let x2 = self.window[2].re; 44 | 45 | self.window.clear(); 46 | 47 | let mut err = ((x2 - x0) * x1) * self.err_gain; 48 | 49 | if err < -max_delta { 50 | err = -max_delta 51 | } else if err > max_delta { 52 | err = max_delta; 53 | } 54 | 55 | err 56 | } 57 | 58 | fn push_samp(&mut self, s: Complex32) -> Option { 59 | self.n -= 1; 60 | 61 | match self.n { 62 | 2 => { 63 | self.window.push(s); 64 | None 65 | } 66 | 1 => { 67 | self.window.push(s); 68 | Some(s) 69 | } 70 | 0 => { 71 | self.window.push(s); 72 | 73 | self.n = self.skip + self.calc_error().round() as i32; 74 | 75 | None 76 | } 77 | _ => None, 78 | } 79 | } 80 | } 81 | 82 | #[async_trait] 83 | impl Kernel for ClockSync { 84 | async fn work( 85 | &mut self, 86 | io: &mut WorkIo, 87 | sio: &mut StreamIo, 88 | _mio: &mut MessageIo, 89 | _b: &mut BlockMeta, 90 | ) -> Result<()> { 91 | let input = sio.input(0); 92 | let is = input.slice::(); 93 | let out_output = sio.output(0); 94 | let os = out_output.slice::(); 95 | let mut consumed = 0; 96 | let mut produced = 0; 97 | 98 | if sio.input(0).finished() { 99 | io.finished = true; 100 | } 101 | 102 | let mut in_iter = is.iter(); 103 | 104 | for out_samp in os.iter_mut() { 105 | 'inner: for in_samp in &mut in_iter { 106 | consumed += 1; 107 | if let Some(o) = self.push_samp(*in_samp) { 108 | *out_samp = o; 109 | produced += 1; 110 | break 'inner; 111 | } 112 | } 113 | } 114 | 115 | sio.input(0).consume(consumed); 116 | sio.output(0).produce(produced); 117 | 118 | Ok(()) 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use futuresdr::num_complex::Complex32; 125 | 126 | use super::ClockSync; 127 | 128 | #[test] 129 | fn err_convergence() { 130 | let mut cs = ClockSync { 131 | skip: 10, 132 | err_gain: 20.0, 133 | n: 0, 134 | window: Vec::with_capacity(3), 135 | }; 136 | 137 | // samples are heading towards the symbol above 0. Err should be +ve so 138 | // we skip more samples. 139 | cs.window.push(Complex32::new(0.1, 0.0)); 140 | cs.window.push(Complex32::new(0.2, 0.0)); 141 | cs.window.push(Complex32::new(0.3, 0.0)); 142 | 143 | assert!(cs.calc_error() > 0.0); 144 | 145 | // samples are heading away the symbol above 0. Err should be -ve so we 146 | // skip less samples. 147 | cs.window.push(Complex32::new(0.3, 0.0)); 148 | cs.window.push(Complex32::new(0.2, 0.0)); 149 | cs.window.push(Complex32::new(0.1, 0.0)); 150 | 151 | assert!(cs.calc_error() < 0.0); 152 | 153 | // samples are heading towards the symbol below 0. Err should be +ve so 154 | // we skip more samples. 155 | cs.window.push(Complex32::new(-0.1, 0.0)); 156 | cs.window.push(Complex32::new(-0.2, 0.0)); 157 | cs.window.push(Complex32::new(-0.3, 0.0)); 158 | 159 | assert!(cs.calc_error() > 0.0); 160 | 161 | // samples are heading towards the symbol below 0. Err should be -ve so 162 | // we skip less samples. 163 | cs.window.push(Complex32::new(-0.5, 0.0)); 164 | cs.window.push(Complex32::new(-0.3, 0.0)); 165 | cs.window.push(Complex32::new(-0.2, 0.0)); 166 | 167 | assert!(cs.calc_error() < 0.0); 168 | 169 | // samples are at the symbol peak above 0. Err should be 0 so we don't 170 | // induce any more or less skip. 171 | cs.window.push(Complex32::new(0.3, 0.0)); 172 | cs.window.push(Complex32::new(0.4, 0.0)); 173 | cs.window.push(Complex32::new(0.3, 0.0)); 174 | 175 | assert_eq!(cs.calc_error(), 0.0); 176 | 177 | // samples are at the symbol peak below 0. Err should be 0 so we don't induce 178 | // any more or less skip. 179 | cs.window.push(Complex32::new(-0.3, 0.0)); 180 | cs.window.push(Complex32::new(-0.4, 0.0)); 181 | cs.window.push(Complex32::new(-0.3, 0.0)); 182 | 183 | assert_eq!(cs.calc_error(), 0.0); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/frame.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use anyhow::Result; 4 | use futuresdr::{ 5 | async_trait::async_trait, 6 | macros::message_handler, 7 | runtime::{ 8 | Block, BlockMeta, BlockMetaBuilder, Kernel, MessageIo, MessageIoBuilder, Pmt, StreamIo, 9 | StreamIoBuilder, WorkIo, 10 | }, 11 | }; 12 | 13 | use crate::sym::{Sym, Symbol}; 14 | use crate::sym_sync::{SymSync, SYNC}; 15 | 16 | pub struct FrameEncoder { 17 | sym_queue: VecDeque, 18 | } 19 | 20 | impl FrameEncoder { 21 | pub fn new() -> Block { 22 | Block::new( 23 | BlockMetaBuilder::new("FrameEncoder").build(), 24 | StreamIoBuilder::new() 25 | .add_output("out", std::mem::size_of::()) 26 | .build(), 27 | MessageIoBuilder::new() 28 | .add_input("in", Self::pkt_handler) 29 | .build(), 30 | Self::create(), 31 | ) 32 | } 33 | 34 | fn create() -> Self { 35 | Self { 36 | sym_queue: VecDeque::new(), 37 | } 38 | } 39 | 40 | fn push_sync(&mut self) { 41 | self.sym_queue.extend(SYNC.iter().map(|x| Some(*x))); 42 | } 43 | 44 | fn push_sz(&mut self, len: u16) { 45 | self.push_byte((len >> 8) as u8); 46 | self.push_byte((len & 0xff) as u8); 47 | } 48 | 49 | fn push_byte(&mut self, byte: u8) { 50 | Sym::syms_from_byte(byte) 51 | .into_iter() 52 | .for_each(|s| self.sym_queue.push_back(Some(s))); 53 | } 54 | 55 | fn push_bytes(&mut self, bytes: &Vec) { 56 | bytes 57 | .iter() 58 | .flat_map(|byte| Sym::syms_from_byte(*byte)) 59 | .for_each(|sym| self.sym_queue.push_back(Some(sym))) 60 | } 61 | 62 | fn push_frame(&mut self, bytes: &Vec) { 63 | self.push_sync(); 64 | self.push_sync(); 65 | self.push_sz(bytes.len() as u16); 66 | self.push_bytes(bytes); 67 | } 68 | 69 | #[message_handler] 70 | async fn pkt_handler( 71 | &mut self, 72 | _mio: &mut MessageIo, 73 | _meta: &mut BlockMeta, 74 | p: Pmt, 75 | ) -> Result { 76 | if let Pmt::Blob(ref data) = p { 77 | self.push_frame(data); 78 | } 79 | 80 | Ok(Pmt::Null) 81 | } 82 | } 83 | 84 | #[async_trait] 85 | impl Kernel for FrameEncoder { 86 | async fn work( 87 | &mut self, 88 | _io: &mut WorkIo, 89 | sio: &mut StreamIo, 90 | _mio: &mut MessageIo, 91 | _b: &mut BlockMeta, 92 | ) -> Result<()> { 93 | let output = sio.output(0); 94 | let o: &mut [Symbol] = output.slice(); 95 | 96 | if self.sym_queue.is_empty() { 97 | o.iter_mut().for_each(|x| *x = None); 98 | output.produce(o.len()); 99 | return Ok(()); 100 | } 101 | 102 | let mut iter = o.iter_mut().enumerate(); 103 | 104 | for (_, o) in &mut iter { 105 | if let Some(sym) = self.sym_queue.pop_front() { 106 | *o = sym; 107 | } else { 108 | break; 109 | } 110 | } 111 | 112 | output.produce(if let Some((i, _)) = iter.next() { 113 | i - 1 114 | } else { 115 | o.len() 116 | }); 117 | 118 | Ok(()) 119 | } 120 | } 121 | 122 | struct ByteDecoder { 123 | cur_byte: u8, 124 | bits_pushed: u8, 125 | } 126 | 127 | impl ByteDecoder { 128 | fn new() -> Self { 129 | Self { 130 | cur_byte: 0, 131 | bits_pushed: 0, 132 | } 133 | } 134 | 135 | fn push_sym(&mut self, s: Sym) -> Option { 136 | self.cur_byte |= >::into(s); 137 | self.bits_pushed += 2; 138 | 139 | if self.bits_pushed == 8 { 140 | let ret = self.cur_byte; 141 | self.reset(); 142 | Some(ret) 143 | } else { 144 | self.cur_byte <<= 2; 145 | None 146 | } 147 | } 148 | 149 | fn reset(&mut self) { 150 | self.bits_pushed = 0; 151 | self.cur_byte = 0; 152 | } 153 | } 154 | 155 | struct U16Decoder { 156 | bd: ByteDecoder, 157 | high_byte: Option, 158 | } 159 | 160 | impl U16Decoder { 161 | fn new() -> Self { 162 | Self { 163 | bd: ByteDecoder::new(), 164 | high_byte: None, 165 | } 166 | } 167 | 168 | fn push_sym(&mut self, s: Sym) -> Option { 169 | if let Some(byte) = self.bd.push_sym(s) { 170 | if let Some(high_byte) = self.high_byte { 171 | self.high_byte = None; 172 | Some((high_byte as u16) << 8 | byte as u16) 173 | } else { 174 | self.high_byte = Some(byte); 175 | None 176 | } 177 | } else { 178 | None 179 | } 180 | } 181 | 182 | fn reset(&mut self) { 183 | self.high_byte = None; 184 | self.bd.reset(); 185 | } 186 | } 187 | 188 | enum DecoderState { 189 | Sync, 190 | Sz, 191 | Data, 192 | } 193 | 194 | pub struct FrameDecoder { 195 | sym_sync: SymSync, 196 | state: DecoderState, 197 | frame_sz: u16, 198 | rotation: usize, 199 | frame_sz_decoder: U16Decoder, 200 | data: Vec, 201 | data_decoder: ByteDecoder, 202 | } 203 | 204 | impl FrameDecoder { 205 | pub fn new() -> Block { 206 | Block::new( 207 | BlockMetaBuilder::new("FrameDecoder").build(), 208 | StreamIoBuilder::new() 209 | .add_input("in", std::mem::size_of::()) 210 | .build(), 211 | MessageIoBuilder::new().add_output("out").build(), 212 | Self::create(), 213 | ) 214 | } 215 | 216 | fn create() -> Self { 217 | Self { 218 | sym_sync: SymSync::new(), 219 | state: DecoderState::Sync, 220 | rotation: 0, 221 | frame_sz: 0, 222 | frame_sz_decoder: U16Decoder::new(), 223 | data: Vec::new(), 224 | data_decoder: ByteDecoder::new(), 225 | } 226 | } 227 | 228 | fn push_sym(&mut self, s: Sym) -> Option> { 229 | if let Some(rotation) = self.sym_sync.push_sym(s) { 230 | self.rotation = rotation; 231 | self.reset(); 232 | self.state = DecoderState::Sz; 233 | return None; 234 | } 235 | 236 | let s = s.sub(self.rotation); 237 | 238 | match self.state { 239 | DecoderState::Sync => None, 240 | DecoderState::Sz => { 241 | if let Some(sz) = self.frame_sz_decoder.push_sym(s) { 242 | self.frame_sz = sz; 243 | self.state = DecoderState::Data; 244 | } 245 | 246 | None 247 | } 248 | 249 | DecoderState::Data => { 250 | if let Some(byte) = self.data_decoder.push_sym(s) { 251 | self.data.push(byte); 252 | 253 | if self.data.len() == self.frame_sz as usize { 254 | let data = self.data.clone(); 255 | self.reset(); 256 | return Some(data); 257 | } 258 | } 259 | 260 | None 261 | } 262 | } 263 | } 264 | 265 | fn reset(&mut self) { 266 | self.state = DecoderState::Sync; 267 | self.data.clear(); 268 | self.frame_sz = 0; 269 | self.frame_sz_decoder.reset(); 270 | self.data_decoder.reset(); 271 | } 272 | } 273 | 274 | #[async_trait] 275 | impl Kernel for FrameDecoder { 276 | async fn work( 277 | &mut self, 278 | io: &mut WorkIo, 279 | sio: &mut StreamIo, 280 | mio: &mut MessageIo, 281 | _b: &mut BlockMeta, 282 | ) -> Result<()> { 283 | let input = sio.input(0).slice::(); 284 | 285 | for samp in input.iter() { 286 | if samp.is_none() { 287 | continue; 288 | } 289 | 290 | if let Some(frame) = self.push_sym(samp.unwrap()) { 291 | mio.post(0, Pmt::Blob(frame)).await; 292 | } 293 | } 294 | 295 | if sio.input(0).finished() { 296 | mio.post(0, Pmt::Null).await; 297 | io.finished = true; 298 | } 299 | 300 | sio.input(0).consume(input.len()); 301 | 302 | Ok(()) 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use anyhow::Result; 309 | 310 | use crate::sym::Sym; 311 | 312 | use super::{FrameDecoder, FrameEncoder}; 313 | 314 | fn run(mut sym_transform: impl FnMut(Sym) -> Sym) -> Result<()> { 315 | let mut encoder = FrameEncoder::create(); 316 | let mut decoder = FrameDecoder::create(); 317 | 318 | let payload = vec![0xde, 0xad, 0xbe, 0xef]; 319 | 320 | encoder.push_frame(&payload); 321 | 322 | let mut it = encoder.sym_queue.iter().peekable(); 323 | 324 | while let Some(sym) = it.next() { 325 | let v = decoder.push_sym(sym_transform(sym.unwrap())); 326 | 327 | if it.peek().is_none() { 328 | assert!(matches!(v, Some(_payload))); 329 | } else { 330 | assert!(v.is_none()) 331 | } 332 | } 333 | 334 | Ok(()) 335 | } 336 | 337 | #[test] 338 | fn encode_decode() -> Result<()> { 339 | run(|s| s) 340 | } 341 | 342 | #[test] 343 | fn encode_decode_rot_1() -> Result<()> { 344 | run(|s| s.add(1)) 345 | } 346 | 347 | #[test] 348 | fn encode_decode_rot_2() -> Result<()> { 349 | run(|s| s.add(2)) 350 | } 351 | 352 | #[test] 353 | fn encode_decode_rot_3() -> Result<()> { 354 | run(|s| s.add(3)) 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod carrier_sync; 2 | pub mod clock_sync; 3 | pub mod frame; 4 | pub mod qam; 5 | mod sym; 6 | mod sym_sync; 7 | pub mod tap; 8 | pub mod test_tone; 9 | -------------------------------------------------------------------------------- /src/qam.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futuresdr::{ 3 | async_trait::async_trait, 4 | blocks::Apply, 5 | num_complex::Complex32, 6 | runtime::{ 7 | Block, BlockMeta, BlockMetaBuilder, Kernel, MessageIo, MessageIoBuilder, StreamIo, 8 | StreamIoBuilder, WorkIo, 9 | }, 10 | }; 11 | 12 | use crate::sym::{Sym, Symbol}; 13 | 14 | pub struct QamMod { 15 | sps: u16, 16 | } 17 | 18 | impl QamMod { 19 | pub fn new(sps: u16) -> Block { 20 | Block::new( 21 | BlockMetaBuilder::new("QamMod").build(), 22 | StreamIoBuilder::new() 23 | .add_input("in", std::mem::size_of::()) 24 | .add_output("out", std::mem::size_of::()) 25 | .build(), 26 | MessageIoBuilder::new().build(), 27 | QamMod { sps }, 28 | ) 29 | } 30 | } 31 | 32 | #[async_trait] 33 | impl Kernel for QamMod { 34 | async fn work( 35 | &mut self, 36 | io: &mut WorkIo, 37 | sio: &mut StreamIo, 38 | _mio: &mut MessageIo, 39 | _b: &mut BlockMeta, 40 | ) -> Result<()> { 41 | let input = sio.input(0); 42 | let is = input.slice::(); 43 | let out_output = sio.output(0); 44 | let os = out_output.slice::(); 45 | let mut syms_processed = 0; 46 | 47 | if os.len() == 0 { 48 | return Ok(()); 49 | } 50 | 51 | if sio.input(0).finished() { 52 | io.finished = true; 53 | } 54 | 55 | for (i, chunk) in os.chunks_exact_mut(self.sps as usize).enumerate() { 56 | if let Some(sym) = is.get(i) { 57 | let samp = if let Some(samp) = sym { 58 | Complex32::from(samp) 59 | } else { 60 | Complex32::new(0.0, 0.0) 61 | }; 62 | 63 | for o in chunk.iter_mut() { 64 | *o = samp 65 | } 66 | 67 | syms_processed = i; 68 | } 69 | } 70 | 71 | sio.input(0).consume(syms_processed); 72 | sio.output(0).produce(syms_processed * self.sps as usize); 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | pub struct QamDemod; 79 | 80 | impl QamDemod { 81 | pub fn new() -> Block { 82 | Apply::new(move |x: &Complex32| { 83 | let pi = std::f32::consts::PI; 84 | let (mag, phase) = x.to_polar(); 85 | 86 | if mag < 0.1 { 87 | return None; 88 | } 89 | 90 | if phase > 0.0 { 91 | if phase < pi / 2.0 { 92 | Some(Sym::A) 93 | } else { 94 | Some(Sym::B) 95 | } 96 | } else { 97 | if phase > -pi / 2.0 { 98 | Some(Sym::C) 99 | } else { 100 | Some(Sym::D) 101 | } 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/sym.rs: -------------------------------------------------------------------------------- 1 | use futuresdr::num_complex::Complex32; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | pub enum Sym { 5 | A, 6 | B, 7 | C, 8 | D, 9 | } 10 | 11 | impl From for u8 { 12 | fn from(value: Sym) -> Self { 13 | match value { 14 | Sym::A => 0b00, 15 | Sym::B => 0b01, 16 | Sym::C => 0b10, 17 | Sym::D => 0b11, 18 | } 19 | } 20 | } 21 | 22 | impl Sym { 23 | fn convert_nibble(nibble: u8) -> Sym { 24 | match nibble & 0x3 { 25 | 0b00 => Sym::A, 26 | 0b01 => Sym::B, 27 | 0b10 => Sym::C, 28 | 0b11 => Sym::D, 29 | _ => unreachable!(), 30 | } 31 | } 32 | 33 | pub fn syms_from_byte(byte: u8) -> Vec { 34 | let mut ret = Vec::new(); 35 | ret.push(Self::convert_nibble(byte >> 6)); 36 | ret.push(Self::convert_nibble(byte >> 4)); 37 | ret.push(Self::convert_nibble(byte >> 2)); 38 | ret.push(Self::convert_nibble(byte)); 39 | ret 40 | } 41 | 42 | pub fn inc(&self) -> Self { 43 | match self { 44 | Sym::A => Sym::C, 45 | Sym::B => Sym::A, 46 | Sym::C => Sym::D, 47 | Sym::D => Sym::B, 48 | } 49 | } 50 | 51 | pub fn dec(&self) -> Self { 52 | match self { 53 | Sym::A => Sym::B, 54 | Sym::B => Sym::D, 55 | Sym::C => Sym::A, 56 | Sym::D => Sym::C, 57 | } 58 | } 59 | 60 | pub fn add(&self, n: usize) -> Self { 61 | match n & 0x3 { 62 | 0 => *self, 63 | 1 => self.inc(), 64 | 2 => self.inc().inc(), 65 | 3 => self.inc().inc().inc(), 66 | _ => unreachable!(), 67 | } 68 | } 69 | 70 | pub fn sub(&self, n: usize) -> Self { 71 | match n & 0x3 { 72 | 0 => *self, 73 | 1 => self.dec(), 74 | 2 => self.dec().dec(), 75 | 3 => self.dec().dec().dec(), 76 | _ => unreachable!(), 77 | } 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::Sym; 84 | 85 | #[test] 86 | fn sym_rotation_symmetry() { 87 | let x = vec![Sym::A, Sym::B, Sym::C, Sym::D]; 88 | 89 | assert_eq!( 90 | x, 91 | x.iter() 92 | .map(|x| x.inc().inc().inc().inc()) 93 | .collect::>() 94 | ); 95 | assert_eq!( 96 | x.iter().map(|x| x.dec()).collect::>(), 97 | x.iter().map(|x| x.inc().inc().inc()).collect::>() 98 | ); 99 | assert_eq!( 100 | x.iter().map(|x| x.dec().dec()).collect::>(), 101 | x.iter().map(|x| x.inc().inc()).collect::>() 102 | ); 103 | assert_eq!( 104 | x.iter().map(|x| x.dec().dec().dec()).collect::>(), 105 | x.iter().map(|x| x.inc()).collect::>() 106 | ); 107 | } 108 | 109 | #[test] 110 | fn sym_addition() { 111 | let x = vec![Sym::A, Sym::B, Sym::C, Sym::D]; 112 | 113 | assert_eq!(x.iter().map(|x| x.add(4)).collect::>(), x); 114 | assert_eq!( 115 | x.iter().map(|x| x.add(5)).collect::>(), 116 | x.iter().map(|x| x.add(1)).collect::>() 117 | ); 118 | assert_eq!( 119 | x.iter().map(|x| x.add(9)).collect::>(), 120 | x.iter().map(|x| x.add(1)).collect::>() 121 | ); 122 | assert_eq!( 123 | x.iter().map(|x| x.add(6)).collect::>(), 124 | x.iter().map(|x| x.add(2)).collect::>() 125 | ); 126 | assert_eq!( 127 | x.iter().map(|x| x.add(7)).collect::>(), 128 | x.iter().map(|x| x.add(3)).collect::>() 129 | ); 130 | assert_eq!(x.iter().map(|x| x.add(8)).collect::>(), x); 131 | } 132 | } 133 | 134 | const N: f32 = 0.3; 135 | 136 | impl From<&Sym> for Complex32 { 137 | fn from(value: &Sym) -> Self { 138 | let _pi = std::f32::consts::PI; 139 | 140 | match value { 141 | Sym::A => Complex32::new(N, N), 142 | Sym::B => Complex32::new(-N, N), 143 | Sym::C => Complex32::new(N, -N), 144 | Sym::D => Complex32::new(-N, -N), 145 | } 146 | } 147 | } 148 | 149 | pub type Symbol = Option; 150 | -------------------------------------------------------------------------------- /src/sym_sync.rs: -------------------------------------------------------------------------------- 1 | use crate::sym::Sym; 2 | use std::collections::BTreeMap; 3 | 4 | pub const SYNC: [Sym; 16] = [ 5 | Sym::A, 6 | Sym::B, 7 | Sym::A, 8 | Sym::C, 9 | Sym::A, 10 | Sym::D, 11 | Sym::B, 12 | Sym::D, 13 | Sym::A, 14 | Sym::C, 15 | Sym::B, 16 | Sym::B, 17 | Sym::A, 18 | Sym::B, 19 | Sym::D, 20 | Sym::C, 21 | ]; 22 | 23 | pub struct SymSync { 24 | rotations: BTreeMap, 25 | n: u32, 26 | } 27 | 28 | impl SymSync { 29 | pub fn new() -> Self { 30 | let mut rotations = BTreeMap::new(); 31 | 32 | (0..4).for_each(|n| { 33 | let mut sync_pattern: u32 = 0; 34 | 35 | SYNC.iter().for_each(|s| { 36 | Self::push_nibble(&mut sync_pattern, s.add(n)); 37 | }); 38 | 39 | rotations.insert(sync_pattern, n); 40 | }); 41 | 42 | Self { rotations, n: 0 } 43 | } 44 | 45 | fn push_nibble(n: &mut u32, s: Sym) { 46 | *n <<= 2; 47 | *n |= u8::from(s) as u32; 48 | } 49 | 50 | pub fn push_sym(&mut self, s: Sym) -> Option { 51 | Self::push_nibble(&mut self.n, s); 52 | 53 | self.rotations.get(&self.n).copied() 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use crate::sym::Sym; 60 | 61 | use super::{SymSync, SYNC}; 62 | 63 | fn run(mut s: SymSync, mut sym_transform: impl FnMut(Sym) -> Sym) -> Option { 64 | let mut it = SYNC.iter().peekable(); 65 | 66 | while let Some(sym) = it.next() { 67 | let v = s.push_sym(sym_transform(*sym)); 68 | 69 | if it.peek().is_none() { 70 | return v; 71 | } else { 72 | assert!(v.is_none()) 73 | } 74 | } 75 | 76 | unreachable!(); 77 | } 78 | 79 | #[test] 80 | fn sync_no_rot() { 81 | assert!(matches!(run(SymSync::new(), |s| s), Some(0))); 82 | } 83 | 84 | #[test] 85 | fn sync_rot_1() { 86 | assert!(matches!(run(SymSync::new(), |s| s.add(1)), Some(1))); 87 | } 88 | 89 | #[test] 90 | fn sync_rot_2() { 91 | assert!(matches!(run(SymSync::new(), |s| s.add(2)), Some(2))); 92 | } 93 | 94 | #[test] 95 | fn sync_rot_3() { 96 | assert!(matches!(run(SymSync::new(), |s| s.add(3)), Some(3))); 97 | } 98 | 99 | #[test] 100 | fn with_preamble() { 101 | let mut s = SymSync::new(); 102 | 103 | s.push_sym(Sym::A); 104 | s.push_sym(Sym::B); 105 | s.push_sym(Sym::D); 106 | s.push_sym(Sym::A); 107 | 108 | assert!(matches!(run(s, |s| s.add(3)), Some(3))); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/tap.rs: -------------------------------------------------------------------------------- 1 | use std::{io::ErrorKind, sync::Arc}; 2 | 3 | use anyhow::{bail, Result}; 4 | use futuresdr::{ 5 | async_trait::async_trait, 6 | macros::message_handler, 7 | runtime::{ 8 | Block, BlockMeta, BlockMetaBuilder, Kernel, MessageIo, MessageIoBuilder, Pmt, StreamIo, 9 | StreamIoBuilder, WorkIo, 10 | }, 11 | }; 12 | use tun_tap::Iface; 13 | 14 | pub struct Tap { 15 | tap: Arc>, 16 | } 17 | 18 | impl Tap { 19 | pub fn new(name: &str, mode: tun_tap::Mode) -> Result { 20 | let tap = Iface::without_packet_info(name, mode)?; 21 | let tap = Arc::new(async_io::Async::new(tap)?); 22 | 23 | Ok(Block::new( 24 | BlockMetaBuilder::new("Tap").build(), 25 | StreamIoBuilder::new().build(), 26 | MessageIoBuilder::new() 27 | .add_input("in", Self::input_handler) 28 | .add_output("out") 29 | .build(), 30 | Tap { tap }, 31 | )) 32 | } 33 | 34 | #[message_handler] 35 | async fn input_handler( 36 | &mut self, 37 | _mio: &mut MessageIo, 38 | _meta: &mut BlockMeta, 39 | p: Pmt, 40 | ) -> Result { 41 | if let Pmt::Blob(data) = p { 42 | match self.tap.write_with(|x| x.send(&data)).await { 43 | Ok(n) => assert_eq!(n, data.len()), 44 | Err(_) => { 45 | eprintln!("Failed to write packet to TAP device, dropping. Is the device up?") 46 | } 47 | } 48 | } 49 | 50 | Ok(Pmt::Null) 51 | } 52 | } 53 | 54 | #[async_trait] 55 | impl Kernel for Tap { 56 | async fn work( 57 | &mut self, 58 | io: &mut WorkIo, 59 | _sio: &mut StreamIo, 60 | mio: &mut MessageIo, 61 | _b: &mut BlockMeta, 62 | ) -> Result<()> { 63 | if io.block_on.is_some() { 64 | return Ok(()); 65 | } 66 | 67 | let mut buf = vec![0; 1500]; 68 | 69 | loop { 70 | match self.tap.as_ref().as_ref().recv(&mut buf) { 71 | Ok(len) => mio.post(0, Pmt::Blob(buf[..len].to_vec())).await, 72 | Err(e) if e.kind() == ErrorKind::WouldBlock => break, 73 | _ => bail!("Error reading from tap interface"), 74 | } 75 | } 76 | 77 | let t2 = self.tap.clone(); 78 | io.block_on(async move { 79 | t2.readable().await.unwrap(); 80 | }); 81 | 82 | Ok(()) 83 | } 84 | } 85 | --------------------------------------------------------------------------------