
Для мониторинга работы персонального компьютера с Windows можно использовать множество параметров, которые можно наблюдать как с помощью различных программ, так и внешних индикаторов.
Выбор параметров
Обратимся к диспетчеру задач и откроем вкладку Производительность. Использование процессора, памяти, диска и графического процессора отображается в процентах использования на временном интервале 60 секунд. Использование Ethernet отображается текущими значениями скорости на том же временном интервале.
Чего нет, так это текущих значений температур процессора, графической платы. Для них приходится использовать сторонние утилиты. Альтернативный вариант, это монитор параметров с индикацией на внешнем устройстве. Очевидный выбор — Arduino с OLED дисплеем.
Сначала я попробовал использовать SSD1306, но экран показался маленьким, поэтому я выбрал SH1106 128x64. Для отображения были выбраны следующие параметры. Использование процессора в процентах (CPU), графической платы (GPU), также в процентах и порта Ethernet по скорости данных. На втором индикаторе отображаются текущие значения температуры CPU и GPU.
Индикатор ресурсов
Для отображения были выбраны вертикальные диаграммы и на экране дисплея это выглядит вот так.

Скетч для Arduino Nano. Подключение дисплея по I2C SH1106 стандартное: SDA – A4, SCL – A5, VCC – 3,3V и GND – GND. В скетче использована библиотека U8glib.
Скрытый текст
#include <U8glib.h>
#include <SPI.h>
// Инициализация дисплея SH1106 128x64
U8GLIB_SH1106_128X64 u8g(U8G_I2C_OPT_NONE);
// Переменные для хранения данных
float cpuUsage = 0.0;
float memoryUsage = 0.0;
float networkUsage = 0.0; // Теперь это значение в Mbps (0-100)
String networkSpeed = "0 Mbps";
// Буферы для числовых значений
char cpuStr[10];
char memStr[10];
char netStr[15];
// Буфер для входящих данных
String inputString = "";
boolean stringComplete = false;
void setup(void) {
// Инициализация последовательного порта
Serial.begin(115200);
inputString.reserve(200);
// Установка контрастности дисплея (опционально)
u8g.setContrast(150);
// Инициализация буферов
strcpy(cpuStr, "0%");
strcpy(memStr, "0%");
strcpy(netStr, "0 Mbps");
// Вывод начального экрана
drawInitialScreen();
}
void loop(void) {
// Чтение данных из последовательного порта
if (stringComplete) {
parseData(inputString);
inputString = "";
stringComplete = false;
}
// Отрисовка интерфейса
u8g.firstPage();
do {
drawInterface();
} while (u8g.nextPage());
delay(200); // Обновление каждые 200 мс
}
// Функция сериального события для чтения данных
void serialEvent() {
while (Serial.available()) {
char inChar = (char)Serial.read();
inputString += inChar;
if (inChar == '\n') {
stringComplete = true;
}
}
}
// Парсинг данных в формате: CPU,MEM,NETWORK,SPEED
void parseData(String data) {
int firstComma = data.indexOf(',');
int secondComma = data.indexOf(',', firstComma + 1);
int thirdComma = data.indexOf(',', secondComma + 1);
if (firstComma != -1 && secondComma != -1 && thirdComma != -1) {
cpuUsage = data.substring(0, firstComma).toFloat();
memoryUsage = data.substring(firstComma + 1, secondComma).toFloat();
networkUsage = data.substring(secondComma + 1, thirdComma).toFloat();
networkSpeed = data.substring(thirdComma + 1);
networkSpeed.trim();
// Обновляем буферы для отображения
updateDisplayBuffers();
}
}
// Обновление буферов для отображения
void updateDisplayBuffers() {
// Форматируем значения CPU и Memory
dtostrf(cpuUsage, 3, 0, cpuStr);
strcat(cpuStr, "%");
dtostrf(memoryUsage, 3, 0, memStr);
strcat(memStr, "%");
// Копируем скорость сети
networkSpeed.toCharArray(netStr, 15);
}
// Отрисовка начального экрана
void drawInitialScreen() {
u8g.firstPage();
do {
u8g.setFont(u8g_font_6x10);
u8g.drawStr(15, 30, "Waiting for data...");
u8g.drawStr(25, 45, "Connect to PC");
} while (u8g.nextPage());
}
// Отрисовка основного интерфейса
void drawInterface() {
// Рисуем вертикальные диаграммы с подписями над столбцами
drawBar(15, 20, 20, 35, cpuUsage, "CPU"); // 0-100%
drawBar(50, 20, 20, 35, memoryUsage, "MEM"); // 0-100%
drawBar(85, 20, 20, 35, networkUsage, "NET"); // 0-100 Mbps
// Выводим числовые значения на 1 пиксель ниже основания столбцов
u8g.setFont(u8g_font_5x7);
// Центрируем текст под столбцами
int cpuWidth = u8g.getStrWidth(cpuStr);
int memWidth = u8g.getStrWidth(memStr);
int netWidth = u8g.getStrWidth(netStr);
// CPU значение
u8g.drawStr(15 + (20 - cpuWidth) / 2, 62, cpuStr);
// Memory значение
u8g.drawStr(50 + (20 - memWidth) / 2, 62, memStr);
// Network значение
u8g.drawStr(85 + (20 - netWidth) / 2, 62, netStr);
}
// Функция для отрисовки вертикальной диаграммы
void drawBar(int x, int y, int width, int height, float value, const char* label) {
// Подпись над столбцом
u8g.setFont(u8g_font_5x7);
int labelWidth = u8g.getStrWidth(label);
u8g.drawStr(x + (width - labelWidth) / 2, y - 3, label);
// Рамка диаграммы
u8g.drawFrame(x, y, width, height);
// Вычисляем высоту заполнения
int fillHeight;
if (strcmp(label, "NET") == 0) {
// Для сети: value уже в Mbps (0-100)
fillHeight = (value / 100.0) * (height - 2);
} else {
// Для CPU и MEM: value в процентах (0-100)
fillHeight = (value / 100.0) * (height - 2);
}
fillHeight = constrain(fillHeight, 0, height - 2);
// Заполняем диаграмму
if (fillHeight > 0) {
u8g.drawBox(x + 1, y + height - 1 - fillHeight, width - 2, fillHeight);
}
}Для отправки данных в Arduino я использовал скрипт на Python, версия 3.13. Также надо установить дополнительные библиотеки, для чего в командной строке наберите pip install psutil pyserial. Бибилиотека psutiul предназначена для для получения информации об использовании процессора и оперативной памяти в реальном времени, а библиотека pyserial библиотека для работы с последовательными портами (COM, USB-Serial).
Скрытый текст
import psutil
import time
import serial
def get_system_stats():
# Загрузка CPU
cpu_percent = psutil.cpu_percent(interval=0.1)
# Использование памяти
memory = psutil.virtual_memory()
memory_percent = memory.percent
# Использование сети
net_before = psutil.net_io_counters()
time.sleep(0.1)
net_after = psutil.net_io_counters()
bytes_sent = net_after.bytes_sent - net_before.bytes_sent
bytes_recv = net_after.bytes_recv - net_before.bytes_recv
# Конвертируем в Mbps (мегабиты в секунду)
total_bytes_per_second = (bytes_sent + bytes_recv) * 10
network_mbps = (total_bytes_per_second * 8) / 1000000 # Mbps
# Форматируем отображение скорости сети
if network_mbps >= 10:
network_display = f"{int(network_mbps)} Mbps" # Целое число для 10+ Mbps
else:
network_display = f"{network_mbps:.1f} Mbps" # С дробной частью для <10 Mbps
# Для диаграммы используем абсолютное значение в Mbps (максимум 100 Mbps)
network_percent = min(100.0, network_mbps)
return cpu_percent, memory_percent, network_percent, network_display, network_mbps
def main():
try:
# Подключение к Arduino на COM6
arduino = serial.Serial('COM6', 115200, timeout=1)
time.sleep(2) # Ждем инициализации Arduino
print("Connected to Arduino on COM6")
print("Sending real system data...")
while True:
cpu, mem, net_percent, net_display, net_mbps = get_system_stats()
# Формируем строку данных
data_string = f"{cpu},{mem},{net_percent},{net_display}\n"
# Отправляем данные на Arduino
arduino.write(data_string.encode())
# Выводим в консоль для отладки
print(f"CPU: {cpu:.1f}% | MEM: {mem:.1f}% | NET: {net_mbps:.1f} Mbps → Display: {net_display}")
time.sleep(0.2) # 200 мс
except serial.SerialException as e:
print(f"Serial connection error: {e}")
print("Make sure Arduino is connected to COM6 and port is available")
except KeyboardInterrupt:
print("\nData transmission stopped by user")
finally:
if 'arduino' in locals():
arduino.close()
print("Serial connection closed")
if __name__ == "__main__":
main()Данные обновляются каждые 200 мс. Этот скрипт выводит данные в порт в следующем формате, парсинг которого выполняет Arduino. Вот такие данные он отправляет в порт при запуске в IDLE

В скрипте порт указан явно, поэтому в любом редакторе укажите свой порт. Значительно удобнее создать из скрипта исполняемый файл с помощью Auto Py to Exe (графическая оболочка для PyInstaller).
В командной строке наберите auto-py-to-exe. Запуск производится из командной строки auto-py-to-exe откроется такое окно (окно командной строки закрывать нельзя):

Надо указать путь к файлу скрипта, выбрать конвертацию в один файл и скрываемую консоль. Исполняемый файл окажется в одной папке со скриптом. При постоянно подключенном Arduino удобно запускать файл одновременно с Windows. Для этого надо создать ярлык для исполняемого файла и поместить его в папку Автозагрузка. Чтобы долго не искать эту папку, в строке проводника наберите shell:startup, ввод, и сразу окажетесь в этой папке.
Индикатор температур
Здесь уже графика не нужна. Экран выглядит так:

Соответствующий скетч для Arduino Nano. Данные температуры обновляются каждые 2 секунды.
Скрытый текст
#include "U8glib.h"
// Инициализация дисплея SH1106 128x64
U8GLIB_SH1106_128X64 u8g(U8G_I2C_OPT_NONE);
// Переменные для хранения температур
float cpuTemp = 0.0;
float gpuTemp = 0.0;
// Буфер для входящих данных
String inputString = "";
bool stringComplete = false;
// Время последнего обновления
unsigned long lastUpdateTime = 0;
bool dataReceived = false;
void setup(void) {
// Инициализация последовательного порта
Serial.begin(9600);
// Резервируем память для строки
inputString.reserve(20);
// Устанавливаем цвет шрифта
if (u8g.getMode() == U8G_MODE_R3G3B2) {
u8g.setColorIndex(255);
} else {
u8g.setColorIndex(1);
}
// Начальное сообщение
Serial.println("Arduino Ready - Send temperatures in format: CPU,GPU");
}
void loop(void) {
// Чтение данных с последовательного порта
serialEvent();
// Если получены новые данные
if (stringComplete) {
if (parseTemperatureData()) {
lastUpdateTime = millis();
dataReceived = true;
Serial.print("OK - CPU: ");
Serial.print(cpuTemp);
Serial.print("C, GPU: ");
Serial.print(gpuTemp);
Serial.println("C");
} else {
Serial.println("ERROR - Invalid data format");
}
inputString = "";
stringComplete = false;
}
// Проверка времени последнего обновления (10 секунд)
if (dataReceived && (millis() - lastUpdateTime > 10000)) {
dataReceived = false;
}
// Отрисовка дисплея
u8g.firstPage();
do {
drawDisplay();
} while (u8g.nextPage());
delay(1000);
}
void serialEvent() {
while (Serial.available()) {
char inChar = (char)Serial.read();
if (inChar == '\n') {
stringComplete = true;
} else if (inChar != '\r') {
inputString += inChar;
}
}
}
bool parseTemperatureData() {
// Ожидаемый формат: "CPU_TEMP,GPU_TEMP"
int commaIndex = inputString.indexOf(',');
if (commaIndex != -1) {
String cpuString = inputString.substring(0, commaIndex);
String gpuString = inputString.substring(commaIndex + 1);
cpuString.trim();
gpuString.trim();
// Проверка валидности данных
if (isValidTemperature(cpuString) && isValidTemperature(gpuString)) {
cpuTemp = cpuString.toFloat();
gpuTemp = gpuString.toFloat();
return true;
}
}
return false;
}
bool isValidTemperature(String tempStr) {
for (unsigned int i = 0; i < tempStr.length(); i++) {
if (!isdigit(tempStr.charAt(i)) && tempStr.charAt(i) != '.' && tempStr.charAt(i) != '-') {
return false;
}
}
float temp = tempStr.toFloat();
return (temp >= -50 && temp <= 150); // Реалистичный диапазон температур
}
void drawDisplay(void) {
// Устанавливаем шрифт для заголовка и подписей
u8g.setFont(u8g_font_6x10);
u8g.setFontRefHeightExtendedText();
u8g.setDefaultForegroundColor();
u8g.setFontPosTop();
// Заголовок
u8g.drawStr(0, 0, "System Temperatures");
// Разделительная линия под заголовком
u8g.drawHLine(0, 12, 128);
// Подписи температур (меньший шрифт)
u8g.drawStr(5, 20, "CPU:");
u8g.drawStr(5, 36, "GPU:");
// Температуры - увеличенный шрифт (7x13 вместо 6x10)
u8g.setFont(u8g_font_7x13);
String cpuString = String(cpuTemp, 1) + " C";
u8g.drawStr(60, 30, cpuString.c_str());
String gpuString = String(gpuTemp, 1) + " C";
u8g.drawStr(60, 46, gpuString.c_str());
// Статус с очень маленьким шрифтом
u8g.setFont(u8g_font_5x7);
if (!dataReceived) {
// Убрали линию перед "Waiting for data"
u8g.drawStr(5, 58, "Wait data...");
} else {
u8g.drawStr(80, 58, "OK COM3");
// Время последнего обновления
unsigned long secondsAgo = (millis() - lastUpdateTime) / 1000;
String timeString = "Upd: " + String(secondsAgo) + "s ago";
u8g.drawStr(5, 58, timeString.c_str());
}
}И соответствующий ему скрипт Python для вывода данных на СОМ3 (вставьте свой):
Скрытый текст
import serial
import psutil
import time
import subprocess
import re
def get_cpu_temperature():
"""
Получение температуры CPU через WMI
"""
try:
# Используем WMI для получения температуры CPU
cmd = 'wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature'
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
for line in lines:
if line.strip() and line.strip().isdigit():
temp_kelvin = int(line.strip())
temp_celsius = (temp_kelvin / 10) - 273.15
return round(temp_celsius, 1)
except:
pass
# Альтернативный метод - примерная температура на основе нагрузки
cpu_load = psutil.cpu_percent(interval=1)
estimated_temp = 30 + (cpu_load / 2) # Базовая температура + нагрузка
return round(estimated_temp, 1)
def get_gpu_temperature():
"""
Получение температуры GPU через NVIDIA SMI или AMD
"""
try:
# Попытка получить температуру NVIDIA GPU
try:
cmd = 'nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader'
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
if result.returncode == 0:
gpu_temp = result.stdout.strip()
if gpu_temp.isdigit():
return int(gpu_temp)
except:
pass
# Попытка получить температуру AMD GPU
try:
cmd = 'aticonfig --odgt --adapter=0'
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
if result.returncode == 0:
# Парсим вывод для температуры
match = re.search(r'Temperature.*?(\d+)', result.stdout)
if match:
return int(match.group(1))
except:
pass
except:
pass
# Если не удалось получить реальную температуру GPU,
# используем примерную на основе CPU
cpu_temp = get_cpu_temperature()
return round(cpu_temp - 3, 1) # GPU обычно немного холоднее CPU
def get_temperatures():
"""
Основная функция для получения температур CPU и GPU
"""
cpu_temp = get_cpu_temperature()
gpu_temp = get_gpu_temperature()
return cpu_temp, gpu_temp
def main():
# Фиксированный порт COM3
port = 'COM3'
baudrate = 9600
print(f"Connecting to Arduino on {port}...")
try:
arduino = serial.Serial(port, baudrate, timeout=1)
time.sleep(2) # Ожидание инициализации Arduino
# Чтение приветственного сообщения от Arduino
if arduino.in_waiting:
welcome_msg = arduino.readline().decode().strip()
print(f"Arduino: {welcome_msg}")
print("Starting temperature monitoring...")
print("Press Ctrl+C to stop")
error_count = 0
max_errors = 5
while True:
try:
# Получение температур
cpu_temp, gpu_temp = get_temperatures()
# Форматирование данных для отправки
data = f"{cpu_temp:.1f},{gpu_temp:.1f}\n"
# Отправка данных на Arduino
arduino.write(data.encode())
print(f"Sent to COM3: CPU={cpu_temp:.1f}°C, GPU={gpu_temp:.1f}°C")
# Чтение ответа от Arduino
if arduino.in_waiting:
response = arduino.readline().decode().strip()
if response:
print(f"Arduino: {response}")
# Сброс счетчика ошибок при успешной отправке
error_count = 0
# Ожидание 3 секунды
time.sleep(3)
except serial.SerialException as e:
error_count += 1
print(f"Serial error ({error_count}/{max_errors}): {e}")
if error_count >= max_errors:
print("Too many errors, stopping...")
break
time.sleep(5)
except Exception as e:
print(f"Error: {e}")
time.sleep(5)
except serial.SerialException as e:
print(f"Failed to connect to {port}: {e}")
print("Please check:")
print("1. Arduino is connected to COM3")
print("2. Correct drivers are installed")
print("3. Port is not busy by other program")
except KeyboardInterrupt:
print("\nProgram interrupted by user")
except Exception as e:
print(f"Unexpected error: {e}")
finally:
if 'arduino' in locals() and arduino.is_open:
arduino.close()
print("Serial connection closed")
if __name__ == "__main__":
main()
Этот скрипт выводит данные в порт в следующем формате, парсинг которого выполняет Arduino.

Все остальные действия точно такие, как для индикатора ресурсов. Удачного повторения и модификации!