Files
oskarvitaliiandClaude Opus 5 f18090b5b7 feat(ui): экран оперативной памяти и переключение кнопкой BOOT
Второй экран показывает загрузку ОЗУ крупной цифрой и её график со
шкалой 0-100 - у памяти она естественная, подбирать нечего. В углу свой
значок, планка памяти вместо микросхемы: он же и говорит, какой экран
открыт.

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:56:33 +07:00

142 lines
5.3 KiB
Arduino

// Индикатор состояния ПК: плата принимает строки "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, -1};
static uint32_t lastData = 0;
static uint32_t lastSample = 0;
static uint32_t lastDraw = 0;
static bool everGot = 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() {
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, !LED_ACTIVE_LEVEL);
pinMode(PIN_BUTTON, INPUT_PULLUP);
linkBegin();
uiBegin();
}
// Гистерезис: включаемся на верхнем пороге, отпускаем только на нижнем.
static void updateAlarm(float tempC) {
if (!alarmActive && tempC >= ALARM_ON_C) alarmActive = true;
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)) {
stats = fresh;
tempShown = tempAvgPush(fresh.tempC);
lastData = millis();
everGot = true;
}
const bool isFresh = everGot && (millis() - lastData < STALE_MS);
// После паузы связи среднее нельзя считать по значениям до обрыва.
if (wasFresh && !isFresh) tempAvgReset();
wasFresh = isFresh;
if (isFresh) {
// Тревога по усреднённому значению: иначе экран показывал бы 84, а мигание
// уже шло бы. Побочно это защищает от срабатывания на одиночный выброс.
updateAlarm(tempShown);
} else {
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 ledOn = false;
digitalWrite(PIN_LED, ledOn ? LED_ACTIVE_LEVEL : !LED_ACTIVE_LEVEL);
// Историю дописываем только при живой связи: пауза не должна рисоваться провалом.
if (isFresh && millis() - lastSample >= SAMPLE_MS) {
lastSample = millis();
uiPushCpuSample(stats.tempC); // график по сырым: 5 градусов на пиксель, шум там не виден
if (ramAvailable()) uiPushRamSample(stats.memPct);
}
if (millis() - lastDraw >= DRAW_MS) {
lastDraw = millis();
uiRender(screen, isFresh, tempShown, stats.loadPct, stats.memPct, alarmActive && blinkOn);
}
}