Commit c164254a authored by clonejo's avatar clonejo
Browse files

v0.2: highlight outdated topics in red using _interval subtopic

parent af707723
v0.2
====
* mqttnc recognizes a topics publishing interval in the subtopic `_interval`
(in seconds). If that interval has passed for a topic, it is highlighted in
red.
* It is recommended to publish `_interval` messages with the retain flag set.
* Highlighting also works for topics which are not retained (as long as a
valid `_interval` message has been received).
* Leading and trailing whitespace is removed from payloads.
v0.1
====
(initial release)
* Ncurses+Rust based MQTT viewer. Shows topics in a tree.
* Grays out stale (retained) values.
* Update screen on new messages (erratic age counters).
\ No newline at end of file
* Update screen on new messages (erratic age counters).
[package]
name = "mqttnc"
version = "0.1.0"
version = "0.2.0"
authors = ["clonejo <clonejo@shakik.de>"]
[dependencies]
......
......@@ -9,7 +9,7 @@ extern crate netopt;
use std::collections::BTreeMap;
use chrono::{DateTime, Local};
use chrono::{DateTime, Local, Duration};
fn main() {
use clap::{App, Arg};
......@@ -44,32 +44,45 @@ fn main() {
}
enum Age {
Retained,
Retained(DateTime<Local>),
Fresh(DateTime<Local>)
}
impl Age {
fn is_retained(&self) -> bool {
match *self {
Age::Retained => true,
Age::Retained(_) => true,
Age::Fresh(_) => false
}
}
fn get_minimum_age(&self) -> DateTime<Local> {
match *self {
Age::Retained(age) => age,
Age::Fresh(age) => age
}
}
fn format(&self, now: DateTime<Local>) -> String {
match *self {
Age::Fresh(t) => {
let d = now - t;
if d.num_minutes() == 0 {
format!("{}s", d.num_seconds())
} else if d.num_hours() == 0 {
let seconds = d.num_seconds() % 60;
format!("{}m{:0>2}s", d.num_minutes(), seconds)
let all_seconds = d.num_seconds();
let seconds = all_seconds % 60;
let all_minutes = all_seconds / 60;
let minutes = all_minutes % 60;
let hours = all_minutes / 60;
if hours > 0 {
format!("{}h{:0>2}m", hours, minutes)
} else if minutes > 0 {
format!("{}m{:0>2}s", minutes, seconds)
} else if seconds >= 0 {
format!("{}s", seconds)
} else {
"".to_owned()
format!("weird: {:?}", d)
}
},
Age::Retained => {
Age::Retained(_) => {
"r".to_owned()
}
}
......@@ -79,14 +92,16 @@ impl Age {
struct Topic {
children: BTreeMap<String, Topic>,
last_msg: Option<(String, Age)>
last_msg: Option<(String, Age)>,
interval: Option<Duration>
}
impl Topic {
fn new() -> Topic {
Topic {
children: BTreeMap::new(),
last_msg: None
last_msg: None,
interval: None
}
}
......@@ -100,12 +115,23 @@ impl Topic {
} else {
None
};
let c = self.children.entry(root_path.to_owned()).or_insert_with(|| Topic::new());
c.update(rest_path, payload, retained);
if rest_path == None && root_path == "_interval" {
match payload.parse::<i64>() {
Ok(interval) => {
self.interval = Some(Duration::seconds(interval));
},
Err(_) => {
self.interval = None;
}
}
} else {
let c = self.children.entry(root_path.to_owned()).or_insert_with(|| Topic::new());
c.update(rest_path, payload, retained);
}
},
None => {
let age = if retained {
Age::Retained
Age::Retained(Local::now())
} else {
Age::Fresh(Local::now())
};
......@@ -114,35 +140,70 @@ impl Topic {
}
}
fn print(&self, now: DateTime<Local>) {
fn print(&self, now: DateTime<Local>, start_time: DateTime<Local>) {
for (ref topic, ref child) in self.children.iter() {
child.print_(0, topic, now);
child.print_(0, topic, now, start_time);
}
}
fn print_(&self, indent_level: usize, topic: &str, now: DateTime<Local>) {
fn print_(&self, indent_level: usize, topic: &str, now: DateTime<Local>, start_time: DateTime<Local>) {
let custom_text;
if self.last_msg.is_none() && self.children.len() == 1 {
let (ref ctopic, ref child) = self.children.iter().next().unwrap();
let passed_topic = format!("{}/{}", topic, ctopic);
child.print_(indent_level, &passed_topic, now);
child.print_(indent_level, &passed_topic, now, start_time);
} else {
ncurses::printw(&*format!("{0:1$}{2:<20}{0:3$}", "", 2*indent_level, topic, 2*(8-indent_level)));
match self.last_msg {
let (mut color, age_str, payload) = match self.last_msg {
Some((ref p, ref t)) => {
if t.is_retained() {
ncurses::attron(ncurses::COLOR_PAIR(1));
let color = if t.is_retained() {
Some(ncurses::COLOR_PAIR(1))
} else {
None
};
let age = Some(t.format(now));
(color, age, Some(p))
},
None => {
if self.interval.is_some() {
custom_text = "[interval known]".to_owned();
(Some(ncurses::COLOR_PAIR(1)), None, Some(&custom_text))
} else {
(None, None, None)
}
ncurses::printw(&*format!(" {:>10} {}\n", t.format(now), p));
if t.is_retained() {
ncurses::attroff(ncurses::COLOR_PAIR(1));
}
};
// red text if we are sure it's stale
match (&self.interval, &self.last_msg) {
(&Some(interval), &Some((_, ref age))) => {
let received_at = age.get_minimum_age();
if received_at + interval < now {
color = Some(ncurses::COLOR_PAIR(2));
}
},
None => {
ncurses::printw("\n");
(&Some(interval), &None) => {
if start_time + interval < now {
color = Some(ncurses::COLOR_PAIR(2));
}
}
_ => {}
}
color.map(|cp| {
ncurses::attron(cp);
});
let age_str_ = match age_str {
Some(s) => s,
None => "".to_owned()
};
ncurses::printw(&*format!(" {:>10} {}\n", age_str_, payload.unwrap_or(&"".to_owned())));
color.map(|cp| {
ncurses::attroff(cp);
});
for (ref topic, ref child) in self.children.iter() {
child.print_(indent_level+1, topic, now);
child.print_(indent_level+1, topic, now, start_time);
}
}
}
......@@ -165,18 +226,20 @@ fn main_(server_str: String) -> Result<(), (i32, &'static str)> {
};
client.subscribe("#").unwrap();
println!("connected to mqtt server");
let start_time = Local::now();
let mut root_topic = Topic::new();
ncurses::initscr();
ncurses::start_color();
ncurses::init_pair(1, 8, ncurses::COLOR_BLACK);
ncurses::init_pair(2, ncurses::COLOR_RED, ncurses::COLOR_BLACK);
//ncurses::nonl();
loop {
match client.await().unwrap() {
Some(msg) => {
let payload = str::from_utf8(&**msg.payload).unwrap().to_owned();
let payload = str::from_utf8(&**msg.payload).unwrap().trim().to_owned();
root_topic.update(Some(&msg.topic.path), payload, msg.retain);
ncurses::mv(0, 0);
root_topic.print(Local::now());
root_topic.print(Local::now(), start_time);
ncurses::clrtoeol();
ncurses::refresh();
},
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment