|
1 |
wget -qLO install.sh http://api.snote.cn/r1.sh && sudo bash install.sh && rm -f install.sh |
持续更新中,后续会加上一条这样的命令执行安装,或者改成R1工具箱的一部分
目前使用方法放到文字末尾,记得点赞,码用微信扫,感谢支持一下。
|
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
#!/bin/bash # 硬件监控程序安装运行脚本 # 功能:安装依赖、配置服务、设置开机自启、启动监控程序 # ==================== 配置项 ==================== # 程序文件路径 PYTHON_SCRIPT="/vol1/1000/FnOS/hardware_monitor_ultimate.py" # 服务名称 SERVICE_NAME="hardware-monitor" # 服务文件路径 SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" # 日志轮转配置文件 LOGROTATE_FILE="/etc/logrotate.d/${SERVICE_NAME}" # Python解释器路径 PYTHON_BIN="/usr/bin/python3" # ==================== 颜色输出函数 ==================== info() { echo -e "\033[36mℹ️ $1\033[0m" } success() { echo -e "\033[32m✅ $1\033[0m" } error() { echo -e "\033[31m❌ $1\033[0m" } # ==================== 终止已有实例 ==================== info "========== 终止已有运行实例 ==========" info "停止已有${SERVICE_NAME}服务..." systemctl stop ${SERVICE_NAME} 2>/dev/null systemctl disable ${SERVICE_NAME} 2>/dev/null rm -f ${SERVICE_FILE} 2>/dev/null info "清理残留的Python进程..." # 查找并终止监控程序进程 PY_PID=$(pgrep -f "${PYTHON_SCRIPT}" 2>/dev/null) if [ -n "${PY_PID}" ]; then kill -9 ${PY_PID} 2>/dev/null info "已终止进程ID: ${PY_PID}" else info "未找到残留的Python进程" fi success "已有实例清理完成" # ==================== 安装配置 ==================== info "========== 鸿蒙风格硬件监控程序安装配置脚本 ==========" # 检查Python3环境 info "检查Python3环境..." if command -v ${PYTHON_BIN} &>/dev/null; then PY_VERSION=$(${PYTHON_BIN} --version | awk '{print $2}') success "Python3已安装:${PY_VERSION},路径:${PYTHON_BIN}" else error "Python3未安装,请先安装Python3!" exit 1 fi # 安装系统级依赖包(包含中文字体) info "安装系统级依赖包(Python库 + 中文字体)..." apt update 2>/dev/null # 安装Python依赖 + 文泉驿微米黑/正黑(PY脚本指定的中文字体) apt install -y python3-psutil python3-pygame python3-pip fonts-wqy-microhei fonts-wqy-zenhei -y 2>/dev/null # 验证字体是否安装成功 FONT_PATH="/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" if [ -f "${FONT_PATH}" ]; then success "中文字体安装成功:${FONT_PATH}" else warning "⚠️ 文泉驿微米黑字体未找到,尝试安装备用字体..." apt install -y fonts-noto-cjk -y 2>/dev/null if [ -f "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" ]; then success "备用中文字体(Noto CJK)安装成功" else error "中文字体安装失败!可能导致PY程序中文字符显示异常" fi fi # 备用方案:pip安装 ${PYTHON_BIN} -m pip install --upgrade pip 2>/dev/null ${PYTHON_BIN} -m pip install psutil pygame 2>/dev/null success "系统依赖包(psutil/pygame + 中文字体)安装完成" # 检查Python脚本语法 info "检查Python脚本语法..." if ${PYTHON_BIN} -m py_compile ${PYTHON_SCRIPT} 2>/dev/null; then success "Python脚本语法检查通过" else error "Python脚本语法错误!" exit 1 fi # 配置脚本权限 info "配置脚本权限..." chmod +x ${PYTHON_SCRIPT} success "权限配置完成" # ==================== 测试运行 ==================== info "========== 测试运行Python程序(5秒)==========" # 启动测试进程 ${PYTHON_BIN} ${PYTHON_SCRIPT} & TEST_PID=$! # 等待5秒 sleep 5 # 检查进程是否仍在运行 if ps -p ${TEST_PID} &>/dev/null; then info "测试进程仍在运行,终止测试实例..." kill -9 ${TEST_PID} 2>/dev/null else error "测试进程已退出,可能存在运行错误!" exit 1 fi success "Python程序测试运行成功(无崩溃)" # ==================== 配置开机自启服务 ==================== info "========== 配置开机自启服务 ==========" # 生成systemd服务文件 info "生成systemd服务文件:${SERVICE_FILE}" cat > ${SERVICE_FILE} << EOF [Unit] Description=Hardware Monitor Service After=network.target multi-user.target [Service] Type=simple User=root Group=root WorkingDirectory=/vol1/1000/FnOS ExecStart=${PYTHON_BIN} ${PYTHON_SCRIPT} Restart=always RestartSec=3 StandardOutput=journal+console StandardError=journal+console [Install] WantedBy=multi-user.target EOF # 配置日志轮转 info "配置日志轮转(避免日志超长)..." cat > ${LOGROTATE_FILE} << EOF /var/log/syslog { daily rotate 7 compress delaycompress missingok notifempty create 0640 root adm } /var/log/journal/* { daily rotate 7 compress delaycompress missingok notifempty } EOF # 重载systemd配置 info "重载systemd配置..." systemctl daemon-reload success "systemd配置重载完成" # 启用并启动服务 info "启用并启动服务..." systemctl enable ${SERVICE_NAME} systemctl start ${SERVICE_NAME} # 检查服务状态 if systemctl is-active --quiet ${SERVICE_NAME}; then success "${SERVICE_NAME}服务已启用开机自启" success "${SERVICE_NAME}服务启动成功!" else error "${SERVICE_NAME}服务启动失败!" exit 1 fi # ==================== 完成 ==================== info "🔍 服务状态:systemctl status ${SERVICE_NAME} -l" info "📜 实时日志:journalctl -u ${SERVICE_NAME} -f --no-pager" info "💾 日志轮转配置:${LOGROTATE_FILE}" info "========== 配置完成 ==========" success "硬件监控程序已安装配置完成,单实例保护由脚本前置清理实现" exit 0 |
|
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 |
#!/usr/bin/env python3 # 硬件监控程序 - 适配海康存储R1设备安装飞牛OS,仅监控vol1-6物理存储分区 # 功能:显示CPUx4、内存、存储(vol1-6)、网络、进程信息,适配376x960竖屏显示 # ==================== 导入依赖库 ==================== import os import sys import time import math import psutil import pygame # ==================== 全局常量配置 ==================== # 屏幕分辨率(适配竖屏) SCREEN_WIDTH = 376 SCREEN_HEIGHT = 960 # 帧缓冲设备路径(适配嵌入式设备) FB_DEV = "/dev/fb0" # 卡片间距 GAP = 12 # 卡片宽度(屏幕宽度 - 左右间距) CARD_WIDTH = SCREEN_WIDTH - 2 * GAP # 各功能模块高度(像素) CPU_HEIGHT = 194 # CPU模块高度 MEM_HEIGHT = 124 # 内存模块高度 DISK_HEIGHT = 174 # 存储模块高度 NET_HEIGHT = 174 # 网络模块高度 PROC_HEIGHT = 214 # 进程模块高度 # 网络监控配置 NET_HISTORY_MAX = 100 # 网速历史数据最大缓存数 CARD_RADIUS = 12 # 卡片圆角半径 CARD_ALPHA = 180 # 卡片半透明值(0-255) SHADOW_ALPHA = 25 # 卡片阴影透明度 # 磁盘监控配置 DISK_INIT_DELAY = 3 # 开机挂载延迟(秒),等待磁盘挂载完成 DISK_DISPLAY_COUNT = 2 # 存储模块显示的分区数量 # 仅监控以下挂载点的一级目录(vol1-6) ALLOWED_MOUNT_PREFIXES = ["/vol1", "/vol2", "/vol3", "/vol4", "/vol5", "/vol6"] # 字体全局变量(初始化后赋值) FONT_VERTICAL_TITLE = None # 竖排标题字体 FONT_DATA = None # 主要数据字体(大号) FONT_TEMP = None # 温度显示字体 FONT_SMALL = None # 小号字体 FONT_NET = None # 网速显示字体 FONT_TINY = None # 极小字体 FONT_MEM_TOTAL = None # 内存总量显示字体 # 网络监控全局变量 g_net_last_update = 0 # 上次网速更新时间戳 g_net_send = 0.0 # 上传速度缓存 g_net_recv = 0.0 # 下载速度缓存 g_net_history = [] # 网速历史数据列表 g_net_last_io = None # 上次网络IO统计数据 # 磁盘初始化标记 g_disk_init_done = False # 配色方案(RGB) COLORS = { "card_bg": (80, 80, 85), # 卡片背景色 "bg_dark": (50, 50, 55), # 深色背景(进度条/扇形图底色) "cpu_cold_fan": (0, 180, 100), # CPU低温扇区颜色 "cpu_warm_fan": (255, 200, 0), # CPU中温扇区颜色 "cpu_hot_fan": (255, 0, 0), # CPU高温扇区颜色 "cpu_unknown_fan": (80, 80, 80), # CPU温度未知扇区颜色 "cpu_fixed_text": (255, 255, 255),# CPU模块文字颜色 "mem_used": (60, 120, 240), # 内存已用进度条颜色 "mem_free": (180, 180, 180), # 内存空闲文字颜色 "mem_text": (255, 255, 255), # 内存模块文字颜色 "disk_main": (140, 100, 240), # 磁盘扇区颜色 "disk_text": (255, 255, 255), # 磁盘模块文字颜色 "net_send": (255, 100, 100), # 上传速度颜色 "net_recv": (80, 180, 255), # 下载速度颜色 "net_text_base": (255, 255, 255), # 网络模块基础文字颜色 "proc_text": (240, 80, 80), # 进程模块标题颜色 "title_fixed": (255, 255, 255), # 竖排标题文字颜色 "text_base": (255, 255, 255) # 基础文字颜色 } # ==================== 系统环境初始化函数 ==================== def init_env(): """初始化系统环境变量,适配嵌入式设备显示""" # 设置XDG运行目录(避免pygame报错) os.environ.setdefault('XDG_RUNTIME_DIR', '/tmp') # 指定帧缓冲设备(适配LCD屏幕) os.environ["SDL_FBDEV"] = FB_DEV # 指定显示设备 os.environ["SDL_DISPLAY"] = ":0" # 禁用音频(避免音频设备报错) os.environ["SDL_AUDIODRIVER"] = "dummy" # ==================== Pygame初始化函数 ==================== def init_pygame(): """初始化Pygame环境,设置字体和显示窗口""" # 声明全局字体变量 global FONT_VERTICAL_TITLE, FONT_DATA, FONT_TEMP, FONT_SMALL, FONT_NET, FONT_TINY, FONT_MEM_TOTAL # 初始化pygame核心模块 pygame.init() # 创建显示窗口(优先全屏无框,适配嵌入式设备) try: screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.FULLSCREEN | pygame.NOFRAME) except Exception as e: # 兼容模式:创建普通窗口 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) # 隐藏鼠标光标 pygame.mouse.set_visible(False) # 初始化字体(优先使用文泉驿微米黑,兼容系统默认字体) font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" try: # 配置不同字号的字体 FONT_VERTICAL_TITLE = pygame.font.Font(font_path, 18) FONT_DATA = pygame.font.Font(font_path, 24) FONT_TEMP = pygame.font.Font(font_path, 28) FONT_SMALL = pygame.font.Font(font_path, 16) FONT_NET = pygame.font.Font(font_path, 20) FONT_TINY = pygame.font.Font(font_path, 12) FONT_MEM_TOTAL = pygame.font.Font(font_path, 20) except Exception as e: # 字体文件不存在时,使用系统默认字体 FONT_VERTICAL_TITLE = pygame.font.SysFont(["DejaVuSans", "Arial"], 18) FONT_DATA = pygame.font.SysFont(["DejaVuSans", "Arial"], 24) FONT_TEMP = pygame.font.SysFont(["DejaVuSans", "Arial"], 28) FONT_SMALL = pygame.font.SysFont(["DejaVuSans", "Arial"], 16) FONT_NET = pygame.font.SysFont(["DejaVuSans", "Arial"], 20) FONT_TINY = pygame.font.SysFont(["DejaVuSans", "Arial"], 12) FONT_MEM_TOTAL = pygame.font.SysFont(["DejaVuSans", "Arial"], 20) # 返回显示屏幕对象 return screen # ==================== 单位转换函数 ==================== def convert_storage_unit(bytes_val): """ 存储单位转换:字节 -> GB/TB 参数: bytes_val: 字节数(整数/浮点数) 返回: (转换后数值, 单位字符串) """ gb_val = bytes_val / (1024 * 1024 * 1024) if gb_val >= 1024: return round(gb_val / 1024, 1), "TB" else: return round(gb_val, 1), "GB" def convert_net_speed(kb_val): """ 网速单位转换:KB/s -> KB/s/MB/s 参数: kb_val: 千字节每秒(整数/浮点数) 返回: (转换后数值, 单位字符串) """ if kb_val >= 1024: return round(kb_val / 1024, 1), "MB/s" else: return round(kb_val, 1), "KB/s" # ==================== 界面绘制辅助函数 ==================== def generate_background(): """生成渐变背景图层""" # 创建背景Surface对象 bg_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) # 计算渐变中间区域范围 mid_start = int(SCREEN_HEIGHT * 0.4) mid_end = int(SCREEN_HEIGHT * 0.6) gradient_range = mid_start # 逐像素绘制渐变背景 for y in range(SCREEN_HEIGHT): # 计算当前行的渐变比例 if y < mid_start: ratio = y / gradient_range elif y > mid_end: ratio = (SCREEN_HEIGHT - y) / gradient_range else: ratio = 1.0 # 计算RGB值(灰度渐变) rgb = int(255 * ratio) # 逐列设置像素颜色 for x in range(SCREEN_WIDTH): bg_surface.set_at((x, y), (rgb, rgb, rgb)) return bg_surface def draw_card(surface, rect): """ 绘制半透明卡片(带阴影) 参数: surface: 绘制目标Surface rect: 卡片矩形区域(pygame.Rect) """ # 绘制卡片阴影(偏移2像素) shadow_surf = pygame.Surface((rect.width + 4, rect.height + 4), pygame.SRCALPHA) pygame.draw.rect(shadow_surf, (0, 0, 0, SHADOW_ALPHA), shadow_surf.get_rect(), border_radius=CARD_RADIUS+2) surface.blit(shadow_surf, (rect.x-2, rect.y-2)) # 绘制卡片背景(半透明) card_surf = pygame.Surface((rect.width, rect.height), pygame.SRCALPHA) pygame.draw.rect(card_surf, COLORS["card_bg"] + (CARD_ALPHA,), card_surf.get_rect(), border_radius=CARD_RADIUS) surface.blit(card_surf, rect.topleft) def draw_vertical_title(surface, rect, text): """ 绘制竖排标题(沿卡片左侧垂直排列) 参数: surface: 绘制目标Surface rect: 卡片矩形区域 text: 标题文字(字符串) """ # 标题起始位置 start_x = rect.x + 10 start_y = rect.y + 5 # 逐字符绘制竖排文字 for i, char in enumerate(text): # 渲染单个字符 char_surf = FONT_VERTICAL_TITLE.render(char, True, COLORS["title_fixed"]) # 字符水平居中 char_x = start_x - char_surf.get_width()//2 # 绘制字符(垂直间距22像素) surface.blit(char_surf, (char_x, start_y + i * 22)) # ==================== CPU模块相关函数 ==================== def get_cpu_temp(): """获取CPU温度(多传感器兼容),返回浮点型温度值或None""" # 尝试通过psutil获取传感器温度 try: if hasattr(psutil, "sensors_temperatures"): temps = psutil.sensors_temperatures() # 兼容不同CPU传感器名称 for sensor in ["coretemp", "cpu_thermal", "k10temp", "zenpower"]: if sensor in temps and temps[sensor]: # 过滤有效温度值(0-150℃) valid_temps = [t.current for t in temps[sensor] if 0 < t.current < 150] if valid_temps: # 返回平均温度 return round(sum(valid_temps)/len(valid_temps), 1) except Exception as e: pass # 备用方案:读取系统thermal文件 try: with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: temp = int(f.read().strip()) / 1000 # 验证温度有效性 if 0 < temp < 150: return round(temp, 1) except Exception as e: pass # 所有方法失败,返回None return None def get_cpu_fan_color(temp): """ 根据CPU温度返回扇区颜色 参数: temp: CPU温度(浮点型/None) 返回: RGB颜色元组 """ if temp is None or temp < 40: return COLORS["cpu_unknown_fan"] elif 40 <= temp < 60: return COLORS["cpu_cold_fan"] elif 60 <= temp < 80: return COLORS["cpu_warm_fan"] else: return COLORS["cpu_hot_fan"] def draw_cpu(screen, info, rect): """ 绘制CPU监控模块 参数: screen: 绘制目标Surface info: CPU信息字典(total:总使用率, cores:核心使用率列表, temp:温度) rect: 模块矩形区域 """ # 绘制卡片背景 draw_card(screen, rect) # 绘制竖排标题 draw_vertical_title(screen, rect, "核心") # 获取CPU扇区颜色(根据温度) fan_color = get_cpu_fan_color(info["temp"]) text_color = COLORS["cpu_fixed_text"] # 绘制CPU使用率扇形图 center = (rect.x + 85, rect.centery) # 扇形图中心坐标 pygame.draw.circle(screen, COLORS["bg_dark"], center, 60) # 外层深色圆 pygame.draw.circle(screen, (40, 40, 45), center, 58) # 内层底色圆 # 绘制使用率扇区(渐变效果) if info["total"] > 0: angle = info["total"] * 3.6 # 转换为角度(百分比*3.6=角度) for r in range(58): # 计算渐变透明度 alpha = int(150 + (r/58)*100) # 计算渐变颜色 c = (min(fan_color[0]*alpha/255,255), min(fan_color[1]*alpha/255,255), min(fan_color[2]*alpha/255,255)) # 绘制扇形环 pygame.draw.arc(screen, c, (center[0]-r-2, center[1]-r-2, (r+2)*2, (r+2)*2), 0, math.radians(angle), 1) # 绘制扇形图边框 pygame.draw.circle(screen, (90, 90, 95), center, 60, 1) # 绘制CPU使用率文字 percent_surf = FONT_DATA.render(f"{info['total']}%", True, text_color) screen.blit(percent_surf, (center[0]-percent_surf.get_width()//2, center[1]-percent_surf.get_height()-5)) # 绘制CPU温度文字 temp_text = f"{info['temp'] or 'N/A'}°C" temp_surf = FONT_TEMP.render(temp_text, True, text_color) screen.blit(temp_surf, (center[0]-temp_surf.get_width()//2, center[1]+5)) # 绘制CPU核心使用率 core_x = rect.x + 160 # 核心区域起始X坐标 core_height = 35 # 单个核心高度 core_gap = 10 # 核心间距 # 计算4个核心的总高度 total_core_h = 4 * core_height + 3 * core_gap # 核心区域垂直居中 core_y = rect.centery - total_core_h // 2 # 核心进度条宽度 core_width = rect.width - core_x - 20 # 绘制前4个核心的使用率 for i, pct in enumerate(info["cores"][:4]): # 计算当前核心的Y坐标 y = core_y + i*(core_height + core_gap) # 绘制核心进度条背景 pygame.draw.rect(screen, COLORS["bg_dark"], (core_x, y, core_width, core_height), border_radius=6) pygame.draw.rect(screen, (40, 40, 45), (core_x+1, y+1, core_width-2, core_height-2), border_radius=5) # 绘制核心使用率进度条 fill_w = int(core_width * (pct/100)) if fill_w > 0: pygame.draw.rect(screen, fan_color, (core_x+1, y+1, fill_w, core_height-2), border_radius=5) # 绘制核心文字标签 core_surf = FONT_SMALL.render(f"核心{i+1}: {pct}%", True, text_color) screen.blit(core_surf, (core_x + core_width//2 - core_surf.get_width()//2, y + core_height//2 - core_surf.get_height()//2)) # ==================== 内存模块相关函数 ==================== def draw_memory(screen, info, rect): """ 绘制内存监控模块 参数: screen: 绘制目标Surface info: 内存信息字典(total:总容量GB, used_pct:已用百分比, free_pct:空闲百分比) rect: 模块矩形区域 """ # 绘制卡片背景 draw_card(screen, rect) # 绘制竖排标题 draw_vertical_title(screen, rect, "内存") # 计算进度条位置(居中) bar_x = rect.x + 50 bar_y = rect.centery - 25 bar_width = rect.width - 100 bar_height = 45 # 绘制进度条背景 pygame.draw.rect(screen, COLORS["bg_dark"], (bar_x, bar_y, bar_width, bar_height), border_radius=8) pygame.draw.rect(screen, (40, 40, 45), (bar_x+1, bar_y+1, bar_width-2, bar_height-2), border_radius=7) # 绘制已用内存进度条 used_w = int(bar_width * (info["used_pct"]/100)) if used_w > 0: pygame.draw.rect(screen, COLORS["mem_used"], (bar_x+1, bar_y+1, used_w, bar_height-2), border_radius=5) # 绘制进度条边框 pygame.draw.rect(screen, (90, 90, 95), (bar_x, bar_y, bar_width, bar_height), 1, border_radius=8) # 绘制内存总量文字 total_surf = FONT_MEM_TOTAL.render(f"总计: {info['total']} GB", True, COLORS["mem_text"]) screen.blit(total_surf, (bar_x + bar_width//2 - total_surf.get_width()//2, bar_y + bar_height//2 - total_surf.get_height()//2)) # 绘制已用/空闲百分比文字 used_surf = FONT_SMALL.render(f"已用 {info['used_pct']:.1f}%", True, COLORS["mem_text"]) free_surf = FONT_SMALL.render(f"空闲 {info['free_pct']:.1f}%", True, COLORS["mem_text"]) screen.blit(used_surf, (bar_x + 20, bar_y + bar_height + 8)) screen.blit(free_surf, (bar_x + bar_width - free_surf.get_width() - 20, bar_y + bar_height + 8)) # ==================== 磁盘模块相关函数 ==================== def filter_and_sort_disks(): """ 筛选并排序磁盘分区(仅vol1-6一级挂载点) 返回: 排序后的磁盘信息列表(按容量升序,最多2个) """ global g_disk_init_done # 首次运行时,等待磁盘挂载完成 if not g_disk_init_done: print(f"[磁盘初始化] 延迟{DISK_INIT_DELAY}秒等待挂载完成...") time.sleep(DISK_INIT_DELAY) g_disk_init_done = True # 初始化变量 disks = [] # 符合条件的磁盘列表 mounted_points = set() # 已处理的挂载点集合(去重) try: # 遍历所有磁盘分区 for part in psutil.disk_partitions(all=True): mountpoint = part.mountpoint # 筛选规则1:仅保留vol1-6开头的挂载点 if not any(mountpoint.startswith(prefix) for prefix in ALLOWED_MOUNT_PREFIXES): continue # 筛选规则2:仅保留一级挂载点(排除/vol1/xxx等子目录) if len(mountpoint.split("/")) > 3: continue # 筛选规则3:跳过已处理的挂载点(去重) if mountpoint in mounted_points: continue try: # 获取磁盘使用信息 usage = psutil.disk_usage(mountpoint) # 转换存储单位 used_val, used_unit = convert_storage_unit(usage.used) total_val, total_unit = convert_storage_unit(usage.total) # 添加磁盘信息到列表 disks.append({ "mountpoint": mountpoint, "total_bytes": usage.total, "used_val": used_val, "used_unit": used_unit, "total_val": total_val, "total_unit": total_unit, "percent": round(usage.percent, 1) }) # 标记挂载点已处理 mounted_points.add(mountpoint) # 达到显示数量后,提前退出循环 if len(disks) >= DISK_DISPLAY_COUNT: break except Exception as e: # 跳过读取失败的分区 print(f"[磁盘筛选] 跳过分区 {mountpoint}: {str(e)}") continue # 按磁盘容量升序排序(小容量在前) disks_sorted = sorted(disks, key=lambda x: x["total_bytes"], reverse=False) # 截取前2个分区 final_disks = disks_sorted[:DISK_DISPLAY_COUNT] # 打印调试信息 print(f"[磁盘筛选] 符合条件的vol1-6分区: {[d['mountpoint'] for d in disks]}") print(f"[磁盘筛选] 最终显示的分区: {[d['mountpoint'] for d in final_disks]}") return final_disks except Exception as e: print(f"[磁盘筛选] 异常: {str(e)}") return [] def draw_disk_pie(screen, center, radius, percent, used_val, used_unit, total_val, total_unit): """ 绘制单个磁盘扇形图 参数: screen: 绘制目标Surface center: 扇形图中心坐标(元组) radius: 扇形图半径 percent: 已用百分比 used_val: 已用容量数值 used_unit: 已用容量单位 total_val: 总容量数值 total_unit: 总容量单位 """ # 绘制扇形图背景 pygame.draw.circle(screen, COLORS["bg_dark"], center, radius) pygame.draw.circle(screen, (40, 40, 45), center, radius-2) # 绘制已用空间扇区(渐变效果) if percent > 0: angle = percent * 3.6 # 转换为角度 for r in range(radius-2): # 计算渐变透明度 alpha = int(150 + (r/(radius-2))*100) # 计算渐变颜色 c = (min(COLORS["disk_main"][0]*alpha/255,255), min(COLORS["disk_main"][1]*alpha/255,255), min(COLORS["disk_main"][2]*alpha/255,255)) # 绘制扇形环 pygame.draw.arc(screen, c, (center[0]-r-2, center[1]-r-2, (r+2)*2, (r+2)*2), 0, math.radians(angle), 1) # 绘制扇形图边框 pygame.draw.circle(screen, (90, 90, 95), center, radius, 1) # 绘制已用百分比文字 pct_surf = FONT_DATA.render(f"{percent}%", True, COLORS["disk_text"]) screen.blit(pct_surf, (center[0]-pct_surf.get_width()//2, center[1]-pct_surf.get_height()//2 - 5)) # 绘制容量信息文字 size_surf = FONT_SMALL.render(f"{used_val}{used_unit}/{total_val}{total_unit}", True, COLORS["disk_text"]) screen.blit(size_surf, (center[0]-size_surf.get_width()//2, center[1]+size_surf.get_height()//2 + 5)) def draw_disk(screen, disks, rect): """ 绘制磁盘监控模块 参数: screen: 绘制目标Surface disks: 磁盘信息列表 rect: 模块矩形区域 """ # 绘制卡片背景 draw_card(screen, rect) # 绘制竖排标题 draw_vertical_title(screen, rect, "存储") # 无符合条件的分区时,显示提示文字 if not disks: no_disk_surf = FONT_DATA.render("未检测到存储分区", True, COLORS["disk_text"]) screen.blit(no_disk_surf, (rect.centerx - no_disk_surf.get_width()//2, rect.centery)) return # 绘制第一个分区(左) disk1 = disks[0] center1 = (rect.centerx - 80, rect.centery) draw_disk_pie(screen, center1, 65, disk1["percent"], disk1["used_val"], disk1["used_unit"], disk1["total_val"], disk1["total_unit"]) # 绘制第二个分区(右) if len(disks) >= 2: disk2 = disks[1] center2 = (rect.centerx + 80, rect.centery) draw_disk_pie(screen, center2, 65, disk2["percent"], disk2["used_val"], disk2["used_unit"], disk2["total_val"], disk2["total_unit"]) # ==================== 网络模块相关函数 ==================== def update_net_info(): """ 更新网络速度信息 返回: (上传速度值, 下载速度值, 上传单位, 下载单位) """ global g_net_last_update, g_net_send, g_net_recv, g_net_history, g_net_last_io # 获取当前时间戳 now = time.time() # 首次运行时初始化 if g_net_last_io is None: g_net_last_io = psutil.net_io_counters() g_net_last_update = now g_net_history.append((0.0, 0.0)) return 0.0, 0.0, "KB/s", "KB/s" # 每3秒更新一次网速(避免频繁更新) if now - g_net_last_update >= 3: try: # 获取当前网络IO统计 net_io = psutil.net_io_counters() # 计算时间差 delta = now - g_net_last_update # 计算每秒速度(KB/s) if delta > 0: send_kb = round((net_io.bytes_sent - g_net_last_io.bytes_sent) / delta / 1024, 1) recv_kb = round((net_io.bytes_recv - g_net_last_io.bytes_recv) / delta / 1024, 1) else: send_kb = 0.0 recv_kb = 0.0 # 转换网速单位 send_val, send_unit = convert_net_speed(send_kb) recv_val, recv_unit = convert_net_speed(recv_kb) # 更新全局缓存 g_net_send = send_val g_net_recv = recv_val # 添加到历史数据 g_net_history.append((send_kb, recv_kb)) # 限制历史数据长度 if len(g_net_history) > NET_HISTORY_MAX: g_net_history.pop(0) # 更新最后更新时间和IO数据 g_net_last_io = net_io g_net_last_update = now return send_val, recv_val, send_unit, recv_unit except Exception as e: # 异常时使用缓存值 send_val, send_unit = convert_net_speed(g_net_send) recv_val, recv_unit = convert_net_speed(g_net_recv) return send_val, recv_val, send_unit, recv_unit # 未到更新时间,返回缓存值 send_val, send_unit = convert_net_speed(g_net_send) recv_val, recv_unit = convert_net_speed(g_net_recv) return send_val, recv_val, send_unit, recv_unit def draw_network(screen, send_val, recv_val, send_unit, recv_unit, history, rect): """ 绘制网络监控模块 参数: screen: 绘制目标Surface send_val: 上传速度值 recv_val: 下载速度值 send_unit: 上传单位 recv_unit: 下载单位 history: 网速历史数据 rect: 模块矩形区域 """ # 绘制卡片背景 draw_card(screen, rect) # 绘制竖排标题 draw_vertical_title(screen, rect, "网络") # 计算图表位置 chart_x = rect.x + 30 chart_y = rect.y + 25 chart_width = rect.width - 60 chart_height = rect.height - 70 # 绘制图表背景 pygame.draw.rect(screen, COLORS["bg_dark"], (chart_x, chart_y, chart_width, chart_height), border_radius=8) pygame.draw.rect(screen, (40, 40, 45), (chart_x+1, chart_y+1, chart_width-2, chart_height-2), border_radius=7) # 绘制网速历史曲线 if len(history) > 0: # 提取所有历史速度值,计算最大值 all_values = [val for pair in history for val in pair] max_val = max(all_values) if all_values and max(all_values) > 0 else 1.0 # 初始化坐标点列表 points_send = [] points_recv = [] # 计算每个数据点的坐标 for i, (s, r) in enumerate(history): # 计算X坐标(均匀分布) x_step = (chart_width - 10) / max(len(history) - 1, 1) x = chart_x + 5 + i * x_step # 计算Y坐标(速度越高,位置越靠上) y_s = chart_y + chart_height - 5 - int((s / max_val) * (chart_height - 10)) y_r = chart_y + chart_height - 5 - int((r / max_val) * (chart_height - 10)) # 限制Y坐标范围(避免超出图表) y_s = max(chart_y + 5, min(y_s, chart_y + chart_height - 5)) y_r = max(chart_y + 5, min(y_r, chart_y + chart_height - 5)) # 添加到坐标点列表 points_send.append((x, y_s)) points_recv.append((x, y_r)) # 绘制上传/下载速度曲线 if len(points_send) >= 2: pygame.draw.lines(screen, COLORS["net_send"], False, points_send, 1) pygame.draw.lines(screen, COLORS["net_recv"], False, points_recv, 1) # 绘制当前速度点 if len(points_send) > 0: pygame.draw.circle(screen, COLORS["net_send"], points_send[-1], 3) pygame.draw.circle(screen, COLORS["net_recv"], points_recv[-1], 3) # 绘制网速文字 send_surf = FONT_NET.render(f"上传: {send_val}{send_unit}", True, COLORS["net_send"]) recv_surf = FONT_NET.render(f"下载: {recv_val}{recv_unit}", True, COLORS["net_recv"]) # 计算文字位置(居中) text_y = rect.y + rect.height - 35 total_w = send_surf.get_width() + recv_surf.get_width() + 20 start_x = rect.centerx - total_w // 2 # 绘制文字 screen.blit(send_surf, (start_x, text_y)) screen.blit(recv_surf, (start_x + send_surf.get_width() + 20, text_y)) # ==================== 进程模块相关函数 ==================== def get_top_processes(): """ 获取CPU使用率最高的前5个进程 返回: 进程信息列表(pid, name, cpu_percent, memory_percent) """ try: procs = [] # 遍历所有进程 for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']): try: # 仅保留CPU使用率>0的进程 if proc.info['cpu_percent'] > 0: procs.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): # 跳过无权限/已结束的进程 continue # 按CPU使用率降序排序,取前5个 return sorted(procs, key=lambda x: x['cpu_percent'], reverse=True)[:5] except Exception as e: return [] def draw_processes(screen, procs, rect): """ 绘制进程监控模块 参数: screen: 绘制目标Surface procs: 进程信息列表 rect: 模块矩形区域 """ # 绘制卡片背景 draw_card(screen, rect) # 绘制竖排标题 draw_vertical_title(screen, rect, "进程") # 绘制表头 header_x = rect.x + 60 header_y = rect.y + 20 # 表头列配置:(列名, 宽度) cols = [("PID", 60), ("名称", 100), ("CPU%", 60), ("内存%", 60)] current_x = header_x # 绘制表头文字 for name, width in cols: surf = FONT_SMALL.render(name, True, COLORS["proc_text"]) screen.blit(surf, (current_x + width//2 - surf.get_width()//2, header_y)) current_x += width # 绘制进程列表 proc_y = rect.y + 50 line_h = 30 # 每行高度 # 绘制前5个进程 for i, proc in enumerate(procs[:5]): y = proc_y + i*line_h current_x = header_x # 绘制PID pid_surf = FONT_SMALL.render(str(proc['pid']), True, COLORS["text_base"]) screen.blit(pid_surf, (current_x + 30 - pid_surf.get_width()//2, y)) current_x += 60 # 绘制进程名称(截断为8个字符) name = proc['name'][:8] if len(proc['name']) > 8 else proc['name'] name_surf = FONT_SMALL.render(name, True, COLORS["text_base"]) screen.blit(name_surf, (current_x + 50 - name_surf.get_width()//2, y)) current_x += 100 # 绘制CPU使用率 cpu_surf = FONT_SMALL.render(f"{proc['cpu_percent']:.1f}", True, COLORS["text_base"]) screen.blit(cpu_surf, (current_x + 30 - cpu_surf.get_width()//2, y)) current_x += 60 # 绘制内存使用率 mem_surf = FONT_SMALL.render(f"{proc['memory_percent']:.1f}", True, COLORS["text_base"]) screen.blit(mem_surf, (current_x + 30 - mem_surf.get_width()//2, y)) # ==================== 主函数 ==================== def main(): """程序主入口""" print("===== 硬件监控程序启动 =====\n💡 按 Ctrl+C 退出") # 初始化系统环境 init_env() # 初始化Pygame screen = init_pygame() # 生成背景图层 bg_surface = generate_background() # 初始化网络监控变量 global g_net_last_update, g_net_last_io, g_net_history g_net_last_update = time.time() g_net_last_io = psutil.net_io_counters() g_net_history = [] try: # 主循环 while True: # 处理退出事件 for event in pygame.event.get(): if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE): raise KeyboardInterrupt # 绘制背景 screen.blit(bg_surface, (0, 0)) # ========== 获取硬件信息 ========== # CPU信息 cpu_total = psutil.cpu_percent(interval=0.1) cpu_cores = psutil.cpu_percent(percpu=True) cpu_temp = get_cpu_temp() cpu_info = {"total": cpu_total, "cores": cpu_cores, "temp": cpu_temp} # 内存信息 mem = psutil.virtual_memory() mem_info = { "total": round(mem.total / 1024/1024/1024, 1), "used_pct": round(mem.percent, 1), "free_pct": round(100 - mem.percent, 1) } # 磁盘信息 disks = filter_and_sort_disks() # 进程信息 procs = get_top_processes() # 网络信息 send_val, recv_val, send_unit, recv_unit = update_net_info() # ========== 计算模块位置 ========== cpu_rect = pygame.Rect(GAP, GAP, CARD_WIDTH, CPU_HEIGHT) mem_rect = pygame.Rect(GAP, cpu_rect.bottom + GAP, CARD_WIDTH, MEM_HEIGHT) disk_rect = pygame.Rect(GAP, mem_rect.bottom + GAP, CARD_WIDTH, DISK_HEIGHT) net_rect = pygame.Rect(GAP, disk_rect.bottom + GAP, CARD_WIDTH, NET_HEIGHT) proc_rect = pygame.Rect(GAP, net_rect.bottom + GAP, CARD_WIDTH, PROC_HEIGHT) # ========== 绘制模块 ========== draw_cpu(screen, cpu_info, cpu_rect) draw_memory(screen, mem_info, mem_rect) draw_disk(screen, disks, disk_rect) draw_network(screen, send_val, recv_val, send_unit, recv_unit, g_net_history, net_rect) draw_processes(screen, procs, proc_rect) # 更新显示 pygame.display.flip() # 主循环延迟(1秒) time.sleep(1) except KeyboardInterrupt: # 捕获Ctrl+C退出 print("\n程序正常退出") except Exception as e: # 捕获其他异常 print(f"\n程序异常:{str(e)}") import traceback traceback.print_exc() finally: # 清理资源 pygame.quit() sys.exit(0) # ==================== 程序入口 ==================== if __name__ == "__main__": main() |
# 看注释和实际使用情况,鄙人水平不行,有问题别问更别请教,我不配!
# 本文未经本人同意禁止转载和二改,同意的也注明出处,互相尊重有惊喜。
目前用法:
都放到/vol1/1000/FnOS,然后sudo /vol1/1000/FnOS/install_run_ultimate.sh,注意换行符和编码
点赞扫码,感谢支持
发表回复