futuresdr-frontend 0.0.3

Web Frontend for an Experimental Async SDR Runtime for Heterogeneous Architectures.
Documentation
use reqwasm::http::Request;
use wasm_bindgen::prelude::*;
use web_sys::HtmlInputElement;
use yew::prelude::*;

use futuresdr_pmt::Pmt;
use futuresdr_pmt::PmtKind;

#[allow(clippy::too_many_arguments)]
#[wasm_bindgen]
pub fn add_slider_u32(
    id: String,
    url: String,
    block: u32,
    callback: u32,
    min: f64,
    max: f64,
    step: f64,
    value: f64,
) {
    let document = gloo_utils::document();
    let div = document.query_selector(&id).unwrap().unwrap();
    yew::start_app_with_props_in_element::<Slider>(
        div,
        Props {
            url,
            block,
            callback,
            pmt_type: PmtKind::U32,
            min: min as i64,
            max: max as i64,
            step: step as i64,
            value: value as i64,
        },
    );
}

pub enum Msg {
    Error,
    ValueChanged(i64),
    Reply(String, u64),
}

#[derive(Clone, Properties, PartialEq, Eq)]
pub struct Props {
    pub url: String,
    pub block: u32,
    pub callback: u32,
    pub pmt_type: PmtKind,
    pub min: i64,
    pub max: i64,
    pub step: i64,
    pub value: i64,
}

pub struct Slider {
    status: String,
    request_id: u64,
    last_request_id: u64,
}

impl Slider {
    fn endpoint(props: &Props) -> String {
        format!(
            "{}/api/block/{}/call/{}",
            props.url, props.block, props.callback
        )
    }

    fn callback(ctx: &Context<Self>, p: &Pmt, id: u64) {
        let p = p.clone();
        let endpoint = Self::endpoint(ctx.props());
        gloo_console::log!(format!("slider: sending request {:?}", &p));

        ctx.link().send_future(async move {
            let response = Request::post(&endpoint)
                .header("Content-Type", "application/json")
                .body(serde_json::to_string(&p).unwrap())
                .send()
                .await;

            if let Ok(response) = response {
                if response.ok() {
                    return Msg::Reply(response.text().await.unwrap(), id);
                }
            }
            Msg::Error
        });
    }

    fn value_to_pmt(value: i64, ctx: &Context<Self>) -> Option<Pmt> {
        match ctx.props().pmt_type {
            PmtKind::U32 => {
                let v = u32::try_from(value).ok()?;
                Some(Pmt::U32(v))
            }
            PmtKind::U64 => {
                let v = u64::try_from(value).ok()?;
                Some(Pmt::U64(v))
            }
            PmtKind::F64 => Some(Pmt::F64(value as f64)),
            _ => None,
        }
    }
}

impl Component for Slider {
    type Message = Msg;
    type Properties = Props;

    fn create(ctx: &Context<Self>) -> Self {
        let mut status = "init".to_string();
        let value = ctx.props().value;

        if let Some(p) = Self::value_to_pmt(value, ctx) {
            Self::callback(ctx, &p, 1);
        } else {
            status = "Invalid Properties".to_string();
        }

        Self {
            status,
            request_id: 1,
            last_request_id: 0,
        }
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::ValueChanged(v) => {
                self.status = "calling".to_string();

                if let Some(p) = Self::value_to_pmt(v, ctx) {
                    self.request_id += 1;
                    Self::callback(ctx, &p, self.request_id);
                } else {
                    self.status = "Invalid Value".to_string();
                }
            }
            Msg::Error => {
                self.status = "Error".to_string();
            }
            Msg::Reply(v, req_id) => {
                if req_id > self.last_request_id {
                    self.status = v;
                }
            }
        };
        true
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let mut classes = "".to_string();
        if self.request_id > self.last_request_id {
            classes.push_str(" fetching");
        }

        let oninput = ctx.link().callback(|e: InputEvent| {
            let input: HtmlInputElement = e.target_unchecked_into();
            Msg::ValueChanged(input.value_as_number() as i64)
        });

        html! {
            <div>
                <input type="range"
                    min={ctx.props().min.to_string()}
                    max={ctx.props().max.to_string()}
                    step={ctx.props().step.to_string()}
                    {oninput}
                />

                <span class={classes}>{ &self.status }</span>
            </div>
        }
    }
}