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"
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
use chrono::DateTime;
use chrono_humanize::HumanTime;
use egui_extras::{Column, TableBuilder};
use figment::providers::Data;
use log::{error, info};
use longitude::{DistanceUnit, Location};
use meshtastic::protobufs::{channel::Role, HardwareModel, Position};
......@@ -45,7 +47,15 @@ pub(crate) fn calc_distance_to_node(away: Position) -> Option<f64> {
/// UI Elements for the Node List
pub fn nodelist_ui(ui: &mut egui::Ui) {
// 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
nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
......