diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a07d391f7..c58290f1b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2569,6 +2569,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" @@ -3165,6 +3175,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nvml-wrapper" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" +dependencies = [ + "bitflags 2.6.0", + "libloading", + "nvml-wrapper-sys", + "static_assertions", + "thiserror", + "wrapcenum-derive", +] + +[[package]] +name = "nvml-wrapper-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "698d45156f28781a4e79652b6ebe2eaa0589057d588d3aec1333f6466f13fcb5" +dependencies = [ + "libloading", +] + [[package]] name = "objc" version = "0.2.7" @@ -5150,6 +5183,7 @@ dependencies = [ "minotari_node_grpc_client", "minotari_wallet_grpc_client", "nix", + "nvml-wrapper", "open 5.3.0", "rand 0.8.5", "reqwest 0.12.5", @@ -7268,6 +7302,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wrapcenum-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "wry" version = "0.24.10" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 37b60c598..268c07fe1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -67,6 +67,7 @@ libsqlite3-sys = { version = "0.25.1", features = ["bundled"] } log = "0.4.22" rand = "0.8.5" device_query = "2.1.0" +nvml-wrapper = "0.10.0" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/src-tauri/src/cpu_miner.rs b/src-tauri/src/cpu_miner.rs index 4bd333013..13da39a16 100644 --- a/src-tauri/src/cpu_miner.rs +++ b/src-tauri/src/cpu_miner.rs @@ -3,11 +3,12 @@ use crate::mm_proxy_manager::MmProxyManager; use crate::xmrig::http_api::XmrigHttpApiClient; use crate::xmrig_adapter::{XmrigAdapter, XmrigNodeConnection}; use crate::{ - CpuMinerConfig, CpuMinerConnection, CpuMinerConnectionStatus, CpuMinerStatus, ProgressTracker, + CpuCoreTemperature, CpuMinerConfig, CpuMinerConnection, CpuMinerConnectionStatus, + CpuMinerStatus, ProgressTracker, }; use log::warn; use std::path::PathBuf; -use sysinfo::{CpuRefreshKind, RefreshKind, System}; +use sysinfo::{Component, Components, CpuRefreshKind, RefreshKind, System}; use tari_core::transactions::tari_amount::MicroMinotari; use tari_shutdown::{Shutdown, ShutdownSignal}; use tauri::async_runtime::JoinHandle; @@ -26,6 +27,7 @@ pub(crate) struct CpuMiner { watcher_task: Option>>, miner_shutdown: Shutdown, api_client: Option, + cpu_temperatures: Vec, } impl CpuMiner { @@ -34,6 +36,7 @@ impl CpuMiner { watcher_task: None, miner_shutdown: Shutdown::new(), api_client: None, + cpu_temperatures: Vec::new(), } } @@ -156,10 +159,60 @@ impl CpuMiner { } pub async fn status( - &self, + &mut self, network_hash_rate: u64, block_reward: MicroMinotari, ) -> Result { + let components = Components::new_with_refreshed_list(); + + let cpu_components: Vec<&Component> = components + .iter() + .filter(|component| component.label().contains("Core")) + .collect(); + + let mut cpu_temperatures: Vec = cpu_components + .iter() + .map(|component| CpuCoreTemperature { + id: component + .label() + .split(" ") + .last() + .unwrap() + .parse() + .unwrap(), + label: component + .label() + .split(" ") + .skip(1) + .collect::>() + .join(" ") + .to_string(), + temperature: component.temperature(), + max_temperature: component.max(), + }) + .collect(); + + cpu_temperatures.sort(); + + if self.cpu_temperatures.is_empty() { + self.cpu_temperatures = cpu_temperatures.clone() + } else { + for (i, component) in cpu_temperatures.clone().iter().enumerate() { + let position = self + .cpu_temperatures + .iter() + .position(|x| x.id == component.id) + .unwrap(); + self.cpu_temperatures[position].temperature = component.temperature; + if component.temperature > self.cpu_temperatures[position].max_temperature { + self.cpu_temperatures[position].max_temperature = + self.cpu_temperatures[i].temperature; + } + } + } + + self.cpu_temperatures.sort(); + let mut s = System::new_with_specifics(RefreshKind::new().with_cpu(CpuRefreshKind::everything())); @@ -171,6 +224,7 @@ impl CpuMiner { let cpu_brand = s.cpus().get(0).map(|cpu| cpu.brand()).unwrap_or("Unknown"); let cpu_usage = s.global_cpu_usage() as u32; + // let cpu_temperature = s. match &self.api_client { Some(client) => { @@ -192,6 +246,7 @@ impl CpuMiner { && xmrig_status.hashrate.total[0].unwrap() > 0.0, hash_rate, cpu_usage: cpu_usage as u32, + cpu_temperatures: self.cpu_temperatures.clone(), cpu_brand: cpu_brand.to_string(), estimated_earnings: MicroMinotari(estimated_earnings).as_u64(), connection: CpuMinerConnectionStatus { @@ -207,6 +262,7 @@ impl CpuMiner { None => Ok(CpuMinerStatus { is_mining: false, hash_rate: 0.0, + cpu_temperatures: self.cpu_temperatures.clone(), cpu_usage: cpu_usage as u32, cpu_brand: cpu_brand.to_string(), estimated_earnings: 0, diff --git a/src-tauri/src/gpu_miner.rs b/src-tauri/src/gpu_miner.rs new file mode 100644 index 000000000..cee3d9d63 --- /dev/null +++ b/src-tauri/src/gpu_miner.rs @@ -0,0 +1,81 @@ +use log::info; +use nvml_wrapper::{ + enum_wrappers::device::{TemperatureSensor, TemperatureThreshold}, + Nvml, +}; + +use crate::{GpuMinerHardwareStatus, GpuMinerStatus}; + +const LOG_TARGET: &str = "tari::universe::cpu_miner"; + +pub(crate) struct GpuMiner { + nvml: Nvml, + status: GpuMinerStatus, +} + +impl GpuMiner { + pub fn new() -> Self { + Self { + nvml: Nvml::init().unwrap(), + status: GpuMinerStatus::from(GpuMinerStatus { + hardware_statuses: Vec::new(), + }), + } + } + + pub fn start(&mut self) { + info!(target: LOG_TARGET, "Starting GPU miner"); + // Start the GPU miner + } + + pub fn stop(&mut self) { + info!(target: LOG_TARGET, "Stopping GPU miner"); + // Stop the GPU miner + } + + pub fn status(&mut self) -> GpuMinerStatus { + if self.status.hardware_statuses.is_empty() { + let devices_count = self.nvml.device_count().unwrap(); + + for i in 0..devices_count { + let device = self.nvml.device_by_index(i).unwrap(); + + let uuid = device.uuid().unwrap(); + let name = device.name().unwrap(); + let temperature = device.temperature(TemperatureSensor::Gpu).unwrap(); + let load = device.utilization_rates().unwrap().gpu; + + self.status.hardware_statuses.push(GpuMinerHardwareStatus { + uuid, + name, + temperature, + max_temperature: temperature, + load, + }); + } + } + + self.status.hardware_statuses = self + .status + .hardware_statuses + .iter() + .map(|status| { + let device = self.nvml.device_by_uuid(status.uuid.clone()).unwrap(); + + let temperature = device.temperature(TemperatureSensor::Gpu).unwrap(); + let load = device.utilization_rates().unwrap().gpu; + let max_temperature = status.max_temperature.max(temperature); + + GpuMinerHardwareStatus { + uuid: status.uuid.clone(), + name: status.name.clone(), + temperature, + max_temperature, + load, + } + }) + .collect(); + + self.status.clone() + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index cdd8d4170..e7eb282e9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod cpu_miner; +mod gpu_miner; mod mm_proxy_manager; mod process_watcher; mod user_listener; @@ -23,6 +24,7 @@ mod process_killer; mod wallet_adapter; use crate::cpu_miner::CpuMiner; +use crate::gpu_miner::GpuMiner; use crate::internal_wallet::InternalWallet; use crate::mm_proxy_manager::MmProxyManager; use crate::node_manager::NodeManager; @@ -365,7 +367,8 @@ async fn get_applications_versions(app: tauri::AppHandle) -> Result) -> Result { - let cpu_miner = state.cpu_miner.read().await; + let mut cpu_miner = state.cpu_miner.write().await; + let mut gpu_miner = state.gpu_miner.write().await; let (_sha_hash_rate, randomx_hash_rate, block_reward, block_height, block_time, is_synced) = state .node_manager @@ -400,10 +403,13 @@ async fn status(state: tauri::State<'_, UniverseAppState>) -> Result) -> Result, pub cpu_brand: String, pub estimated_earnings: u64, pub connection: CpuMinerConnectionStatus, @@ -465,10 +472,50 @@ struct CpuMinerConfig { tari_address: TariAddress, } +#[derive(Debug, Serialize, PartialEq, Clone)] +pub struct CpuCoreTemperature { + pub id: u32, + pub label: String, + pub temperature: f32, + pub max_temperature: f32, +} + +impl Eq for CpuCoreTemperature { + fn assert_receiver_is_total_eq(&self) { + self.id.assert_receiver_is_total_eq(); + } +} + +impl Ord for CpuCoreTemperature { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for CpuCoreTemperature { + fn partial_cmp(&self, other: &Self) -> Option { + self.id.partial_cmp(&other.id) + } +} + +#[derive(Debug, Serialize, Clone)] +struct GpuMinerStatus { + hardware_statuses: Vec, +} +#[derive(Debug, Serialize, Clone)] +struct GpuMinerHardwareStatus { + uuid: String, + temperature: u32, + max_temperature: u32, + name: String, + load: u32, +} + struct UniverseAppState { config: Arc>, shutdown: Shutdown, cpu_miner: RwLock, + gpu_miner: RwLock, cpu_miner_config: Arc>, user_listener: Arc>, mm_proxy_manager: MmProxyManager, @@ -500,6 +547,7 @@ fn main() { config: app_config.clone(), shutdown: shutdown.clone(), cpu_miner: CpuMiner::new().into(), + gpu_miner: GpuMiner::new().into(), cpu_miner_config: cpu_config.clone(), user_listener: Arc::new(RwLock::new(UserListener::new())), mm_proxy_manager: mm_proxy_manager.clone(), diff --git a/src/containers/SideBar/components/Heading.tsx b/src/containers/SideBar/components/Heading.tsx index 93861447e..5554f8fa2 100644 --- a/src/containers/SideBar/components/Heading.tsx +++ b/src/containers/SideBar/components/Heading.tsx @@ -1,6 +1,6 @@ import { Stack, Typography, IconButton } from '@mui/material'; import { CgArrowsExpandRight, CgCompressRight } from 'react-icons/cg'; -import SettingsDialog from './Settings'; +import SettingsDialog from './Settings/Settings.tsx'; import { useUIStore } from '../../../store/useUIStore.ts'; function Heading() { diff --git a/src/containers/SideBar/components/Settings/Card.component.tsx b/src/containers/SideBar/components/Settings/Card.component.tsx new file mode 100644 index 000000000..4df26f5a1 --- /dev/null +++ b/src/containers/SideBar/components/Settings/Card.component.tsx @@ -0,0 +1,25 @@ +import { Stack, Typography } from '@mui/material'; +import { CardItem } from './Settings.styles'; + +export interface CardComponentProps { + heading: string; + labels: { labelText: string; labelValue: string }[]; +} + +export const CardComponent = ({ heading, labels }: CardComponentProps) => { + return ( + + {heading} + + {labels.map(({ labelText, labelValue }) => ( + + {labelText}: + + {labelValue} + + + ))} + + + ); +}; diff --git a/src/containers/SideBar/components/Settings/Settings.styles.tsx b/src/containers/SideBar/components/Settings/Settings.styles.tsx new file mode 100644 index 000000000..d27417818 --- /dev/null +++ b/src/containers/SideBar/components/Settings/Settings.styles.tsx @@ -0,0 +1,14 @@ +import { Box, Stack, styled } from '@mui/material'; + +export const CardContainer = styled(Box)(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: theme.spacing(1), +})); + +export const CardItem = styled(Stack)(({ theme }) => ({ + padding: theme.spacing(1, 1.5), + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + boxShadow: '0px 4px 45px 0px rgba(0, 0, 0, 0.08)', +})); diff --git a/src/containers/SideBar/components/Settings.tsx b/src/containers/SideBar/components/Settings/Settings.tsx similarity index 58% rename from src/containers/SideBar/components/Settings.tsx rename to src/containers/SideBar/components/Settings/Settings.tsx index e6dcc78dc..7ed83d246 100644 --- a/src/containers/SideBar/components/Settings.tsx +++ b/src/containers/SideBar/components/Settings/Settings.tsx @@ -15,10 +15,13 @@ import { Tooltip, } from '@mui/material'; import { IoSettingsOutline, IoClose } from 'react-icons/io5'; -import { useGetSeedWords } from '../../../hooks/useGetSeedWords'; -import truncateString from '../../../utils/truncateString'; +import { useGetSeedWords } from '../../../../hooks/useGetSeedWords'; +import truncateString from '../../../../utils/truncateString'; import { invoke } from '@tauri-apps/api/tauri'; -import { useGetApplicatonsVersions } from '../../../hooks/useGetApplicatonsVersions'; +import { useGetApplicatonsVersions } from '../../../../hooks/useGetApplicatonsVersions'; +import { useAppStatusStore } from '../../../../store/useAppStatusStore'; +import { CardContainer } from './Settings.styles'; +import { CardComponent } from './Card.component'; const Settings: React.FC = () => { const { refreshVersions, applicationsVersions, mainAppVersion } = @@ -29,6 +32,13 @@ const Settings: React.FC = () => { const [isCopyTooltipHidden, setIsCopyTooltipHidden] = useState(true); const { seedWords, getSeedWords, seedWordsFetched, seedWordsFetching } = useGetSeedWords(); + const cpuTemperatures = useAppStatusStore( + (state) => state.cpu?.cpu_temperatures + ); + + const gpuHardwareStatuses = useAppStatusStore( + (state) => state.gpu?.hardware_statuses + ); const handleClickOpen = () => setOpen(true); const handleClose = () => { @@ -171,9 +181,10 @@ const Settings: React.FC = () => { {applicationsVersions && ( - + Versions @@ -181,17 +192,93 @@ const Settings: React.FC = () => { Refresh Versions - - mainApp: {mainAppVersion} - {Object.entries(applicationsVersions).map( - ([key, value]) => ( - - {key}: {value} - - ) - )} + + + + {Object.entries(applicationsVersions).map( + ([key, value]) => ( + + ) + )} + )} + + + + Hardware Temperatures + + + {gpuHardwareStatuses && + gpuHardwareStatuses.map((gpu) => ( + + ))} + {cpuTemperatures && + cpuTemperatures.map((core) => ( + + ))} + + +