From ed726f7fb692c64d2475465650197fe72ccf22d9 Mon Sep 17 00:00:00 2001 From: Vitali Date: Tue, 15 Sep 2026 00:50:15 +0700 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20=D0=B2=D0=BD=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=B1=D1=8D=D0=BA=D0=BB=D0=BE=D0=B3=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D1=87=D0=B5=D0=BD=D1=8C=20=D0=B4=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=83=D0=BF=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D1=82=D1=87?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверено перебором через LibreHardwareMonitorLib после установки PawnIO. Ключевой вывод: температура видеокарты NVIDIA читается без прав администратора, потому что идёт через её драйвер, а не через MSR. Админ нужен только процессору. Дисков и датчиков материнской платы с вентиляторами на этой машине нет. Кандидаты на экраны расставлены по возрастанию стоимости: видеокарта, память, батарея, сеть. Последняя требует автоматической смены масштаба, потому что скорость гуляет от килобайт до мегабайт. Co-Authored-By: Claude Opus 5 --- CPU_temp/docs/BACKLOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CPU_temp/docs/BACKLOG.md b/CPU_temp/docs/BACKLOG.md index 4f2cff7..2ac1af3 100644 --- a/CPU_temp/docs/BACKLOG.md +++ b/CPU_temp/docs/BACKLOG.md @@ -41,6 +41,43 @@ Последний пункт — самый важный: он затрагивает смысл устройства, а не удобство. +### Что фактически доступно на этой машине + +Проверено перебором датчиков через `LibreHardwareMonitorLib` после установки +PawnIO (ноутбук с i5-11400H и RTX 3050 Ti): + +| Железо | Датчики | Права | +|---|---|---| +| Процессор | температура (15 датчиков: пакет, среднее, максимум, по ядрам, запас до предела), мощность | админ | +| Процессор | загрузка (общая, по ядрам, по потокам), частоты | обычные | +| Видеокарта NVIDIA | температура ядра и горячей точки, загрузка, мощность, напряжение, частоты, память, PCIe | **обычные** | +| Встроенная графика Intel | загрузка по блокам (3D, декод видео, копирование) | обычные | +| Оперативная память | занято и свободно в ГБ, загрузка в процентах | обычные | +| Сеть (5 адаптеров) | скорость приёма и отдачи, объём, утилизация | обычные | +| Батарея | заряд, деградация, ток, напряжение, мощность заряда и разряда, ёмкость | обычные | + +Важное следствие: **температура видеокарты не требует прав администратора** — +NVIDIA отдаёт её через свой драйвер, ядро для этого не нужно. Админ нужен только +процессору, потому что там чтение MSR. + +Чего нет: дисков (раздел не появился даже при включённом `IsStorageEnabled` — +вероятно, тоже упирается в права) и датчиков материнской платы с вентиляторами. +Для ноутбуков второе обычно: доступ к встроенному контроллеру закрыт. + +### Разобранные кандидаты на экраны + +1. **Видеокарта** — температура и загрузка. Полный аналог текущего экрана, + макет переиспользуется целиком. Работает даже без прав администратора. +2. **Память** — занято в гигабайтах и процент. Меняется медленно, читается с + расстояния, сглаживание не нужно. +3. **Батарея** — заряд и мощность разряда в ваттах: сразу видно, сколько ест + ноутбук прямо сейчас. Для стационарного ПК бесполезно, экран надо скрывать. +4. **Сеть** — скорость приёма и отдачи. Живее всего смотрится на графике, но + единицы прыгают от килобайт до мегабайт, и понадобится автоматическая смена + масштаба — это единственный кандидат, требующий новой логики отрисовки. + +Порядок реализации по возрастанию стоимости: видеокарта, память, батарея, сеть. + --- ## 2. Обрезание строки при 100° и 100% From b15421305c31827d28ae48b8e66c9964ab8a7b9a Mon Sep 17 00:00:00 2001 From: Vitali Date: Tue, 15 Sep 2026 01:08:11 +0700 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=20=D0=B1=D1=8D=D0=BA=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=BF=D0=BE=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=D0=BE=D0=BA=D0=B0=D1=80=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Выбран приоритетный экран, а не листалка: устройство смотрится боковым зрением и должно само показывать то, что ближе к своему порогу. Совмещённый экран отвергнут по измерениям - вторая температура влезает только шрифтом 5x7, и смысл крупной цифры теряется. Отдельным пунктом записан опыт, который надо поставить до реализации: проверить, будит ли опрос дискретную видеокарту и не начнёт ли мониторинг сам расходовать батарею. Измеритель есть - датчик мощности разряда батареи, прав администратора он не требует. Записана и главная ловушка подхода: при близких к порогам показателях экран начнёт скакать между ними, нужна задержка переключения. Co-Authored-By: Claude Opus 5 --- CPU_temp/docs/BACKLOG.md | 88 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/CPU_temp/docs/BACKLOG.md b/CPU_temp/docs/BACKLOG.md index 2ac1af3..af5a0f7 100644 --- a/CPU_temp/docs/BACKLOG.md +++ b/CPU_temp/docs/BACKLOG.md @@ -41,6 +41,9 @@ Последний пункт — самый важный: он затрагивает смысл устройства, а не удобство. +**Для видеокарты выбран другой путь** — приоритетный экран без кнопки, см. +пункт 2. Листалка остаётся в силе для остального: памяти, батареи, сети. + ### Что фактически доступно на этой машине Проверено перебором датчиков через `LibreHardwareMonitorLib` после установки @@ -80,7 +83,84 @@ NVIDIA отдаёт её через свой драйвер, ядро для э --- -## 2. Обрезание строки при 100° и 100% +## 2. Мониторинг видеокарты: приоритетный экран + +**Выбранный подход.** Не листалка, а **приоритетный экран**: плата сама +показывает то, что ближе к своему порогу. Греется видеокарта — показывает её, +в простое показывает процессор по умолчанию. + +Обоснование: устройство стоит на столе и смотрится боковым зрением, от него +нужен ответ на один вопрос — «всё ли в порядке». Листалка заставляет либо жать +кнопку, либо ждать нужного экрана. Значок в углу при этом естественно +превращается в указатель, чьи данные показаны, и понадобится второй значок — +видеокарты. + +Совмещённый экран отвергнут по измерениям: свободно 17 пикселей по ширине, +вторая температура влезет только шрифтом 5×7, и смысл крупной цифры теряется. + +### Сторона агента + +Работа одинакова для любого способа отображения и не ломает совместимость: +прошивка игнорирует незнакомые ключи, поэтому агента можно обновить раньше +прошивки. + +- включить `IsGpuEnabled`; +- брать `Temperature/GPU Core` и `Load/GPU Core`; +- добавить в строку ключи `G=` (температура) и `GL=` (загрузка): + `T=67.0;L=34;G=61.0;GL=88`; +- **опрашивать видеокарту реже процессора**, примерно раз в 5 секунд (см. ниже). + +Права администратора для видеокарты не нужны — NVIDIA отдаёт температуру через +свой драйвер, а не через MSR. + +### Сторона прошивки + +- **Свои пороги на каждый показатель.** У процессора 85/82. У ноутбучной + RTX 3050 Ti предел около 87, так что пороги будут другими — подобрать, + а не копировать. +- **Задержка переключения.** Главная ловушка подхода: когда оба показателя + близки к своим порогам, экран начнёт скакать между ними каждую секунду. Нужно + либо минимальное время удержания экрана (5–10 секунд), либо заметный отрыв + по «близости к порогу», при котором происходит переключение. +- **Свой буфер истории на каждый показатель** — по 72 байта, памяти достаточно. +- **Свой значок** для видеокарты, рядом с существующим значком процессора. +- **Устаревание по каждому ключу отдельно.** Если данных по видеокарте нет + дольше N секунд, её экран просто не участвует в выборе. Это же снимает вопрос, + что делать на машине без дискретной видеокарты: экран не появится никогда. +- **Сглаживание.** Температуру видеокарты усреднять так же, как процессорную, + но окно считать в замерах, а не в секундах: при опросе раз в 5 секунд окно + из 5 замеров даст 25 секунд, что слишком много. Вероятно, хватит 2–3. + +--- + +## 2a. Сначала проверить: будит ли опрос видеокарту + +**Выполнить до реализации.** У ноутбука переключаемая графика, и в простое +дискретная видеокарта уходит в сон. Опрос её датчиков может будить её и мешать +энергосбережению — то есть мониторинг начнёт сам расходовать батарею и греть +машину. Сейчас она отвечает (39 °C), значит не спит, но в глубоком простое +поведение будет другим. + +**Как проверить.** Прямой измеритель уже есть: у батареи читается датчик +`Power/Charge/Discharge Rate` в ваттах, и права администратора для него не +нужны. + +1. Отключить ноутбук от сети, оставить машину в простое, не трогать ввод. +2. Замер А: 10 минут писать мощность разряда **без** опроса видеокарты + (`IsGpuEnabled = false`), раз в 10 секунд. +3. Замер Б: 10 минут то же самое **с** опросом видеокарты раз в секунду. +4. Сравнить средние. Заодно записывать `GpuNvidia Power/GPU Package`: если при + опросе он перестаёт падать до нуля — видеокарта не засыпает. + +**Что делать с результатом.** Разница в пределах шума — опрашивать раз в +секунду, как процессор. Разница заметная (скажем, больше половины ватта) — +оставить опрос раз в 5 секунд или реже и запомнить это решение здесь. + +Оценка: около десяти минут работы плюс двадцать минут ожидания замеров. + +--- + +## 3. Обрезание строки при 100° и 100% При трёхзначной температуре и трёхзначной загрузке верхняя строка занимает 79 пикселей из 72: `100` — 49 px, зазор 2, `100%` — 28 px. Лишние 7 пикселей @@ -96,7 +176,7 @@ NVIDIA отдаёт её через свой драйвер, ядро для э --- -## 3. Автозапуск не проверен перезагрузкой +## 4. Автозапуск не проверен перезагрузкой Задание Планировщика `CPU Temp Agent` зарегистрировано с триггером «при входе в систему», запускается вручную и работает. Живой перезагрузкой триггер не @@ -107,7 +187,7 @@ NVIDIA отдаёт её через свой драйвер, ядро для э --- -## 4. Скрипт заливки +## 5. Скрипт заливки Цикл «остановить агента → залить прошивку → запустить агента» делается тремя командами вручную. Пока правок мало, это терпимо; если работа по экрану @@ -118,7 +198,7 @@ NVIDIA отдаёт её через свой драйвер, ядро для э --- -## 5. Вес обзорного изображения +## 6. Вес обзорного изображения `docs/promo.png` весит 2,1 МБ при исходниках примерно на 30 КБ. Сжатие до 300–500 КБ не ухудшило бы читаемость, но старый файл всё равно останется в From 4344fa887336ecd2dde9e4503e8c24193e3205ac Mon Sep 17 00:00:00 2001 From: Vitali Date: Thu, 17 Sep 2026 19:04:11 +0700 Subject: [PATCH 3/4] =?UTF-8?q?feat(agent):=20=D1=81=D0=BB=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D0=B8=20=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=BE=D0=BC=20M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прошивка её пока не показывает - на 72x40 нет места под третий показатель. Это намеренно: неизвестный ключ прошивка игнорирует, поэтому агента можно обновлять независимо от плат. Память берётся у железа с именем Total Memory - это физическая память. У соседнего Virtual Memory датчик называется точно так же, но считает вместе с файлом подкачки и показывает заниженную цифру: 87% против фактических 97%. Co-Authored-By: Claude Opus 5 --- CPU_temp/README.md | 11 ++++++++++- CPU_temp/agent/cpu_temp_agent.ps1 | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CPU_temp/README.md b/CPU_temp/README.md index 3ae7c3b..3d18ca1 100644 --- a/CPU_temp/README.md +++ b/CPU_temp/README.md @@ -299,13 +299,22 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent' Агент шлёт плате по одной строке в секунду, в конце — перевод строки: ``` -T=67.0;L=34 +T=67.0;L=34;M=97 ``` | Ключ | Смысл | |---|---| | `T` | температура CPU Package, °C | | `L` | общая загрузка CPU, % | +| `M` | загрузка оперативной памяти, % | + +Ключ `M` агент шлёт, а прошивка пока **не показывает** — на экране 72×40 нет +места для третьего показателя. Это и есть демонстрация расширяемости: агента +обновили, платы не трогали, ничего не сломалось. + +Берётся он у железа с именем `Total Memory` — это физическая память. У соседнего +`Virtual Memory` датчик называется точно так же, но считает вместе с файлом +подкачки и показывает заниженную цифру. Неизвестные ключи прошивка молча игнорирует, поэтому протокол расширяем: можно добавить, например, `G=` для видеокарты, не трогая уже прошитые платы. diff --git a/CPU_temp/agent/cpu_temp_agent.ps1 b/CPU_temp/agent/cpu_temp_agent.ps1 index e073bde..83fdeec 100644 --- a/CPU_temp/agent/cpu_temp_agent.ps1 +++ b/CPU_temp/agent/cpu_temp_agent.ps1 @@ -29,6 +29,7 @@ Add-Type -Path $LhmPath $computer = New-Object LibreHardwareMonitor.Hardware.Computer $computer.IsCpuEnabled = $true +$computer.IsMemoryEnabled = $true $computer.Open() function Get-CpuStats { @@ -36,10 +37,15 @@ function Get-CpuStats { $temp = $null $load = $null + $mem = $null foreach ($hw in $computer.Hardware) { foreach ($s in $hw.Sensors) { if ($s.SensorType -eq 'Temperature' -and $s.Name -eq 'CPU Package') { $temp = $s.Value } if ($s.SensorType -eq 'Load' -and $s.Name -eq 'CPU Total') { $load = $s.Value } + # Имя железа проверяем обязательно: у "Virtual Memory" датчик зовётся + # точно так же, но считает физическую память вместе с файлом подкачки + # и показывает заниженную цифру. + if ($hw.Name -eq 'Total Memory' -and $s.SensorType -eq 'Load' -and $s.Name -eq 'Memory') { $mem = $s.Value } } } @@ -48,6 +54,7 @@ function Get-CpuStats { return @{ Temp = $temp Load = $load + Mem = $mem } } @@ -73,7 +80,13 @@ function Assert-SensorsAvailable { # Форматирование только через InvariantCulture: в русской локали вышло бы # "T=67,0", и atof на плате прочитал бы 67, молча потеряв дробную часть. function Format-StatsLine($stats) { - return [string]::Format([cultureinfo]::InvariantCulture, 'T={0:F1};L={1}', [double]$stats.Temp, [int][math]::Round([double]$stats.Load)) + $line = [string]::Format([cultureinfo]::InvariantCulture, 'T={0:F1};L={1}', [double]$stats.Temp, [int][math]::Round([double]$stats.Load)) + # Память необязательна: если датчика нет, строка просто идёт без ключа M, + # и прошивка этого не заметит. + if ($null -ne $stats.Mem) { + $line += [string]::Format([cultureinfo]::InvariantCulture, ';M={0}', [int][math]::Round([double]$stats.Mem)) + } + return $line } # Номер порта не хардкодим: он меняется при перетыкании в другой разъём. From f18090b5b73238ac084dd390eeeea8fc65d078c7 Mon Sep 17 00:00:00 2001 From: Vitali Date: Thu, 17 Sep 2026 19:56:33 +0700 Subject: [PATCH 4/4] =?UTF-8?q?feat(ui):=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D0=B8=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=BE=D0=B9=20BOOT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Второй экран показывает загрузку ОЗУ крупной цифрой и её график со шкалой 0-100 - у памяти она естественная, подбирать нечего. В углу свой значок, планка памяти вместо микросхемы: он же и говорит, какой экран открыт. Тревога в приоритете: при перегреве плата принудительно возвращается на экран процессора и кнопка не работает, пока температура не упадёт. Смысл тревоги в том, что её нельзя прощёлкать. Экран памяти выпадает из листания, если агент не шлёт ключ M - со старым агентом кнопка просто ничего не делает. То же решение подойдёт машинам без дискретной видеокарты. Выбранный экран не запоминается при перезагрузке: хранить негде. Тревоги по памяти нет намеренно - загрузка ОЗУ под 100% здесь обычное рабочее состояние. Co-Authored-By: Claude Opus 5 --- CPU_temp/CPU_temp.ino | 53 +++++++++++++++++++--- CPU_temp/README.md | 41 ++++++++++++++---- CPU_temp/config.h | 10 +++++ CPU_temp/docs/BACKLOG.md | 39 +++++++++++------ CPU_temp/link.cpp | 6 ++- CPU_temp/link.h | 3 +- CPU_temp/ui.cpp | 94 +++++++++++++++++++++++++++++----------- CPU_temp/ui.h | 14 ++++-- 8 files changed, 201 insertions(+), 59 deletions(-) diff --git a/CPU_temp/CPU_temp.ino b/CPU_temp/CPU_temp.ino index eb6c06a..77cf3f9 100644 --- a/CPU_temp/CPU_temp.ino +++ b/CPU_temp/CPU_temp.ino @@ -1,13 +1,14 @@ -// Индикатор температуры CPU: плата принимает строки "T=67.0;L=34" по USB-COM +// Индикатор состояния ПК: плата принимает строки "T=67.0;L=34;M=97" по USB-COM // от агента на ПК и рисует их на встроенном OLED 72x40. Сама ничего не меряет. // +// Два экрана: процессор и оперативная память, переключение кнопкой BOOT. // Все настройки — в config.h. #include "config.h" #include "link.h" #include "ui.h" -static CpuStats stats = {0.0f, 0}; +static CpuStats stats = {0.0f, 0, -1}; static uint32_t lastData = 0; static uint32_t lastSample = 0; static uint32_t lastDraw = 0; @@ -16,6 +17,8 @@ static bool alarmActive = false; static bool wasFresh = false; static float tempShown = 0.0f; // усреднённое значение, именно оно на экране +static UiScreen screen = UI_SCREEN_CPU; + // Скользящее среднее температуры. Сумма пересчитывается заново, а не копится: // на пяти числах это бесплатно, зато нет накопления ошибки округления за сутки // непрерывной работы. @@ -41,6 +44,7 @@ static float tempAvgPush(float v) { void setup() { pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, !LED_ACTIVE_LEVEL); + pinMode(PIN_BUTTON, INPUT_PULLUP); linkBegin(); uiBegin(); @@ -52,6 +56,38 @@ static void updateAlarm(float tempC) { else if (alarmActive && tempC <= ALARM_OFF_C) alarmActive = false; } +// Экран памяти показываем, только если данные по ней действительно приходят: +// со старым агентом ключа M в строке нет вовсе. +static bool ramAvailable() { + return stats.memPct >= 0; +} + +static void onButtonPress() { + // Тревога в приоритете: уйти с экрана процессора нельзя, пока он горячий. + if (alarmActive) return; + if (!ramAvailable()) return; + + screen = (screen == UI_SCREEN_CPU) ? UI_SCREEN_RAM : UI_SCREEN_CPU; +} + +// Кнопка BOOT замыкает пин на землю, поэтому нажатию соответствует LOW. +static void pollButton() { + static bool stable = HIGH; + static bool lastRead = HIGH; + static uint32_t changedAt = 0; + + const bool now = digitalRead(PIN_BUTTON); + if (now != lastRead) { + lastRead = now; + changedAt = millis(); + } + + if (millis() - changedAt >= BUTTON_DEBOUNCE_MS && now != stable) { + stable = now; + if (stable == LOW) onButtonPress(); + } +} + void loop() { CpuStats fresh; if (linkPoll(fresh)) { @@ -75,24 +111,31 @@ void loop() { alarmActive = false; // без данных о перегреве судить не по чему } + pollButton(); + + // Тревога перебивает выбор пользователя и возвращает на экран процессора. + if (alarmActive) screen = UI_SCREEN_CPU; + if (screen == UI_SCREEN_RAM && !ramAvailable()) screen = UI_SCREEN_CPU; + // Общая фаза для экрана и светодиода, чтобы они моргали синхронно. const bool blinkOn = (millis() / ALARM_BLINK_MS) % 2 == 0; // Тревога — мигает, нет данных — горит ровно, норма — погашен. bool ledOn; if (!isFresh) ledOn = true; - else if (alarmActive) ledOn = blinkOn; + else if (alarmActive) ledOn = blinkOn; else ledOn = false; digitalWrite(PIN_LED, ledOn ? LED_ACTIVE_LEVEL : !LED_ACTIVE_LEVEL); // Историю дописываем только при живой связи: пауза не должна рисоваться провалом. if (isFresh && millis() - lastSample >= SAMPLE_MS) { lastSample = millis(); - uiPushSample(stats.tempC); // график по сырым: 5 градусов на пиксель, шум там не виден + uiPushCpuSample(stats.tempC); // график по сырым: 5 градусов на пиксель, шум там не виден + if (ramAvailable()) uiPushRamSample(stats.memPct); } if (millis() - lastDraw >= DRAW_MS) { lastDraw = millis(); - uiRender(isFresh, tempShown, stats.loadPct, alarmActive && blinkOn); + uiRender(screen, isFresh, tempShown, stats.loadPct, stats.memPct, alarmActive && blinkOn); } } diff --git a/CPU_temp/README.md b/CPU_temp/README.md index 3d18ca1..f55db16 100644 --- a/CPU_temp/README.md +++ b/CPU_temp/README.md @@ -1,8 +1,9 @@ # CPU Temp — индикатор температуры процессора на ESP32-C3 Небольшая плата ESP32-C3 со встроенным OLED-экраном 0.42″ стоит на столе и -показывает температуру процессора компьютера: крупная цифра, загрузка CPU рядом -и график за последнюю минуту. При перегреве экран мигает негативом. +показывает состояние компьютера: крупная цифра, мелкий показатель рядом и график +за последнюю минуту. Экранов два — процессор и оперативная память, переключаются +кнопкой BOOT. При перегреве процессора экран мигает негативом. ``` LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40 @@ -240,6 +241,8 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent' | `ALARM_OFF_C` | 82.0 | температура отпускания тревоги | | `ALARM_BLINK_MS` | 500 | полупериод мигания | | `PIN_LED` | 8 | светодиод | +| `PIN_BUTTON` | 9 | кнопка BOOT, переключает экраны | +| `BUTTON_DEBOUNCE_MS` | 50 | защита от дребезга контактов | | `LED_ACTIVE_LEVEL` | `LOW` | уровень, при котором светодиод горит | | `TEMP_AVG_SAMPLES` | 5 | окно сглаживания температуры, замеров | | `SCALE_MIN`, `SCALE_MAX` | 30.0, 90.0 | шкала графика, °C | @@ -283,14 +286,34 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent' ## Что показывает экран +Экранов два, переключаются коротким нажатием кнопки **BOOT**: + +| Экран | Крупно | Мелко | График | +|---|---|---|---| +| Процессор | температура, °C | загрузка, % | температура, шкала 30–90 °C | +| Память | загрузка, % | знак процента | загрузка, шкала 0–100 % | + +Значок в правом верхнем углу говорит, какой экран открыт: микросхема — процессор, +планка памяти — ОЗУ. + | Состояние | Экран | Светодиод GPIO8 | |---|---|---| -| Норма | температура, загрузка, график | погашен | -| Перегрев | то же, мигает негативом | мигает синхронно с экраном | +| Норма | выбранный экран | погашен | +| Перегрев | **принудительно** экран процессора, мигает негативом | мигает синхронно с экраном | | Нет данных дольше 5 с | `NO DATA` | горит ровно | -В режиме `NO DATA` история графика не пополняется — иначе обрыв связи выглядел бы -как резкое падение температуры. +**Тревога в приоритете.** При перегреве плата сама возвращается на экран +процессора, и кнопка перестаёт работать до тех пор, пока температура не упадёт. +Смысл тревоги в том, что её нельзя прощёлкать. + +Выбранный экран **не запоминается** при перезагрузке платы: энергонезависимого +хранилища в прошивке нет вовсе, после сброса всегда показывается процессор. + +Тревога считается только по температуре процессора. По памяти её нет намеренно: +загрузка ОЗУ под 100 % — обычное рабочее состояние, мигать на него бессмысленно. + +В режиме `NO DATA` история графиков не пополняется — иначе обрыв связи выглядел +бы как резкое падение значений. --- @@ -308,9 +331,9 @@ T=67.0;L=34;M=97 | `L` | общая загрузка CPU, % | | `M` | загрузка оперативной памяти, % | -Ключ `M` агент шлёт, а прошивка пока **не показывает** — на экране 72×40 нет -места для третьего показателя. Это и есть демонстрация расширяемости: агента -обновили, платы не трогали, ничего не сломалось. +Ключ `M` показывается на отдельном экране памяти — третьему показателю на экране +72×40 места нет. Если агент старый и ключа `M` не шлёт, экран памяти из листания +выпадает сам, и кнопка ничего не делает. Берётся он у железа с именем `Total Memory` — это физическая память. У соседнего `Virtual Memory` датчик называется точно так же, но считает вместе с файлом diff --git a/CPU_temp/config.h b/CPU_temp/config.h index 7b1b4d3..eaf7ae5 100644 --- a/CPU_temp/config.h +++ b/CPU_temp/config.h @@ -37,6 +37,16 @@ static const uint8_t PIN_LED = 8; // высокий уровень и нужен. static const uint8_t LED_ACTIVE_LEVEL = LOW; +// --- Кнопка переключения экранов ------------------------------------------- + +// Кнопка BOOT. Это пин начальной загрузки: если держать его нажатым в момент +// сброса, плата уйдёт в режим прошивки - штатное поведение кнопки, а не наша +// беда. В работе читается как обычный вход с подтяжкой. +static const uint8_t PIN_BUTTON = 9; + +// Порог защиты от дребезга контактов. +static const uint32_t BUTTON_DEBOUNCE_MS = 50; + // --- Сглаживание показаний ------------------------------------------------- // Скользящее среднее температуры: цифра не должна мельтешить на +-1 градус, diff --git a/CPU_temp/docs/BACKLOG.md b/CPU_temp/docs/BACKLOG.md index af5a0f7..e6e4253 100644 --- a/CPU_temp/docs/BACKLOG.md +++ b/CPU_temp/docs/BACKLOG.md @@ -5,7 +5,25 @@ --- -## 1. Другие показатели и листалка по кнопке BOOT +## 1. Другие показатели и листалка по кнопке BOOT — частично сделано + +**Сделано 2026-09-17:** экран оперативной памяти и переключение кнопкой BOOT. +Решения, принятые при реализации, чтобы не пересматривать заново: + +- **Тревога в приоритете.** При перегреве плата принудительно возвращается на + экран процессора, и кнопка не работает, пока температура не упадёт. Из трёх + вариантов, перечисленных ниже, выбран самый жёсткий: смысл тревоги в том, что + её нельзя прощёлкать. +- **Выбранный экран не запоминается** при перезагрузке — хранить негде. +- **Тревоги по памяти нет:** загрузка ОЗУ под 100 % на этой машине обычное + состояние. +- **Защита от дребезга** — 50 мс, порог в `config.h`. +- **Экран без данных выпадает из листания.** Если агент не шлёт ключ `M`, экран + памяти недоступен, и кнопка просто ничего не делает. Это же решение подойдёт + для машин без дискретной видеокарты. + +**Осталось:** экраны батареи и сети, а также видеокарта — но у неё свой путь, +см. пункт 2. **Что хочется.** Показывать не только процессор: видеокарту, оперативную память, диски. Переключать экраны кнопкой **BOOT** на плате — нажал, увидел следующий @@ -18,11 +36,9 @@ **Что придётся решить при реализации:** -- **Кнопка BOOT сидит на GPIO9.** Это пин начальной загрузки: при сбросе он - должен быть в высоком уровне, иначе плата уйдёт в режим прошивки. Читать его - как `INPUT_PULLUP` в работе безопасно, но проверить фактически. -- **Дребезг контактов.** Нужна программная защита, иначе одно нажатие пролистает - три экрана. +- ~~**Кнопка BOOT сидит на GPIO9.**~~ Проверено на практике: чтение через + `INPUT_PULLUP` в работе никак не мешает загрузке платы. +- ~~**Дребезг контактов.**~~ Сделано: 50 мс, `BUTTON_DEBOUNCE_MS`. - **Свой макет под каждый экран.** На 72×40 не получится «то же самое, только другие цифры»: у памяти уместнее проценты и гигабайты, у диска — температура без графика. Каждый экран проектировать отдельно. @@ -32,14 +48,9 @@ - **Что показывать, если данных нет.** Например, в системе нет дискретной видеокарты — экран GPU надо либо пропускать при листании, либо честно писать, что источника нет. -- **Запоминать ли выбранный экран** между перезагрузками. Если да, понадобится - энергонезависимое хранилище, которого у прошивки сейчас нет вовсе. -- **Тревога при листании.** Сейчас перегрев виден всегда, потому что экран один. - С листалкой можно стоять на экране памяти и не увидеть, что горит процессор. - Нужно решить: принудительно перекидывать на экран CPU, показывать тревогу - поверх любого экрана или мигать только светодиодом. - -Последний пункт — самый важный: он затрагивает смысл устройства, а не удобство. +- ~~**Запоминать ли выбранный экран**~~ Решено не запоминать: хранилища нет. +- ~~**Тревога при листании.**~~ Решено: принудительно перекидывать на экран + процессора и блокировать кнопку. **Для видеокарты выбран другой путь** — приоритетный экран без кнопки, см. пункт 2. Листалка остаётся в силе для остального: памяти, батареи, сети. diff --git a/CPU_temp/link.cpp b/CPU_temp/link.cpp index c234bf3..b5c8963 100644 --- a/CPU_temp/link.cpp +++ b/CPU_temp/link.cpp @@ -16,7 +16,7 @@ void linkBegin() { static bool parseLine(char *line, CpuStats &out) { bool haveTemp = false; bool haveLoad = false; - CpuStats parsed = {0.0f, 0}; + CpuStats parsed = {0.0f, 0, -1}; char *save = nullptr; for (char *tok = strtok_r(line, ";", &save); tok; tok = strtok_r(nullptr, ";", &save)) { @@ -32,6 +32,10 @@ static bool parseLine(char *line, CpuStats &out) { } else if (strcmp(key, "L") == 0) { parsed.loadPct = atoi(val); haveLoad = true; + } else if (strcmp(key, "M") == 0) { + // Память необязательна: старый агент её не шлёт, и это не повод + // отбрасывать строку целиком. + parsed.memPct = atoi(val); } // прочие ключи игнорируются намеренно: протокол расширяемый } diff --git a/CPU_temp/link.h b/CPU_temp/link.h index 67f0810..65f4ada 100644 --- a/CPU_temp/link.h +++ b/CPU_temp/link.h @@ -3,10 +3,11 @@ struct CpuStats { float tempC; int loadPct; + int memPct; // -1 = ключа M в строке не было }; void linkBegin(); // Неблокирующее чтение. Возвращает true, когда собрана и разобрана -// целая строка вида "T=67.0;L=34". Неизвестные ключи игнорируются. +// целая строка вида "T=67.0;L=34;M=97". Неизвестные ключи игнорируются. bool linkPoll(CpuStats &out); diff --git a/CPU_temp/ui.cpp b/CPU_temp/ui.cpp index 4c0c206..4e3ae6f 100644 --- a/CPU_temp/ui.cpp +++ b/CPU_temp/ui.cpp @@ -8,20 +8,23 @@ // Порядок параметров: (rotation, reset, clock, data) — SCL идёт раньше SDA. static U8G2_SSD1306_72X40_ER_F_HW_I2C display(DISPLAY_ROTATION, U8X8_PIN_NONE, PIN_SCL, PIN_SDA); -static const uint8_t GRAPH_W = 72; // по пикселю на отсчёт: 72 точки = 72 секунды +static const uint8_t SCREEN_W = 72; + +static const uint8_t GRAPH_W = SCREEN_W; // по пикселю на отсчёт: 72 точки = 72 секунды static const uint8_t GRAPH_TOP = 28; static const uint8_t GRAPH_H = 12; -// Значок процессора в правом верхнем углу. Полоса y=0..10 выбрана не случайно: -// строка загрузки лежит ниже (y=11..24), а цифры температуры не дотягиваются -// сюда даже в трёхзначном виде (49 px + градус = 56). Значит значок не перекроет -// ничего ни при каких значениях. Размеры измерены на плате, не на глаз. +// Значок в правом верхнем углу. Полоса y=0..10 выбрана не случайно: строка под +// цифрами лежит ниже (y=11..24), а сами цифры не дотягиваются сюда даже +// трёхзначными (49 px + знак = 56). Значок не перекроет ничего. static const uint8_t ICON_W = 11; static const uint8_t ICON_H = 11; -static const uint8_t ICON_X = 72 - ICON_W; +static const uint8_t ICON_X = SCREEN_W - ICON_W; static const uint8_t ICON_Y = 0; -static uint8_t history[GRAPH_W]; // 0 = нет данных, иначе высота столбика 1..GRAPH_H +// 0 = нет данных, иначе высота столбика 1..GRAPH_H +static uint8_t cpuHistory[GRAPH_W]; +static uint8_t ramHistory[GRAPH_W]; void uiBegin() { display.begin(); @@ -31,11 +34,12 @@ void uiBegin() { display.sendBuffer(); } -void uiPushSample(float tempC) { - if (tempC < SCALE_MIN) tempC = SCALE_MIN; - if (tempC > SCALE_MAX) tempC = SCALE_MAX; +// Общая часть для обеих историй: нормировать значение и сдвинуть буфер. +static void pushSample(uint8_t *history, float value, float scaleMin, float scaleMax) { + if (value < scaleMin) value = scaleMin; + if (value > scaleMax) value = scaleMax; - const float norm = (tempC - SCALE_MIN) / (SCALE_MAX - SCALE_MIN); + const float norm = (value - scaleMin) / (scaleMax - scaleMin); uint8_t h = (uint8_t)(norm * GRAPH_H + 0.5f); if (h < 1) h = 1; @@ -43,7 +47,16 @@ void uiPushSample(float tempC) { history[GRAPH_W - 1] = h; } -static void drawGraph() { +void uiPushCpuSample(float tempC) { + pushSample(cpuHistory, tempC, SCALE_MIN, SCALE_MAX); +} + +void uiPushRamSample(int memPct) { + // У памяти шкала естественная, подбирать нечего. + pushSample(ramHistory, (float)memPct, 0.0f, 100.0f); +} + +static void drawGraph(const uint8_t *history) { for (uint8_t x = 0; x < GRAPH_W; x++) { const uint8_t h = history[x]; if (h == 0) continue; @@ -64,6 +77,17 @@ static void drawCpuIcon(uint8_t x, uint8_t y) { } } +// Планка памяти: корпус, три микросхемы на ней и контакты снизу. +static void drawRamIcon(uint8_t x, uint8_t y) { + display.drawFrame(x, y + 1, ICON_W, 7); + for (uint8_t i = 0; i < 3; i++) { + display.drawBox(x + 2 + i * 3, y + 3, 2, 3); + } + for (uint8_t i = 0; i < 5; i++) { + display.drawPixel(x + 1 + i * 2, y + 9); + } +} + // Негатив всего кадра. Через буфер, а не командой контроллера: так поведение // не зависит от конкретной микросхемы дисплея. static void invertBuffer() { @@ -72,21 +96,15 @@ static void invertBuffer() { for (size_t i = 0; i < n; i++) b[i] = (uint8_t)~b[i]; } -void uiRender(bool fresh, float tempC, int loadPct, bool invert) { - display.clearBuffer(); - - if (!fresh) { - display.setFont(u8g2_font_6x10_tf); - display.drawStr(0, 14, "NO"); - display.drawStr(0, 26, "DATA"); - if (invert) invertBuffer(); - display.sendBuffer(); - return; - } +static void drawNoData() { + display.setFont(u8g2_font_6x10_tf); + display.drawStr(0, 14, "NO"); + display.drawStr(0, 26, "DATA"); +} +static void drawCpuScreen(float tempC, int loadPct) { char buf[8]; - // Крупные цифры температуры. snprintf(buf, sizeof(buf), "%d", (int)(tempC + 0.5f)); display.setFont(u8g2_font_logisoso24_tn); display.drawStr(0, 24, buf); @@ -102,8 +120,34 @@ void uiRender(bool fresh, float tempC, int loadPct, bool invert) { display.drawStr(numWidth + 2, 24, buf); drawCpuIcon(ICON_X, ICON_Y); + drawGraph(cpuHistory); +} - drawGraph(); +static void drawRamScreen(int memPct) { + char buf[8]; + + snprintf(buf, sizeof(buf), "%d", memPct); + display.setFont(u8g2_font_logisoso24_tn); + display.drawStr(0, 24, buf); + const int numWidth = display.getStrWidth(buf); + + display.setFont(u8g2_font_7x14_tf); + display.drawStr(numWidth + 2, 24, "%"); + + drawRamIcon(ICON_X, ICON_Y); + drawGraph(ramHistory); +} + +void uiRender(UiScreen screen, bool fresh, float tempC, int loadPct, int memPct, bool invert) { + display.clearBuffer(); + + if (!fresh) { + drawNoData(); + } else if (screen == UI_SCREEN_RAM) { + drawRamScreen(memPct); + } else { + drawCpuScreen(tempC, loadPct); + } if (invert) invertBuffer(); diff --git a/CPU_temp/ui.h b/CPU_temp/ui.h index f124d92..83871cd 100644 --- a/CPU_temp/ui.h +++ b/CPU_temp/ui.h @@ -2,14 +2,20 @@ #include +enum UiScreen : uint8_t { + UI_SCREEN_CPU = 0, + UI_SCREEN_RAM = 1, +}; + // Инициализирует дисплей и показывает заставку ожидания. void uiBegin(); -// Добавляет значение в кольцевой буфер истории. Вызывать раз в секунду -// и только при свежих данных, чтобы пауза связи не рисовалась как провал. -void uiPushSample(float tempC); +// Пополняют историю соответствующего графика. Вызывать раз в секунду и только +// при свежих данных, чтобы пауза связи не рисовалась как провал. +void uiPushCpuSample(float tempC); +void uiPushRamSample(int memPct); // Перерисовывает экран целиком. // fresh == false - данные устарели, вместо цифр выводится NO DATA. // invert == true - кадр показывается в негативе (фаза мигания тревоги). -void uiRender(bool fresh, float tempC, int loadPct, bool invert); +void uiRender(UiScreen screen, bool fresh, float tempC, int loadPct, int memPct, bool invert);