Всем доброго дня! Мой никнейм 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 - ссылка ;
-
Резисторы на 150 ом;
-
Провода дюпонты;
-
Arduino Uno (в дальнейшем заменю на компактную Arduino Nano);
-
Макетные плата (без пайки);
-
Пластик.
Сборка конструкции
Главная идея конструкции заключается в том, что она просто стоит за клавиатурой. Так как это прототип, я не стал заморачиваться с дизайном и красивым корпусом. Просто взял пару уголков от упаковки шкафа и пластик от крышки с влажными салфетками.
Приклеил макетные платы на двусторонний скотч, соединил их и вставил друг в друга, чтобы две половинки конструкции держались вместе.
Из пластика вырезал несколько деталей для крепления экрана и просверлил четыре отверстия под его крепление. В итоге получилась конструкция, которая идеально подходит по высоте клавиатуры.
Экран прикрутил четырьмя винтами к пластиковому креплению. Пластину согнул под углом, чтобы дисплей смотрел вверх, как в панели управления или спидометре.
Провода пришлось укоротить и припаять Dupont-папа вместо Dupont-мама.
Для изоляции использовал тонкую термоусадку. Сами провода аккуратно уложил, прижал их хомутом, а разъёмы вставил по порядку в макетную плату.
Вторая макетная плата пригодится позже. Я собираюсь установить туда Arduino Nano, но пока её нет в наличии, поэтому временно использую Arduino Uno, которая будет лежать рядом.
Подключение к Arduino
Пины дисплея:
-
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
Для начала я напишу код для 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
Основная задача Python кода это подключаться к Arduino по Serial и передавать данные PC на неё. Для получения данных PC я буду пользоваться готовыми библиотеками.
Начну реализацию с конфига, в котором будут храниться настройки для подключения.
В конфигурации нет секретных данных, поэтому я не буду усложнять её чтением .env, pydantic_settings и другими инструментами, предназначенными для работы с конфиденциальными данными. Я создам самый простой конфиг и просто захардкодю в нём необходимые параметры.
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; -
Вызывается функция
send_spec_str_serial(port=PORT, speed=SPEED, timeout=TIMEOUT), которая начинает отправку данных через последовательный порт.
Да простят меня гуру Python, я не обработал ошибки в прототипе. Я понимаю, насколько это важно, и планирую заняться этим в следующих статьях. Сейчас для меня главное — работоспособность проекта, так как я уже немало намучался с работой самого дисплея.
Запуск
Настало время протестировать мой прототип. Для запуска Python-кода я просто нажимаю кнопку Run в IDE. Для запуска Arduino-проекта достаточно просто подключить USB-кабель к компьютеру.
На экране отображается вся системная информация, которую Python-код передаёт через Serial. Правда, обновление экрана пока не очень плавное, а скорее прерывистое. Кроме того, обновляется не одно число, а вся строка, что вызывает мерцание всех строк каждые 5 секунд. Но я уже рад, что смог получить данные и вывести их на экран.
На втором фото видно экран с тестовыми данными и красным цветом строки. Это было нужно для тестирования всех цветов.
Заключение
Получился вполне рабочий прототип, который пока далёк от идеала. В будущем я добавлю возможность запускать его на разных операционных системах и постараюсь добавить поддержку видеокарт, помимо Nvidia. Также нужно реализовать обработку ошибок, разбить код для Arduino на отдельные файлы, обновлять только числа, а не всю строку, и выводить отладочную информацию в консоль.
В более далёкой перспективе я хочу научиться рисовать кольца и линии на этом экране, чтобы отображать шкалы заполнения, стрелки и графики.
Если у вас есть идеи по улучшению проекта, пишите в комментариях — с удовольствием выслушаю ваши предложения!
Впереди ещё много интересного! =)
Ссылки к статье
-
Канал в Telegram Arduinum628
-
Библиотека для дисплея Adafruit_GC9A01A
-
Репозиторий проекта Spec_pc_to_lcd
Автор: Arduinum
