use yew::prelude::*;
use yewtil::NeqAssign as _;
use log::*;
use coap_message::{MinimalWritableMessage, ReadableMessage};
use crate::{coapws, coapwsmessage};
const UUID_US: &'static str = "8df804b7-3300-496d-9dfa-f8fb40a236bc";
const UUID_UC: &'static str = "2a58fc3f-3c62-4ecc-8167-d66d4d9410c2";
static LT: core::time::Duration = core::time::Duration::from_secs(60);
pub struct BleBridge {
ws_connection: Box<dyn Bridge<coapws::Connection>>,
ws_connection_status: &'static str,
link: ComponentLink<Self>,
characteristic: Option<web_sys::BluetoothRemoteGattCharacteristic>,
}
#[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,
}
#[derive(Properties, Clone)]
pub struct Props {
}
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)
}
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;
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())
.unwrap();
self.buffer.extend_from_slice(data)
.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)
.unwrap();
self.buffer.extend(data);
}
}
}
}
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();
buf.access_buffer().extend((0..ble_response.byte_length()).map(|i| ble_response.get_uint8(i)));
response.set_from_message(
&buf.parse()
.ok_or("Error parsing the response")?
);
Ok((ble, response))
}
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";
Self { ws_connection, link, characteristic: None, ws_connection_status, }
}
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 }
}
}
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) => {
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, <query);
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(_) => {
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 {
response.set_code(coap_numbers::code::SERVICE_UNAVAILABLE);
self.ws_connection.send(coapws::Input::Message(response));
}
}
coap_numbers::code::Range::Empty => {}
_ => {
info!("Ignoring empty message event {:?}", m);
}
}
true
}
Msg::Socket(coapws::Output::SignalingInfo(_)) => {
false
}
Msg::Socket(coapws::Output::Error(e)) => {
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 }
</> }
}
}