Второй экран показывает загрузку ОЗУ крупной цифрой и её график со шкалой 0-100 - у памяти она естественная, подбирать нечего. В углу свой значок, планка памяти вместо микросхемы: он же и говорит, какой экран открыт. Тревога в приоритете: при перегреве плата принудительно возвращается на экран процессора и кнопка не работает, пока температура не упадёт. Смысл тревоги в том, что её нельзя прощёлкать. Экран памяти выпадает из листания, если агент не шлёт ключ M - со старым агентом кнопка просто ничего не делает. То же решение подойдёт машинам без дискретной видеокарты. Выбранный экран не запоминается при перезагрузке: хранить негде. Тревоги по памяти нет намеренно - загрузка ОЗУ под 100% здесь обычное рабочее состояние. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
5.3 KiB
Arduino
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);
|
|
}
|
|
}
|