wfb-rs 0.1.31

Rust WFB-style command-line tools built on openipc-rs
use std::net::{SocketAddr, UdpSocket};
use std::time::Duration;

use rand_core::{OsRng, RngCore};

#[path = "../common.rs"]
mod common;
#[path = "../tx_cmd_proto.rs"]
mod tx_cmd_proto;

use common::{next_arg, parse_u16, parse_u8, CliResult};
use tx_cmd_proto::{
    expected_response_payload_len, CommandRequest, CommandResponse, FecSettings, RadioSettings,
    CMD_GET_FEC, CMD_GET_RADIO, CMD_SET_FEC, CMD_SET_RADIO,
};

fn main() {
    if let Err(err) = run() {
        eprintln!("error: {err}");
        std::process::exit(1);
    }
}

fn run() -> CliResult<()> {
    let mut args = std::env::args().skip(1).peekable();
    if matches!(
        args.peek().map(String::as_str),
        None | Some("-h") | Some("--help")
    ) {
        print_help();
        return Ok(());
    }

    let port = parse_u16(&next_arg(&mut args, "<port>")?)?;
    let command = next_arg(&mut args, "<command>")?;
    let req_id_be = random_req_id().to_be();
    let (cmd_id, request) = match command.as_str() {
        "set_fec" => (
            CMD_SET_FEC,
            CommandRequest::SetFec {
                req_id_be,
                fec: parse_set_fec(args)?,
            },
        ),
        "set_radio" => (
            CMD_SET_RADIO,
            CommandRequest::SetRadio {
                req_id_be,
                radio: parse_set_radio(args)?,
            },
        ),
        "get_fec" => (CMD_GET_FEC, CommandRequest::GetFec { req_id_be }),
        "get_radio" => (CMD_GET_RADIO, CommandRequest::GetRadio { req_id_be }),
        _ => return Err(format!("unknown command: {command}").into()),
    };

    let dest: SocketAddr = format!("127.0.0.1:{port}").parse()?;
    let socket = UdpSocket::bind("0.0.0.0:0")?;
    socket.set_read_timeout(Some(Duration::from_secs(3)))?;
    socket.send_to(&request.encode(), dest)?;

    let mut buf = [0u8; 128];
    let (amount, _) = socket.recv_from(&mut buf)?;
    let expected_payload = expected_response_payload_len(cmd_id);
    let response =
        CommandResponse::parse(&buf[..amount], expected_payload).ok_or("invalid response")?;
    if response.req_id_be() != req_id_be {
        return Err("response req_id did not match request".into());
    }
    if response.errno() != 0 {
        let err = std::io::Error::from_raw_os_error(response.errno() as i32);
        return Err(format!("command failed: {err}").into());
    }

    match response {
        CommandResponse::Fec { fec, .. } => {
            println!("k={}", fec.k);
            println!("n={}", fec.n);
        }
        CommandResponse::Radio { radio, .. } => {
            println!("stbc={}", radio.stbc);
            println!("ldpc={}", u8::from(radio.ldpc));
            println!("short_gi={}", u8::from(radio.short_gi));
            println!("bandwidth={}", radio.bandwidth);
            println!("mcs_index={}", radio.mcs_index);
            println!("vht_mode={}", u8::from(radio.vht_mode));
            println!("vht_nss={}", radio.vht_nss);
        }
        CommandResponse::Ack { .. } => {}
    }

    Ok(())
}

fn parse_set_fec(args: impl Iterator<Item = String>) -> CliResult<FecSettings> {
    let mut fec = FecSettings { k: 8, n: 12 };
    let mut args = args.peekable();
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "-k" => fec.k = parse_u8(&next_arg(&mut args, "-k")?)?,
            "-n" => fec.n = parse_u8(&next_arg(&mut args, "-n")?)?,
            "-h" | "--help" => {
                print_help();
                std::process::exit(0);
            }
            _ => return Err(format!("unknown set_fec option: {arg}").into()),
        }
    }
    if !fec.valid() {
        return Err("invalid FEC settings; require 1 <= k <= n <= 255".into());
    }
    Ok(fec)
}

fn parse_set_radio(args: impl Iterator<Item = String>) -> CliResult<RadioSettings> {
    let mut radio = RadioSettings {
        stbc: 0,
        ldpc: false,
        short_gi: false,
        bandwidth: 20,
        mcs_index: 1,
        vht_mode: false,
        vht_nss: 1,
    };
    let mut args = args.peekable();
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "-B" => {
                radio.bandwidth = parse_u8(&next_arg(&mut args, "-B")?)?;
                if radio.bandwidth >= 80 {
                    radio.vht_mode = true;
                }
            }
            "-G" => {
                let value = next_arg(&mut args, "-G")?;
                radio.short_gi = value.starts_with('s') || value.starts_with('S');
            }
            "-S" => radio.stbc = parse_u8(&next_arg(&mut args, "-S")?)?,
            "-L" => radio.ldpc = parse_u8(&next_arg(&mut args, "-L")?)? != 0,
            "-M" => radio.mcs_index = parse_u8(&next_arg(&mut args, "-M")?)?,
            "-N" => radio.vht_nss = parse_u8(&next_arg(&mut args, "-N")?)?,
            "-V" => radio.vht_mode = true,
            "-h" | "--help" => {
                print_help();
                std::process::exit(0);
            }
            _ => return Err(format!("unknown set_radio option: {arg}").into()),
        }
    }
    Ok(radio)
}

fn random_req_id() -> u32 {
    OsRng.next_u32()
}

fn print_help() {
    println!(
        r#"wfb_tx_cmd

Usage:
  wfb_tx_cmd <port> set_fec [-k RS_K] [-n RS_N]
  wfb_tx_cmd <port> set_radio [-B bandwidth] [-G short|long] [-S stbc] [-L ldpc] [-M mcs] [-N nss] [-V]
  wfb_tx_cmd <port> get_fec
  wfb_tx_cmd <port> get_radio

This is the Rust version of the small WFB-ng UDP control helper."#
    );
}