From bd0a87ea495b118068b215c8df5ef5bfb82036fa Mon Sep 17 00:00:00 2001 From: Vitali Date: Mon, 14 Sep 2026 22:32:31 +0700 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=D1=82=D1=80=D0=B5=D0=B2=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=20=D0=BF=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=83=20=D0=B8=20config.h=20=D1=81=D0=BE=20?= =?UTF-8?q?=D0=B2=D1=81=D0=B5=D0=BC=D0=B8=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настройки собраны в 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 --- CPU_temp/CPU_temp.ino | 35 ++++++++++++++++++++++++----- CPU_temp/README.md | 30 ++++++++++++++++++++++++- CPU_temp/config.h | 52 +++++++++++++++++++++++++++++++++++++++++++ CPU_temp/link.cpp | 4 +++- CPU_temp/ui.cpp | 26 ++++++++++++---------- CPU_temp/ui.h | 5 +++-- 6 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 CPU_temp/config.h diff --git a/CPU_temp/CPU_temp.ino b/CPU_temp/CPU_temp.ino index 8c8182c..c04cff6 100644 --- a/CPU_temp/CPU_temp.ino +++ b/CPU_temp/CPU_temp.ino @@ -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); } } diff --git a/CPU_temp/README.md b/CPU_temp/README.md index a43980e..8a7b1f4 100644 --- a/CPU_temp/README.md +++ b/CPU_temp/README.md @@ -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 — порядок установки и разбор diff --git a/CPU_temp/config.h b/CPU_temp/config.h new file mode 100644 index 0000000..236329d --- /dev/null +++ b/CPU_temp/config.h @@ -0,0 +1,52 @@ +// Все настройки проекта в одном месте: пины, пороги, шкалы, периоды. +// Менять поведение прошивки здесь, а не по коду модулей. + +#pragma once + +#include + +// --- Дисплей --------------------------------------------------------------- + +// Пины 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; // период перерисовки экрана diff --git a/CPU_temp/link.cpp b/CPU_temp/link.cpp index 658d801..c234bf3 100644 --- a/CPU_temp/link.cpp +++ b/CPU_temp/link.cpp @@ -2,12 +2,14 @@ #include +#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; } diff --git a/CPU_temp/ui.cpp b/CPU_temp/ui.cpp index 555e876..b2a861d 100644 --- a/CPU_temp/ui.cpp +++ b/CPU_temp/ui.cpp @@ -3,24 +3,15 @@ #include #include -// Подтверждено сканером из 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(); } diff --git a/CPU_temp/ui.h b/CPU_temp/ui.h index 75f953b..f124d92 100644 --- a/CPU_temp/ui.h +++ b/CPU_temp/ui.h @@ -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);