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:
1 parent
db86597433
commit
bd0a87ea49
6 files changed
+131
-21
No files matched your search
+30
-5
@@ -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
@@ -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 — порядок установки и разбор
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
Reference in new issue
Block a user