use std::convert::{TryFrom, TryInto};
use futures::StreamExt;
use log::{debug, error, info};
use simplelog::{LevelFilter, SimpleLogger, TermLogger, TerminalMode};
use structopt::StructOpt;
use coap_client::{ClientOptions, HostOptions, RequestOptions, TokioClient};
#[derive(PartialEq, Clone, Debug, StructOpt)]
pub struct Options {
#[structopt(flatten)]
pub request_opts: RequestOptions,
#[structopt(flatten)]
pub client_opts: ClientOptions,
#[structopt(parse(try_from_str = HostOptions::try_from))]
pub target: HostOptions,
#[structopt(subcommand)]
pub command: Command,
#[structopt(long, default_value = "1")]
pub repeat: usize,
#[structopt(long = "log-level", default_value = "info")]
pub log_level: LevelFilter,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HexData(pub Vec<u8>);
impl std::str::FromStr for HexData {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
hex::decode(s).map(HexData)
}
}
#[derive(PartialEq, Clone, Debug, StructOpt)]
pub struct Data {
#[structopt(long, group = "d")]
pub file: Option<String>,
#[structopt(long, group = "d")]
pub string: Option<String>,
#[structopt(long, group = "d")]
pub hex: Option<HexData>,
}
impl TryFrom<&Data> for Option<Vec<u8>> {
type Error = std::io::Error;
fn try_from(d: &Data) -> Result<Self, Self::Error> {
let data = if let Some(f) = d.file.as_ref() {
debug!("Loading file: {}", f);
Some(std::fs::read(f)?)
} else if let Some(s) = d.string.as_ref() {
Some(s.as_bytes().to_vec())
} else if let Some(h) = d.hex.as_ref() {
Some(h.0.clone())
} else {
None
};
Ok(data)
}
}
#[derive(PartialEq, Clone, Debug, StructOpt)]
pub enum Command {
Get,
Put(Data),
Post(Data),
Observe,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let opts = Options::from_args();
let log_config = simplelog::ConfigBuilder::new().build();
if let Err(_e) = TermLogger::init(opts.log_level, log_config.clone(), TerminalMode::Mixed) {
SimpleLogger::init(opts.log_level, log_config).unwrap();
}
debug!("Connecting client to target: '{}'", opts.target.to_string());
let mut client = match TokioClient::connect(opts.target.clone(), &opts.client_opts).await {
Ok(c) => c,
Err(e) => return Err(anyhow::anyhow!("Error connecting to client: {:?}", e)),
};
debug!("Connected, executing command");
if let Command::Observe = &opts.command {
let mut o = client
.observe(&opts.target.resource, &opts.request_opts)
.await?;
loop {
tokio::select! {
r = o.next() => {
match r {
Some(Ok(d)) => {
debug!("Observe RX: {:?}", d);
display_resp(&d.payload)
},
Some(Err(e)) => {
error!("Observe error: {:?}", e);
break;
},
None => {
info!("Observe channel closed");
break;
}
}
},
_ = tokio::signal::ctrl_c() => {
break;
}
}
}
client.unobserve(o).await?;
} else {
for _i in 0..opts.repeat {
match &opts.command {
Command::Get => {
let r = client
.get(&opts.target.resource, &opts.request_opts)
.await?;
display_resp(&r);
}
Command::Put(data) => {
let d: Option<Vec<u8>> = data.try_into()?;
let r = client
.put(&opts.target.resource, d.as_deref(), &opts.request_opts)
.await?;
display_resp(&r);
}
Command::Post(data) => {
let d: Option<Vec<u8>> = data.try_into()?;
let r = client
.post(&opts.target.resource, d.as_deref(), &opts.request_opts)
.await?;
display_resp(&r);
}
_ => (),
};
}
}
if let Err(e) = client.close().await {
return Err(anyhow::anyhow!("Error closing client: {:?}", e));
}
Ok(())
}
fn display_resp(d: &[u8]) {
match std::str::from_utf8(&d) {
Ok(s) if s.len() > 0 => println!("Received: {}", s),
Err(_) if d.len() > 0 => println!("Received: {:02x?}", d),
_ => (),
}
}