feat(ui): экран оперативной памяти и переключение кнопкой BOOT

Второй экран показывает загрузку ОЗУ крупной цифрой и её график со
шкалой 0-100 - у памяти она естественная, подбирать нечего. В углу свой
значок, планка памяти вместо микросхемы: он же и говорит, какой экран
открыт.

Тревога в приоритете: при перегреве плата принудительно возвращается на
экран процессора и кнопка не работает, пока температура не упадёт. Смысл
тревоги в том, что её нельзя прощёлкать.

Экран памяти выпадает из листания, если агент не шлёт ключ M - со старым
агентом кнопка просто ничего не делает. То же решение подойдёт машинам
без дискретной видеокарты.

Выбранный экран не запоминается при перезагрузке: хранить негде.
Тревоги по памяти нет намеренно - загрузка ОЗУ под 100% здесь обычное
рабочее состояние.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
oskarvitaliiandClaude Opus 5 committed 2026-09-17 19:56:33 +07:00
1 parent 4344fa8873
commit f18090b5b7
8 files changed
+201 -59

No files matched your search

+48 -5
View File
@@ -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);
}
}