Skip to content
Snippets Groups Projects
Verified Commit b31a65c4 authored by Volkor Barbarian Warrior's avatar Volkor Barbarian Warrior
Browse files

add base node info view

parent 0e62abfa
No related merge requests found
......@@ -10,7 +10,7 @@ fern = "0.7.1"
log = "0.4.25"
meshtastic = "0.1.6"
tokio = "1.42.0"
rusqlite = { version = "0.32.1", features = ["bundled"] }
rusqlite = { version = "0.32.1", features = ["bundled", "backup"] }
rusqlite_migration = "1.3.1"
lazy_static = "1.5.0"
figment = {version = "0.10.19", features = ["toml", "env"]}
......
......@@ -53,6 +53,21 @@ pub struct GuiConfig {
pub units: String
}
impl GuiConfig {
pub fn get_unit_shorthand(&self) -> Option<&'static str> {
match self.units.to_lowercase().as_str() {
"centimeters" => Some("cm"),
"meters" => Some("m"),
"kilometers" => Some("km"),
"inches" => Some("inch"),
"feet" => Some("ft"),
"yards" => Some("yd"),
"miles" => Some("mi"),
_ => None,
}
}
}
// Defaults
impl Default for ServerConfig {
......
......@@ -12,6 +12,10 @@ use meshtastic::protobufs::{Channel, DeviceMetrics, MeshPacket, MyNodeInfo, Node
use crate::CONFIG;
// General TODO:
// Spin this off into a thread? Have some form of caching? reading from the DB every single frame seems a little... retarded.
// Do we have separate dbs for different types of MeshPackets? (table for each type? each node? )
pub(crate) struct Database {
conn: Connection,
}
......@@ -58,85 +62,99 @@ impl Database {
// Open it up, and run migrations!
let mut conn = Connection::open(&db_path)?;
// Apply migrations
MIGRATIONS.to_latest(&mut conn).expect("Migration failed");
if let Err(e) = MIGRATIONS.to_latest(&mut conn) {
error!("DB Migration failed: {}", e);
}
Ok(Database { conn })
}
/// Updates the database with a new MyNodeInfo packet
pub fn update_mynodeinfo(my_node_info: &MyNodeInfo) -> Result<bool> {
// Open DB
let db = Database::open().expect("Failure to open database");
db.conn.execute(
"INSERT INTO MyNodeInfo (my_node_num, reboot_count, min_app_version) VALUES (?1, ?2, ?3)",
rusqlite::params![my_node_info.my_node_num, my_node_info.reboot_count, my_node_info.min_app_version],
)?;
debug!("Saved MyNodeInfo to database");
Ok(true)
match Database::open() {
Ok(db) => {
db.conn.execute(
"INSERT INTO MyNodeInfo (my_node_num, reboot_count, min_app_version) VALUES (?1, ?2, ?3)",
rusqlite::params![my_node_info.my_node_num, my_node_info.reboot_count, my_node_info.min_app_version],
)?;
debug!("Saved MyNodeInfo to database");
Ok(true)
},
Err(e) => {
error!("Failed to open database: {}", e);
Ok(false)
},
}
}
/// Updates the database with a new NodeInfo packet
pub fn update_nodeinfo(node_info: &NodeInfo) -> Result<bool> {
// Open DB
let db = Database::open().expect("Failure to open database");
db.conn.execute(
"INSERT INTO Node (
num, user_id, user_lname, user_sname, hw_model, is_licensed, role,
latitude_i, longitude_i, altitude, location_source, gps_timestamp,
altitude_source, pdop, hdop, vdop, gps_accuracy, ground_speed,
ground_track, fix_quality, fix_type, sats_in_view, sensor_id,
next_update, seq_number, precision_bits, snr, last_heard,
battery_level, voltage, channel_utilization, air_util_tx,
channel, via_mqtt, hops_away
) VALUES (
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26,
?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35
)",
rusqlite::params![
node_info.num,
// This is gibberish to me, so lets explain.
// We're mapping the value user.id as the value, but chucking a NULL in if it's a None type.
node_info.user.as_ref().map(|user| user.id.clone()),
node_info.user.as_ref().map(|user| user.long_name.clone()),
node_info.user.as_ref().map(|user| user.short_name.clone()),
node_info.user.as_ref().map(|user| user.hw_model),
node_info.user.as_ref().map(|user| user.is_licensed),
node_info.user.as_ref().map(|user| user.role),
node_info.position.as_ref().map(|position| position.latitude_i),
node_info.position.as_ref().map(|position| position.longitude_i),
node_info.position.as_ref().map(|position| position.altitude),
node_info.position.as_ref().map(|position| position.location_source),
node_info.position.as_ref().map(|position| position.timestamp),
node_info.position.as_ref().map(|position| position.altitude_source),
node_info.position.as_ref().map(|position| position.pdop),
node_info.position.as_ref().map(|position| position.hdop),
node_info.position.as_ref().map(|position| position.vdop),
node_info.position.as_ref().map(|position| position.gps_accuracy),
node_info.position.as_ref().map(|position| position.ground_speed),
node_info.position.as_ref().map(|position| position.ground_track),
node_info.position.as_ref().map(|position| position.fix_quality),
node_info.position.as_ref().map(|position| position.fix_type),
node_info.position.as_ref().map(|position| position.sats_in_view),
node_info.position.as_ref().map(|position| position.sensor_id),
node_info.position.as_ref().map(|position| position.next_update),
node_info.position.as_ref().map(|position| position.seq_number),
node_info.position.as_ref().map(|position| position.precision_bits),
node_info.snr,
node_info.last_heard,
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.battery_level),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.voltage),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.channel_utilization),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.air_util_tx),
node_info.channel,
node_info.via_mqtt,
node_info.hops_away
]
)?;
debug!("Saved NodeInfo to database");
Ok(true)
match Database::open() {
Ok(db) => {
db.conn.execute(
"INSERT INTO Node (
num, user_id, user_lname, user_sname, hw_model, is_licensed, role,
latitude_i, longitude_i, altitude, location_source, gps_timestamp,
altitude_source, pdop, hdop, vdop, gps_accuracy, ground_speed,
ground_track, fix_quality, fix_type, sats_in_view, sensor_id,
next_update, seq_number, precision_bits, snr, last_heard,
battery_level, voltage, channel_utilization, air_util_tx,
channel, via_mqtt, hops_away
) VALUES (
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26,
?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35
)",
rusqlite::params![
node_info.num,
// This is gibberish to me, so lets explain.
// We're mapping the value user.id as the value, but chucking a NULL in if it's a None type.
node_info.user.as_ref().map(|user| user.id.clone()),
node_info.user.as_ref().map(|user| user.long_name.clone()),
node_info.user.as_ref().map(|user| user.short_name.clone()),
node_info.user.as_ref().map(|user| user.hw_model),
node_info.user.as_ref().map(|user| user.is_licensed),
node_info.user.as_ref().map(|user| user.role),
node_info.position.as_ref().map(|position| position.latitude_i),
node_info.position.as_ref().map(|position| position.longitude_i),
node_info.position.as_ref().map(|position| position.altitude),
node_info.position.as_ref().map(|position| position.location_source),
node_info.position.as_ref().map(|position| position.timestamp),
node_info.position.as_ref().map(|position| position.altitude_source),
node_info.position.as_ref().map(|position| position.pdop),
node_info.position.as_ref().map(|position| position.hdop),
node_info.position.as_ref().map(|position| position.vdop),
node_info.position.as_ref().map(|position| position.gps_accuracy),
node_info.position.as_ref().map(|position| position.ground_speed),
node_info.position.as_ref().map(|position| position.ground_track),
node_info.position.as_ref().map(|position| position.fix_quality),
node_info.position.as_ref().map(|position| position.fix_type),
node_info.position.as_ref().map(|position| position.sats_in_view),
node_info.position.as_ref().map(|position| position.sensor_id),
node_info.position.as_ref().map(|position| position.next_update),
node_info.position.as_ref().map(|position| position.seq_number),
node_info.position.as_ref().map(|position| position.precision_bits),
node_info.snr,
node_info.last_heard,
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.battery_level),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.voltage),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.channel_utilization),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.air_util_tx),
node_info.channel,
node_info.via_mqtt,
node_info.hops_away
]
)?;
debug!("Saved NodeInfo to database");
Ok(true)
},
Err(e) => {
error!("Failed to open database: {}", e);
Ok(false)
},
}
}
/// Updates the database with a new Channel packet
......
......@@ -40,7 +40,9 @@ pub fn render_widget(widget_type: &WidgetType, ui: &mut egui::Ui) {
WidgetType::NodeList => {
node_list::nodelist_ui(ui);
},
WidgetType::NodeInfo => todo!(),
WidgetType::NodeInfo => {
node_info::nodeinfo_ui(ui);
},
WidgetType::Log => {
logging::log_ui(ui);
},
......
use chrono::DateTime;
use chrono_humanize::HumanTime;
use egui::Label;
use egui_extras::{Column, TableBuilder};
use log::info;
use longitude::{DistanceUnit, Location};
use meshtastic::protobufs::{channel::Role, HardwareModel, Position};
use meshtastic::{protobufs::{channel::Role, HardwareModel, Position}, types::NodeId};
use meshtastic::protobufs::{Channel, DeviceMetrics, MeshPacket, MyNodeInfo, NodeInfo, User};
use crate::{db::Database, CONFIG};
use crate::{db::Database, gui::widgets::node_list::calc_distance_to_node, nodes, CONFIG};
use super::node_list;
......@@ -22,122 +25,84 @@ use super::node_list;
/// UI Elements for the Node List
pub fn nodeinfo_ui(ui: &mut egui::Ui) {
// Get nodes from DB
let mut nodes = Database::read_node_list().expect("Failed to read nodes");
// Sort nodes by `last_heard` in descending order
nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
TableBuilder::new(ui)
// TODO for the table:
// - Clickable Table Sorting
// - Add more info
// - Auto hide/show scrollbar on smaller devices
// - Add to a panel that we can move around / shitty windowing.
.column(Column::remainder()) // node id
.column(Column::remainder().clip(true)) // node name
.column(Column::remainder()) // snr
.column(Column::remainder()) // dist
.column(Column::remainder()) // hops
.column(Column::remainder().clip(true)) // hw model
.column(Column::remainder()) // role
.column(Column::remainder()) // battery
.column(Column::remainder().clip(true)) // last heard
.striped(true)
.header(15.0, |mut header| {
header.col(|ui| {
ui.heading("Node ID");
});
header.col(|ui| {
ui.heading("Name");
});
header.col(|ui| {
ui.heading("SNR");
});
header.col(|ui| {
// Get distance units
let unit = match CONFIG.gui.units.as_ref() {
"Centimeters" => "cm",
"Meters" => "m",
"Kilometers" => "km",
"Inches" => "in",
"Feet" => "ft",
"Yards" => "yd",
"Miles" => "mi",
_ => "km", // Default to km if no match found.
};
let nodes: Vec<NodeInfo> = Database::read_node_list().expect("Failed to read nodes");
info!("Nodes: {:?}", nodes);
// Set the first node in the db to be selected
// This needs to be set elsewhere, but for testing... meh.
let mut selected_node = nodes.first().unwrap().num;
// Add a dropdown selector for changing the node
egui::ComboBox::from_label("Selected Node")
.selected_text(format!("{:?}", selected_node))
.show_ui(ui, |ui| {
for node in &nodes {
ui.selectable_value(&mut selected_node, node.num, format!("{:?}", node.user.clone().unwrap().id));
}
}
);
// Iterate through the list until we find the selected node.
let mut found_selected_node = false;
for node in &nodes {
if node.num == selected_node {
// Render Info when found
ui.label(format!("Node: {:?}", node));
ui.label(format!("User ID: {}", node.user.clone().unwrap().id));
// Compute First Heard
// How will we do this? the database doesn't store first heard. TODO!
// Compute Last Heard
let user = node.user.clone().unwrap();
let device = node.device_metrics.clone().unwrap();
// Convert the last heard to a time, then make it nice to read.
let now = chrono::Local::now().to_utc();
let humanised = if node.last_heard == 0 {
String::from("Unknown")
} else {
let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
let duration = last_heard - now;
HumanTime::from(duration).to_string()
};
ui.label(format!("Last Heard: {}", humanised));
// Find Messages to or from said node
// TODO: Requires parsing all MeshPackets in the DB
ui.heading(format!("Distance ({})", unit));
});
header.col(|ui| {
ui.heading("Hops Away");
});
header.col(|ui| {
ui.heading("HW Model");
});
header.col(|ui| {
ui.heading("Role");
});
header.col(|ui| {
ui.heading("Battery");
});
header.col(|ui| {
ui.heading("Last Heard");
});
})
.body(|mut body| {
for node in nodes {
let user = node.user.unwrap();
let device = node.device_metrics.unwrap();
// Convert the last heard to a time, then make it nice to read.
let now = chrono::Local::now().to_utc();
let humanised = if node.last_heard == 0 {
String::from("Unknown")
// Find all Battery stats from said node
// TODO: Requires parsing all MeshPackets in the DB
// Find all temperature stats from said node
// TODO: Requires parsing all MeshPackets in the DB
// Find Location history from said node
// TODO: Requires parsing all MeshPackets in the DB
// Perhaps with the 3 above, we parse all packets to or from the node, and then filter depending on the above 4
// Find distance to node.
if let Some(distance) = calc_distance_to_node(node.position.clone().unwrap()) {
if distance.is_nan() {
ui.label("-");
} else {
let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
let duration = last_heard - now;
HumanTime::from(duration).to_string()
};
body.row(15.0, |mut row| {
row.col(|ui| {
ui.label(user.id);
});
row.col(|ui| {
ui.label(format!("{} ({})", user.long_name, user.short_name));
});
row.col(|ui| {
ui.label(node.snr.to_string());
});
row.col(|ui| {
if let Some(distance) = node_list::calc_distance_to_node(node.position.unwrap()) {
if distance.is_nan() {
ui.label("-");
} else {
ui.label(format!("{:.2}", distance));
}
} else {
ui.label("-");
}
});
row.col(|ui| {
ui.label(node.hops_away.to_string());
});
row.col(|ui| {
// Convert the i32 to a HardwareModel object, then convert to string
ui.label(HardwareModel::from_i32(user.hw_model).unwrap_or_default().as_str_name());
});
row.col(|ui| {
// Convert the i32 to a Role object, convert to string.
ui.label(Role::from_i32(user.role).unwrap_or_default().as_str_name());
});
row.col(|ui| {
ui.label(device.battery_level.to_string());
});
row.col(|ui| {
ui.label(humanised);
});
});
// Get the unit shorthand from the GuiConfig struct
let unit_shorthand = CONFIG.gui.get_unit_shorthand().unwrap_or("");
ui.label(format!("Distance to Node: {:.2}{}", distance, unit_shorthand));
}
} else {
ui.label("-");
}
});
// Find Direct (radio) Neighbours of said node
found_selected_node = true;
break; // Exit loop once the selected node is found
}
}
if !found_selected_node {
ui.label("Selected node not found.");
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment