├── myrktop ├── LICENSE ├── README.md └── myrktop.py /myrktop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run monitoring script in the background 4 | python3 ~/myrktop.py 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Misha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🖥️ myrktop - Orange Pi 5 (RK3588) System Monitor COLORED BRANCH 2 | 3 | 🔥 **myrktop** is a lightweight system monitor for **Orange Pi 5 (RK3588)**, providing real-time information about **CPU, GPU, NPU, RAM, RGA, and system temperatures**. 4 | 5 | ## **📥 Installation Instructions** 6 | ### **1️⃣ Install Required Dependencies** 7 | Before running the script, install dependencies to fetch readings: 8 | ```bash 9 | sudo apt update && sudo apt install -y python3 python3-pip lm-sensors smartmontools nvme-cli && sudo sensors-detect --auto && pip3 install urwid 10 | ``` 11 | 12 | ### **2️⃣ Download and Install myrktop** 13 | Run the following command to download and install the script: 14 | ```bash 15 | wget -O ~/myrktop.py https://raw.githubusercontent.com/mhl221135/myrktop/refs/heads/main/myrktop.py 16 | wget -O /usr/local/bin/myrktop https://raw.githubusercontent.com/mhl221135/myrktop/refs/heads/main/myrktop 17 | ``` 18 | Then, make the script executable: 19 | ```bash 20 | sudo chmod +x /usr/local/bin/myrktop 21 | ``` 22 | 23 | ### **3️⃣ Run the Monitoring Script** 24 | To run the script use: 25 | ```bash 26 | myrktop 27 | ``` 28 | 29 | --- 30 | 31 | ## **📊 Features** 32 | - **Real-time CPU load & frequency monitoring (per core)** 33 | - **Live GPU usage & frequency** 34 | - **NPU & RGA usage** 35 | - **RAM & Swap usage** 36 | - **System temperature readings** 37 | - **Network interfaces: Down/Up readings** 38 | - **Storage Usage (/etc/fstab)** 39 | - **NVMe & ATA Storage Info:** 40 | 41 | 42 | --- 43 | 44 | ## **📌 Example Output** 45 | ```bash 46 | ────────────────────────────────────────────────── 47 | 🔥 System Monitor 48 | ────────────────────────────────────────────────── 49 | Device: rockchip,rk3588s-orangepi-5rockchip,rk3588 50 | NPU Version: RKNPU driver: v0.9.8 51 | System Uptime: up 17 hours, 30 minutes 52 | Docker Status: Running ✅ 53 | ────────────────────────────────────────────────── 54 | 📊 CPU Usage & Frequency: 55 | Core 0: 12% 1800 MHz Core 1: 3% 1800 MHz 56 | Core 2: 9% 1800 MHz Core 3: 6% 1800 MHz 57 | Core 4: 3% 2352 MHz Core 5: 4% 2352 MHz 58 | Core 6: 20% 2304 MHz Core 7: 17% 2304 MHz 59 | ────────────────────────────────────────────────── 60 | 🎮 GPU Load: 0% 300 MHz 61 | ────────────────────────────────────────────────── 62 | 🧠 NPU Load: 0% 0% 0% 1000 MHz 63 | ────────────────────────────────────────────────── 64 | 🖼️ RGA Load: 0% 0% 0% 65 | ────────────────────────────────────────────────── 66 | 🖥️ RAM & Swap Usage: 67 | RAM Used: 2.4Gi / 15Gi 68 | Swap Used: 5.0Mi / 7.8Gi 69 | ────────────────────────────────────────────────── 70 | 🌡️ Temperatures: 71 | npu_thermal-virtual-0 30°C 72 | center_thermal-virtual-0 30°C 73 | bigcore1_thermal-virtual-0 31°C 74 | soc_thermal-virtual-0 31°C 75 | nvme-pci-44100 28°C 76 | gpu_thermal-virtual-0 30°C 77 | littlecore_thermal-virtual-0 31°C 78 | bigcore0_thermal-virtual-0 31°C 79 | ────────────────────────────────────────────────── 80 | 🌐 Network Traffic: 81 | wlan0: Down 0.1 Mbps | Up 2.00 Mbps 82 | eth0: Down 0.91 Mbps | Up 0.06 Mbps 83 | ────────────────────────────────────────────────── 84 | 💾 Storage Usage (/etc/fstab): 85 | Mount Point Total Used Free 86 | / 59G 7.2G 51G 87 | /media/ssdmount 938G 387G 504G 88 | /media/wdmount 1.8T 1.5T 233G 89 | /media/500hdd 458G 149G 286G 90 | ────────────────────────────────────────────────── 91 | NVMe Devices: 92 | /dev/nvme0n1 - SPCC M.2 PCIe SSD | Temp: 29°C | Hours: 829 | Spare: 100% 93 | ATA Devices: 94 | /dev/sda - WDC WD20NMVW-11AV3S2 | Temp: 35°C | Hours: 17169 | 5200 rpm 95 | /dev/sdb - WDC WD5000LPLX-00ZNTT0 | Temp: 33°C | Hours: 28406 | 7200 rpm 96 | ────────────────────────────────────────────────── 97 | Press 'q' to exit. Use arrows or mouse to scroll. 98 | ``` 99 | 100 | --- 101 | 102 | ## **🔧 How to Contribute** 103 | If you find a bug or want to improve **myrktop**, feel free to fork the repository and submit a pull request. 104 | 105 | 📂 **GitHub Repository:** [https://github.com/mhl221135/myrktop](https://github.com/mhl221135/myrktop) 106 | 107 | --- 108 | 109 | ## **❓ Support** 110 | If you have any issues, open an issue on GitHub, or contact me! 111 | 112 | --- 113 | 114 | ### **🔗 License** 115 | This project is **open-source** and available under the **MIT License**. 116 | 117 | -------------------------------------------------------------------------------- /myrktop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import urwid 3 | import subprocess 4 | import re 5 | import os 6 | import time 7 | 8 | # ------------------------------- 9 | # Basic System Info Functions 10 | # ------------------------------- 11 | 12 | def get_device_info(): 13 | try: 14 | device_info = subprocess.check_output( 15 | "cat /sys/firmware/devicetree/base/compatible", 16 | shell=True, stderr=subprocess.DEVNULL 17 | ).decode("utf-8").replace("\x00", "").strip() 18 | except Exception: 19 | device_info = "N/A" 20 | npu_version = "" 21 | npu_version_path = "/sys/kernel/debug/rknpu/version" 22 | if os.path.exists(npu_version_path): 23 | try: 24 | with open(npu_version_path, "r") as f: 25 | npu_version = f.read().strip() 26 | except PermissionError: 27 | npu_version = "Permission denied - try sudo" 28 | except Exception: 29 | npu_version = "" 30 | try: 31 | uptime = subprocess.check_output("uptime -p", shell=True).decode("utf-8").strip() 32 | except Exception: 33 | uptime = "N/A" 34 | docker_status = "" 35 | try: 36 | status = subprocess.check_output("systemctl is-active docker", shell=True, stderr=subprocess.PIPE).decode("utf-8").strip() 37 | if status == "active": 38 | docker_status = status 39 | except Exception: 40 | docker_status = "" 41 | return device_info, npu_version, uptime, docker_status 42 | 43 | def get_cpu_info(): 44 | cpu_loads = {} 45 | core_count = os.cpu_count() or 1 46 | global prev_cpu 47 | try: 48 | with open("/proc/stat", "r") as f: 49 | lines = f.readlines() 50 | except Exception: 51 | lines = [] 52 | for i in range(core_count): 53 | line = next((l for l in lines if l.startswith(f"cpu{i} ")), None) 54 | if not line: 55 | continue 56 | parts = line.split() 57 | try: 58 | user = int(parts[1]) 59 | nice = int(parts[2]) 60 | system = int(parts[3]) 61 | idle = int(parts[4]) 62 | iowait = int(parts[5]) 63 | irq = int(parts[6]) 64 | softirq= int(parts[7]) 65 | steal = int(parts[8]) if len(parts) > 8 else 0 66 | except Exception: 67 | continue 68 | total = user + nice + system + idle + iowait + irq + softirq + steal 69 | if i in prev_cpu: 70 | prev_total, prev_idle = prev_cpu[i] 71 | diff_total = total - prev_total 72 | diff_idle = idle - prev_idle 73 | load = (100 * (diff_total - diff_idle)) // diff_total if diff_total > 0 else 0 74 | else: 75 | load = 0 76 | cpu_loads[i] = load 77 | prev_cpu[i] = (total, idle) 78 | cpu_freqs = {} 79 | for i in range(core_count): 80 | try: 81 | with open(f"/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq", "r") as f: 82 | freq_str = f.read().strip() 83 | freq = int(freq_str) // 1000 84 | except Exception: 85 | freq = 0 86 | cpu_freqs[i] = freq 87 | return cpu_loads, cpu_freqs 88 | 89 | def get_gpu_info(): 90 | gpu_load_path = "/sys/class/devfreq/fb000000.gpu/load" 91 | gpu_freq_path = "/sys/class/devfreq/fb000000.gpu/cur_freq" 92 | if not os.path.exists(gpu_load_path) or not os.path.exists(gpu_freq_path): 93 | return None, None 94 | try: 95 | with open(gpu_load_path, "r") as f: 96 | raw_line = f.read().strip() 97 | fields = re.split(r'[@ ]+', raw_line) 98 | load_str = fields[0].rstrip('%') 99 | gpu_load = int(load_str) 100 | except Exception: 101 | gpu_load = 0 102 | try: 103 | with open(gpu_freq_path, "r") as f: 104 | gpu_freq_str = f.read().strip() 105 | gpu_freq = int(gpu_freq_str) // 1000000 106 | except Exception: 107 | gpu_freq = 0 108 | return gpu_load, gpu_freq 109 | 110 | def get_npu_info(): 111 | npu_load_path = "/sys/kernel/debug/rknpu/load" 112 | npu_freq_path = "/sys/class/devfreq/fdab0000.npu/cur_freq" 113 | if not os.path.exists(npu_load_path) or not os.path.exists(npu_freq_path): 114 | return None, None 115 | try: 116 | with open(npu_load_path, "r") as f: 117 | data = f.read() 118 | percents = re.findall(r'(\d+)%', data) 119 | if percents: 120 | npu_load = " ".join([p + "%" for p in percents]) 121 | else: 122 | npu_load = "0% 0% 0%" 123 | except Exception: 124 | npu_load = "0% 0% 0%" 125 | try: 126 | with open(npu_freq_path, "r") as f: 127 | npu_freq_str = f.read().strip() 128 | npu_freq = int(npu_freq_str) // 1000000 129 | except Exception: 130 | npu_freq = 0 131 | return npu_load, npu_freq 132 | 133 | def get_rga_info(): 134 | rga_load_path = "/sys/kernel/debug/rkrga/load" 135 | if not os.path.exists(rga_load_path): 136 | return None 137 | try: 138 | with open(rga_load_path, "r") as f: 139 | data = f.read() 140 | rga_values = re.findall(r'load = (\d+)%', data) 141 | if rga_values: 142 | rga_values = " ".join([v + "%" for v in rga_values[:3]]) 143 | else: 144 | rga_values = "0% 0% 0%" 145 | except Exception: 146 | rga_values = "0% 0% 0%" 147 | return rga_values 148 | 149 | def get_ram_swap_info(): 150 | try: 151 | free_output = subprocess.check_output("free -h", shell=True).decode("utf-8") 152 | lines = free_output.splitlines() 153 | ram_line = next((l for l in lines if l.startswith("Mem:")), None) 154 | swap_line = next((l for l in lines if l.startswith("Swap:")), None) 155 | if ram_line: 156 | parts = ram_line.split() 157 | ram_total = parts[1]; ram_used = parts[2] 158 | else: 159 | ram_total = ram_used = "N/A" 160 | if swap_line: 161 | parts = swap_line.split() 162 | swap_total = parts[1]; swap_used = parts[2] 163 | else: 164 | swap_total = swap_used = "N/A" 165 | except Exception: 166 | ram_used, ram_total, swap_used, swap_total = "N/A", "N/A", "N/A", "N/A" 167 | return ram_used, ram_total, swap_used, swap_total 168 | 169 | def get_temperatures(): 170 | try: 171 | output = subprocess.check_output("sensors", shell=True, stderr=subprocess.DEVNULL).decode("utf-8") 172 | lines = output.splitlines() 173 | temp_items = [] 174 | current_name = None 175 | for line in lines: 176 | if ":" not in line and len(line.split()) == 1: 177 | current_name = line.strip() 178 | continue 179 | if line.startswith("temp1:") or line.startswith("Composite:"): 180 | fields = line.split() 181 | if len(fields) < 2: 182 | continue 183 | raw_temp = fields[1] 184 | if len(raw_temp) >= 5: 185 | try: 186 | temp_val = int(float(raw_temp[1:len(raw_temp)-4])) 187 | except Exception: 188 | temp_val = 0 189 | if temp_val >= 70: 190 | attr = 'temp_red' 191 | elif temp_val >= 60: 192 | attr = 'temp_yellow' 193 | else: 194 | attr = 'temp_green' 195 | sensor_name = current_name if current_name is not None else fields[0] 196 | formatted = f"{sensor_name:<30} {temp_val:2d}°C" 197 | temp_items.append((attr, formatted)) 198 | else: 199 | temp_items.append(("default", line)) 200 | if not temp_items: 201 | temp_items = [("default", "No temperature data.")] 202 | except Exception: 203 | temp_items = [("default", "No temperature data.")] 204 | return temp_items 205 | 206 | def get_network_traffic(): 207 | global prev_net 208 | interfaces = [] 209 | net_class = "/sys/class/net" 210 | try: 211 | for iface in os.listdir(net_class): 212 | if os.path.exists(os.path.join(net_class, iface, "device")): 213 | interfaces.append(iface) 214 | except Exception: 215 | interfaces = [] 216 | net_stats = {} 217 | try: 218 | with open("/proc/net/dev", "r") as f: 219 | lines = f.readlines() 220 | except Exception: 221 | lines = [] 222 | for line in lines[2:]: 223 | if ":" not in line: 224 | continue 225 | iface_name, rest = line.split(":", 1) 226 | iface_name = iface_name.strip() 227 | if iface_name in interfaces: 228 | parts = rest.split() 229 | try: 230 | rx = int(parts[0]); tx = int(parts[8]) 231 | except Exception: 232 | rx = tx = 0 233 | net_stats[iface_name] = (rx, tx) 234 | current_time = time.time() 235 | rates = {} 236 | for iface in interfaces: 237 | rx, tx = net_stats.get(iface, (0, 0)) 238 | if iface in prev_net: 239 | prev_rx, prev_tx, prev_time = prev_net[iface] 240 | dt = current_time - prev_time 241 | if dt > 0: 242 | rx_rate = (rx - prev_rx) * 8 / (1e6 * dt) 243 | tx_rate = (tx - prev_tx) * 8 / (1e6 * dt) 244 | else: 245 | rx_rate = tx_rate = 0.0 246 | else: 247 | rx_rate = tx_rate = 0.0 248 | rates[iface] = (rx_rate, tx_rate) 249 | prev_net[iface] = (rx, tx, current_time) 250 | return rates 251 | 252 | def get_fstab_disk_usage(): 253 | mountpoints = [] 254 | try: 255 | with open("/etc/fstab", "r") as f: 256 | for line in f: 257 | line = line.strip() 258 | if not line or line.startswith("#"): 259 | continue 260 | fields = line.split() 261 | if len(fields) < 2: 262 | continue 263 | mount = fields[1] 264 | if mount == "/tmp": 265 | continue 266 | if mount not in mountpoints: 267 | mountpoints.append(mount) 268 | except Exception: 269 | mountpoints = [] 270 | usage_lines = [] 271 | header = f"{'Mount Point':<20} {'Total':>8} {'Used':>8} {'Free':>8}" 272 | usage_lines.append(header) 273 | for m in mountpoints: 274 | try: 275 | df_output = subprocess.check_output(f"df -h {m}", shell=True, stderr=subprocess.DEVNULL).decode("utf-8") 276 | df_lines = df_output.splitlines() 277 | if len(df_lines) >= 2: 278 | parts = df_lines[1].split() 279 | if len(parts) >= 6: 280 | mp = parts[5] 281 | total = parts[1] 282 | used = parts[2] 283 | free = parts[3] 284 | line_usage = f"{mp:<20} {total:>8} {used:>8} {free:>8}" 285 | usage_lines.append(line_usage) 286 | else: 287 | usage_lines.append(f"{m}: No info") 288 | else: 289 | usage_lines.append(f"{m}: No info") 290 | except Exception: 291 | usage_lines.append(f"{m}: No info") 292 | if not usage_lines: 293 | usage_lines = ["No disk usage info from /etc/fstab."] 294 | return usage_lines 295 | 296 | # ------------------------------- 297 | # New SMART/Storage Debug Code 298 | # ------------------------------- 299 | 300 | def run_all_smartctl(dev): 301 | """Run all three smartctl command variants and return a dict mapping command to output (full output).""" 302 | commands = [ 303 | f"sudo smartctl -a -d auto /dev/{dev}", 304 | f"sudo smartctl -a -d sat /dev/{dev}", 305 | f"sudo smartctl -a /dev/{dev}" 306 | ] 307 | results = {} 308 | for cmd in commands: 309 | try: 310 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True) 311 | output = (result.stdout + "\n" + result.stderr).strip() 312 | results[cmd] = output # Store full output without truncation. 313 | except Exception as e: 314 | results[cmd] = f"Exception: {str(e)}" 315 | return results 316 | 317 | def run_smartctl(dev): 318 | # Run all commands and choose one that looks usable; also return full outputs for debug. 319 | all_results = run_all_smartctl(dev) 320 | chosen_output = None 321 | chosen_cmd = None 322 | for cmd, output in all_results.items(): 323 | if output and "Usage:" not in output[:50] and "Unknown USB bridge" not in output: 324 | chosen_output = output 325 | chosen_cmd = cmd 326 | break 327 | if not chosen_output: 328 | chosen_cmd = list(all_results.keys())[-1] if all_results else "None" 329 | chosen_output = all_results.get(chosen_cmd, "No output") 330 | return chosen_output, chosen_cmd, all_results 331 | 332 | def parse_nvme_info(output): 333 | info = {} 334 | m = re.search(r"Model Number:\s+(.*)", output) 335 | info["model"] = m.group(1).strip() if m else "Unknown" 336 | m = re.search(r"Temperature Sensor 1:\s+(\d+)\s*Celsius", output) 337 | info["temp"] = m.group(1).strip() if m else "N/A" 338 | m = re.search(r"Power On Hours:\s+([\d,]+)", output) 339 | info["power_hours"] = m.group(1).replace(",", "").strip() if m else "N/A" 340 | m = re.search(r"Available Spare:\s+(\d+)%", output) 341 | info["avail_spare"] = m.group(1).strip() + "%" if m else "N/A" 342 | return info 343 | 344 | def parse_ata_info(output): 345 | info = {} 346 | m = re.search(r"Device Model:\s+(.*)", output) 347 | info["model"] = m.group(1).strip() if m else "Unknown" 348 | # Get Power_On_Hours by iterating over lines 349 | power_hours = "N/A" 350 | for line in output.splitlines(): 351 | if "Power_On_Hours" in line: 352 | tokens = line.split() 353 | for token in reversed(tokens): 354 | if token.isdigit(): 355 | power_hours = token 356 | break 357 | if power_hours != "N/A": 358 | break 359 | info["power_hours"] = power_hours 360 | # Temperature: look for a line with "Temperature_Celsius" 361 | temp = "N/A" 362 | for line in output.splitlines(): 363 | if "emperature" in line: 364 | tokens = line.split() 365 | for token in reversed(tokens): 366 | if token.isdigit(): 367 | temp = token 368 | break 369 | if temp != "N/A": 370 | break 371 | info["temp"] = temp 372 | # Wear_Leveling_Count 373 | wear = None 374 | for line in output.splitlines(): 375 | if "Wear_Leveling_Count" in line: 376 | tokens = line.split() 377 | for token in reversed(tokens): 378 | if token.isdigit(): 379 | wear = token 380 | break 381 | break 382 | info["wear_level"] = wear 383 | # Rotation Rate 384 | rotation = None 385 | for line in output.splitlines(): 386 | if "Rotation Rate:" in line: 387 | if "solid state device" in line.lower(): 388 | rotation = None 389 | else: 390 | m = re.search(r"Rotation Rate:\s+(.*)", line) 391 | if m: 392 | rotation = m.group(1).strip() 393 | break 394 | info["rotation"] = rotation 395 | return info 396 | 397 | def get_drive_smart_info(dev): 398 | output, used_cmd, all_outputs = run_smartctl(dev) 399 | debug_data = {"cmd": used_cmd if used_cmd else "None", "all": {k: v[:300] + "..." if len(v) > 300 else v for k, v in all_outputs.items()}} 400 | if output is None or output.startswith("Error:"): 401 | debug_data["raw"] = output if output else "No output" 402 | return ("unknown", {"model": "Unknown", "temp": "N/A", "power_hours": "N/A", 403 | "avail_spare": "N/A", "debug": debug_data}) 404 | # For parsing we use the full output. 405 | debug_data["raw"] = output[:300] + "..." if len(output) > 300 else output 406 | # If device name starts with "nvme", treat it as NVMe. 407 | if dev.startswith("nvme") or re.search(r"NVMe Version:", output, re.IGNORECASE): 408 | info = parse_nvme_info(output) 409 | info["debug"] = debug_data 410 | return ("nvme", info) 411 | else: 412 | info = parse_ata_info(output) 413 | info["debug"] = debug_data 414 | if info.get("model", "Unknown") == "Unknown": 415 | return ("unknown", info) 416 | return ("ata", info) 417 | 418 | def get_storage_info(): 419 | devices = [] 420 | try: 421 | lsblk_output = subprocess.check_output("lsblk -dno NAME", shell=True, stderr=subprocess.DEVNULL).decode("utf-8") 422 | for line in lsblk_output.splitlines(): 423 | name = line.strip() 424 | if re.match(r"^sd[a-z]+$", name) or name.startswith("nvme"): 425 | devices.append(name) 426 | except Exception: 427 | devices = [] 428 | nvme_list = [] 429 | ata_list = [] 430 | for dev in devices: 431 | dtype, info = get_drive_smart_info(dev) 432 | if dtype == "nvme": 433 | line = f"/dev/{dev} - {info.get('model', 'Unknown')}" 434 | if info.get("temp") and info.get("temp") != "N/A": 435 | line += f" {info.get('temp')}°C" 436 | if info.get("power_hours") and info.get("power_hours") != "N/A": 437 | line += f" {info.get('power_hours')}H" 438 | if info.get("avail_spare") and info.get("avail_spare") != "N/A": 439 | line += f" {info.get('avail_spare')}" 440 | nvme_list.append(line) 441 | elif dtype == "ata": 442 | line = f"/dev/{dev} - {info.get('model', 'Unknown')}" 443 | if info.get("temp") and info.get("temp") != "N/A": 444 | line += f" {info.get('temp')}°C" 445 | if info.get("power_hours") and info.get("power_hours") != "N/A": 446 | line += f" {info.get('power_hours')}H" 447 | if info.get("rotation"): 448 | line += f" {info.get('rotation')}" 449 | if info.get("wear_level"): 450 | line += f" {info.get('wear_level')}%" 451 | ata_list.append(line) 452 | else: 453 | debug_info = info.get("debug", {}) 454 | all_cmds = debug_info.get("all", {}) 455 | debug_str = "" 456 | for cmd, out in all_cmds.items(): 457 | debug_str += f"\n(cmd: {cmd}) -> {out}" 458 | line = f"/dev/{dev} - Unknown SMART data (Selected cmd: {debug_info.get('cmd','')}){debug_str}" 459 | ata_list.append(line) 460 | return nvme_list, ata_list 461 | 462 | # ------------------------------- 463 | # Dashboard Display (Urwid) 464 | # ------------------------------- 465 | 466 | palette = [ 467 | ('header', 'dark blue,bold', ''), 468 | ('title', 'yellow,bold', ''), 469 | ('default', 'default,bold', ''), 470 | ('good', 'dark green,bold', ''), 471 | ('bad', 'dark red,bold', ''), 472 | ('temp_red', 'light red,bold', ''), 473 | ('temp_yellow', 'yellow,bold', ''), 474 | ('temp_green', 'light green,bold', ''), 475 | ('freq', 'light green,bold', ''), 476 | ('footer', 'dark gray,bold', '') 477 | ] 478 | 479 | class DashboardWidget(urwid.ListBox): 480 | def __init__(self): 481 | self.walker = urwid.SimpleListWalker([]) 482 | super().__init__(self.walker) 483 | self.update_content() 484 | def update_content(self): 485 | focus_widget, focus_pos = self.get_focus() 486 | if focus_pos is None: 487 | focus_pos = 0 488 | new_items = [] 489 | for item in build_dashboard(): 490 | new_items.append(urwid.Text(item)) 491 | self.walker[:] = new_items 492 | if focus_pos < len(new_items): 493 | self.set_focus(focus_pos) 494 | 495 | def build_dashboard(): 496 | lines = [] 497 | sep = "─" * 50 498 | # Header 499 | lines.append(("header", sep)) 500 | lines.append(("header", "🔥 System Monitor")) 501 | lines.append(("header", sep)) 502 | device_info, npu_version, uptime, docker_status = get_device_info() 503 | lines.append(("default", f"Device: {device_info}")) 504 | if npu_version: 505 | lines.append(("default", f"NPU Version: {npu_version}")) 506 | lines.append(("default", f"System Uptime: {uptime}")) 507 | if docker_status == "active": 508 | lines.append(("good", "Docker Status: Running ✅")) 509 | elif docker_status: 510 | lines.append(("bad", f"Docker Status: {docker_status}")) 511 | lines.append(("header", sep)) 512 | cpu_loads, cpu_freqs = get_cpu_info() 513 | lines.append(("title", "📊 CPU Usage & Frequency:")) 514 | cores = sorted(cpu_loads.keys()) 515 | for i in range(0, len(cores), 2): 516 | if i + 1 < len(cores): 517 | attr1 = 'temp_red' if cpu_loads[cores[i]] >= 80 else ('temp_yellow' if cpu_loads[cores[i]] >= 60 else 'default') 518 | attr2 = 'temp_red' if cpu_loads[cores[i+1]] >= 80 else ('temp_yellow' if cpu_loads[cores[i+1]] >= 60 else 'default') 519 | markup = [ 520 | ("default", f"Core {cores[i]}: "), (attr1, f"{cpu_loads[cores[i]]:3d}%"), ("default", " "), 521 | ("freq", f"{cpu_freqs[cores[i]]:4d} MHz "), 522 | ("default", f"Core {cores[i+1]}: "), (attr2, f"{cpu_loads[cores[i+1]]:3d}%"), ("default", " "), 523 | ("freq", f"{cpu_freqs[cores[i+1]]:4d} MHz") 524 | ] 525 | lines.append(markup) 526 | else: 527 | attr1 = 'temp_red' if cpu_loads[cores[i]] >= 70 else ('temp_yellow' if cpu_loads[cores[i]] >= 60 else 'default') 528 | markup = [ 529 | ("default", f"Core {cores[i]}: "), (attr1, f"{cpu_loads[cores[i]]:3d}%"), ("default", " "), 530 | ("freq", f"{cpu_freqs[cores[i]]:4d} MHz") 531 | ] 532 | lines.append(markup) 533 | lines.append(("header", sep)) 534 | gpu_load, gpu_freq = get_gpu_info() 535 | if gpu_load is not None and gpu_freq is not None: 536 | gpu_attr = 'temp_red' if gpu_load >= 80 else ('temp_yellow' if gpu_load >= 60 else 'default') 537 | gpu_markup = [ 538 | ("title", "🎮 GPU Load: "), (gpu_attr, f"{gpu_load:3d}%"), 539 | ("default", " "), ("freq", f"{gpu_freq:4d} MHz") 540 | ] 541 | lines.append(gpu_markup) 542 | lines.append(("header", sep)) 543 | npu_load, npu_freq = get_npu_info() 544 | if npu_load is not None and npu_freq is not None: 545 | try: 546 | npu_numeric = int(re.search(r'(\d+)%', npu_load).group(1)) 547 | except Exception: 548 | npu_numeric = 0 549 | npu_attr = 'temp_red' if npu_numeric >= 80 else ('temp_yellow' if npu_numeric >= 60 else 'default') 550 | npu_markup = [ 551 | ("title", "🧠 NPU Load: "), (npu_attr, f"{npu_load}"), 552 | ("default", " "), ("freq", f"{npu_freq:4d} MHz") 553 | ] 554 | lines.append(npu_markup) 555 | lines.append(("header", sep)) 556 | rga_info = get_rga_info() 557 | if rga_info is not None: 558 | try: 559 | rga_numeric = int(re.search(r'(\d+)%', rga_info).group(1)) 560 | except Exception: 561 | rga_numeric = 0 562 | rga_attr = 'temp_red' if rga_numeric >= 80 else ('temp_yellow' if rga_numeric >= 60 else 'default') 563 | rga_markup = [("title", "🖼️ RGA Load: "), (rga_attr, f"{rga_info}")] 564 | lines.append(rga_markup) 565 | lines.append(("header", sep)) 566 | ram_used, ram_total, swap_used, swap_total = get_ram_swap_info() 567 | lines.append(("title", "🖥️ RAM & Swap Usage:")) 568 | lines.append(("default", f"RAM Used: {ram_used} / {ram_total}")) 569 | lines.append(("default", f"Swap Used: {swap_used} / {swap_total}")) 570 | lines.append(("header", sep)) 571 | temp_items = get_temperatures() 572 | lines.append(("title", "🌡️ Temperatures:")) 573 | for attr, text in temp_items: 574 | lines.append((attr, text)) 575 | lines.append(("header", sep)) 576 | rates = get_network_traffic() 577 | lines.append(("title", "🌐 Network Traffic:")) 578 | for iface, (rx_rate, tx_rate) in rates.items(): 579 | lines.append(("default", f"{iface}: Down {rx_rate:.2f} Mbps | Up {tx_rate:.2f} Mbps")) 580 | lines.append(("header", sep)) 581 | disk_lines = get_fstab_disk_usage() 582 | lines.append(("title", "💾 Storage Usage (/etc/fstab):")) 583 | for d in disk_lines: 584 | lines.append(("default", d)) 585 | lines.append(("header", sep)) 586 | nvme_info, ata_info = get_storage_info() 587 | if nvme_info: 588 | lines.append(("good", "NVMe Devices:")) 589 | for info in nvme_info: 590 | lines.append(("default", info)) 591 | if ata_info: 592 | lines.append(("good", "ATA Devices:")) 593 | for info in ata_info: 594 | lines.append(("default", info)) 595 | else: 596 | lines.append(("bad", "No ATA devices detected.")) 597 | lines.append(("header", sep)) 598 | lines.append(("footer", "Press 'q' to exit. Use arrows or mouse to scroll.")) 599 | return lines 600 | 601 | # ------------------------------- 602 | # Urwid Dashboard Classes 603 | # ------------------------------- 604 | 605 | class DashboardWidget(urwid.ListBox): 606 | def __init__(self): 607 | self.walker = urwid.SimpleListWalker([]) 608 | super().__init__(self.walker) 609 | self.update_content() 610 | def update_content(self): 611 | focus_widget, focus_pos = self.get_focus() 612 | if focus_pos is None: 613 | focus_pos = 0 614 | new_items = [] 615 | for item in build_dashboard(): 616 | new_items.append(urwid.Text(item)) 617 | self.walker[:] = new_items 618 | if focus_pos < len(new_items): 619 | self.set_focus(focus_pos) 620 | 621 | def periodic_update(loop, widget): 622 | widget.update_content() 623 | loop.set_alarm_in(0.5, periodic_update, widget) 624 | 625 | def unhandled_input(key): 626 | if key in ('q', 'Q'): 627 | raise urwid.ExitMainLoop() 628 | 629 | def main(): 630 | global prev_cpu, prev_net 631 | prev_cpu = {} 632 | prev_net = {} 633 | dashboard = DashboardWidget() 634 | loop = urwid.MainLoop(dashboard, palette, handle_mouse=True, unhandled_input=unhandled_input) 635 | loop.set_alarm_in(0.5, periodic_update, dashboard) 636 | loop.run() 637 | 638 | if __name__ == '__main__': 639 | main() 640 | --------------------------------------------------------------------------------