use yew::prelude::*;
use std::convert::{TryInto, TryFrom};
use crate::{coapws, coapwsmessage};
use coap_message::{MinimalWritableMessage, ReadableMessage};
use coap_handler_implementations::option_processing::OptionsExt;
static DEFAULT_CONNECTION_URI: &'static str = "wss://proxy.coap.amsuess.com/.well-known/coap";
static DEFAULT_RD_HOST: &'static str = "rd.coap.amsuess.com";
pub struct ColorPicker {
connection: Result<Box<dyn Bridge<coapws::Connection>>, &'static str>,
link: ComponentLink<Self>,
connection_uri: String,
rd_host: String,
color: String,
found: Vec<String>,
token: u32,
}
impl ColorPicker {
fn handle_response(&mut self, m: coapwsmessage::CoAPWSMessageR<impl AsRef<[u8]>>) -> Result<(), &'static str> {
match m.token() {
b"d" => {
m.options()
.ignore_elective_others()
.expect("Can't handle discovery response option");
let nextlink = stupid_parse_linkformat(
core::str::from_utf8(m.payload())
.map_err(|_| "Non-UTF discovery response")?)
.next()
.ok_or("Nothing found in discovery")?;
let mut msg = coapwsmessage::CoAPWSMessageW::new(b"l");
msg.set_code(coap_numbers::code::GET);
let options = crate::util::OptionFeedBuilder::new()
.add_option(coap_numbers::option::URI_QUERY, "saul=ACT_LED_RGB".as_bytes())
.add_option(coap_numbers::option::OBSERVE, &[])
.finish();
set_msg_uri(&mut msg, options, nextlink, Some("coap"), Some(&self.rd_host), None)
.map_err(|_| "Invalid reference in discovery")?;
self.connection
.as_mut()
.expect("coapws violated its message sequence")
.send(coapws::Input::Message(msg))
},
b"l" => {
m.options()
.ignore_elective_others()
.expect("Can't handle lookup response option");
let parsed = stupid_parse_linkformat(
core::str::from_utf8(m.payload())
.map_err(|_| "Non-UTF discovery response")?)
.map(|s| s.to_string())
.collect();
self.found = parsed;
}
[b's', _, _, _, _] => {
}
t => {
log::warn!("Unexpected token {:?} received", t);
return Err("Unexpected token");
}
}
Ok(())
}
}
#[derive(Debug)]
pub enum Msg {
Socket(coapws::Output),
SetConnection(String),
SetRdHost(String),
Discover, PickColor(String),
ApplyAll,
ApplyTo(String),
}
fn stupid_parse_linkformat<'a>(input: &'a str) -> impl Iterator<Item=&'a str> {
struct Cursor<'a> {
s: &'a str,
}
impl<'a> Iterator for Cursor<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let index = self.s.find('>')?;
let (head, tail) = self.s.split_at(index);
self.s = &tail[1..];
let index = head.find('<').expect("Format error");
let (_, ret) = head.split_at(index);
Some(&ret[1..])
}
}
Cursor { s: input }
}
use crate::util::OptionFeed;
fn set_msg_uri(msg: &mut impl MinimalWritableMessage, mut mixins: impl OptionFeed<u16>, reference: &str, scheme: Option<&str>, host: Option<&str>, port: Option<u16>) -> Result<(), &'static str> {
use coap_numbers::option::*;
let uri_host = URI_HOST.try_into().map_err(|_| ()).expect("Message type can't express Uri-Host");
let uri_port = URI_PORT.try_into().map_err(|_| ()).expect("Message type can't express Uri-Port");
let uri_path = || URI_PATH.try_into().map_err(|_| ()).expect("Message type can't express Uri-Path");
let proxy_scheme = PROXY_SCHEME.try_into().map_err(|_| ()).expect("Message type can't express Proxy-Scheme");
let parsed = uriparse::URIReference::try_from(reference).map_err(|_| "URI parse error")?;
mixins.apply_to_message_limited(msg, URI_HOST).map_err(|_| ()).expect("Feeder options unprocessable");
if let Some(a) = parsed.authority() {
if a.has_username() || a.has_password() {
return Err("User/password in authority");
}
msg.add_option(uri_host, a.to_string().as_bytes());
} else {
if let Some(h) = host {
msg.add_option(uri_host, h.as_bytes());
}
if let Some(p) = port {
mixins.apply_to_message_limited(msg, URI_PORT).map_err(|_| ()).expect("Feeder options unprocessable");
msg.add_option_uint(uri_port, p);
}
}
mixins.apply_to_message_limited(msg, URI_PATH).map_err(|_| ()).expect("Feeder options unprocessable");
for p in parsed.path().segments() {
msg.add_option(uri_path(), p.as_str().as_bytes());
}
if let Some(_) = parsed.query() {
unimplemented!()
}
mixins.apply_to_message_limited(msg, PROXY_SCHEME).map_err(|_| ()).expect("Feeder options unprocessable");
if let Some(s) = parsed.scheme() {
msg.add_option(proxy_scheme, s.as_str().as_bytes())
} else if let Some(s) = scheme {
msg.add_option(proxy_scheme, s.as_bytes())
} else {
return Err("Relative reference without base scheme");
}
Ok(())
}
impl Component for ColorPicker {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let color = "#43B3AE".to_string();
let connection_uri = DEFAULT_CONNECTION_URI.to_string();
let rd_host = DEFAULT_RD_HOST.to_string();
let mut s = Self { connection: Err("Not started yet"), link, color, found: vec![], connection_uri, rd_host, token: 0 };
s.update(Msg::Discover);
s
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
let options_for_rgb = || crate::util::OptionFeedBuilder::new()
.add_option_uint(coap_numbers::option::CONTENT_FORMAT, 65362u16)
.finish();
match msg {
Msg::SetConnection(s) => { self.connection_uri = s; }
Msg::SetRdHost(s) => { self.rd_host = s; }
Msg::Discover => {
let mut connection = coapws::Connection::bridge(self.link.callback(|o| Msg::Socket(o)));
connection.send(coapws::Input::Initialize { uri: self.connection_uri.clone() });
self.connection = Ok(connection);
self.found.clear();
}
Msg::PickColor(c) => {
self.color = c;
}
Msg::ApplyAll => {
let c = match self.connection.as_mut() {
Ok(c) => c,
_ => return false
};
for f in self.found.iter() {
self.token += 1;
let mut token = vec![b's'];
token.extend_from_slice(&self.token.to_be_bytes());
let mut msg = coapwsmessage::CoAPWSMessageW::new(&token);
msg.set_code(coap_numbers::code::PUT);
if set_msg_uri(&mut msg, options_for_rgb(), &f, None, None, None).is_err() {
continue;
}
msg.set_payload(self.color.as_bytes());
c.send(coapws::Input::Message(msg));
}
}
Msg::ApplyTo(s) => {
self.token += 1;
let mut token = vec![b's'];
token.extend_from_slice(&self.token.to_be_bytes());
let mut msg = coap_message::heapmessage::HeapMessage::new();
msg.set_code(coap_numbers::code::PUT);
if set_msg_uri(&mut msg, options_for_rgb(), &s, None, None, None).is_err() {
return false;
}
msg.set_payload(self.color.as_bytes());
let mut actualmsg = coapwsmessage::CoAPWSMessageW::new(&token);
actualmsg.set_from_message(&msg);
self.connection.as_mut().map(|c| c.send(coapws::Input::Message(actualmsg)))
.ok();
}
Msg::Socket(coapws::Output::Connected) => {
let mut msg = coapwsmessage::CoAPWSMessageW::new(b"d");
msg.set_code(coap_numbers::code::GET);
msg.add_option(coap_numbers::option::URI_HOST, self.rd_host.as_bytes());
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"rt=core.rd-lookup-res");
msg.add_option(coap_numbers::option::PROXY_SCHEME, b"coap");
self.connection
.as_mut()
.expect("coapws violated its message sequence")
.send(coapws::Input::Message(msg))
}
Msg::Socket(coapws::Output::Message(m)) => {
match coap_numbers::code::classify(m.code().into()) {
coap_numbers::code::Range::Response(coap_numbers::code::Class::Success) => {
match self.handle_response(m) {
Ok(()) => {}
Err(e) => {
self.connection = Err(e);
}
}
}
coap_numbers::code::Range::Response(_) => {
self.connection = Err("Unsuccessful response received");
}
coap_numbers::code::Range::Request => {
log::warn!("Odd, we're being requested");
let mut response = coapwsmessage::CoAPWSMessageW::new( m.token());
response.set_code(coap_numbers::code::NOT_IMPLEMENTED);
self.connection
.as_mut()
.expect("Connection should be here")
.send(coapws::Input::Message(response));
}
coap_numbers::code::Range::Empty => {}
_ => {
self.connection = Err("Socket produced unidentified event");
}
}
}
Msg::Socket(coapws::Output::SignalingInfo(_)) => {
}
Msg::Socket(coapws::Output::Error(_)) => {
self.connection = Err("Socket produced error");
}
}
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! { <>
<h1>{"CoAP over WebSockets: Color Picker Client"}</h1>
<p> { "This client queries a resource directory and scans it for compatible, settable color resources. It allows setting them to a selected color. (Compatible here currently means 'the Color Server from the other tab', or any RGB LED on a RIOT board using " } <a href="https://gitlab.com/etonomy/riot-examples/-/tree/master/coap_saulserver"> { "the Rust RIOT demo" } </a> { ")." } </p>
<p><label> { "Contact CoAP-over-WS proxy at " } <input
type="text"
onchange=self.link.callback(move |v| Msg::SetConnection(match v {
yew::events::ChangeData::Value(u) => u,
_ => unreachable!("Text input has value"),
}))
value=self.connection_uri
/></label>
<label> { " and discover the RD starting from " } <input
type="text"
onchange=self.link.callback(move |v| Msg::SetRdHost(match v {
yew::events::ChangeData::Value(u) => u,
_ => unreachable!("Text input has value"),
}))
value=self.rd_host
/></label>
{ format!(" ({}) ", self.connection.as_ref().map(|_| "Active").unwrap_or_else(|e| e)) }
<button
onclick=self.link.callback(|_| Msg::Discover)
> { "Discover" } </button>
</p>
<p> <label> { "Color: " } <input type="color"
value=self.color
onchange=self.link.callback(move |v| Msg::PickColor(match v {
yew::events::ChangeData::Value(u) => u,
_ => unreachable!("Color input has value"),
}))
/></label> { &self.color }
<button onclick=self.link.callback(|_| Msg::ApplyAll) > { "Set all" } </button>
</p>
<ul>
{ for self.found.iter().map(|f| { let f = f.clone(); html! {
<li> { &f } <button onclick=self.link.callback(move |_| Msg::ApplyTo(f.clone())) > { "Apply" } </button> </li>
} } ) }
</ul>
</> }
}
}