// Индикатор состояния ПК: плата принимает строки "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); } }