use core::convert::TryInto;
use yew::prelude::*;
use coap_message::{MinimalWritableMessage, ReadableMessage};
use coap_handler_implementations::option_processing::OptionsExt;
use crate::{coapws, coapwsmessage};
static LT: core::time::Duration = core::time::Duration::from_secs(60);
pub struct ColorServer {
props: Props,
connection: Box<dyn Bridge<coapws::Connection>>,
link: ComponentLink<Self>,
server_uri: String,
health: Health,
ep: String,
color: String,
_timer: yew::services::interval::IntervalTask,
}
#[derive(Debug)]
pub enum Msg {
Socket(coapws::Output),
SetServer(String),
SetEP(String),
Connect,
}
#[derive(Properties, Clone)]
pub struct Props {
pub ep: Option<String>,
pub rd: Option<String>,
pub onchange: Callback<(String, String)>,
}
#[derive(Copy, Clone)]
enum Health {
Connecting,
Registering,
Failed,
Registered,
}
impl core::fmt::Display for Health {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
f.write_str(match self {
Health::Connecting => "Connecting",
Health::Registering => "Registering",
Health::Failed => "Failed",
Health::Registered => "Registered",
})
}
}
struct Color {
value: String
}
impl core::convert::TryFrom<&str> for Color {
type Error = ();
fn try_from(input: &str) -> Result<Self, ()> {
if input.len() == 7 && input.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(Color { value: input.to_string() })
} else {
Err(())
}
}
}
impl core::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
f.write_str(&self.value)
}
}
impl ColorServer {
fn handle_request<B: 'static + AsRef<[u8]>>(&mut self, request: coapwsmessage::CoAPWSMessageR<B>) {
let mut response_message = coapwsmessage::CoAPWSMessageW::new(request.token());
if let Err(e) = self.handle_request_inner(request, &mut response_message) {
response_message.set_code(e);
}
self.connection.send(coapws::Input::Message(response_message));
}
fn handle_request_inner<R>(&mut self, request: R, response: &mut coapwsmessage::CoAPWSMessageW) -> Result<(), u8>
where R: ReadableMessage
{
#[derive(Copy, Clone)]
enum Path {
Root,
WellKnown,
WellKnownCore,
Color,
Other
}
use coap_numbers::option::*;
use coap_numbers::code;
impl Path {
fn push(self, component: &str) -> Self {
match (self, component) {
(Path::Root, ".well-known") => Path::WellKnown,
(Path::Root, "color") => Path::Color,
(Path::WellKnown, "core") => Path::WellKnownCore,
_ => Path::Other
}
}
}
use coap_message::MessageOption;
let mut path = Path::Root;
let mut accept: Option<Option<u16>> = None;
request.options()
.ignore_uri_host()
.filter(|o| match o.number().into() {
coap_numbers::option::ACCEPT => {
accept = Some(o.value_uint());
false
},
_ => true,
})
.take_uri_path(|p| path = path.push(p))
.ignore_elective_others()
.map_err(|_| code::BAD_REQUEST)?;
let accept_is_not = |target| match (target, accept) {
(_, Some(None)) => true, (_, None) => false, (Some(x), Some(Some(y))) if x == y => false,
_ => true,
};
match (path, request.code().into()) {
(Path::WellKnownCore, code::GET) => {
response.set_code(code::CONTENT);
if accept_is_not(Some(40)) { return Err(code::NOT_ACCEPTABLE); }
response.add_option(CONTENT_FORMAT, &[40]);
response.set_payload(b"</color>;saul=ACT_LED_RGB;if=core.p,<https://github.com/chrysn/verdigris>;rel=\"impl-info\"");
Ok(())
},
(Path::WellKnownCore, code::PUT) => {
Err(code::FORBIDDEN)
}
(Path::Color, code::GET) => {
if accept_is_not(None) { return Err(code::NOT_ACCEPTABLE); }
response.set_code(code::CONTENT);
response.set_payload(&self.color.as_bytes());
Ok(())
}
(Path::Color, code::PUT) => {
if accept_is_not(None) { return Err(code::NOT_ACCEPTABLE); }
self.color = core::str::from_utf8(request.payload())
.map_err(|_| code::BAD_REQUEST)?
.try_into()
.map_err(|_| code::BAD_REQUEST)?
;
response.set_code(code::CHANGED);
Ok(())
}
_ => {
response.set_code(code::NOT_FOUND);
Ok(())
}
}
}
}
static DEFAULT_SERVER: &'static str = "wss://rd.coap.amsuess.com/.well-known/coap";
impl Component for ColorServer {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let mut connection = coapws::Connection::bridge(link.callback(|o| Msg::Socket(o)));
connection.send(coapws::Input::Initialize { uri: DEFAULT_SERVER.to_string() });
let health = Health::Connecting;
let color = "#43B3AE".to_string();
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());
let timer = yew::services::IntervalService::spawn(LT, link.callback(|()| Msg::Connect));
Self { props, connection, link, health, color, server_uri, ep, _timer: timer }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
let mut emit = false;
match msg {
Msg::SetServer(s) => { self.server_uri = s; emit = true; }
Msg::SetEP(s) => { self.ep = s; emit = true; }
Msg::Connect => {
let mut connection = coapws::Connection::bridge(self.link.callback(|o| Msg::Socket(o)));
connection.send(coapws::Input::Initialize { uri: self.server_uri.clone() });
self.connection = connection;
self.health = Health::Connecting;
}
Msg::Socket(coapws::Output::Connected) => {
self.health = Health::Registering;
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");
let epquery = format!("ep={}", self.ep).into_bytes();
msg.add_option(coap_numbers::option::URI_QUERY, &epquery);
let ltquery = format!("lt={}", LT.as_secs()).into_bytes();
msg.add_option(coap_numbers::option::URI_QUERY, <query);
self.connection.send(coapws::Input::Message(msg))
}
Msg::Socket(coapws::Output::Message(m)) => {
match coap_numbers::code::classify(m.code().into()) {
coap_numbers::code::Range::Response(responseclass) => {
if m.token() == b"" {
match responseclass {
coap_numbers::code::Class::Success => {
self.health = Health::Registered;
},
_ => {
self.health = Health::Failed;
}
}
} else {
self.health = Health::Failed;
}
}
coap_numbers::code::Range::Request => {
self.handle_request(m);
}
coap_numbers::code::Range::Empty => {}
_ => {
self.health = Health::Failed;
}
}
}
Msg::Socket(coapws::Output::SignalingInfo(_)) => {
}
Msg::Socket(coapws::Output::Error(_)) => {
self.health = Health::Failed;
}
}
if emit {
self.props.onchange.emit((self.ep.clone(), self.server_uri.clone()));
}
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! { <>
<h1>{"CoAP over WebSockets: Color Panel Server"}</h1>
<p> { "This server registers its color changing services at the given CoAP-over-WS server using Resource Directory Simple Registration." } </p>
<p><label> { "Server URI:" } <input
type="text"
onchange=self.link.callback(move |v| Msg::SetServer(match v {
yew::events::ChangeData::Value(u) => u,
_ => unreachable!("Text input has value"),
}))
value=self.server_uri
/></label>
<label> { " as " } <input
type="text"
onchange=self.link.callback(move |v| Msg::SetEP(match v {
yew::events::ChangeData::Value(u) => u,
_ => unreachable!("Text input has value"),
}))
value=self.ep
/></label>
{ ": " }{ self.health }{ " " }
<button
onclick=self.link.callback(|_| Msg::Connect)
> { "(Re)connect" } </button>
</p>
<div style=format!("width: 100%; height: 100vh; background-color:{}", self.color)></div>
<link
rel="icon"
href=format!("data:image/svg+xml;urlencode,<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"background-color: %23{}\"></svg>", &self.color[1..])
/>
</> }
}
}