- PVSM.RU - https://www.pvsm.ru -
Всем доброго дня! Мой никнейм Arduinum628, я занимаюсь DIY проектами и программированием на Python и C++. В этой статье пойдёт речь о выводе системной информации с ПК на круглый LCD дисплей GC9A01.
Сама идея проекта мне пришла во время разговора с другом Иваном. Я рассказал ему, что заказал пару LCD дисплей GC9A01 с Ali Express для своих будущих DIY проектов. Во время разговора Иван внезапно сказал, что ему-бы пригодился девайс для вывода системной информации с ПК. Я подумал - почему-бы не научиться использовать этот дисплей на подобном проекте?!
Сам проект я буду писать не для нужд друга, а скорее в целях обучения работы с этим дисплеем. Как я понял, что ему нужно что-то более компактное и встраиваемое в корпус ПК. По моему совету он купил компактную плату esp32 с дисплеем и будет писать своё решение сам. Я же собираюсь делать что-то вроде приборной панели и поставлю её за клавиатурой. Это чем-то будет напоминать спидометр автомобиля =)
Главная идея проекта — выводить системную информацию на дисплей. Какие параметры можно отображать?
CPU Load — загруженность процессора;
CPU Temp — температура процессора;
RAM Usage — загруженность оперативной памяти;
GPU Load — загруженность видеокарты;
GPU Memory Used — загруженность видеопамяти;
GPU Temp — температура видеокарты;
Disk Usage — загруженность диска.
Шкалы будут подписаны: название параметра у начала шкалы и единица измерения в конце.
Кроме того, шкала будет менять цвет в зависимости от уровня нагрузки (пример):
зелёный — низкая нагрузка (0 - 50%);
жёлтый — средняя нагрузка (51 - 80%);
красный — высокая нагрузка (81 - 100%).
Дисплей будет установлен рядом с клавиатурой и работать как приборная панель. Он подключится к Arduino Uno, которая, в свою очередь, будет соединена с ПК через USB.
Дальше начинается самое интересное — получение данных по USB и их вывод на дисплей. Передача данных будет осуществляться через COM-порт. Данные будут отображаться в виде текста на экране (по крайней мере в первой версии, а дальше посмотрим).
Я хочу, чтобы программа работала на трёх основных платформах: MacOS, Linux и Windows. Поэтому для получения системной информации я буду использовать кроссплатформенные библиотеки, такие как psutil и другие, написанные на Python. Пока первая версия будет написана исключительно для Linux, но со временем я добавлю поддержку и других операционных систем.
Код в этой статье является прототипом и не претендует на идеальную реализацию.
Для прототипа мне понадобятся следующие компоненты:
LSD дисплей 240х240 с чипом GC9A01 - ссылка [1];
Резисторы на 150 ом;
Провода дюпонты;
Arduino Uno (в дальнейшем заменю на компактную Arduino Nano);
Макетные плата (без пайки);
Пластик.
Главная идея конструкции заключается в том, что она просто стоит за клавиатурой. Так как это прототип, я не стал заморачиваться с дизайном и красивым корпусом. Просто взял пару уголков от упаковки шкафа и пластик от крышки с влажными салфетками.
Приклеил макетные платы на двусторонний скотч, соединил их и вставил друг в друга, чтобы две половинки конструкции держались вместе.
Из пластика вырезал несколько деталей для крепления экрана и просверлил четыре отверстия под его крепление. В итоге получилась конструкция, которая идеально подходит по высоте клавиатуры.
Экран прикрутил четырьмя винтами к пластиковому креплению. Пластину согнул под углом, чтобы дисплей смотрел вверх, как в панели управления или спидометре.
Провода пришлось укоротить и припаять Dupont-папа вместо Dupont-мама.
Для изоляции использовал тонкую термоусадку. Сами провода аккуратно уложил, прижал их хомутом, а разъёмы вставил по порядку в макетную плату.
Вторая макетная плата пригодится позже. Я собираюсь установить туда Arduino Nano, но пока её нет в наличии, поэтому временно использую Arduino Uno, которая будет лежать рядом.
Пины дисплея:
VIN — +;
GND — земля;
CS - Chip Select — активация дисплея для обмена данными;
DC - Data/Command — определяет, передаётся ли команда (настройка дисплея) или данные (графика, текст);
RES - Reset — используется для сброса дисплея, помогает корректно перезапустить его;
SDA - MOSI (Master Out Slave In) — передача данных;
SCL - SCK (Serial Clock) — тактовый сигнал SPI, синхронизирует передачу данных.
BLK - подсветка;
Подключение GC9A01 -> Arduino:
VIN -> 3.3V / 5V (в зависимости от модуля)
GND -> GND
CS -> resistor 150om/200om -> D10
DC -> resistor 150om/200om -> D9
RES -> D8 (не будет задействован)
SDA -> resistor 150om/200om -> D11 (MOSI)
SCL -> resistor 150om/200om -> D13 (SCK)
BLK -> 3.3V / PWM (например, D6) (не будет задействован)
Собранный проект у меня за клавиатурой:
Для начала я напишу код для Arduino, который будет принимать данные от Python-программы через Serial и выводить их на LCD-дисплей GC9A01A.
spec_pc_to_lcd.ino:
#include <SPI.h>
#include <Adafruit_GC9A01A.h>
// Определение пинов:
#define TFT_CS 10 // Chip Select
#define TFT_DC 9 // Data/Command
#define TFT_RES 8 // Reset
// Создаём объект дисплея:
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RES);
const int MAX_NUMBERS = 7; // Максимум чисел в одной строке
int numbers[MAX_NUMBERS]; // Массив для хранения чисел
int numberCount = 0; // Сколько чисел было принято
String inputString = ""; // Буфер ввода
bool inputComplete = false;
void setup() {
Serial.begin(9600); // Скорость передачи данных
tft.begin(); // Инициализация дисплея
tft.fillScreen(GC9A01A_BLACK); // Заливаем экран чёрным цветом
tft.setTextSize(2); // Установка размера шрифта
}
void loop() {
while (Serial.available()) {
// Читаем данные из Serial
char inChar = (char)Serial.read();
if (inChar == 'n') { // Конец строки
inputComplete = true;
break;
} else {
inputString += inChar;
}
}
// Если строка получена полностью
if (inputComplete) {
parseInputString(); // Разбор строки в числа
setCpuLd(numbers[0]); // Установка загрузки CPU
setCpuTp(numbers[1]); // Установка температуры CPU
setRumUs(numbers[2]); // Установка загрузки ОЗУ
setGpuLd(numbers[3]); // Установка загрузки GPU
setGpuMe(numbers[4]); // Установка загрузки видеопамяти GPU
setGpuTp(numbers[5]); // Установка температуры GPU
setDscUs(numbers[6]); // Установка загрузки диска
// Сброс буфера
inputString = "";
inputComplete = false;
}
}
// Функция для получения цвета текста в зависимости от уровня загрузки (CPU, RAM, GPU, диск)
uint16_t getRateColorText(int lvl) {
uint16_t color;
if (lvl <= 50) {
color = GC9A01A_GREEN; // Низкая загрузка - зелёный
} else if (lvl <= 80) {
color = GC9A01A_YELLOW; // Средняя загрузка - жёлтый
} else {
color = GC9A01A_RED; // Высокая загрузка - красный
}
return color;
}
// Функция для получения цвета текста в зависимости от температуры CPU
uint16_t getCpuTpColorText(int lvl) {
uint16_t color;
if (lvl <= 65) {
color = GC9A01A_GREEN; // Низкая температура - зелёный
} else if (lvl <= 85) {
color = GC9A01A_YELLOW; // Средняя температура - жёлтый
} else {
color = GC9A01A_RED; // Высокая температура - красный
}
return color;
}
// Функция для получения цвета текста в зависимости от температуры GPU
uint16_t getGpuTpColorText(int lvl) {
uint16_t color;
if (lvl <= 70) {
color = GC9A01A_GREEN; // Низкая температура - зелёный
} else if (lvl <= 85) {
color = GC9A01A_YELLOW; // Средняя температура - жёлтый
} else {
color = GC9A01A_RED; // Высокая температура - красный
}
return color;
}
// Функция устанавливает строку загруженности процессора
void setCpuLd(int lvl) {
tft.setCursor(50, 50);
tft.setTextColor(getRateColorText(lvl));
tft.println("CPU ld: " + String(lvl) + "%");
}
// Функция устанавливает строку температуры процессора
void setCpuTp(int lvl) {
tft.setCursor(50, 75);
tft.setTextColor(getCpuTpColorText(lvl));
tft.println("CPU tp: " + String(lvl) + "C");
}
// Функция устанавливает строку загруженности оперативной памяти
void setRumUs(int lvl) {
tft.setCursor(50, 95);
tft.setTextColor(getRateColorText(lvl));
tft.println("RAM us: " + String(lvl) + "%");
}
// Функция устанавливает строку загруженности видеокарты
void setGpuLd(int lvl) {
tft.setCursor(50, 115);
tft.setTextColor(getRateColorText(lvl));
tft.println("GPU ld: " + String(lvl) + "%");
}
// Функция устанавливает строку загруженности видеопамяти видеокарты
void setGpuMe(int lvl) {
tft.setCursor(50, 135);
tft.setTextColor(getRateColorText(lvl));
tft.println("GPU me: " + String(lvl) + "%");
}
// Функция устанавливает строку температуры видеокарты
void setGpuTp(int lvl) {
tft.setCursor(50, 155);
tft.setTextColor(getGpuTpColorText(lvl));
tft.println("GPU tp: " + String(lvl) + "C");
}
// Функция устанавливает строку загруженности жёсткого диска
void setDscUs(int lvl) {
tft.setCursor(50, 175);
tft.setTextColor(getRateColorText(lvl));
tft.println("DSC us: " + String(lvl) + "%");
}
// Функция парсит числа из строки, кладя их в массив
void parseInputString() {
numberCount = 0;
char inputBuffer[100];
inputString.toCharArray(inputBuffer, 100);
char* token = strtok(inputBuffer, " ");
while (token != NULL && numberCount < MAX_NUMBERS) {
numbers[numberCount++] = atoi(token); // Преобразование строки в число
token = strtok(NULL, " ");
}
}
Импорты:
#include <SPI.h> — библиотека для работы с SPI-интерфейсом;
#include <Adafruit_GC9A01A.h> — библиотека для управления дисплеем GC9A01A.
Определение пинов и создание объекта дисплея:
#define TFT_CS 10 — пин для Chip Select;
#define TFT_DC 9 — пин для Data/Command;
#define TFT_RES 8 — пин для Reset;
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RES); — создание объекта дисплея.
Переменные для хранения данных:
const int MAX_NUMBERS = 7; — максимальное количество чисел в одной строке;
int numbers[MAX_NUMBERS]; — массив для хранения чисел;
int numberCount = 0; — количество принятых чисел;
String inputString = ""; — строка для хранения входных данных;
bool inputComplete = false; — флаг завершения ввода.
setup() — настройка дисплея и последовательного порта:
Serial.begin(9600); — установка скорости передачи данных;
tft.begin(); — инициализация дисплея;
tft.fillScreen(GC9A01A_BLACK); — заливка экрана чёрным цветом;
tft.setTextSize(2); — установка размера шрифта.
loop() — основной цикл программы:
Чтение данных из Serial;
Проверка окончания строки ('n');
Вызов parseInputString() для обработки входных данных;
Вызов функций для отображения значений на экране;
Очистка буфера inputString.
getRateColorText(int lvl) — цвет для загрузки CPU, RAM, GPU, диска:
Зелёный (GC9A01A_GREEN) при загрузке до 50%;
Жёлтый (GC9A01A_YELLOW) при загрузке до 80%;
Красный (GC9A01A_RED) при загрузке выше 80%.
getCpuTpColorText(int lvl) — цвет для температуры CPU:
Зелёный (GC9A01A_GREEN) при температуре до 65°C;
Жёлтый (GC9A01A_YELLOW) при температуре до 85°C;
Красный (GC9A01A_RED) при температуре выше 85°C.
getGpuTpColorText(int lvl) — цвет для температуры GPU:
Зелёный (GC9A01A_GREEN) при температуре до 70°C;
Жёлтый (GC9A01A_YELLOW) при температуре до 85°C;
Красный (GC9A01A_RED) при температуре выше 85°C.
setCpuLd(int lvl) — загрузка CPU:
Устанавливает курсор на (50, 50);
Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит CPU ld: X%.
setCpuTp(int lvl) — температура CPU:
Устанавливает курсор на (50, 75);
Устанавливает цвет текста в зависимости от температуры;
Выводит CPU tp: X°C.
setRumUs(int lvl) — загрузка оперативной памяти:
Устанавливает курсор на (50, 95);
Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит RAM us: X%.
setGpuLd(int lvl) — загрузка GPU:
Устанавливает курсор на (50, 115);
Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит GPU ld: X%.
setGpuMe(int lvl) — загрузка видеопамяти GPU:
Устанавливает курсор на (50, 135);
Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит GPU me: X%.
setGpuTp(int lvl) — температура GPU:
Устанавливает курсор на (50, 155);
Устанавливает цвет текста в зависимости от температуры;
Выводит GPU tp: X°C.
setDscUs(int lvl) — загрузка диска:
Устанавливает курсор на (50, 175);
Устанавливает цвет текста в зависимости от уровня загрузки;
Выводит DSC us: X%.
parseInputString() — парсинг входной строки:
Преобразует строку inputString в массив numbers;
Использует strtok() для разделения строки по пробелам;
Преобразует каждое значение в int и сохраняет в numbers.
После написания кода я проверяю его и загружаю на Arduino. Первая часть программы готова, и теперь нужно написать Python-код, который будет отправлять данные на Arduino.
Основная задача Python кода это подключаться к Arduino по Serial и передавать данные PC на неё. Для получения данных PC я буду пользоваться готовыми библиотеками.
Начну реализацию с конфига, в котором будут храниться настройки для подключения.
В конфигурации нет секретных данных, поэтому я не буду усложнять её чтением .env, pydantic_settings и другими инструментами, предназначенными для работы с конфиденциальными данными. Я создам самый простой конфиг и просто захардкодю в нём необходимые параметры.
config.py [2]:
PORT = "/dev/ttyACM1" # заменить на свой
SPEED = 9600
TIMEOUT = 1
Разбор кода:
Этот код определяет параметры для последовательного соединения с устройством;
PORT = "/dev/ttyACM1" — задаёт порт, к которому подключено устройство. Нужно заменить на актуальный порт, если он отличается;
SPEED = 9600 — устанавливает скорость передачи данных в бодах (битах в секунду);
TIMEOUT = 1 — задаёт тайм-аут в секундах для ожидания ответа от устройства.
Теперь я напишу функцию, которая будет отправлять данные на Arduino по Serial.
get_spec_pc.py:
# Импорт необходимых библиотек
from psutil import cpu_percent, sensors_temperatures, virtual_memory, disk_usage
# Библиотека для работы с видеокартами Nvidia
from pynvml import (
nvmlInit,
nvmlDeviceGetHandleByIndex,
nvmlDeviceGetUtilizationRates,
nvmlDeviceGetTemperature,
NVML_TEMPERATURE_GPU,
nvmlShutdown
)
from serial import Serial
from time import sleep
def send_spec_str_serial(port: str, speed: int, timeout: int) -> None:
"""Функция для отправки строки со спецификацией ПК в Serial"""
while True:
with Serial(port=port, baudrate=speed, timeout=timeout) as ser:
sleep(1) # Даем время на подключение Arduino
# Получаем загрузку CPU в процентах
cpu_load = int(cpu_percent(interval=1)) # Средняя загрузка за 1 секунду
# Получаем температуру CPU
cpu_temp = int(sensors_temperatures().get("coretemp")[0].current)
# Получаем загрузку ОЗУ в процентах
ram_usage = int(virtual_memory().percent)
# Получаем загрузку диска (работает только на Linux)
dsk_usage = int(disk_usage("/").percent)
# Работаем с видеокартой Nvidia
nvmlInit()
handle = nvmlDeviceGetHandleByIndex(0) # 0 — первая видеокарта
gpu_util = nvmlDeviceGetUtilizationRates(handle)
gpu_temp = nvmlDeviceGetTemperature(handle, NVML_TEMPERATURE_GPU)
# Формируем строку данных
data_str = f"{cpu_load} {cpu_temp} {ram_usage} {gpu_util.gpu} {gpu_util.memory} {gpu_temp} {dsk_usage}n"
ser.write(data_str.encode("ascii"))
# Для проверки (раскомментируйте при необходимости)
# print(f"CPU Load: {cpu_load}%")
# print(f"CPU Temp: {cpu_temp}C")
# print(f"RAM Usage: {ram_usage}%")
# print(f"GPU Load: {gpu_util.gpu}%")
# print(f"GPU Memory Used: {gpu_util.memory}%")
# print(f"GPU Temp: {gpu_temp}C")
# print(f"Disk Usage: {dsk_usage}%")
# print(data_str)
sleep(4)
nvmlShutdown()
if __name__ == "__main__":
from config import PORT, SPEED, TIMEOUT
send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT)
Импорты:
from psutil import cpu_percent, sensors_temperatures, virtual_memory, disk_usage — библиотека для мониторинга загрузки процессора, температуры, оперативной памяти и диска;
from pynvml import nvmlInit, nvmlDeviceGetHandleByIndex, nvmlDeviceGetUtilizationRates, nvmlDeviceGetTemperature, NVML_TEMPERATURE_GPU, nvmlShutdown — библиотека для работы с видеокартами Nvidia;
from serial import Serial — библиотека для работы с последовательным портом;
from time import sleep — библиотека для работы с задержками.
Определение переменных
port: str — строка с именем последовательного порта;
speed: int — скорость передачи данных;
timeout: int — тайм-аут ожидания ответа от устройства.
Функция send_spec_str_serial(port: str, speed: int, timeout: int) -> None:
Запускается бесконечный цикл для отправки данных;
Создаётся соединение с устройством через Serial;
sleep(1) — даёт время на подключение Arduino перед отправкой данных;
Получается загрузка процессора с помощью cpu_percent(interval=1), вычисляется средняя загрузка за одну секунду;
Получается температура процессора через sensors_temperatures().get("coretemp")[0].current;
Получается загрузка оперативной памяти через virtual_memory().percent;
Получается загрузка диска с помощью disk_usage("/"), работает только на Linux;
Инициализируется работа с видеокартой Nvidia через nvmlInit();
Получается загрузка видеокарты с помощью nvmlDeviceGetUtilizationRates(handle);
Получается температура видеокарты через nvmlDeviceGetTemperature(handle, NVML_TEMPERATURE_GPU);
Формируется строка данных, содержащая загрузку и температуру CPU, загрузку RAM, загрузку и температуру GPU, а также загрузку диска;
Строка отправляется через ser.write(data_str.encode("ascii"));
sleep(4) — задержка перед следующим циклом отправки данных;
Завершается работа с видеокартой Nvidia через nvmlShutdown().
Блок if __name__ == "__main__"::
Импортируются параметры PORT, SPEED и TIMEOUT из файла config.py [2];
Вызывается функция send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT), которая начинает отправку данных через последовательный порт.
Да простят меня гуру Python, я не обработал ошибки в прототипе. Я понимаю, насколько это важно, и планирую заняться этим в следующих статьях. Сейчас для меня главное — работоспособность проекта, так как я уже немало намучался с работой самого дисплея.
Настало время протестировать мой прототип. Для запуска Python-кода я просто нажимаю кнопку Run в IDE. Для запуска Arduino-проекта достаточно просто подключить USB-кабель к компьютеру.
На экране отображается вся системная информация, которую Python-код передаёт через Serial. Правда, обновление экрана пока не очень плавное, а скорее прерывистое. Кроме того, обновляется не одно число, а вся строка, что вызывает мерцание всех строк каждые 5 секунд. Но я уже рад, что смог получить данные и вывести их на экран.
На втором фото видно экран с тестовыми данными и красным цветом строки. Это было нужно для тестирования всех цветов.
Получился вполне рабочий прототип, который пока далёк от идеала. В будущем я добавлю возможность запускать его на разных операционных системах и постараюсь добавить поддержку видеокарт, помимо Nvidia. Также нужно реализовать обработку ошибок, разбить код для Arduino на отдельные файлы, обновлять только числа, а не всю строку, и выводить отладочную информацию в консоль.
В более далёкой перспективе я хочу научиться рисовать кольца и линии на этом экране, чтобы отображать шкалы заполнения, стрелки и графики.
Если у вас есть идеи по улучшению проекта, пишите в комментариях — с удовольствием выслушаю ваши предложения!
Впереди ещё много интересного! =)
Канал в Telegram Arduinum628 [3]
Библиотека для дисплея Adafruit_GC9A01A [4]
Репозиторий проекта Spec_pc_to_lcd [5]
Автор: Arduinum
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/arduino/421938
Ссылки в тексте:
[1] ссылка : https://aliexpress.ru/item/1005004911604497.html?sku_id=12000032757861044&srcSns=sns_More&businessType=ProductDetail&spreadType=socialShare&tt=MG&utm_medium=sharing
[2] config.py: http://config.py
[3] Arduinum628: https://t.me/+lbzO88SiqtA2ODNi
[4] Adafruit_GC9A01A: https://github.com/adafruit/Adafruit_GC9A01A/tree/main
[5] Spec_pc_to_lcd: https://github.com/Arduinum/spec_pc_to_lcd
[6] Источник: https://habr.com/ru/articles/916486/?utm_campaign=916486&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.