Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • Volkor/yams
1 result
Show changes
Commits on Source (2)
...@@ -10,7 +10,7 @@ fern = "0.7.1" ...@@ -10,7 +10,7 @@ fern = "0.7.1"
log = "0.4.25" log = "0.4.25"
meshtastic = "0.1.6" meshtastic = "0.1.6"
tokio = "1.42.0" tokio = "1.42.0"
rusqlite = { version = "0.32.1", features = ["bundled"] } rusqlite = { version = "0.32.1", features = ["bundled", "backup"] }
rusqlite_migration = "1.3.1" rusqlite_migration = "1.3.1"
lazy_static = "1.5.0" lazy_static = "1.5.0"
figment = {version = "0.10.19", features = ["toml", "env"]} figment = {version = "0.10.19", features = ["toml", "env"]}
......
...@@ -53,6 +53,21 @@ pub struct GuiConfig { ...@@ -53,6 +53,21 @@ pub struct GuiConfig {
pub units: String 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 // Defaults
impl Default for ServerConfig { impl Default for ServerConfig {
......
...@@ -12,6 +12,10 @@ use meshtastic::protobufs::{Channel, DeviceMetrics, MeshPacket, MyNodeInfo, Node ...@@ -12,6 +12,10 @@ use meshtastic::protobufs::{Channel, DeviceMetrics, MeshPacket, MyNodeInfo, Node
use crate::CONFIG; 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 { pub(crate) struct Database {
conn: Connection, conn: Connection,
} }
...@@ -58,85 +62,99 @@ impl Database { ...@@ -58,85 +62,99 @@ impl Database {
// Open it up, and run migrations! // Open it up, and run migrations!
let mut conn = Connection::open(&db_path)?; let mut conn = Connection::open(&db_path)?;
// Apply migrations // 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 }) Ok(Database { conn })
} }
/// Updates the database with a new MyNodeInfo packet /// Updates the database with a new MyNodeInfo packet
pub fn update_mynodeinfo(my_node_info: &MyNodeInfo) -> Result<bool> { pub fn update_mynodeinfo(my_node_info: &MyNodeInfo) -> Result<bool> {
// Open DB // Open DB
let db = Database::open().expect("Failure to open database"); match Database::open() {
Ok(db) => {
db.conn.execute( db.conn.execute(
"INSERT INTO MyNodeInfo (my_node_num, reboot_count, min_app_version) VALUES (?1, ?2, ?3)", "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], rusqlite::params![my_node_info.my_node_num, my_node_info.reboot_count, my_node_info.min_app_version],
)?; )?;
debug!("Saved MyNodeInfo to database"); debug!("Saved MyNodeInfo to database");
Ok(true) Ok(true)
},
Err(e) => {
error!("Failed to open database: {}", e);
Ok(false)
},
}
} }
/// Updates the database with a new NodeInfo packet /// Updates the database with a new NodeInfo packet
pub fn update_nodeinfo(node_info: &NodeInfo) -> Result<bool> { pub fn update_nodeinfo(node_info: &NodeInfo) -> Result<bool> {
// Open DB // Open DB
let db = Database::open().expect("Failure to open database"); match Database::open() {
Ok(db) => {
db.conn.execute( db.conn.execute(
"INSERT INTO Node ( "INSERT INTO Node (
num, user_id, user_lname, user_sname, hw_model, is_licensed, role, num, user_id, user_lname, user_sname, hw_model, is_licensed, role,
latitude_i, longitude_i, altitude, location_source, gps_timestamp, latitude_i, longitude_i, altitude, location_source, gps_timestamp,
altitude_source, pdop, hdop, vdop, gps_accuracy, ground_speed, altitude_source, pdop, hdop, vdop, gps_accuracy, ground_speed,
ground_track, fix_quality, fix_type, sats_in_view, sensor_id, ground_track, fix_quality, fix_type, sats_in_view, sensor_id,
next_update, seq_number, precision_bits, snr, last_heard, next_update, seq_number, precision_bits, snr, last_heard,
battery_level, voltage, channel_utilization, air_util_tx, battery_level, voltage, channel_utilization, air_util_tx,
channel, via_mqtt, hops_away channel, via_mqtt, hops_away
) VALUES ( ) VALUES (
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?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, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26,
?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35 ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35
)", )",
rusqlite::params![ rusqlite::params![
node_info.num, node_info.num,
// This is gibberish to me, so lets explain. // 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. // 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.id.clone()),
node_info.user.as_ref().map(|user| user.long_name.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.short_name.clone()),
node_info.user.as_ref().map(|user| user.hw_model), 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.is_licensed),
node_info.user.as_ref().map(|user| user.role), 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.latitude_i),
node_info.position.as_ref().map(|position| position.longitude_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.altitude),
node_info.position.as_ref().map(|position| position.location_source), 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.timestamp),
node_info.position.as_ref().map(|position| position.altitude_source), 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.pdop),
node_info.position.as_ref().map(|position| position.hdop), 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.vdop),
node_info.position.as_ref().map(|position| position.gps_accuracy), 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_speed),
node_info.position.as_ref().map(|position| position.ground_track), 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_quality),
node_info.position.as_ref().map(|position| position.fix_type), 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.sats_in_view),
node_info.position.as_ref().map(|position| position.sensor_id), 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.next_update),
node_info.position.as_ref().map(|position| position.seq_number), node_info.position.as_ref().map(|position| position.seq_number),
node_info.position.as_ref().map(|position| position.precision_bits), node_info.position.as_ref().map(|position| position.precision_bits),
node_info.snr, node_info.snr,
node_info.last_heard, 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.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.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.channel_utilization),
node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.air_util_tx), node_info.device_metrics.as_ref().map(|device_metrics| device_metrics.air_util_tx),
node_info.channel, node_info.channel,
node_info.via_mqtt, node_info.via_mqtt,
node_info.hops_away node_info.hops_away
] ]
)?; )?;
debug!("Saved NodeInfo to database"); debug!("Saved NodeInfo to database");
Ok(true) Ok(true)
},
Err(e) => {
error!("Failed to open database: {}", e);
Ok(false)
},
}
} }
/// Updates the database with a new Channel packet /// Updates the database with a new Channel packet
......
...@@ -40,7 +40,9 @@ pub fn render_widget(widget_type: &WidgetType, ui: &mut egui::Ui) { ...@@ -40,7 +40,9 @@ pub fn render_widget(widget_type: &WidgetType, ui: &mut egui::Ui) {
WidgetType::NodeList => { WidgetType::NodeList => {
node_list::nodelist_ui(ui); node_list::nodelist_ui(ui);
}, },
WidgetType::NodeInfo => todo!(), WidgetType::NodeInfo => {
node_info::nodeinfo_ui(ui);
},
WidgetType::Log => { WidgetType::Log => {
logging::log_ui(ui); logging::log_ui(ui);
}, },
......
use chrono::DateTime; use chrono::DateTime;
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
use egui::Label;
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder};
use log::info;
use longitude::{DistanceUnit, Location}; 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; use super::node_list;
...@@ -22,122 +25,84 @@ use super::node_list; ...@@ -22,122 +25,84 @@ use super::node_list;
/// UI Elements for the Node List /// UI Elements for the Node List
pub fn nodeinfo_ui(ui: &mut egui::Ui) { pub fn nodeinfo_ui(ui: &mut egui::Ui) {
// Get nodes from DB // Get nodes from DB
let mut nodes = Database::read_node_list().expect("Failed to read nodes"); let nodes: Vec<NodeInfo> = Database::read_node_list().expect("Failed to read nodes");
// Sort nodes by `last_heard` in descending order info!("Nodes: {:?}", nodes);
nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
// Set the first node in the db to be selected
TableBuilder::new(ui) // This needs to be set elsewhere, but for testing... meh.
// TODO for the table: let mut selected_node = nodes.first().unwrap().num;
// - Clickable Table Sorting
// - Add more info // Add a dropdown selector for changing the node
// - Auto hide/show scrollbar on smaller devices egui::ComboBox::from_label("Selected Node")
// - Add to a panel that we can move around / shitty windowing. .selected_text(format!("{:?}", selected_node))
.column(Column::remainder()) // node id .show_ui(ui, |ui| {
.column(Column::remainder().clip(true)) // node name for node in &nodes {
.column(Column::remainder()) // snr ui.selectable_value(&mut selected_node, node.num, format!("{:?}", node.user.clone().unwrap().id));
.column(Column::remainder()) // dist }
.column(Column::remainder()) // hops }
.column(Column::remainder().clip(true)) // hw model );
.column(Column::remainder()) // role
.column(Column::remainder()) // battery // Iterate through the list until we find the selected node.
.column(Column::remainder().clip(true)) // last heard let mut found_selected_node = false;
.striped(true) for node in &nodes {
.header(15.0, |mut header| { if node.num == selected_node {
header.col(|ui| { // Render Info when found
ui.heading("Node ID"); ui.label(format!("Node: {:?}", node));
}); ui.label(format!("User ID: {}", node.user.clone().unwrap().id));
header.col(|ui| {
ui.heading("Name"); // Compute First Heard
}); // How will we do this? the database doesn't store first heard. TODO!
header.col(|ui| {
ui.heading("SNR"); // Compute Last Heard
}); let user = node.user.clone().unwrap();
header.col(|ui| { let device = node.device_metrics.clone().unwrap();
// Convert the last heard to a time, then make it nice to read.
// Get distance units let now = chrono::Local::now().to_utc();
let unit = match CONFIG.gui.units.as_ref() { let humanised = if node.last_heard == 0 {
"Centimeters" => "cm", String::from("Unknown")
"Meters" => "m", } else {
"Kilometers" => "km", let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
"Inches" => "in", let duration = last_heard - now;
"Feet" => "ft", HumanTime::from(duration).to_string()
"Yards" => "yd", };
"Miles" => "mi", ui.label(format!("Last Heard: {}", humanised));
_ => "km", // Default to km if no match found.
}; // Find Messages to or from said node
// TODO: Requires parsing all MeshPackets in the DB
ui.heading(format!("Distance ({})", unit)); // Find all Battery stats from said node
}); // TODO: Requires parsing all MeshPackets in the DB
header.col(|ui| {
ui.heading("Hops Away"); // Find all temperature stats from said node
}); // TODO: Requires parsing all MeshPackets in the DB
header.col(|ui| {
ui.heading("HW Model"); // Find Location history from said node
}); // TODO: Requires parsing all MeshPackets in the DB
header.col(|ui| { // Perhaps with the 3 above, we parse all packets to or from the node, and then filter depending on the above 4
ui.heading("Role");
}); // Find distance to node.
header.col(|ui| { if let Some(distance) = calc_distance_to_node(node.position.clone().unwrap()) {
ui.heading("Battery"); if distance.is_nan() {
}); ui.label("-");
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")
} else { } else {
let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap(); // Get the unit shorthand from the GuiConfig struct
let duration = last_heard - now; let unit_shorthand = CONFIG.gui.get_unit_shorthand().unwrap_or("");
HumanTime::from(duration).to_string() ui.label(format!("Distance to Node: {:.2}{}", distance, unit_shorthand));
}; }
} else {
body.row(15.0, |mut row| { ui.label("-");
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);
});
});
} }
});
// 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
use chrono::DateTime; use chrono::DateTime;
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder};
use figment::providers::Data;
use log::{error, info};
use longitude::{DistanceUnit, Location}; use longitude::{DistanceUnit, Location};
use meshtastic::protobufs::{channel::Role, HardwareModel, Position}; use meshtastic::protobufs::{channel::Role, HardwareModel, Position};
...@@ -45,7 +47,15 @@ pub(crate) fn calc_distance_to_node(away: Position) -> Option<f64> { ...@@ -45,7 +47,15 @@ pub(crate) fn calc_distance_to_node(away: Position) -> Option<f64> {
/// UI Elements for the Node List /// UI Elements for the Node List
pub fn nodelist_ui(ui: &mut egui::Ui) { pub fn nodelist_ui(ui: &mut egui::Ui) {
// Get nodes from DB // Get nodes from DB
let mut nodes = Database::read_node_list().expect("Failed to read nodes"); let mut nodes = Database::read_node_list().unwrap_or_else(|e| {
error!("Failed to read nodes: {}", e);
// Return a default value or handle the error as needed
Vec::new() // Example default value, replace with appropriate handling
});
info!("Loaded {} nodes from DB", nodes.len());
// let mut nodes = Database::read_node_list().expect("Failed to read nodes");
// Sort nodes by `last_heard` in descending order // Sort nodes by `last_heard` in descending order
nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard)); nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
......