Compare commits
25
Commits
03db58473c
...
master
No files matched your search
+79
-5
@@ -1,22 +1,50 @@
|
|||||||
// Индикатор температуры CPU: плата принимает строки "T=67.0;L=34" по USB-COM
|
// Индикатор состояния ПК: плата принимает строки "T=67.0;L=34;M=97" по USB-COM
|
||||||
// от агента на ПК и рисует их на встроенном OLED 72x40. Сама ничего не меряет.
|
// от агента на ПК и рисует их на встроенном OLED 72x40. Сама ничего не меряет.
|
||||||
//
|
//
|
||||||
|
// Два экрана: процессор и оперативная память, переключение кнопкой BOOT.
|
||||||
// Все настройки — в config.h.
|
// Все настройки — в config.h.
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "link.h"
|
#include "link.h"
|
||||||
#include "ui.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 lastData = 0;
|
||||||
static uint32_t lastSample = 0;
|
static uint32_t lastSample = 0;
|
||||||
static uint32_t lastDraw = 0;
|
static uint32_t lastDraw = 0;
|
||||||
static bool everGot = false;
|
static bool everGot = false;
|
||||||
static bool alarmActive = false;
|
static bool alarmActive = false;
|
||||||
|
static bool wasFresh = false;
|
||||||
|
static float tempShown = 0.0f; // усреднённое значение, именно оно на экране
|
||||||
|
|
||||||
|
static UiScreen screen = UI_SCREEN_CPU;
|
||||||
|
|
||||||
|
// Скользящее среднее температуры. Сумма пересчитывается заново, а не копится:
|
||||||
|
// на пяти числах это бесплатно, зато нет накопления ошибки округления за сутки
|
||||||
|
// непрерывной работы.
|
||||||
|
static float tempWindow[TEMP_AVG_SAMPLES];
|
||||||
|
static uint8_t tempCount = 0;
|
||||||
|
static uint8_t tempIndex = 0;
|
||||||
|
|
||||||
|
static void tempAvgReset() {
|
||||||
|
tempCount = 0;
|
||||||
|
tempIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float tempAvgPush(float v) {
|
||||||
|
tempWindow[tempIndex] = v;
|
||||||
|
tempIndex = (uint8_t)((tempIndex + 1) % TEMP_AVG_SAMPLES);
|
||||||
|
if (tempCount < TEMP_AVG_SAMPLES) tempCount++;
|
||||||
|
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < tempCount; i++) sum += tempWindow[i];
|
||||||
|
return sum / tempCount;
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
pinMode(PIN_LED, OUTPUT);
|
pinMode(PIN_LED, OUTPUT);
|
||||||
digitalWrite(PIN_LED, !LED_ACTIVE_LEVEL);
|
digitalWrite(PIN_LED, !LED_ACTIVE_LEVEL);
|
||||||
|
pinMode(PIN_BUTTON, INPUT_PULLUP);
|
||||||
|
|
||||||
linkBegin();
|
linkBegin();
|
||||||
uiBegin();
|
uiBegin();
|
||||||
@@ -28,22 +56,67 @@ static void updateAlarm(float tempC) {
|
|||||||
else if (alarmActive && tempC <= ALARM_OFF_C) alarmActive = false;
|
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() {
|
void loop() {
|
||||||
CpuStats fresh;
|
CpuStats fresh;
|
||||||
if (linkPoll(fresh)) {
|
if (linkPoll(fresh)) {
|
||||||
stats = fresh;
|
stats = fresh;
|
||||||
|
tempShown = tempAvgPush(fresh.tempC);
|
||||||
lastData = millis();
|
lastData = millis();
|
||||||
everGot = true;
|
everGot = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool isFresh = everGot && (millis() - lastData < STALE_MS);
|
const bool isFresh = everGot && (millis() - lastData < STALE_MS);
|
||||||
|
|
||||||
|
// После паузы связи среднее нельзя считать по значениям до обрыва.
|
||||||
|
if (wasFresh && !isFresh) tempAvgReset();
|
||||||
|
wasFresh = isFresh;
|
||||||
|
|
||||||
if (isFresh) {
|
if (isFresh) {
|
||||||
updateAlarm(stats.tempC);
|
// Тревога по усреднённому значению: иначе экран показывал бы 84, а мигание
|
||||||
|
// уже шло бы. Побочно это защищает от срабатывания на одиночный выброс.
|
||||||
|
updateAlarm(tempShown);
|
||||||
} else {
|
} else {
|
||||||
alarmActive = false; // без данных о перегреве судить не по чему
|
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;
|
const bool blinkOn = (millis() / ALARM_BLINK_MS) % 2 == 0;
|
||||||
|
|
||||||
@@ -57,11 +130,12 @@ void loop() {
|
|||||||
// Историю дописываем только при живой связи: пауза не должна рисоваться провалом.
|
// Историю дописываем только при живой связи: пауза не должна рисоваться провалом.
|
||||||
if (isFresh && millis() - lastSample >= SAMPLE_MS) {
|
if (isFresh && millis() - lastSample >= SAMPLE_MS) {
|
||||||
lastSample = millis();
|
lastSample = millis();
|
||||||
uiPushSample(stats.tempC);
|
uiPushCpuSample(stats.tempC); // график по сырым: 5 градусов на пиксель, шум там не виден
|
||||||
|
if (ramAvailable()) uiPushRamSample(stats.memPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (millis() - lastDraw >= DRAW_MS) {
|
if (millis() - lastDraw >= DRAW_MS) {
|
||||||
lastDraw = millis();
|
lastDraw = millis();
|
||||||
uiRender(isFresh, stats.tempC, stats.loadPct, alarmActive && blinkOn);
|
uiRender(screen, isFresh, tempShown, stats.loadPct, stats.memPct, alarmActive && blinkOn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+70
-12
@@ -1,18 +1,24 @@
|
|||||||
# CPU Temp — индикатор температуры процессора на ESP32-C3
|
# CPU Temp — индикатор температуры процессора на ESP32-C3
|
||||||
|
|
||||||
Небольшая плата ESP32-C3 со встроенным OLED-экраном 0.42″ стоит на столе и
|
Небольшая плата ESP32-C3 со встроенным OLED-экраном 0.42″ стоит на столе и
|
||||||
показывает температуру процессора компьютера: крупная цифра, загрузка CPU рядом
|
показывает состояние компьютера: крупная цифра, мелкий показатель рядом и график
|
||||||
и график за последнюю минуту. При перегреве экран мигает негативом.
|
за последнюю минуту. Экранов два — процессор и оперативная память, переключаются
|
||||||
|
кнопкой BOOT. При перегреве процессора экран мигает негативом.
|
||||||
|
|
||||||
```
|
```
|
||||||
LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40
|
LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40
|
||||||
(через драйвер PawnIO) раз в секунду "T=67.0;L=34" U8g2
|
(через драйвер PawnIO) раз в секунду "T=67.0;L=34;M=97" U8g2
|
||||||
```
|
```
|
||||||
|
|
||||||
**Плата ничего не измеряет сама.** Она принимает готовые строки по USB и рисует
|
**Плата ничего не измеряет сама.** Она принимает готовые строки по USB и рисует
|
||||||
их. Всё чтение датчиков происходит на компьютере — иначе никак: температура CPU
|
их. Всё чтение датчиков происходит на компьютере — иначе никак: температура CPU
|
||||||
доступна только через драйвер уровня ядра.
|
доступна только через драйвер уровня ядра.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Две поправки к изображению: экран на этой плате 0.42″ (72×40), а не 0.96″;
|
||||||
|
график строится по температуре, а не по загрузке — загрузка выводится цифрой.*
|
||||||
|
|
||||||
Подключение — по USB-кабелю, которым плата и так питается. Wi-Fi не
|
Подключение — по USB-кабелю, которым плата и так питается. Wi-Fi не
|
||||||
используется: настраивать нечего, задержки нет.
|
используется: настраивать нечего, задержки нет.
|
||||||
|
|
||||||
@@ -167,8 +173,8 @@ powershell -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_tem
|
|||||||
|
|
||||||
```
|
```
|
||||||
connected: COM4
|
connected: COM4
|
||||||
T=47.0;L=24
|
T=47.0;L=24;M=97
|
||||||
T=48.0;L=14
|
T=48.0;L=14;M=97
|
||||||
```
|
```
|
||||||
|
|
||||||
Одновременно на экране платы появится температура. Остановить — Ctrl+C.
|
Одновременно на экране платы появится температура. Остановить — Ctrl+C.
|
||||||
@@ -235,7 +241,10 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|||||||
| `ALARM_OFF_C` | 82.0 | температура отпускания тревоги |
|
| `ALARM_OFF_C` | 82.0 | температура отпускания тревоги |
|
||||||
| `ALARM_BLINK_MS` | 500 | полупериод мигания |
|
| `ALARM_BLINK_MS` | 500 | полупериод мигания |
|
||||||
| `PIN_LED` | 8 | светодиод |
|
| `PIN_LED` | 8 | светодиод |
|
||||||
|
| `PIN_BUTTON` | 9 | кнопка BOOT, переключает экраны |
|
||||||
|
| `BUTTON_DEBOUNCE_MS` | 50 | защита от дребезга контактов |
|
||||||
| `LED_ACTIVE_LEVEL` | `LOW` | уровень, при котором светодиод горит |
|
| `LED_ACTIVE_LEVEL` | `LOW` | уровень, при котором светодиод горит |
|
||||||
|
| `TEMP_AVG_SAMPLES` | 5 | окно сглаживания температуры, замеров |
|
||||||
| `SCALE_MIN`, `SCALE_MAX` | 30.0, 90.0 | шкала графика, °C |
|
| `SCALE_MIN`, `SCALE_MAX` | 30.0, 90.0 | шкала графика, °C |
|
||||||
| `SERIAL_BAUD` | 115200 | скорость обмена; менять вместе с агентом |
|
| `SERIAL_BAUD` | 115200 | скорость обмена; менять вместе с агентом |
|
||||||
| `STALE_MS` | 5000 | молчание, после которого показывается `NO DATA` |
|
| `STALE_MS` | 5000 | молчание, после которого показывается `NO DATA` |
|
||||||
@@ -248,6 +257,26 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|||||||
пока температура топчется на границе. Поэтому включение и отпускание разведены:
|
пока температура топчется на границе. Поэтому включение и отпускание разведены:
|
||||||
загорается на 85, гаснет только при спуске до 82.
|
загорается на 85, гаснет только при спуске до 82.
|
||||||
|
|
||||||
|
### Почему цифра температуры усредняется
|
||||||
|
|
||||||
|
Мгновенное значение плавает на ±1 °C вокруг стабильного, и цифра мельтешит
|
||||||
|
каждую секунду, хотя ничего не происходит — это отвлекает. На экран выводится
|
||||||
|
среднее за `TEMP_AVG_SAMPLES` последних замеров, то есть за 5 секунд: агент
|
||||||
|
шлёт раз в секунду.
|
||||||
|
|
||||||
|
Плата за это — запаздывание реакции на реальный нагрев примерно на 2–3 секунды.
|
||||||
|
Для температуры процессора незаметно: она физически инертна.
|
||||||
|
|
||||||
|
Что осталось несглаженным и почему:
|
||||||
|
|
||||||
|
- **загрузка CPU** — её скачки настоящие, ради них на неё и смотрят;
|
||||||
|
- **график** — 12 пикселей на диапазон 60 °C дают 5 °C на пиксель, шум там и так
|
||||||
|
не виден, зато история остаётся сырой.
|
||||||
|
|
||||||
|
Тревога считается по усреднённому значению, а не по сырому: иначе экран
|
||||||
|
показывал бы 84, когда мигание уже началось. Побочно это защищает от
|
||||||
|
срабатывания на одиночный выброс — перегрев должен продержаться пару секунд.
|
||||||
|
|
||||||
### Почему шкала графика фиксированная
|
### Почему шкала графика фиксированная
|
||||||
|
|
||||||
Автомасштаб отвергнут сознательно: в простое колебания ±1 °C рисовались бы во
|
Автомасштаб отвергнут сознательно: в простое колебания ±1 °C рисовались бы во
|
||||||
@@ -257,14 +286,34 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|||||||
|
|
||||||
## Что показывает экран
|
## Что показывает экран
|
||||||
|
|
||||||
|
Экранов два, переключаются нажатием кнопки **BOOT** (длительность не важна):
|
||||||
|
|
||||||
|
| Экран | Крупно | Мелко | График |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Процессор | температура, °C | загрузка, % | температура, шкала 30–90 °C |
|
||||||
|
| Память | загрузка, % | знак процента | загрузка, шкала 0–100 % |
|
||||||
|
|
||||||
|
Значок в правом верхнем углу говорит, какой экран открыт: микросхема — процессор,
|
||||||
|
планка памяти — ОЗУ.
|
||||||
|
|
||||||
| Состояние | Экран | Светодиод GPIO8 |
|
| Состояние | Экран | Светодиод GPIO8 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Норма | температура, загрузка, график | погашен |
|
| Норма | выбранный экран | погашен |
|
||||||
| Перегрев | то же, мигает негативом | мигает синхронно с экраном |
|
| Перегрев | **принудительно** экран процессора, мигает негативом | мигает синхронно с экраном |
|
||||||
| Нет данных дольше 5 с | `NO DATA` | горит ровно |
|
| Нет данных дольше 5 с | `NO DATA` | горит ровно |
|
||||||
|
|
||||||
В режиме `NO DATA` история графика не пополняется — иначе обрыв связи выглядел бы
|
**Тревога в приоритете.** При перегреве плата сама возвращается на экран
|
||||||
как резкое падение температуры.
|
процессора, и кнопка перестаёт работать до тех пор, пока температура не упадёт.
|
||||||
|
Смысл тревоги в том, что её нельзя прощёлкать.
|
||||||
|
|
||||||
|
Выбранный экран **не запоминается** при перезагрузке платы: энергонезависимого
|
||||||
|
хранилища в прошивке нет вовсе, после сброса всегда показывается процессор.
|
||||||
|
|
||||||
|
Тревога считается только по температуре процессора. По памяти её нет намеренно:
|
||||||
|
загрузка ОЗУ под 100 % — обычное рабочее состояние, мигать на него бессмысленно.
|
||||||
|
|
||||||
|
В режиме `NO DATA` история графиков не пополняется — иначе обрыв связи выглядел
|
||||||
|
бы как резкое падение значений.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -273,13 +322,22 @@ Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|||||||
Агент шлёт плате по одной строке в секунду, в конце — перевод строки:
|
Агент шлёт плате по одной строке в секунду, в конце — перевод строки:
|
||||||
|
|
||||||
```
|
```
|
||||||
T=67.0;L=34
|
T=67.0;L=34;M=97
|
||||||
```
|
```
|
||||||
|
|
||||||
| Ключ | Смысл |
|
| Ключ | Смысл |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `T` | температура CPU Package, °C |
|
| `T` | температура CPU Package, °C |
|
||||||
| `L` | общая загрузка CPU, % |
|
| `L` | общая загрузка CPU, % |
|
||||||
|
| `M` | загрузка оперативной памяти, % |
|
||||||
|
|
||||||
|
Ключ `M` показывается на отдельном экране памяти — третьему показателю на экране
|
||||||
|
72×40 места нет. Если агент старый и ключа `M` не шлёт, экран памяти из листания
|
||||||
|
выпадает сам, и кнопка ничего не делает.
|
||||||
|
|
||||||
|
Берётся он у железа с именем `Total Memory` — это физическая память. У соседнего
|
||||||
|
`Virtual Memory` датчик называется точно так же, но считает вместе с файлом
|
||||||
|
подкачки и показывает заниженную цифру.
|
||||||
|
|
||||||
Неизвестные ключи прошивка молча игнорирует, поэтому протокол расширяем: можно
|
Неизвестные ключи прошивка молча игнорирует, поэтому протокол расширяем: можно
|
||||||
добавить, например, `G=` для видеокарты, не трогая уже прошитые платы.
|
добавить, например, `G=` для видеокарты, не трогая уже прошитые платы.
|
||||||
@@ -324,13 +382,13 @@ $sp.Close()
|
|||||||
| Путь | Что это |
|
| Путь | Что это |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `config.h` | все настройки |
|
| `config.h` | все настройки |
|
||||||
| `CPU_temp.ino` | свежесть данных, гистерезис тревоги, светодиод |
|
| `CPU_temp.ino` | свежесть данных, гистерезис тревоги, кнопка, выбор экрана, светодиод |
|
||||||
| `link.h` / `link.cpp` | неблокирующее чтение строки из Serial и её разбор |
|
| `link.h` / `link.cpp` | неблокирующее чтение строки из Serial и её разбор |
|
||||||
| `ui.h` / `ui.cpp` | отрисовка через U8g2, кольцевой буфер истории, негатив |
|
| `ui.h` / `ui.cpp` | отрисовка через U8g2, кольцевой буфер истории, негатив |
|
||||||
| `tools/i2c_scan/` | одноразовый скетч поиска пинов дисплея |
|
| `tools/i2c_scan/` | одноразовый скетч поиска пинов дисплея |
|
||||||
| `agent/cpu_temp_agent.ps1` | агент для ПК |
|
| `agent/cpu_temp_agent.ps1` | агент для ПК |
|
||||||
| `agent/README.md` | подробности по агенту |
|
| `agent/README.md` | подробности по агенту |
|
||||||
| `docs/` | спецификация и план реализации |
|
| `docs/` | спецификация, план реализации и [бэклог](docs/BACKLOG.md) |
|
||||||
|
|
||||||
Разделение простое: `link` ничего не знает об экране, `ui` не знает, откуда
|
Разделение простое: `link` ничего не знает об экране, `ui` не знает, откуда
|
||||||
взялись данные, а логика состояний живёт в скетче.
|
взялись данные, а логика состояний живёт в скетче.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Агент CPU Temp
|
# Агент CPU Temp
|
||||||
|
|
||||||
Читает температуру и загрузку процессора и раз в секунду отдаёт их плате
|
Читает температуру и загрузку процессора и раз в секунду отдаёт их плате
|
||||||
ESP32-C3 по USB-COM строкой вида `T=67.0;L=34`.
|
ESP32-C3 по USB-COM строкой вида `T=67.0;L=34;M=97`.
|
||||||
|
|
||||||
## Что нужно установить
|
## Что нужно установить
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Add-Type -Path $LhmPath
|
|||||||
|
|
||||||
$computer = New-Object LibreHardwareMonitor.Hardware.Computer
|
$computer = New-Object LibreHardwareMonitor.Hardware.Computer
|
||||||
$computer.IsCpuEnabled = $true
|
$computer.IsCpuEnabled = $true
|
||||||
|
$computer.IsMemoryEnabled = $true
|
||||||
$computer.Open()
|
$computer.Open()
|
||||||
|
|
||||||
function Get-CpuStats {
|
function Get-CpuStats {
|
||||||
@@ -36,10 +37,15 @@ function Get-CpuStats {
|
|||||||
|
|
||||||
$temp = $null
|
$temp = $null
|
||||||
$load = $null
|
$load = $null
|
||||||
|
$mem = $null
|
||||||
foreach ($hw in $computer.Hardware) {
|
foreach ($hw in $computer.Hardware) {
|
||||||
foreach ($s in $hw.Sensors) {
|
foreach ($s in $hw.Sensors) {
|
||||||
if ($s.SensorType -eq 'Temperature' -and $s.Name -eq 'CPU Package') { $temp = $s.Value }
|
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 }
|
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 @{
|
return @{
|
||||||
Temp = $temp
|
Temp = $temp
|
||||||
Load = $load
|
Load = $load
|
||||||
|
Mem = $mem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +80,13 @@ function Assert-SensorsAvailable {
|
|||||||
# Форматирование только через InvariantCulture: в русской локали вышло бы
|
# Форматирование только через InvariantCulture: в русской локали вышло бы
|
||||||
# "T=67,0", и atof на плате прочитал бы 67, молча потеряв дробную часть.
|
# "T=67,0", и atof на плате прочитал бы 67, молча потеряв дробную часть.
|
||||||
function Format-StatsLine($stats) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
# Номер порта не хардкодим: он меняется при перетыкании в другой разъём.
|
# Номер порта не хардкодим: он меняется при перетыкании в другой разъём.
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ static const uint8_t PIN_LED = 8;
|
|||||||
// высокий уровень и нужен.
|
// высокий уровень и нужен.
|
||||||
static const uint8_t LED_ACTIVE_LEVEL = LOW;
|
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 градус,
|
||||||
|
// пока ничего не происходит. Единица - один принятый замер, а агент шлёт раз
|
||||||
|
// в секунду, поэтому 5 означает "среднее за последние 5 секунд".
|
||||||
|
// Загрузка сознательно не сглаживается: её скачки настоящие, ради них на неё
|
||||||
|
// и смотрят.
|
||||||
|
static const uint8_t TEMP_AVG_SAMPLES = 5;
|
||||||
|
|
||||||
// --- График ----------------------------------------------------------------
|
// --- График ----------------------------------------------------------------
|
||||||
|
|
||||||
// Шкала фиксированная: при автомасштабе шум +-1 градус в простое
|
// Шкала фиксированная: при автомасштабе шум +-1 градус в простое
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Бэклог
|
||||||
|
|
||||||
|
Идеи и незакрытые хвосты по проекту. Не план работ: сюда складывается то, что
|
||||||
|
решено не делать сейчас, чтобы не потерялось.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Другие показатели и листалка по кнопке BOOT — частично сделано
|
||||||
|
|
||||||
|
**Сделано 2026-09-17:** экран оперативной памяти и переключение кнопкой BOOT.
|
||||||
|
Решения, принятые при реализации, чтобы не пересматривать заново:
|
||||||
|
|
||||||
|
- **Тревога в приоритете.** При перегреве плата принудительно возвращается на
|
||||||
|
экран процессора, и кнопка не работает, пока температура не упадёт. Из трёх
|
||||||
|
вариантов, перечисленных ниже, выбран самый жёсткий: смысл тревоги в том, что
|
||||||
|
её нельзя прощёлкать.
|
||||||
|
- **Выбранный экран не запоминается** при перезагрузке — хранить негде.
|
||||||
|
- **Тревоги по памяти нет:** загрузка ОЗУ под 100 % на этой машине обычное
|
||||||
|
состояние.
|
||||||
|
- **Защита от дребезга** — 50 мс, порог в `config.h`.
|
||||||
|
- **Экран без данных выпадает из листания.** Если агент не шлёт ключ `M`, экран
|
||||||
|
памяти недоступен, и кнопка просто ничего не делает. Это же решение подойдёт
|
||||||
|
для машин без дискретной видеокарты.
|
||||||
|
|
||||||
|
**Осталось:** экраны батареи и сети, а также видеокарта — но у неё свой путь,
|
||||||
|
см. пункт 2.
|
||||||
|
|
||||||
|
**Что хочется.** Показывать не только процессор: видеокарту, оперативную память,
|
||||||
|
диски. Переключать экраны кнопкой **BOOT** на плате — нажал, увидел следующий
|
||||||
|
набор данных.
|
||||||
|
|
||||||
|
**Почему это дёшево со стороны протокола.** Формат `T=67.0;L=34` изначально
|
||||||
|
сделан расширяемым: прошивка молча игнорирует незнакомые ключи. Агент может
|
||||||
|
начать слать `G=61;GL=88;M=73` (видеокарта, её загрузка, память), и уже прошитые
|
||||||
|
платы от этого не сломаются — просто не покажут новое.
|
||||||
|
|
||||||
|
**Что придётся решить при реализации:**
|
||||||
|
|
||||||
|
- ~~**Кнопка BOOT сидит на GPIO9.**~~ Проверено на практике: чтение через
|
||||||
|
`INPUT_PULLUP` в работе никак не мешает загрузке платы.
|
||||||
|
- ~~**Дребезг контактов.**~~ Сделано: 50 мс, `BUTTON_DEBOUNCE_MS`.
|
||||||
|
- **Свой макет под каждый экран.** На 72×40 не получится «то же самое, только
|
||||||
|
другие цифры»: у памяти уместнее проценты и гигабайты, у диска — температура
|
||||||
|
без графика. Каждый экран проектировать отдельно.
|
||||||
|
- **Отдельная история на каждый показатель.** Сейчас кольцевой буфер один и
|
||||||
|
хранит температуру CPU. На каждый график нужен свой буфер: 72 байта на штуку,
|
||||||
|
памяти достаточно.
|
||||||
|
- **Что показывать, если данных нет.** Например, в системе нет дискретной
|
||||||
|
видеокарты — экран GPU надо либо пропускать при листании, либо честно писать,
|
||||||
|
что источника нет.
|
||||||
|
- ~~**Запоминать ли выбранный экран**~~ Решено не запоминать: хранилища нет.
|
||||||
|
- ~~**Тревога при листании.**~~ Решено: принудительно перекидывать на экран
|
||||||
|
процессора и блокировать кнопку.
|
||||||
|
|
||||||
|
**Для видеокарты выбран другой путь** — приоритетный экран без кнопки, см.
|
||||||
|
пункт 2. Листалка остаётся в силе для остального: памяти, батареи, сети.
|
||||||
|
|
||||||
|
### Что фактически доступно на этой машине
|
||||||
|
|
||||||
|
Проверено перебором датчиков через `LibreHardwareMonitorLib` после установки
|
||||||
|
PawnIO (ноутбук с i5-11400H и RTX 3050 Ti):
|
||||||
|
|
||||||
|
| Железо | Датчики | Права |
|
||||||
|
|---|---|---|
|
||||||
|
| Процессор | температура (15 датчиков: пакет, среднее, максимум, по ядрам, запас до предела), мощность | админ |
|
||||||
|
| Процессор | загрузка (общая, по ядрам, по потокам), частоты | обычные |
|
||||||
|
| Видеокарта NVIDIA | температура ядра и горячей точки, загрузка, мощность, напряжение, частоты, память, PCIe | **обычные** |
|
||||||
|
| Встроенная графика Intel | загрузка по блокам (3D, декод видео, копирование) | обычные |
|
||||||
|
| Оперативная память | занято и свободно в ГБ, загрузка в процентах | обычные |
|
||||||
|
| Сеть (5 адаптеров) | скорость приёма и отдачи, объём, утилизация | обычные |
|
||||||
|
| Батарея | заряд, деградация, ток, напряжение, мощность заряда и разряда, ёмкость | обычные |
|
||||||
|
|
||||||
|
Важное следствие: **температура видеокарты не требует прав администратора** —
|
||||||
|
NVIDIA отдаёт её через свой драйвер, ядро для этого не нужно. Админ нужен только
|
||||||
|
процессору, потому что там чтение MSR.
|
||||||
|
|
||||||
|
Чего нет: дисков (раздел не появился даже при включённом `IsStorageEnabled` —
|
||||||
|
вероятно, тоже упирается в права) и датчиков материнской платы с вентиляторами.
|
||||||
|
Для ноутбуков второе обычно: доступ к встроенному контроллеру закрыт.
|
||||||
|
|
||||||
|
### Разобранные кандидаты на экраны
|
||||||
|
|
||||||
|
1. **Видеокарта** — температура и загрузка. Полный аналог текущего экрана,
|
||||||
|
макет переиспользуется целиком. Работает даже без прав администратора.
|
||||||
|
2. **Память** — занято в гигабайтах и процент. Меняется медленно, читается с
|
||||||
|
расстояния, сглаживание не нужно.
|
||||||
|
3. **Батарея** — заряд и мощность разряда в ваттах: сразу видно, сколько ест
|
||||||
|
ноутбук прямо сейчас. Для стационарного ПК бесполезно, экран надо скрывать.
|
||||||
|
4. **Сеть** — скорость приёма и отдачи. Живее всего смотрится на графике, но
|
||||||
|
единицы прыгают от килобайт до мегабайт, и понадобится автоматическая смена
|
||||||
|
масштаба — это единственный кандидат, требующий новой логики отрисовки.
|
||||||
|
|
||||||
|
Порядок реализации по возрастанию стоимости: видеокарта, память, батарея, сеть.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 пикселей
|
||||||
|
уходят за правый край, и загрузка обрезается.
|
||||||
|
|
||||||
|
Дефект существовал с самого начала, проявляется только под предельной нагрузкой.
|
||||||
|
Для i5-11400H это не гипотеза: предельная температура около 100 °C.
|
||||||
|
|
||||||
|
Решение на выбор: при трёхзначной температуре переходить на более мелкий шрифт
|
||||||
|
загрузки либо убирать знак процента. Оценка — около получаса вместе с проверкой.
|
||||||
|
|
||||||
|
Решено оставить как есть: в такой момент экран всё равно мигает тревогой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Автозапуск не проверен перезагрузкой
|
||||||
|
|
||||||
|
Задание Планировщика `CPU Temp Agent` зарегистрировано с триггером «при входе в
|
||||||
|
систему», запускается вручную и работает. Живой перезагрузкой триггер не
|
||||||
|
испытан.
|
||||||
|
|
||||||
|
Проверится само при следующей перезагрузке: экран должен ожить без участия
|
||||||
|
пользователя в течение примерно полуминуты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Скрипт заливки
|
||||||
|
|
||||||
|
Цикл «остановить агента → залить прошивку → запустить агента» делается тремя
|
||||||
|
командами вручную. Пока правок мало, это терпимо; если работа по экрану
|
||||||
|
продолжится, стоит свести в `flash.ps1`.
|
||||||
|
|
||||||
|
Причина, по которой шаг вообще нужен: плата прошивается через тот же COM-порт,
|
||||||
|
по которому идут данные, и занятый порт валит заливку с `PermissionError`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Вес обзорного изображения
|
||||||
|
|
||||||
|
`docs/promo.png` весит 2,1 МБ при исходниках примерно на 30 КБ. Сжатие до
|
||||||
|
300–500 КБ не ухудшило бы читаемость, но старый файл всё равно останется в
|
||||||
|
истории git, так что выигрыш только для будущих клонов.
|
||||||
|
|
||||||
|
Решено оставить как есть.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
+5
-1
@@ -16,7 +16,7 @@ void linkBegin() {
|
|||||||
static bool parseLine(char *line, CpuStats &out) {
|
static bool parseLine(char *line, CpuStats &out) {
|
||||||
bool haveTemp = false;
|
bool haveTemp = false;
|
||||||
bool haveLoad = false;
|
bool haveLoad = false;
|
||||||
CpuStats parsed = {0.0f, 0};
|
CpuStats parsed = {0.0f, 0, -1};
|
||||||
|
|
||||||
char *save = nullptr;
|
char *save = nullptr;
|
||||||
for (char *tok = strtok_r(line, ";", &save); tok; tok = strtok_r(nullptr, ";", &save)) {
|
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) {
|
} else if (strcmp(key, "L") == 0) {
|
||||||
parsed.loadPct = atoi(val);
|
parsed.loadPct = atoi(val);
|
||||||
haveLoad = true;
|
haveLoad = true;
|
||||||
|
} else if (strcmp(key, "M") == 0) {
|
||||||
|
// Память необязательна: старый агент её не шлёт, и это не повод
|
||||||
|
// отбрасывать строку целиком.
|
||||||
|
parsed.memPct = atoi(val);
|
||||||
}
|
}
|
||||||
// прочие ключи игнорируются намеренно: протокол расширяемый
|
// прочие ключи игнорируются намеренно: протокол расширяемый
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,10 +3,11 @@
|
|||||||
struct CpuStats {
|
struct CpuStats {
|
||||||
float tempC;
|
float tempC;
|
||||||
int loadPct;
|
int loadPct;
|
||||||
|
int memPct; // -1 = ключа M в строке не было
|
||||||
};
|
};
|
||||||
|
|
||||||
void linkBegin();
|
void linkBegin();
|
||||||
|
|
||||||
// Неблокирующее чтение. Возвращает true, когда собрана и разобрана
|
// Неблокирующее чтение. Возвращает true, когда собрана и разобрана
|
||||||
// целая строка вида "T=67.0;L=34". Неизвестные ключи игнорируются.
|
// целая строка вида "T=67.0;L=34;M=97". Неизвестные ключи игнорируются.
|
||||||
bool linkPoll(CpuStats &out);
|
bool linkPoll(CpuStats &out);
|
||||||
+84
-16
@@ -8,11 +8,23 @@
|
|||||||
// Порядок параметров: (rotation, reset, clock, data) — SCL идёт раньше SDA.
|
// Порядок параметров: (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 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_TOP = 28;
|
||||||
static const uint8_t GRAPH_H = 12;
|
static const uint8_t GRAPH_H = 12;
|
||||||
|
|
||||||
static uint8_t history[GRAPH_W]; // 0 = нет данных, иначе высота столбика 1..GRAPH_H
|
// Значок в правом верхнем углу. Полоса 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 = SCREEN_W - ICON_W;
|
||||||
|
static const uint8_t ICON_Y = 0;
|
||||||
|
|
||||||
|
// 0 = нет данных, иначе высота столбика 1..GRAPH_H
|
||||||
|
static uint8_t cpuHistory[GRAPH_W];
|
||||||
|
static uint8_t ramHistory[GRAPH_W];
|
||||||
|
|
||||||
void uiBegin() {
|
void uiBegin() {
|
||||||
display.begin();
|
display.begin();
|
||||||
@@ -22,11 +34,12 @@ void uiBegin() {
|
|||||||
display.sendBuffer();
|
display.sendBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiPushSample(float tempC) {
|
// Общая часть для обеих историй: нормировать значение и сдвинуть буфер.
|
||||||
if (tempC < SCALE_MIN) tempC = SCALE_MIN;
|
static void pushSample(uint8_t *history, float value, float scaleMin, float scaleMax) {
|
||||||
if (tempC > SCALE_MAX) tempC = SCALE_MAX;
|
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);
|
uint8_t h = (uint8_t)(norm * GRAPH_H + 0.5f);
|
||||||
if (h < 1) h = 1;
|
if (h < 1) h = 1;
|
||||||
|
|
||||||
@@ -34,7 +47,16 @@ void uiPushSample(float tempC) {
|
|||||||
history[GRAPH_W - 1] = h;
|
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++) {
|
for (uint8_t x = 0; x < GRAPH_W; x++) {
|
||||||
const uint8_t h = history[x];
|
const uint8_t h = history[x];
|
||||||
if (h == 0) continue;
|
if (h == 0) continue;
|
||||||
@@ -42,6 +64,30 @@ static void drawGraph() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Микросхема: корпус, ядро внутри и по три вывода с каждой стороны.
|
||||||
|
static void drawCpuIcon(uint8_t x, uint8_t y) {
|
||||||
|
display.drawFrame(x + 1, y + 1, ICON_W - 2, ICON_H - 2);
|
||||||
|
display.drawBox(x + 4, y + 4, 3, 3);
|
||||||
|
for (uint8_t i = 0; i < 3; i++) {
|
||||||
|
const uint8_t o = 3 + i * 2;
|
||||||
|
display.drawPixel(x + o, y);
|
||||||
|
display.drawPixel(x + o, y + ICON_H - 1);
|
||||||
|
display.drawPixel(x, y + o);
|
||||||
|
display.drawPixel(x + ICON_W - 1, y + o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Планка памяти: корпус, три микросхемы на ней и контакты снизу.
|
||||||
|
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() {
|
static void invertBuffer() {
|
||||||
@@ -50,21 +96,15 @@ static void invertBuffer() {
|
|||||||
for (size_t i = 0; i < n; i++) b[i] = (uint8_t)~b[i];
|
for (size_t i = 0; i < n; i++) b[i] = (uint8_t)~b[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiRender(bool fresh, float tempC, int loadPct, bool invert) {
|
static void drawNoData() {
|
||||||
display.clearBuffer();
|
|
||||||
|
|
||||||
if (!fresh) {
|
|
||||||
display.setFont(u8g2_font_6x10_tf);
|
display.setFont(u8g2_font_6x10_tf);
|
||||||
display.drawStr(0, 14, "NO");
|
display.drawStr(0, 14, "NO");
|
||||||
display.drawStr(0, 26, "DATA");
|
display.drawStr(0, 26, "DATA");
|
||||||
if (invert) invertBuffer();
|
|
||||||
display.sendBuffer();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void drawCpuScreen(float tempC, int loadPct) {
|
||||||
char buf[8];
|
char buf[8];
|
||||||
|
|
||||||
// Крупные цифры температуры.
|
|
||||||
snprintf(buf, sizeof(buf), "%d", (int)(tempC + 0.5f));
|
snprintf(buf, sizeof(buf), "%d", (int)(tempC + 0.5f));
|
||||||
display.setFont(u8g2_font_logisoso24_tn);
|
display.setFont(u8g2_font_logisoso24_tn);
|
||||||
display.drawStr(0, 24, buf);
|
display.drawStr(0, 24, buf);
|
||||||
@@ -79,7 +119,35 @@ void uiRender(bool fresh, float tempC, int loadPct, bool invert) {
|
|||||||
snprintf(buf, sizeof(buf), "%d%%", loadPct);
|
snprintf(buf, sizeof(buf), "%d%%", loadPct);
|
||||||
display.drawStr(numWidth + 2, 24, buf);
|
display.drawStr(numWidth + 2, 24, buf);
|
||||||
|
|
||||||
drawGraph();
|
drawCpuIcon(ICON_X, ICON_Y);
|
||||||
|
drawGraph(cpuHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
if (invert) invertBuffer();
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -2,14 +2,20 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
enum UiScreen : uint8_t {
|
||||||
|
UI_SCREEN_CPU = 0,
|
||||||
|
UI_SCREEN_RAM = 1,
|
||||||
|
};
|
||||||
|
|
||||||
// Инициализирует дисплей и показывает заставку ожидания.
|
// Инициализирует дисплей и показывает заставку ожидания.
|
||||||
void uiBegin();
|
void uiBegin();
|
||||||
|
|
||||||
// Добавляет значение в кольцевой буфер истории. Вызывать раз в секунду
|
// Пополняют историю соответствующего графика. Вызывать раз в секунду и только
|
||||||
// и только при свежих данных, чтобы пауза связи не рисовалась как провал.
|
// при свежих данных, чтобы пауза связи не рисовалась как провал.
|
||||||
void uiPushSample(float tempC);
|
void uiPushCpuSample(float tempC);
|
||||||
|
void uiPushRamSample(int memPct);
|
||||||
|
|
||||||
// Перерисовывает экран целиком.
|
// Перерисовывает экран целиком.
|
||||||
// fresh == false - данные устарели, вместо цифр выводится NO DATA.
|
// fresh == false - данные устарели, вместо цифр выводится NO DATA.
|
||||||
// invert == true - кадр показывается в негативе (фаза мигания тревоги).
|
// 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);
|
||||||
@@ -14,3 +14,9 @@ A collection of Arduino / ESP microcontroller projects.
|
|||||||
panel, and posts events to an HTTPS webhook. Settings live in LittleFS.
|
panel, and posts events to an HTTPS webhook. Settings live in LittleFS.
|
||||||
See its [README](pump_controller_8_2_OLED_DONE/README.md) and
|
See its [README](pump_controller_8_2_OLED_DONE/README.md) and
|
||||||
[MANUAL](pump_controller_8_2_OLED_DONE/MANUAL.md).
|
[MANUAL](pump_controller_8_2_OLED_DONE/MANUAL.md).
|
||||||
|
|
||||||
|
- **[CPU_temp](CPU_temp/)** — desktop CPU temperature monitor on an ESP32-C3 with
|
||||||
|
a built-in 0.42-inch OLED: a Windows agent reads the sensors and feeds the
|
||||||
|
board over USB serial, which shows the temperature, CPU load and a one-minute
|
||||||
|
history graph, and blinks an overheat alarm. See its
|
||||||
|
[README](CPU_temp/README.md).
|
||||||
Reference in new issue
Block a user