verdigris 0.2.0

Browser application to explore, learn and debug CoAP
//! A (visual) component to pass on a connection to a BLE device to a proxy
//!
//! (Think "Turning your browser into a router")

use yew::prelude::*;
use yewtil::NeqAssign as _;

use log::*;

use coap_message::{MinimalWritableMessage, ReadableMessage};

use crate::{coapws, coapwsmessage};

// FIXME copied over and adapted from coap-gatt-demo
const UUID_US: &'static str = "8df804b7-3300-496d-9dfa-f8fb40a236bc";
const UUID_UC: &'static str = "2a58fc3f-3c62-4ecc-8167-d66d4d9410c2";

/// Lifetime of registrations. Chosen low as the default aiocoap based RD neither has support for
/// infinite lifetimes, nor cancels proxy-based registrations that become inactive the moment the
/// connection is dropped.
static LT: core::time::Duration = core::time::Duration::from_secs(60);

pub struct BleBridge {
    /* props: Props, */
    ws_connection: Box<dyn Bridge<coapws::Connection>>,
    ws_connection_status: &'static str,
    link: ComponentLink<Self>,
    characteristic: Option<web_sys::BluetoothRemoteGattCharacteristic>,
    /*
    server_uri: String,
    health: Health,
    ep: String,
    color: String,
    _timer: yew::services::interval::IntervalTask,
    */
}

#[derive(Debug)]
pub enum Msg {
    Socket(coapws::Output),
    GattConnect,
    GattConnectDone(Result<web_sys::BluetoothRemoteGattCharacteristic, wasm_bindgen::JsValue>),
    GattRequestDone(Result<(web_sys::BluetoothRemoteGattCharacteristic, coapwsmessage::CoAPWSMessageW), wasm_bindgen::JsValue>),
    WSConnect,
    /*
    SetServer(String),
    SetEP(String),
    */
}

#[derive(Properties, Clone)]
pub struct Props {
    /*
    pub ep: Option<String>,
    pub rd: Option<String>,
    pub onchange: Callback<(String, String)>,
    */
}

static DEFAULT_SERVER: &'static str = "wss://rd.coap.amsuess.com/.well-known/coap";

trait PromiseExt {
    type Out;
    fn js2rs(self) -> Self::Out;
}

impl PromiseExt for js_sys::Promise {
    type Out = wasm_bindgen_futures::JsFuture;
    fn js2rs(self) -> Self::Out {
        self.into()
    }
}

async fn discover() -> Result<web_sys::BluetoothRemoteGattCharacteristic, wasm_bindgen::JsValue> {
    use web_sys::*;

    let navigator = window().expect("This is running inside a web browser")
        .navigator();

    let bluetooth = navigator.bluetooth()
        .ok_or("No Bluetooth available in this browser")?;

    let device: BluetoothDevice = bluetooth
        .request_device(
            RequestDeviceOptions::new().filters(
                &[BluetoothLeScanFilterInit::new().services(
                    &[wasm_bindgen::JsValue::from(UUID_US)].iter().collect::<js_sys::Array>())
                ].iter().collect::<js_sys::Array>()
        ))
        .js2rs().await?.into();

    let gatt = device.gatt()
        .ok_or("No access to GATT part of device")?;

    let server: BluetoothRemoteGattServer = gatt.connect()
        .js2rs().await?.into();

    let service: BluetoothRemoteGattService = server
        .get_primary_service_with_str(UUID_US)
        .js2rs().await?.into();

    let characteristic: BluetoothRemoteGattCharacteristic = service
        .get_characteristic_with_str(UUID_UC)
        .js2rs().await?.into();

    Ok(characteristic)
}

/* copy-paste from riot implementation */

mod from_riot {
use std::convert::TryInto;

pub struct CoapBleBuffer(heapless::Vec<u8, heapless::consts::U512>);

use coap_message_utils::*;

impl CoapBleBuffer {
    pub const fn new() -> Self {
        Self(heapless::Vec(heapless::i::Vec::new()))
    }

    pub fn access_buffer(&mut self) -> &mut heapless::Vec<u8, heapless::consts::U512> {
        &mut self.0
    }

    pub fn parse<'a>(&'a mut self) -> Option<inmemory::Message<'a>> {
        self.0.get(0)
            .map(|code| *code)
            .map(move |code| inmemory::Message::new(code, &self.0[1..]))
    }

    pub fn write<'a>(&'a mut self) -> CoapBleWriter<'a> {
        let buf = self.access_buffer();
        buf.clear();
        buf.push(0).unwrap();
        CoapBleWriter {
            buffer: buf,
            option_base: Some(0)
        }
    }
}

pub struct CoapBleWriter<'a> {
    buffer: &'a mut heapless::Vec<u8, heapless::consts::U512>,
    option_base: Option<u16>,
}

impl<'a> coap_message::MinimalWritableMessage for CoapBleWriter<'a> {
    type Code = u8;
    type OptionNumber = u16;

    fn set_code(&mut self, code: u8) {
        self.buffer[0] = code;
    }

    fn add_option(&mut self, number: u16, data: &[u8]) {
        use coap_message_utils::option_extension::encode_extensions;

        // FIXME error handling?
        let option_base = self.option_base.expect("Payload already set");

        if option_base > number {
            panic!("Wrong option sequence");
        }
        let delta = number - option_base;
        self.option_base = Some(number);
        self.buffer.extend_from_slice(encode_extensions(
                delta,
                data.len().try_into().expect("Data length can't be expressed")
                ).as_ref())
            // FIXME: For this case a fallible version *would* be practical
            .unwrap();
        self.buffer.extend_from_slice(data)
             // FIXME: For this case a fallible version *would* be practical
            .unwrap();
    }

    fn set_payload(&mut self, data: &[u8]) {
        if self.option_base.is_none() {
            panic!("Content set twice");
        }
        self.option_base = None;
        if self.buffer.len() != 0 {
           self.buffer.push(0xff)
               // FIXME: For this case a fallible version *would* be practical
               .unwrap();
            self.buffer.extend(data);
        }
    }
}

}

/* end copy-paste */

async fn forward<RES: MinimalWritableMessage>(ble: web_sys::BluetoothRemoteGattCharacteristic, request: coapwsmessage::CoAPWSMessageR<Box<[u8]>>, mut response: RES) -> Result<(web_sys::BluetoothRemoteGattCharacteristic, RES), wasm_bindgen::JsValue> {
    let mut buf = from_riot::CoapBleBuffer::new();
    buf.write().set_from_message(&request);
    let req_data = buf.access_buffer();

    ble.write_value_with_u8_array(req_data)
        .js2rs().await?;

    let ble_response: js_sys::DataView = ble.read_value()
        .js2rs().await?.into();

    let mut buf = from_riot::CoapBleBuffer::new();
    // That looks terrible; I don't suppose this can be memmapped in any way?
    buf.access_buffer().extend((0..ble_response.byte_length()).map(|i| ble_response.get_uint8(i)));
    // ↑ FIXME panics
    //    .ok_or("Response does not fit in allocated buffer")?;
    response.set_from_message(
        &buf.parse()
            .ok_or("Error parsing the response")?
            );

    Ok((ble, response))
}

// This might be the archetypical case for something that returns but doesn't to any intermediate
// updates.
async fn wrap<F: std::future::Future>(f: F, done_cb: yew::Callback<F::Output>) {
    done_cb.emit(f.await);
}

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

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        let ws_connection = coapws::Connection::bridge(link.callback(|o| Msg::Socket(o)));
        let ws_connection_status = "Uninitialized";
        /*

        let ep = props.ep.as_ref().cloned().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        let server_uri = props.rd.as_ref().cloned().unwrap_or_else(|| DEFAULT_SERVER.to_string());

        // It's crude to reconnect everything, but it's simple and works
        let timer = yew::services::IntervalService::spawn(LT, link.callback(|()|  Msg::Connect));
        */

        Self { /* props,*/ ws_connection, link, characteristic: None, ws_connection_status, /* health, color, server_uri, ep, _timer: timer */ }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::GattConnect => {
                wasm_bindgen_futures::spawn_local(wrap(discover(),
                self.link.callback(|e| Msg::GattConnectDone(e))));
                self.characteristic.neq_assign(None)
            }
            Msg::GattConnectDone(d) => {
                match d {
                    Ok(d) => {
                        self.characteristic = Some(d);
                        true
                    }
                    Err(d) => {
                        info!("Error: {:?}", d);
                        false
                    }
                }
            }
            Msg::GattRequestDone(d) => {
                match d {
                    Ok((characteristic, response)) => {
                        self.ws_connection.send(coapws::Input::Message(response));
                        self.characteristic = Some(characteristic);
                        true
                    }
                    Err(d) => {
                        info!("Error: {:?}", d);
                        false // would be true if we'd distinguish between "busy" and "connection broken"
                    }
                }
            }
            Msg::WSConnect => {
                let mut ws_connection = coapws::Connection::bridge(self.link.callback(|o| Msg::Socket(o)));
                ws_connection.send(coapws::Input::Initialize { uri: DEFAULT_SERVER.to_string() });
                self.ws_connection = ws_connection;
                self.ws_connection_status = "Connecting";
                true
            }
            Msg::Socket(coapws::Output::Connected) => {
                // FIXME pull into a separate step

                // We only send one request, can just as well use the empty token here
                let mut msg = coapwsmessage::CoAPWSMessageW::new(b"");
                msg.set_code(coap_numbers::code::POST);
                msg.add_option(coap_numbers::option::URI_PATH, b".well-known");
                msg.add_option(coap_numbers::option::URI_PATH, b"core");
                msg.add_option(coap_numbers::option::URI_QUERY, b"proxy=on");
                msg.add_option(coap_numbers::option::URI_QUERY, b"ep=bledevice");
                let ltquery = format!("lt={}", LT.as_secs()).into_bytes();
                msg.add_option(coap_numbers::option::URI_QUERY, &ltquery);
                self.ws_connection.send(coapws::Input::Message(msg));

                self.ws_connection_status = "Connected";

                true
            }
            Msg::Socket(coapws::Output::Message(m)) => {
                match coap_numbers::code::classify(m.code().into()) {
                    coap_numbers::code::Range::Response(_) => {
                        // FIXME That's for when the registrations are well managed -- as above,
                        // outside of here.
                        info!("Ignoring response event {:?}", m);
                    }
                    coap_numbers::code::Range::Request => {
                        let mut response = coapwsmessage::CoAPWSMessageW::new(m.token());

                        if let Some(ble) = self.characteristic.take() {
                            wasm_bindgen_futures::spawn_local(wrap(forward(ble, m, response),
                                self.link.callback(|e| Msg::GattRequestDone(e))));
                        } else {
                            // We might distinguish between "not connected" and "just busy
                            // processing something" later; for now, 5.03 seems good enough
                            response.set_code(coap_numbers::code::SERVICE_UNAVAILABLE);
                            self.ws_connection.send(coapws::Input::Message(response));
                        }
                    }
                    coap_numbers::code::Range::Empty => {}
                    _ => {
                        // FIXME close connection?
                        info!("Ignoring empty message event {:?}", m);
                    }
                }
                // actually only if we really changed a status
                true
            }
            Msg::Socket(coapws::Output::SignalingInfo(_)) => {
                // We don't care, this is a very simple server that has nothing transport
                // dependent, let alone would interpret signaling messages (and there's no need to
                // actually *do* anything, the CoAPWSMessageExchanger took care of that)
                false
            }
            Msg::Socket(coapws::Output::Error(e)) => {
                // FIXME as above (just not as bad -- the connection was already closed, and it's
                // not like we're sending messages of our own)
                error!("Error from CoAP-over-WS: {:?}", e);
                self.ws_connection_status = "Failed";
                true
            }
        }
    }

    fn change(&mut self, _: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        html! { <>
            <h1>{"CoAP over WebSockets – CoAP over GATT (Bluetooth Low Energy) Bridge"}</h1>

            <p><i> { "This part is even more highly experimental than the rest; check your browser console while using it." } </i></p>

            <h2> { "Bluetooth side" } </h2>
            <button onclick=self.link.callback(|_| Msg::GattConnect)> { "(Re)connect" } </button>
            { if self.characteristic.is_some() { "Connected" } else { "Disconnected or busy" } }

            <h2> { "WebSocket side" } </h2>
            <button onclick=self.link.callback(|_| Msg::WSConnect)> { "(Re)connect" } </button>
            { self.ws_connection_status }
        </> }
    }
}