feat(ui): тревога по перегреву и config.h со всеми настройками

Настройки собраны в config.h: пины, ориентация, пороги тревоги, шкала
графика, тайминги и скорость Serial. Раньше они были раскиданы по
модулям, и подстройка требовала лазить по коду.

Тревога включается на ALARM_ON_C и отпускает только на ALARM_OFF_C
(85 и 82 градуса). Один порог заставлял бы её дёргаться каждую секунду,
пока температура топчется на границе.

В тревоге экран мигает негативом (инверсия буфера, а не команда
контроллера - не зависит от микросхемы дисплея), светодиод GPIO8 мигает
с ним в общей фазе. Без данных светодиод горит ровно, в норме погашен.
Зажигается низким уровнем: проверено опытом, заодно в простое пин стоит
высоким, что безопасно для GPIO8 как пина начальной загрузки.

Переменная alarm переименована в alarmActive: имя alarm занято функцией
из unistd.h, которая попадает в сборку ESP32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
oskarvitaliiandClaude Opus 5 committed 2026-09-14 22:32:31 +07:00
1 parent db86597433
commit bd0a87ea49
6 files changed
+131 -21

No files matched your search

+30 -5
View File
@@ -1,24 +1,33 @@
// Индикатор температуры CPU: плата принимает строки "T=67.0;L=34" по USB-COM
// от агента на ПК и рисует их на встроенном OLED 72x40. Сама ничего не меряет.
//
// Все настройки — в config.h.
#include "config.h"
#include "link.h"
#include "ui.h"
static const uint32_t STALE_MS = 5000; // после этого молчания показываем NO DATA
static const uint32_t SAMPLE_MS = 1000; // шаг истории графика
static const uint32_t DRAW_MS = 200; // период перерисовки
static CpuStats stats = {0.0f, 0};
static uint32_t lastData = 0;
static uint32_t lastSample = 0;
static uint32_t lastDraw = 0;
static bool everGot = false;
static bool alarmActive = false;
void setup() {
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, !LED_ACTIVE_LEVEL);
linkBegin();
uiBegin();
}
// Гистерезис: включаемся на верхнем пороге, отпускаем только на нижнем.
static void updateAlarm(float tempC) {
if (!alarmActive && tempC >= ALARM_ON_C) alarmActive = true;
else if (alarmActive && tempC <= ALARM_OFF_C) alarmActive = false;
}
void loop() {
CpuStats fresh;
if (linkPoll(fresh)) {
@@ -29,6 +38,22 @@ void loop() {
const bool isFresh = everGot && (millis() - lastData < STALE_MS);
if (isFresh) {
updateAlarm(stats.tempC);
} else {
alarmActive = false; // без данных о перегреве судить не по чему
}
// Общая фаза для экрана и светодиода, чтобы они моргали синхронно.
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();
@@ -37,6 +62,6 @@ void loop() {
if (millis() - lastDraw >= DRAW_MS) {
lastDraw = millis();
uiRender(isFresh, stats.tempC, stats.loadPct);
uiRender(isFresh, stats.tempC, stats.loadPct, alarmActive && blinkOn);
}
}
+29 -1
View File
@@ -16,7 +16,8 @@ LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)─
| Путь | Что это |
|---|---|
| `CPU_temp.ino` | основной скетч: тайм-аут свежести данных, частота перерисовки |
| `config.h` | **все настройки**: пины, пороги тревоги, шкала графика, тайминги |
| `CPU_temp.ino` | основной скетч: свежесть данных, гистерезис тревоги, светодиод |
| `link.h` / `link.cpp` | неблокирующее чтение строки из Serial и её разбор |
| `ui.h` / `ui.cpp` | отрисовка через U8g2 и кольцевой буфер истории |
| `tools/i2c_scan/` | одноразовый скетч: поиск пинов встроенного дисплея |
@@ -73,6 +74,33 @@ T=67.0;L=34
Шкала графика фиксированная, 30–90 °C. Автомасштаб сознательно не используется:
при нём шум ±1 °C в простое рисовался бы горами и создавал ложную тревогу.
Ориентация задаётся константой `DISPLAY_ROTATION` в `config.h`: `U8G2_R0` —
как разведено на плате, `U8G2_R2` — поворот на 180°.
## Тревога по перегреву
Порогов два, `ALARM_ON_C` и `ALARM_OFF_C` (по умолчанию 85 и 82 °C). Тревога
включается на верхнем и отпускает только на нижнем — с одним порогом она
дёргалась бы каждую секунду, пока температура топчется на границе.
В тревоге экран мигает негативом, а светодиод на GPIO8 мигает с ним в такт
(общая фаза, период `ALARM_BLINK_MS`).
Светодиод по состояниям:
| Состояние | Светодиод |
|---|---|
| Норма | погашен |
| Тревога | мигает синхронно с экраном |
| `NO DATA` | горит ровно |
Светодиод зажигается **низким** уровнем (`LED_ACTIVE_LEVEL = LOW`, определено
опытом). Попутно это удобно: в обычном режиме пин удерживается высоким, а GPIO8
— пин начальной загрузки, которому высокий уровень и нужен.
Проверять тревогу можно, не трогая пороги: остановить агента и подать значения
вручную.
## Требования к ПК
Windows, LibreHardwareMonitor и драйвер PawnIO — порядок установки и разбор
+52
View File
@@ -0,0 +1,52 @@
// Все настройки проекта в одном месте: пины, пороги, шкалы, периоды.
// Менять поведение прошивки здесь, а не по коду модулей.
#pragma once
#include <Arduino.h>
// --- Дисплей ---------------------------------------------------------------
// Пины I2C встроенного OLED. Подтверждены сканером tools/i2c_scan:
// подписи GPIO8/GPIO9 на шелкографии - это дефолтные Wire-пины чипа,
// а не разводка экрана.
static const uint8_t PIN_SDA = 5;
static const uint8_t PIN_SCL = 6;
// Ориентация: U8G2_R0 - как разведено на плате, U8G2_R2 - поворот на 180.
#define DISPLAY_ROTATION U8G2_R2
// --- Тревога по перегреву --------------------------------------------------
// Два порога, а не один: с одним тревога дёргалась бы каждую секунду,
// пока температура топчется на границе.
static const float ALARM_ON_C = 85.0f; // включить тревогу при этой температуре
static const float ALARM_OFF_C = 82.0f; // выключить, только опустившись до этой
// Период полуфазы мигания: экран и светодиод моргают синхронно.
static const uint32_t ALARM_BLINK_MS = 500;
// --- Светодиод -------------------------------------------------------------
// Тревога - мигает, нет данных - горит ровно, норма - погашен.
static const uint8_t PIN_LED = 8;
// Уровень, при котором светодиод горит. Определено опытом: светодиод на GPIO8
// зажигается НИЗКИМ уровнем. Побочно это удобно - в обычном режиме пин стоит
// в высоком уровне, а GPIO8 является пином начальной загрузки, которому
// высокий уровень и нужен.
static const uint8_t LED_ACTIVE_LEVEL = LOW;
// --- График ----------------------------------------------------------------
// Шкала фиксированная: при автомасштабе шум +-1 градус в простое
// рисовался бы горами и создавал ложную тревогу.
static const float SCALE_MIN = 30.0f;
static const float SCALE_MAX = 90.0f;
// --- Связь и тайминги ------------------------------------------------------
static const uint32_t SERIAL_BAUD = 115200;
static const uint32_t STALE_MS = 5000; // после этого молчания показываем NO DATA
static const uint32_t SAMPLE_MS = 1000; // шаг истории графика
static const uint32_t DRAW_MS = 200; // период перерисовки экрана
+3 -1
View File
@@ -2,12 +2,14 @@
#include <Arduino.h>
#include "config.h"
static const size_t BUF_SIZE = 64;
static char buf[BUF_SIZE];
static size_t len = 0;
void linkBegin() {
Serial.begin(115200);
Serial.begin(SERIAL_BAUD);
len = 0;
}
+14 -12
View File
@@ -3,24 +3,15 @@
#include <U8g2lib.h>
#include <string.h>
// Подтверждено сканером из tools/i2c_scan: экран отвечает только на этой паре.
static const uint8_t PIN_SDA = 5;
static const uint8_t PIN_SCL = 6;
// Ориентация экрана: U8G2_R0 — как разведено на плате, U8G2_R2 — поворот на 180 градусов.
static const u8g2_cb_t *const DISPLAY_ROTATION = U8G2_R2;
#include "config.h"
// Порядок параметров: (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 const uint8_t GRAPH_W = 72; // по пикселю на отсчёт: 72 точки = 72 секунды
static const uint8_t GRAPH_W = 72; // по пикселю на отсчёт: 72 точки = 72 секунды
static const uint8_t GRAPH_TOP = 28;
static const uint8_t GRAPH_H = 12;
// Шкала фиксированная: при автомасштабе шум +-1 градус в простое рисовался бы горами.
static const float SCALE_MIN = 30.0f;
static const float SCALE_MAX = 90.0f;
static uint8_t history[GRAPH_W]; // 0 = нет данных, иначе высота столбика 1..GRAPH_H
void uiBegin() {
@@ -51,13 +42,22 @@ static void drawGraph() {
}
}
void uiRender(bool fresh, float tempC, int loadPct) {
// Негатив всего кадра. Через буфер, а не командой контроллера: так поведение
// не зависит от конкретной микросхемы дисплея.
static void invertBuffer() {
uint8_t *b = display.getBufferPtr();
const size_t n = (size_t)display.getBufferTileWidth() * display.getBufferTileHeight() * 8;
for (size_t i = 0; i < n; i++) b[i] = (uint8_t)~b[i];
}
void uiRender(bool fresh, float tempC, int loadPct, bool invert) {
display.clearBuffer();
if (!fresh) {
display.setFont(u8g2_font_6x10_tf);
display.drawStr(0, 14, "NO");
display.drawStr(0, 26, "DATA");
if (invert) invertBuffer();
display.sendBuffer();
return;
}
@@ -81,5 +81,7 @@ void uiRender(bool fresh, float tempC, int loadPct) {
drawGraph();
if (invert) invertBuffer();
display.sendBuffer();
}
+3 -2
View File
@@ -10,5 +10,6 @@ void uiBegin();
void uiPushSample(float tempC);
// Перерисовывает экран целиком.
// fresh == false означает, что данные устарели: вместо цифр выводится NO DATA.
void uiRender(bool fresh, float tempC, int loadPct);
// fresh == false - данные устарели, вместо цифр выводится NO DATA.
// invert == true - кадр показывается в негативе (фаза мигания тревоги).
void uiRender(bool fresh, float tempC, int loadPct, bool invert);