Compare commits
8
Commits
dev
..
f77ff376d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77ff376d4 | ||
|
|
d53035a5ef | ||
|
|
bda2ecb7a3 | ||
|
|
208669d19f | ||
|
|
68bdfdc879 | ||
|
|
2bf49c14ac | ||
|
|
f8294d4aed | ||
|
|
4b59b8936b |
No files matched your search
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
!/WeatherPredictor/
|
!/WeatherPredictor/
|
||||||
!/pump_controller_8_2_OLED_DONE/
|
!/pump_controller_8_2_OLED_DONE/
|
||||||
!/CPU_temp/
|
|
||||||
|
|
||||||
# --- Правила ниже действуют внутри разрешённых проектов ---
|
# --- Правила ниже действуют внутри разрешённых проектов ---
|
||||||
# Идут после белого списка намеренно: более поздний шаблон побеждает, поэтому
|
# Идут после белого списка намеренно: более поздний шаблон побеждает, поэтому
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
// Индикатор состояния ПК: плата принимает строки "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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
# CPU Temp — индикатор температуры процессора на ESP32-C3
|
|
||||||
|
|
||||||
Небольшая плата ESP32-C3 со встроенным OLED-экраном 0.42″ стоит на столе и
|
|
||||||
показывает состояние компьютера: крупная цифра, мелкий показатель рядом и график
|
|
||||||
за последнюю минуту. Экранов два — процессор и оперативная память, переключаются
|
|
||||||
кнопкой BOOT. При перегреве процессора экран мигает негативом.
|
|
||||||
|
|
||||||
```
|
|
||||||
LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40
|
|
||||||
(через драйвер PawnIO) раз в секунду "T=67.0;L=34;M=97" U8g2
|
|
||||||
```
|
|
||||||
|
|
||||||
**Плата ничего не измеряет сама.** Она принимает готовые строки по USB и рисует
|
|
||||||
их. Всё чтение датчиков происходит на компьютере — иначе никак: температура CPU
|
|
||||||
доступна только через драйвер уровня ядра.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*Две поправки к изображению: экран на этой плате 0.42″ (72×40), а не 0.96″;
|
|
||||||
график строится по температуре, а не по загрузке — загрузка выводится цифрой.*
|
|
||||||
|
|
||||||
Подключение — по USB-кабелю, которым плата и так питается. Wi-Fi не
|
|
||||||
используется: настраивать нечего, задержки нет.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что понадобится
|
|
||||||
|
|
||||||
| | Что | Примечание |
|
|
||||||
|---|---|---|
|
|
||||||
| Железо | Плата ESP32-C3 со встроенным OLED 0.42″ (72×40) | «фиолетовая» плата с разъёмом Type-C |
|
|
||||||
| | Кабель USB Type-C | он же питание, он же канал данных |
|
|
||||||
| ПО на ПК | Windows | агент написан под Windows PowerShell 5.1 |
|
|
||||||
| | Arduino IDE 2.x | сборка и заливка прошивки |
|
|
||||||
| | LibreHardwareMonitor | чтение датчиков |
|
|
||||||
| | PawnIO | драйвер, без него температуры не будет |
|
|
||||||
|
|
||||||
Всё программное обеспечение бесплатное.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Часть 1. Прошивка платы
|
|
||||||
|
|
||||||
### 1.1 Arduino IDE
|
|
||||||
|
|
||||||
Скачать с <https://www.arduino.cc/en/software> и установить. Проект собран на
|
|
||||||
версии 2.3.10.
|
|
||||||
|
|
||||||
### 1.2 Ядро ESP32
|
|
||||||
|
|
||||||
**Инструменты → Плата → Менеджер плат**, найти **esp32** (Espressif Systems),
|
|
||||||
установить. Проект собран на версии 3.3.8.
|
|
||||||
|
|
||||||
Добавлять URL в настройках IDE не нужно: пакет есть в штатном индексе Arduino.
|
|
||||||
|
|
||||||
### 1.3 Библиотека U8g2
|
|
||||||
|
|
||||||
**Инструменты → Управлять библиотеками**, найти **U8g2** (автор oliver),
|
|
||||||
установить. Проект собран на версии 2.36.19.
|
|
||||||
|
|
||||||
Библиотека поддерживает экран 72×40 готовым драйвером. Это важнее, чем кажется:
|
|
||||||
у такого экрана ненулевое смещение по обеим осям, и вручную его подбирать
|
|
||||||
пришлось бы вслепую.
|
|
||||||
|
|
||||||
### 1.4 Настройки платы
|
|
||||||
|
|
||||||
Подключить плату кабелем. В меню **Инструменты** выставить:
|
|
||||||
|
|
||||||
| Пункт меню | Значение |
|
|
||||||
|---|---|
|
|
||||||
| Плата | **ESP32C3 Dev Module** |
|
|
||||||
| **USB CDC On Boot** | **Enabled** ← обязательно |
|
|
||||||
| Flash Size | **4MB (32Mb)** |
|
|
||||||
| Partition Scheme | Default 4MB with spiffs |
|
|
||||||
| Upload Speed | 921600 |
|
|
||||||
| Порт | тот, что появился после подключения платы |
|
|
||||||
|
|
||||||
Два пункта критичны:
|
|
||||||
|
|
||||||
- **USB CDC On Boot = Enabled.** По умолчанию стоит `Disabled`, и тогда `Serial`
|
|
||||||
уходит в аппаратный UART вместо USB. Прошивка соберётся и зальётся, но данные
|
|
||||||
до платы доходить не будут, а на экране навсегда останется `NO DATA`.
|
|
||||||
- **Flash Size должен совпадать с физическим объёмом флеша.** У этой платы 4 МБ.
|
|
||||||
Посмотреть фактический объём можно через **Инструменты → Get Board Info** или
|
|
||||||
в выводе утилиты прошивки. При несовпадении разделы уезжают за границу
|
|
||||||
микросхемы.
|
|
||||||
|
|
||||||
Если номер COM-порта неочевиден: **Диспетчер устройств → Порты (COM и LPT)**,
|
|
||||||
плата видна как устройство с идентификатором `VID_303A&PID_1001` (Espressif).
|
|
||||||
|
|
||||||
### 1.5 Определить пины дисплея
|
|
||||||
|
|
||||||
На **этой** плате экран подключён к `SDA = GPIO5`, `SCL = GPIO6`, адрес `0x3C` —
|
|
||||||
уже прописано в `config.h`, шаг можно пропустить.
|
|
||||||
|
|
||||||
Если плата похожая, но другая, не доверяйте надписям на текстолите: подписи
|
|
||||||
`GPIO8/GPIO9` рядом с пометкой I2C относятся к стандартным пинам чипа, а не к
|
|
||||||
разводке встроенного экрана. Залейте `tools/i2c_scan/i2c_scan.ino` и откройте
|
|
||||||
монитор порта на скорости 115200. Сканер перебирает пары пинов и печатает
|
|
||||||
найденное:
|
|
||||||
|
|
||||||
```
|
|
||||||
--- I2C scan ---
|
|
||||||
SDA=5 SCL=6: device 0x3C
|
|
||||||
SDA=6 SCL=5: nothing
|
|
||||||
SDA=8 SCL=9: nothing
|
|
||||||
SDA=9 SCL=8: nothing
|
|
||||||
```
|
|
||||||
|
|
||||||
Найденную пару вписать в `config.h` (`PIN_SDA`, `PIN_SCL`).
|
|
||||||
|
|
||||||
### 1.6 Залить прошивку
|
|
||||||
|
|
||||||
Открыть `CPU_temp.ino` в Arduino IDE и нажать **Загрузить**.
|
|
||||||
|
|
||||||
Сразу после заливки на экране появится `NO DATA` — так и должно быть: агента на
|
|
||||||
компьютере ещё нет.
|
|
||||||
|
|
||||||
То же самое из командной строки:
|
|
||||||
|
|
||||||
```
|
|
||||||
arduino-cli compile -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default .
|
|
||||||
arduino-cli upload -p COM4 -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default .
|
|
||||||
```
|
|
||||||
|
|
||||||
Вместо `COM4` подставить свой порт.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Часть 2. Агент на компьютере
|
|
||||||
|
|
||||||
Агент — скрипт `agent/cpu_temp_agent.ps1`. Он читает датчики и раз в секунду
|
|
||||||
шлёт плате строку.
|
|
||||||
|
|
||||||
### 2.1 LibreHardwareMonitor
|
|
||||||
|
|
||||||
Скачать со страницы релизов
|
|
||||||
<https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases> архив
|
|
||||||
**`LibreHardwareMonitor.zip`** и распаковать в `D:\Soft\LibreHardwareMonitor`.
|
|
||||||
|
|
||||||
- Путь можно выбрать другой, но тогда поправьте переменную `$LhmPath` в начале
|
|
||||||
скрипта.
|
|
||||||
- **Не берите `LibreHardwareMonitor.NET.10.zip`** — это сборка под .NET 10, а
|
|
||||||
Windows PowerShell 5.1 работает на .NET Framework и такую сборку не загрузит.
|
|
||||||
- Запускать саму программу не нужно. Используются только библиотека
|
|
||||||
`LibreHardwareMonitorLib.dll` и лежащий рядом `HidSharp.dll`.
|
|
||||||
|
|
||||||
### 2.2 Драйвер PawnIO
|
|
||||||
|
|
||||||
Скачать со страницы <https://github.com/namazso/PawnIO.Setup/releases> файл
|
|
||||||
`PawnIO_setup.exe` и запустить **без аргументов**, двойным щелчком, затем пройти
|
|
||||||
мастер. Потребуются права администратора.
|
|
||||||
|
|
||||||
Установщик не принимает ключей командной строки: на `/S` или `/quiet` он
|
|
||||||
отвечает кодом 87 и молча ничего не делает.
|
|
||||||
|
|
||||||
**Зачем это нужно.** Начиная с версии 0.9.5 LibreHardwareMonitor читает регистры
|
|
||||||
процессора через PawnIO, а не через прежний WinRing0, и в архив с программой
|
|
||||||
драйвер не входит. Без него **загрузка CPU читается, а все температуры пустые** —
|
|
||||||
даже из консоли администратора. Симптом сбивает с толку: выглядит как нехватка
|
|
||||||
прав, хотя права ни при чём.
|
|
||||||
|
|
||||||
### 2.3 Проверить вручную
|
|
||||||
|
|
||||||
Открыть PowerShell **от имени администратора** (правый щелчок по значку →
|
|
||||||
«Запуск от имени администратора») и выполнить:
|
|
||||||
|
|
||||||
```
|
|
||||||
powershell -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1" -Echo
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается поток строк раз в секунду:
|
|
||||||
|
|
||||||
```
|
|
||||||
connected: COM4
|
|
||||||
T=47.0;L=24;M=97
|
|
||||||
T=48.0;L=14;M=97
|
|
||||||
```
|
|
||||||
|
|
||||||
Одновременно на экране платы появится температура. Остановить — Ctrl+C.
|
|
||||||
|
|
||||||
Права администратора нужны всегда: без них драйвер не открывается. Агент
|
|
||||||
проверяет это при старте и называет причину, а не молчит.
|
|
||||||
|
|
||||||
### 2.4 Автозапуск при входе в систему
|
|
||||||
|
|
||||||
В консоли администратора:
|
|
||||||
|
|
||||||
```
|
|
||||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1"'
|
|
||||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
|
|
||||||
$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
||||||
Register-ScheduledTask -TaskName 'CPU Temp Agent' -Action $action -Trigger $trigger -Settings $set -RunLevel Highest -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
- `-RunLevel Highest` обязателен — это и есть «выполнить с наивысшими правами».
|
|
||||||
- Нулевой `ExecutionTimeLimit` нужен, иначе Планировщик убьёт агента через трое
|
|
||||||
суток.
|
|
||||||
|
|
||||||
Проверить, не перезагружаясь:
|
|
||||||
|
|
||||||
```
|
|
||||||
Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
(Get-ScheduledTask -TaskName 'CPU Temp Agent').State
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается `Running`.
|
|
||||||
|
|
||||||
Удалить задание:
|
|
||||||
|
|
||||||
```
|
|
||||||
Unregister-ScheduledTask -TaskName 'CPU Temp Agent' -Confirm:$false
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Повторная заливка прошивки
|
|
||||||
|
|
||||||
Плата прошивается через тот же COM-порт, по которому идут данные. Пока агент
|
|
||||||
держит порт, заливка падает с `PermissionError`. Порядок такой:
|
|
||||||
|
|
||||||
```
|
|
||||||
Stop-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
# ... залить прошивку ...
|
|
||||||
Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
```
|
|
||||||
|
|
||||||
Порт может держать и монитор порта Arduino IDE — его тоже надо закрыть.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Настройка
|
|
||||||
|
|
||||||
Всё, что имеет смысл менять, собрано в `config.h`:
|
|
||||||
|
|
||||||
| Константа | По умолчанию | Смысл |
|
|
||||||
|---|---|---|
|
|
||||||
| `PIN_SDA`, `PIN_SCL` | 5, 6 | пины экрана |
|
|
||||||
| `DISPLAY_ROTATION` | `U8G2_R2` | ориентация; `U8G2_R0` — повернуть обратно |
|
|
||||||
| `ALARM_ON_C` | 85.0 | температура включения тревоги |
|
|
||||||
| `ALARM_OFF_C` | 82.0 | температура отпускания тревоги |
|
|
||||||
| `ALARM_BLINK_MS` | 500 | полупериод мигания |
|
|
||||||
| `PIN_LED` | 8 | светодиод |
|
|
||||||
| `PIN_BUTTON` | 9 | кнопка BOOT, переключает экраны |
|
|
||||||
| `BUTTON_DEBOUNCE_MS` | 50 | защита от дребезга контактов |
|
|
||||||
| `LED_ACTIVE_LEVEL` | `LOW` | уровень, при котором светодиод горит |
|
|
||||||
| `TEMP_AVG_SAMPLES` | 5 | окно сглаживания температуры, замеров |
|
|
||||||
| `SCALE_MIN`, `SCALE_MAX` | 30.0, 90.0 | шкала графика, °C |
|
|
||||||
| `SERIAL_BAUD` | 115200 | скорость обмена; менять вместе с агентом |
|
|
||||||
| `STALE_MS` | 5000 | молчание, после которого показывается `NO DATA` |
|
|
||||||
| `SAMPLE_MS` | 1000 | шаг истории графика |
|
|
||||||
| `DRAW_MS` | 200 | период перерисовки экрана |
|
|
||||||
|
|
||||||
### Почему порогов тревоги два, а не один
|
|
||||||
|
|
||||||
С одним порогом тревога дёргалась бы включениями и выключениями каждую секунду,
|
|
||||||
пока температура топчется на границе. Поэтому включение и отпускание разведены:
|
|
||||||
загорается на 85, гаснет только при спуске до 82.
|
|
||||||
|
|
||||||
### Почему цифра температуры усредняется
|
|
||||||
|
|
||||||
Мгновенное значение плавает на ±1 °C вокруг стабильного, и цифра мельтешит
|
|
||||||
каждую секунду, хотя ничего не происходит — это отвлекает. На экран выводится
|
|
||||||
среднее за `TEMP_AVG_SAMPLES` последних замеров, то есть за 5 секунд: агент
|
|
||||||
шлёт раз в секунду.
|
|
||||||
|
|
||||||
Плата за это — запаздывание реакции на реальный нагрев примерно на 2–3 секунды.
|
|
||||||
Для температуры процессора незаметно: она физически инертна.
|
|
||||||
|
|
||||||
Что осталось несглаженным и почему:
|
|
||||||
|
|
||||||
- **загрузка CPU** — её скачки настоящие, ради них на неё и смотрят;
|
|
||||||
- **график** — 12 пикселей на диапазон 60 °C дают 5 °C на пиксель, шум там и так
|
|
||||||
не виден, зато история остаётся сырой.
|
|
||||||
|
|
||||||
Тревога считается по усреднённому значению, а не по сырому: иначе экран
|
|
||||||
показывал бы 84, когда мигание уже началось. Побочно это защищает от
|
|
||||||
срабатывания на одиночный выброс — перегрев должен продержаться пару секунд.
|
|
||||||
|
|
||||||
### Почему шкала графика фиксированная
|
|
||||||
|
|
||||||
Автомасштаб отвергнут сознательно: в простое колебания ±1 °C рисовались бы во
|
|
||||||
всю высоту экрана и выглядели тревожно, хотя ничего не происходит.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что показывает экран
|
|
||||||
|
|
||||||
Экранов два, переключаются нажатием кнопки **BOOT** (длительность не важна):
|
|
||||||
|
|
||||||
| Экран | Крупно | Мелко | График |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Процессор | температура, °C | загрузка, % | температура, шкала 30–90 °C |
|
|
||||||
| Память | загрузка, % | знак процента | загрузка, шкала 0–100 % |
|
|
||||||
|
|
||||||
Значок в правом верхнем углу говорит, какой экран открыт: микросхема — процессор,
|
|
||||||
планка памяти — ОЗУ.
|
|
||||||
|
|
||||||
| Состояние | Экран | Светодиод GPIO8 |
|
|
||||||
|---|---|---|
|
|
||||||
| Норма | выбранный экран | погашен |
|
|
||||||
| Перегрев | **принудительно** экран процессора, мигает негативом | мигает синхронно с экраном |
|
|
||||||
| Нет данных дольше 5 с | `NO DATA` | горит ровно |
|
|
||||||
|
|
||||||
**Тревога в приоритете.** При перегреве плата сама возвращается на экран
|
|
||||||
процессора, и кнопка перестаёт работать до тех пор, пока температура не упадёт.
|
|
||||||
Смысл тревоги в том, что её нельзя прощёлкать.
|
|
||||||
|
|
||||||
Выбранный экран **не запоминается** при перезагрузке платы: энергонезависимого
|
|
||||||
хранилища в прошивке нет вовсе, после сброса всегда показывается процессор.
|
|
||||||
|
|
||||||
Тревога считается только по температуре процессора. По памяти её нет намеренно:
|
|
||||||
загрузка ОЗУ под 100 % — обычное рабочее состояние, мигать на него бессмысленно.
|
|
||||||
|
|
||||||
В режиме `NO DATA` история графиков не пополняется — иначе обрыв связи выглядел
|
|
||||||
бы как резкое падение значений.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Протокол обмена
|
|
||||||
|
|
||||||
Агент шлёт плате по одной строке в секунду, в конце — перевод строки:
|
|
||||||
|
|
||||||
```
|
|
||||||
T=67.0;L=34;M=97
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Смысл |
|
|
||||||
|---|---|
|
|
||||||
| `T` | температура CPU Package, °C |
|
|
||||||
| `L` | общая загрузка CPU, % |
|
|
||||||
| `M` | загрузка оперативной памяти, % |
|
|
||||||
|
|
||||||
Ключ `M` показывается на отдельном экране памяти — третьему показателю на экране
|
|
||||||
72×40 места нет. Если агент старый и ключа `M` не шлёт, экран памяти из листания
|
|
||||||
выпадает сам, и кнопка ничего не делает.
|
|
||||||
|
|
||||||
Берётся он у железа с именем `Total Memory` — это физическая память. У соседнего
|
|
||||||
`Virtual Memory` датчик называется точно так же, но считает вместе с файлом
|
|
||||||
подкачки и показывает заниженную цифру.
|
|
||||||
|
|
||||||
Неизвестные ключи прошивка молча игнорирует, поэтому протокол расширяем: можно
|
|
||||||
добавить, например, `G=` для видеокарты, не трогая уже прошитые платы.
|
|
||||||
|
|
||||||
**Разделитель дробной части — только точка.** Агент форматирует числа через
|
|
||||||
`InvariantCulture`: в русской локали иначе вышло бы `T=67,0`, и разбор на плате
|
|
||||||
прочитал бы `67`, молча потеряв дробную часть.
|
|
||||||
|
|
||||||
Плату можно проверить без агента, отправив строки вручную. Порт при этом должен
|
|
||||||
быть свободен, то есть агента надо остановить:
|
|
||||||
|
|
||||||
```
|
|
||||||
$sp = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One'
|
|
||||||
$sp.DtrEnable = $true; $sp.RtsEnable = $false
|
|
||||||
$sp.Open()
|
|
||||||
40..90 | ForEach-Object { $sp.WriteLine("T=$_.0;L=50"); Start-Sleep -Milliseconds 400 }
|
|
||||||
$sp.Close()
|
|
||||||
```
|
|
||||||
|
|
||||||
Так же удобно проверять тревогу, не меняя пороги в `config.h`: подать значение
|
|
||||||
выше 85, затем 83 (тревога должна сохраниться) и 81 (должна отпустить).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Если не работает
|
|
||||||
|
|
||||||
| Симптом | Причина | Что делать |
|
|
||||||
|---|---|---|
|
|
||||||
| На экране всегда `NO DATA`, хотя агент шлёт строки | `USB CDC On Boot` выключен, `Serial` ушёл в аппаратный UART | включить в меню платы и перезалить |
|
|
||||||
| Экран не светится вовсе | неверные пины I2C | залить `tools/i2c_scan`, найти пару, вписать в `config.h` |
|
|
||||||
| Агент пишет, что температура недоступна | не установлен PawnIO | установить драйвер, см. 2.2 |
|
|
||||||
| Агент отказывается запускаться из-за прав | консоль не от администратора | открыть «от имени администратора» |
|
|
||||||
| Заливка падает с `PermissionError` | порт держит агент или монитор порта IDE | `Stop-ScheduledTask`, закрыть монитор |
|
|
||||||
| Температура без дробной части | число пришло с запятой вместо точки | агент должен форматировать через `InvariantCulture` |
|
|
||||||
| Скрипт `.ps1` падает с ошибками про скобки | файл сохранён в UTF-8 **без BOM** | пересохранить в UTF-8 **с BOM**: PowerShell 5.1 иначе читает файл как ANSI и портит кириллицу |
|
|
||||||
| После перетыкания в другой разъём данные не идут | сменился номер порта | ничего не делать: агент ищет плату по `VID_303A&PID_1001` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Как устроено
|
|
||||||
|
|
||||||
| Путь | Что это |
|
|
||||||
|---|---|
|
|
||||||
| `config.h` | все настройки |
|
|
||||||
| `CPU_temp.ino` | свежесть данных, гистерезис тревоги, кнопка, выбор экрана, светодиод |
|
|
||||||
| `link.h` / `link.cpp` | неблокирующее чтение строки из Serial и её разбор |
|
|
||||||
| `ui.h` / `ui.cpp` | отрисовка через U8g2, кольцевой буфер истории, негатив |
|
|
||||||
| `tools/i2c_scan/` | одноразовый скетч поиска пинов дисплея |
|
|
||||||
| `agent/cpu_temp_agent.ps1` | агент для ПК |
|
|
||||||
| `agent/README.md` | подробности по агенту |
|
|
||||||
| `docs/` | спецификация, план реализации и [бэклог](docs/BACKLOG.md) |
|
|
||||||
|
|
||||||
Разделение простое: `link` ничего не знает об экране, `ui` не знает, откуда
|
|
||||||
взялись данные, а логика состояний живёт в скетче.
|
|
||||||
|
|
||||||
## Расход ресурсов
|
|
||||||
|
|
||||||
Агент занимает около 70 МБ оперативной памяти и со временем не растёт. Почти всё
|
|
||||||
это — обвязка Windows PowerShell и .NET Framework; сам цикл держит в памяти
|
|
||||||
несколько чисел.
|
|
||||||
|
|
||||||
Прошивка занимает 26% флеша платы и 4% её оперативной памяти.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Агент CPU Temp
|
|
||||||
|
|
||||||
Читает температуру и загрузку процессора и раз в секунду отдаёт их плате
|
|
||||||
ESP32-C3 по USB-COM строкой вида `T=67.0;L=34;M=97`.
|
|
||||||
|
|
||||||
## Что нужно установить
|
|
||||||
|
|
||||||
### 1. LibreHardwareMonitor
|
|
||||||
|
|
||||||
Скачать со <https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases>
|
|
||||||
архив **`LibreHardwareMonitor.zip`** и распаковать в `D:\Soft\LibreHardwareMonitor`.
|
|
||||||
|
|
||||||
Не брать `LibreHardwareMonitor.NET.10.zip`: это сборка под .NET 10, а Windows
|
|
||||||
PowerShell 5.1 работает на .NET Framework и такую сборку не загрузит.
|
|
||||||
|
|
||||||
Само приложение запускать не нужно — используются только
|
|
||||||
`LibreHardwareMonitorLib.dll` и лежащий рядом `HidSharp.dll`.
|
|
||||||
|
|
||||||
### 2. Драйвер PawnIO
|
|
||||||
|
|
||||||
Скачать со <https://github.com/namazso/PawnIO.Setup/releases> файл
|
|
||||||
`PawnIO_setup.exe` и запустить **без аргументов**: установщик не принимает
|
|
||||||
никаких ключей, и на `/S` или `/quiet` отвечает кодом 87.
|
|
||||||
|
|
||||||
Зачем: с версии 0.9.5 LibreHardwareMonitor читает MSR-регистры процессора через
|
|
||||||
PawnIO, а не через WinRing0, и в архив с программой этот драйвер не входит.
|
|
||||||
|
|
||||||
**Без PawnIO загрузка CPU читается, а все температуры пустые** — даже из консоли
|
|
||||||
администратора. Выглядит как нехватка прав, но причина другая.
|
|
||||||
|
|
||||||
## Запуск вручную
|
|
||||||
|
|
||||||
Нужны права администратора: без них драйвер не открывается.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1" -Echo
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключ `-Echo` печатает каждую отправленную строку — только для проверки.
|
|
||||||
|
|
||||||
При старте агент сам проверяет, доступна ли температура, и при отказе называет
|
|
||||||
причину, а не крутит молча пустой цикл.
|
|
||||||
|
|
||||||
## Автозапуск
|
|
||||||
|
|
||||||
Задание Планировщика `CPU Temp Agent`: триггер «при входе в систему», наивысшие
|
|
||||||
права, скрытое окно.
|
|
||||||
|
|
||||||
Регистрация (из консоли администратора):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1"'
|
|
||||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
||||||
$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
||||||
Register-ScheduledTask -TaskName 'CPU Temp Agent' -Action $action -Trigger $trigger -Settings $set -RunLevel Highest -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
`-RunLevel Highest` обязателен. Нулевой `ExecutionTimeLimit` нужен, иначе
|
|
||||||
Планировщик убьёт агента через трое суток.
|
|
||||||
|
|
||||||
Удаление:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Unregister-ScheduledTask -TaskName 'CPU Temp Agent' -Confirm:$false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Перед заливкой прошивки — остановить агента
|
|
||||||
|
|
||||||
Плата прошивается через тот же COM-порт, по которому идут данные. Пока агент
|
|
||||||
держит порт, заливка падает с `PermissionError`.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Stop-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
```
|
|
||||||
|
|
||||||
После заливки задание можно запустить снова: `Start-ScheduledTask -TaskName 'CPU Temp Agent'`.
|
|
||||||
|
|
||||||
## Замечания по коду
|
|
||||||
|
|
||||||
- Файл `cpu_temp_agent.ps1` сохранён в **UTF-8 с BOM**. Без BOM PowerShell 5.1
|
|
||||||
читает его как ANSI, кириллица в комментариях превращается в мусор, и скрипт
|
|
||||||
перестаёт разбираться с невнятными ошибками про скобки.
|
|
||||||
- Номер COM-порта не задан жёстко: плата ищется по `VID_303A&PID_1001`, потому
|
|
||||||
что номер меняется при перетыкании в другой разъём.
|
|
||||||
- Числа форматируются через `InvariantCulture`: в русской локали иначе вышло бы
|
|
||||||
`T=67,0`, и `atof` на плате прочитал бы `67`, потеряв дробную часть.
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Читает температуру и загрузку CPU и раз в секунду отдаёт их плате ESP32-C3
|
|
||||||
по USB-COM в виде строки "T=67.0;L=34".
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
Требует прав администратора: без них драйвер LibreHardwareMonitor не
|
|
||||||
загружается, и температуры приходят пустыми (загрузка при этом читается —
|
|
||||||
отсюда ложное ощущение, что всё работает).
|
|
||||||
#>
|
|
||||||
#Requires -RunAsAdministrator
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
# Печатать каждую отправленную строку в консоль (для отладки).
|
|
||||||
[switch]$Echo
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$LhmPath = 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
|
|
||||||
|
|
||||||
if (-not (Test-Path $LhmPath)) {
|
|
||||||
throw "Не найден $LhmPath. Скачайте LibreHardwareMonitor (архив LibreHardwareMonitor.zip, не .NET.10) и распакуйте в D:\Soft\LibreHardwareMonitor"
|
|
||||||
}
|
|
||||||
|
|
||||||
Add-Type -Path $LhmPath
|
|
||||||
|
|
||||||
$computer = New-Object LibreHardwareMonitor.Hardware.Computer
|
|
||||||
$computer.IsCpuEnabled = $true
|
|
||||||
$computer.IsMemoryEnabled = $true
|
|
||||||
$computer.Open()
|
|
||||||
|
|
||||||
function Get-CpuStats {
|
|
||||||
foreach ($hw in $computer.Hardware) { $hw.Update() }
|
|
||||||
|
|
||||||
$temp = $null
|
|
||||||
$load = $null
|
|
||||||
$mem = $null
|
|
||||||
foreach ($hw in $computer.Hardware) {
|
|
||||||
foreach ($s in $hw.Sensors) {
|
|
||||||
if ($s.SensorType -eq 'Temperature' -and $s.Name -eq 'CPU Package') { $temp = $s.Value }
|
|
||||||
if ($s.SensorType -eq 'Load' -and $s.Name -eq 'CPU Total') { $load = $s.Value }
|
|
||||||
# Имя железа проверяем обязательно: у "Virtual Memory" датчик зовётся
|
|
||||||
# точно так же, но считает физическую память вместе с файлом подкачки
|
|
||||||
# и показывает заниженную цифру.
|
|
||||||
if ($hw.Name -eq 'Total Memory' -and $s.SensorType -eq 'Load' -and $s.Name -eq 'Memory') { $mem = $s.Value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Возвращаем обе величины даже неполными: вызывающий код должен уметь
|
|
||||||
# сказать, какая именно отсутствует, а не валить всё в одно "нет данных".
|
|
||||||
return @{
|
|
||||||
Temp = $temp
|
|
||||||
Load = $load
|
|
||||||
Mem = $mem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Температура читается только через драйвер ядра. У LibreHardwareMonitor 0.9.5+
|
|
||||||
# это PawnIO, который ставится отдельным установщиком и в архив с программой не
|
|
||||||
# входит. Без него загрузка читается (она из счётчиков Windows), а температура —
|
|
||||||
# нет. Права администратора этого не заменяют.
|
|
||||||
function Assert-SensorsAvailable {
|
|
||||||
$probe = Get-CpuStats
|
|
||||||
if ($null -eq $probe.Temp) {
|
|
||||||
$hasPawnIo = (Get-CimInstance Win32_SystemDriver | Where-Object { $_.Name -match 'PawnIO' }) -ne $null
|
|
||||||
Write-Host 'Температура CPU недоступна.' -ForegroundColor Red
|
|
||||||
if (-not $hasPawnIo) {
|
|
||||||
Write-Host 'Причина: не установлен драйвер PawnIO (https://pawnio.eu).' -ForegroundColor Red
|
|
||||||
} else {
|
|
||||||
Write-Host 'Драйвер PawnIO найден, но значение пустое — запущено без прав администратора?' -ForegroundColor Red
|
|
||||||
}
|
|
||||||
throw 'Нет источника температуры'
|
|
||||||
}
|
|
||||||
if ($null -eq $probe.Load) { throw 'Недоступна загрузка CPU (сенсор "CPU Total" не найден)' }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Форматирование только через InvariantCulture: в русской локали вышло бы
|
|
||||||
# "T=67,0", и atof на плате прочитал бы 67, молча потеряв дробную часть.
|
|
||||||
function Format-StatsLine($stats) {
|
|
||||||
$line = [string]::Format([cultureinfo]::InvariantCulture, 'T={0:F1};L={1}', [double]$stats.Temp, [int][math]::Round([double]$stats.Load))
|
|
||||||
# Память необязательна: если датчика нет, строка просто идёт без ключа M,
|
|
||||||
# и прошивка этого не заметит.
|
|
||||||
if ($null -ne $stats.Mem) {
|
|
||||||
$line += [string]::Format([cultureinfo]::InvariantCulture, ';M={0}', [int][math]::Round([double]$stats.Mem))
|
|
||||||
}
|
|
||||||
return $line
|
|
||||||
}
|
|
||||||
|
|
||||||
# Номер порта не хардкодим: он меняется при перетыкании в другой разъём.
|
|
||||||
function Find-BoardPort {
|
|
||||||
Get-CimInstance Win32_PnPEntity |
|
|
||||||
Where-Object { $_.DeviceID -like 'USB\VID_303A&PID_1001*' -and $_.Name -match '\(COM\d+\)' } |
|
|
||||||
ForEach-Object { if ($_.Name -match '\((COM\d+)\)') { $Matches[1] } } |
|
|
||||||
Select-Object -First 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function Open-BoardPort {
|
|
||||||
$name = Find-BoardPort
|
|
||||||
if (-not $name) { return $null }
|
|
||||||
|
|
||||||
$sp = New-Object System.IO.Ports.SerialPort $name, 115200, 'None', 8, 'One'
|
|
||||||
# RTS явно выключен: одновременное дёрганье DTR и RTS переводит плату в загрузчик.
|
|
||||||
$sp.DtrEnable = $true
|
|
||||||
$sp.RtsEnable = $false
|
|
||||||
$sp.WriteTimeout = 1000
|
|
||||||
$sp.NewLine = "`n"
|
|
||||||
$sp.Open()
|
|
||||||
Write-Host "connected: $name"
|
|
||||||
return $sp
|
|
||||||
}
|
|
||||||
|
|
||||||
# Падаем сразу и с внятной причиной, а не крутим бесконечный цикл молча.
|
|
||||||
Assert-SensorsAvailable
|
|
||||||
|
|
||||||
$port = $null
|
|
||||||
while ($true) {
|
|
||||||
try {
|
|
||||||
if ($null -eq $port -or -not $port.IsOpen) {
|
|
||||||
if ($null -ne $port) { $port.Dispose(); $port = $null }
|
|
||||||
$port = Open-BoardPort
|
|
||||||
if ($null -eq $port) { Start-Sleep -Seconds 2; continue }
|
|
||||||
}
|
|
||||||
|
|
||||||
$stats = Get-CpuStats
|
|
||||||
if ($null -ne $stats.Temp -and $null -ne $stats.Load) {
|
|
||||||
$line = Format-StatsLine $stats
|
|
||||||
$port.WriteLine($line)
|
|
||||||
if ($Echo) { Write-Host $line }
|
|
||||||
}
|
|
||||||
elseif ($Echo) {
|
|
||||||
Write-Host "пропуск: Temp=$($stats.Temp) Load=$($stats.Load)"
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# Порт исчез: перезагрузка платы, перезаливка прошивки, выдёргивание кабеля.
|
|
||||||
Write-Host "port lost: $($_.Exception.Message)"
|
|
||||||
if ($null -ne $port) { try { $port.Dispose() } catch {} ; $port = $null }
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// Все настройки проекта в одном месте: пины, пороги, шкалы, периоды.
|
|
||||||
// Менять поведение прошивки здесь, а не по коду модулей.
|
|
||||||
|
|
||||||
#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;
|
|
||||||
|
|
||||||
// --- Кнопка переключения экранов -------------------------------------------
|
|
||||||
|
|
||||||
// Кнопка BOOT. Это пин начальной загрузки: если держать его нажатым в момент
|
|
||||||
// сброса, плата уйдёт в режим прошивки - штатное поведение кнопки, а не наша
|
|
||||||
// беда. В работе читается как обычный вход с подтяжкой.
|
|
||||||
static const uint8_t PIN_BUTTON = 9;
|
|
||||||
|
|
||||||
// Порог защиты от дребезга контактов.
|
|
||||||
static const uint32_t BUTTON_DEBOUNCE_MS = 50;
|
|
||||||
|
|
||||||
// --- Сглаживание показаний -------------------------------------------------
|
|
||||||
|
|
||||||
// Скользящее среднее температуры: цифра не должна мельтешить на +-1 градус,
|
|
||||||
// пока ничего не происходит. Единица - один принятый замер, а агент шлёт раз
|
|
||||||
// в секунду, поэтому 5 означает "среднее за последние 5 секунд".
|
|
||||||
// Загрузка сознательно не сглаживается: её скачки настоящие, ради них на неё
|
|
||||||
// и смотрят.
|
|
||||||
static const uint8_t TEMP_AVG_SAMPLES = 5;
|
|
||||||
|
|
||||||
// --- График ----------------------------------------------------------------
|
|
||||||
|
|
||||||
// Шкала фиксированная: при автомасштабе шум +-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; // период перерисовки экрана
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
# CPU Temp — настольный индикатор температуры процессора
|
|
||||||
|
|
||||||
Дата: 2026-09-14
|
|
||||||
Статус: дизайн согласован, реализация не начата
|
|
||||||
|
|
||||||
## Задача
|
|
||||||
|
|
||||||
Показывать температуру CPU домашнего ПК на отдельной плате ESP32-C3 с
|
|
||||||
OLED-экраном 72×40, стоящей на столе. Кроме текущего значения — история за
|
|
||||||
последнюю минуту, чтобы было видно, греется машина под нагрузкой или нет.
|
|
||||||
|
|
||||||
## Оборудование
|
|
||||||
|
|
||||||
| Параметр | Значение |
|
|
||||||
|---|---|
|
|
||||||
| Плата | ESP32-C3 (QFN32), ревизия v0.4, встроенный OLED 0.42″ |
|
|
||||||
| Флеш | 4 МБ (XMC), кристалл 40 МГц |
|
|
||||||
| Подключение | нативный USB Espressif, `VID_303A&PID_1001`, на текущем ПК — COM4 |
|
|
||||||
| FQBN | `esp32:esp32:esp32c3` (ядро esp32 3.3.8) |
|
|
||||||
| ПК | Windows 10 Pro, Intel Core i5-11400H |
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM4 (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40
|
|
||||||
(драйвер, MSR) раз в секунду текстовая строка U8g2
|
|
||||||
```
|
|
||||||
|
|
||||||
Плата ничего не измеряет сама и не хранит настроек: принимает строки и рисует.
|
|
||||||
Вся работа с железом ПК — на стороне агента.
|
|
||||||
|
|
||||||
### Почему так
|
|
||||||
|
|
||||||
- **Данные с ПК, а не встроенный датчик ESP32.** Встроенный сенсор меряет
|
|
||||||
собственный кристалл платы — величина малоинформативная.
|
|
||||||
- **Температуру CPU читает LibreHardwareMonitor, а не наш код.** Прямое чтение
|
|
||||||
требует MSR-регистров через подписанный драйвер ядра и отдельных формул под
|
|
||||||
каждое поколение CPU. LHM даёт готовую `LibreHardwareMonitorLib.dll`.
|
|
||||||
- **Транспорт — COM-порт, не Wi-Fi.** Плата и так питается по USB от этого же
|
|
||||||
ПК: провод уже есть. Нет настройки сети, паролей, IP и mDNS.
|
|
||||||
- **Агент на PowerShell, не на C#.** Загрузка .NET-сборки доступна из
|
|
||||||
PowerShell напрямую; компилятор и IDE не нужны.
|
|
||||||
|
|
||||||
## Протокол
|
|
||||||
|
|
||||||
Одна строка в секунду, `\n` в конце:
|
|
||||||
|
|
||||||
```
|
|
||||||
T=67.0;L=34
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Смысл | Формат |
|
|
||||||
|---|---|---|
|
|
||||||
| `T` | CPU Package, °C | дробное |
|
|
||||||
| `L` | общая загрузка CPU, % | целое |
|
|
||||||
|
|
||||||
Формат «ключ=значение» выбран ради расширяемости: добавление `G=` (GPU) или
|
|
||||||
`M=` (RAM) не ломает старую прошивку — неизвестные ключи прошивка молча
|
|
||||||
игнорирует. Скорость 115200.
|
|
||||||
|
|
||||||
**Отсутствие данных.** Если валидная строка не приходила дольше 5 секунд,
|
|
||||||
прошивка показывает `NO DATA` и перестаёт дописывать историю (чтобы пауза не
|
|
||||||
рисовалась как провал температуры).
|
|
||||||
|
|
||||||
## Экран
|
|
||||||
|
|
||||||
Разрешение 72×40, монохром, драйвер SSD1306 с ненулевым смещением по обеим осям.
|
|
||||||
Используется `U8G2_SSD1306_72X40_ER_F_HW_I2C` из U8g2 2.36.19 (уже установлена).
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────┐
|
|
||||||
│ 67° 34% │ крупные цифры (~24 px) + мелкая загрузка
|
|
||||||
│ │
|
|
||||||
│ ▁▂▃▃▄▆█▆▅▄▃▂▂▁▁▂▃▄▅▆ │ спарклайн: 72 точки = 72 секунды
|
|
||||||
└────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Шкала графика фиксированная: 30–90 °C.** Автомасштаб отвергнут: в простое
|
|
||||||
шум ±1 °C рисовался бы горами и создавал ложную тревогу. Значения вне диапазона
|
|
||||||
обрезаются по краю.
|
|
||||||
|
|
||||||
История — кольцевой буфер на 72 значения (по пикселю на точку, одно значение в
|
|
||||||
секунду).
|
|
||||||
|
|
||||||
## Состав проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
CPU_temp/
|
|
||||||
CPU_temp.ino setup/loop, тайм-аут «нет данных»
|
|
||||||
link.h / link.cpp чтение строки из Serial, разбор в структуру CpuStats
|
|
||||||
ui.h / ui.cpp отрисовка U8g2, кольцевой буфер истории
|
|
||||||
docs/ этот документ
|
|
||||||
agent/
|
|
||||||
cpu_temp_agent.ps1 агент для ПК
|
|
||||||
README.md установка и автозапуск
|
|
||||||
```
|
|
||||||
|
|
||||||
Границы модулей: `link` ничего не знает об экране и отдаёт только структуру с
|
|
||||||
данными и признаком свежести; `ui` не знает, откуда данные взялись, и принимает
|
|
||||||
ту же структуру. Оба тестируются и меняются независимо.
|
|
||||||
|
|
||||||
## Агент
|
|
||||||
|
|
||||||
Файл `agent/cpu_temp_agent.ps1`:
|
|
||||||
|
|
||||||
1. загружает `LibreHardwareMonitorLib.dll` из `D:\Soft\LibreHardwareMonitor`;
|
|
||||||
2. включает мониторинг CPU, ищет сенсоры `Temperature/CPU Package` и
|
|
||||||
`Load/CPU Total`;
|
|
||||||
3. находит COM-порт по `VID_303A&PID_1001` (не по фиксированному номеру: номер
|
|
||||||
меняется при перетыкании в другой разъём);
|
|
||||||
4. раз в секунду пишет строку протокола;
|
|
||||||
5. при исчезновении порта (перезагрузка платы, перезаливка прошивки) —
|
|
||||||
переподключается в цикле, не падая.
|
|
||||||
|
|
||||||
**Требует прав администратора**: без них драйвер LHM не загружается и
|
|
||||||
температура приходит пустой.
|
|
||||||
|
|
||||||
**Автозапуск** — задание в Планировщике заданий: триггер «при входе в систему»,
|
|
||||||
галка «Выполнить с наивысшими правами», окно скрыто.
|
|
||||||
|
|
||||||
## Установка LibreHardwareMonitor
|
|
||||||
|
|
||||||
Каталог `D:\Soft\LibreHardwareMonitor` — вне репозитория Arduino. Само
|
|
||||||
приложение не запускается, нужны только `LibreHardwareMonitorLib.dll` и её
|
|
||||||
драйвер.
|
|
||||||
|
|
||||||
## Порядок работ и риски
|
|
||||||
|
|
||||||
1. **Определить пины I2C дисплея эмпирически.** Шелкография подписывает
|
|
||||||
`GPIO8/GPIO9` (дефолтные Wire-пины ESP32-C3), но встроенный OLED у плат этого
|
|
||||||
семейства обычно на `GPIO5/GPIO6`. Первый шаг — залить I2C-сканер и найти
|
|
||||||
адрес экрана перебором обеих пар. Пины не угадывать.
|
|
||||||
2. Прошивка: вывод фиксированной заглушки на экран — убедиться в геометрии и
|
|
||||||
смещении 72×40.
|
|
||||||
3. Приём и разбор строк, `NO DATA` по тайм-ауту.
|
|
||||||
4. График.
|
|
||||||
5. Агент, затем автозапуск.
|
|
||||||
|
|
||||||
Прочие риски:
|
|
||||||
|
|
||||||
- **COM4 — тот же порт, через который прошивается плата.** Перед каждой
|
|
||||||
заливкой агента надо останавливать, иначе `PermissionError`. То же ограничение
|
|
||||||
уже описано в `CLAUDE.md` для монитора порта Arduino IDE.
|
|
||||||
- **`USB CDC On Boot` должен быть включён** в настройках платы, иначе `Serial`
|
|
||||||
уйдёт в аппаратный UART, а не в USB.
|
|
||||||
|
|
||||||
## Решения, принятые явно
|
|
||||||
|
|
||||||
- Git: проект пока **не** добавляется в белый список `.gitignore`. Заливаем,
|
|
||||||
когда появятся первые рабочие результаты, чтобы не засорять публичную историю.
|
|
||||||
- Автозапуск агента: через Планировщик заданий (вариант B), а не ручной ярлык.
|
|
||||||
- Каталог LHM: `D:\Soft\LibreHardwareMonitor`.
|
|
||||||
|
|
||||||
## Вне рамок (YAGNI)
|
|
||||||
|
|
||||||
- GPU, RAM, частоты, вентиляторы — протокол расширяем, но сейчас не делаем.
|
|
||||||
- Переключение экранов кнопкой BOOT.
|
|
||||||
- Wi-Fi, веб-панель, сохранение настроек в LittleFS — прошивке нечего хранить.
|
|
||||||
|
|
||||||
## Подтверждённые пины (Задача 1, 2026-09-14)
|
|
||||||
|
|
||||||
Сканер `tools/i2c_scan` перебрал пары (5,6), (6,5), (8,9), (9,8). Устройство
|
|
||||||
отвечает только на одной:
|
|
||||||
|
|
||||||
```
|
|
||||||
SDA=5 SCL=6: device 0x3C
|
|
||||||
```
|
|
||||||
|
|
||||||
Итого: **SDA = GPIO5, SCL = GPIO6, адрес 0x3C**. Подпись `GPIO8/GPIO9` на
|
|
||||||
шелкографии относится к дефолтным Wire-пинам ESP32-C3, а не к разводке
|
|
||||||
встроенного экрана.
|
|
||||||
@@ -1,755 +0,0 @@
|
|||||||
# CPU Temp Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Плата ESP32-C3 с OLED 72×40 показывает температуру CPU домашнего ПК крупными цифрами и график за последнюю минуту, получая данные по USB-COM от агента на ПК.
|
|
||||||
|
|
||||||
**Architecture:** PowerShell-агент на ПК читает сенсоры через `LibreHardwareMonitorLib.dll` и раз в секунду пишет в COM-порт строку `T=67.0;L=34`. Прошивка разбирает строку и рисует экран через U8g2. Плата ничего не измеряет и ничего не хранит.
|
|
||||||
|
|
||||||
**Tech Stack:** Arduino IDE + arduino-cli (внутри IDE), ядро `esp32:esp32 3.3.8`, библиотека U8g2 2.36.19, Windows PowerShell 5.1, LibreHardwareMonitor (сборка net472).
|
|
||||||
|
|
||||||
Спека: [2026-09-14-cpu-temp-design.md](2026-09-14-cpu-temp-design.md)
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- **FQBN со всеми опциями:** `esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default`.
|
|
||||||
`CDCOnBoot=cdc` обязателен — по умолчанию в ядре стоит `Disabled`, и тогда `Serial` уйдёт в аппаратный UART, а не в USB, и данные до платы не дойдут.
|
|
||||||
- **`FlashSize=4M` соответствует физическому чипу** (4 МБ XMC, подтверждено сводкой платы). Не менять.
|
|
||||||
- **arduino-cli только из состава IDE:** `C:/Program Files/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe`. Проверка сборки — только через `arduino-cli compile`, не через `g++ -fsyntax-only`.
|
|
||||||
- **Порт платы:** `COM4` на текущем ПК (`USB\VID_303A&PID_1001`). В коде агента номер не хардкодить — искать по VID/PID.
|
|
||||||
- **Скорость Serial: 115200** и в прошивке, и в агенте.
|
|
||||||
- **Один и тот же COM4 используется и для заливки, и для данных.** Перед каждой заливкой останавливать агента и закрывать монитор порта, иначе `PermissionError`.
|
|
||||||
- **Числа в протоколе — только с точкой как разделителем** (`67.0`). Локаль системы русская, поэтому агент обязан форматировать через `InvariantCulture`, иначе уйдёт `67,0` и `atof` на плате прочитает `67`.
|
|
||||||
- **Git: коммитов в этих задачах нет.** Проект намеренно вне белого списка `.gitignore` до первых рабочих результатов; добавление в репозиторий — отдельная Задача 8 по явному слову пользователя.
|
|
||||||
- Каталог LibreHardwareMonitor: `D:\Soft\LibreHardwareMonitor` (вне репозитория Arduino).
|
|
||||||
|
|
||||||
## Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
CPU_temp/
|
|
||||||
CPU_temp.ino setup/loop, тайм-аут свежести данных
|
|
||||||
link.h / link.cpp неблокирующее чтение строки из Serial и разбор в CpuStats
|
|
||||||
ui.h / ui.cpp U8g2, кольцевой буфер истории, вся отрисовка
|
|
||||||
tools/i2c_scan/
|
|
||||||
i2c_scan.ino одноразовый скетч для поиска пинов дисплея
|
|
||||||
agent/
|
|
||||||
cpu_temp_agent.ps1 агент для ПК
|
|
||||||
README.md установка и автозапуск
|
|
||||||
docs/ спека и этот план
|
|
||||||
```
|
|
||||||
|
|
||||||
Границы: `link` не знает об экране, `ui` не знает об источнике данных. Обмен — через структуру `CpuStats`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Определить пины I2C дисплея
|
|
||||||
|
|
||||||
Шелкография подписывает `GPIO8/GPIO9` — это дефолтные Wire-пины ESP32-C3, но встроенный OLED у плат этого семейства обычно разведён на `GPIO5/GPIO6`. Пины не угадываем, а находим перебором.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `tools/i2c_scan/i2c_scan.ino`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: ничего
|
|
||||||
- Produces: подтверждённая пара `(SDA, SCL)` и адрес дисплея — записываются в `ui.cpp` в Задаче 2
|
|
||||||
|
|
||||||
- [ ] **Step 1: Написать скетч-сканер**
|
|
||||||
|
|
||||||
Сканирование повторяется в `loop` каждые 5 секунд: при нативном USB порт переоткрывается после заливки, и однократный вывод из `setup` можно не успеть поймать.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include <Wire.h>
|
|
||||||
|
|
||||||
struct PinPair { uint8_t sda; uint8_t scl; };
|
|
||||||
|
|
||||||
static const PinPair CANDIDATES[] = { {5, 6}, {6, 5}, {8, 9}, {9, 8} };
|
|
||||||
|
|
||||||
static void scanPair(uint8_t sda, uint8_t scl) {
|
|
||||||
Wire.end();
|
|
||||||
if (!Wire.begin(sda, scl)) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: init failed\n", sda, scl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t found = 0;
|
|
||||||
for (uint8_t addr = 1; addr < 127; addr++) {
|
|
||||||
Wire.beginTransmission(addr);
|
|
||||||
if (Wire.endTransmission() == 0) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: device 0x%02X\n", sda, scl, addr);
|
|
||||||
found++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (found == 0) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: nothing\n", sda, scl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
Serial.println("--- I2C scan ---");
|
|
||||||
for (const PinPair &p : CANDIDATES) {
|
|
||||||
scanPair(p.sda, p.scl);
|
|
||||||
}
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Собрать**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
"C:/Program Files/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" compile -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default "D:/Soft/Arduino/CPU_temp/tools/i2c_scan"
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается: `Sketch uses ... bytes`, ошибок нет.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Залить**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
"C:/Program Files/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" upload -p COM4 -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default "D:/Soft/Arduino/CPU_temp/tools/i2c_scan"
|
|
||||||
```
|
|
||||||
|
|
||||||
Если падает с `PermissionError` — порт держит монитор Arduino IDE (процесс `serial-monitor`) или прошлый сеанс чтения порта; закрыть и повторить.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Прочитать вывод**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$sp = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One'
|
|
||||||
$sp.DtrEnable = $true; $sp.ReadTimeout = 2000
|
|
||||||
$sp.Open()
|
|
||||||
$deadline = (Get-Date).AddSeconds(15)
|
|
||||||
while ((Get-Date) -lt $deadline) { try { $sp.ReadLine() } catch {} }
|
|
||||||
$sp.Close()
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается строка вида `SDA=5 SCL=6: device 0x3C` ровно для одной пары (0x3C — типовой адрес SSD1306; 0x3D тоже допустим).
|
|
||||||
|
|
||||||
- [ ] **Step 5: Зафиксировать результат**
|
|
||||||
|
|
||||||
Дописать найденную пару и адрес в конец файла `docs/2026-09-14-cpu-temp-design.md` разделом «Подтверждённые пины». Если ни одна пара не дала устройства — остановиться и доложить пользователю, дальше идти нельзя.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Экран с заглушкой
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `ui.h`, `ui.cpp`, `CPU_temp.ino`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: пара пинов из Задачи 1
|
|
||||||
- Produces:
|
|
||||||
- `void uiBegin()`
|
|
||||||
- `void uiRender(bool fresh, float tempC, int loadPct)`
|
|
||||||
- (`uiPushSample` добавляется в Задаче 3)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Написать `ui.h`**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
// Инициализирует дисплей и показывает заставку ожидания.
|
|
||||||
void uiBegin();
|
|
||||||
|
|
||||||
// Перерисовывает экран целиком.
|
|
||||||
// fresh == false означает, что данные устарели: вместо цифр выводится NO DATA.
|
|
||||||
void uiRender(bool fresh, float tempC, int loadPct);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Написать `ui.cpp`**
|
|
||||||
|
|
||||||
Пины `PIN_SDA`/`PIN_SCL` подставить из Задачи 1. Конструктор `U8G2_SSD1306_72X40_ER_F_HW_I2C` принимает параметры в порядке `(rotation, reset, clock, data)` — то есть SCL идёт раньше SDA, это частый источник ошибок.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "ui.h"
|
|
||||||
|
|
||||||
#include <U8g2lib.h>
|
|
||||||
|
|
||||||
static const uint8_t PIN_SDA = 5; // подтверждено Задачей 1
|
|
||||||
static const uint8_t PIN_SCL = 6; // подтверждено Задачей 1
|
|
||||||
|
|
||||||
static U8G2_SSD1306_72X40_ER_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE, PIN_SCL, PIN_SDA);
|
|
||||||
|
|
||||||
void uiBegin() {
|
|
||||||
display.begin();
|
|
||||||
display.clearBuffer();
|
|
||||||
display.setFont(u8g2_font_6x10_tf);
|
|
||||||
display.drawStr(0, 24, "WAIT");
|
|
||||||
display.sendBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiRender(bool fresh, float tempC, int loadPct) {
|
|
||||||
display.clearBuffer();
|
|
||||||
|
|
||||||
if (!fresh) {
|
|
||||||
display.setFont(u8g2_font_6x10_tf);
|
|
||||||
display.drawStr(0, 14, "NO");
|
|
||||||
display.drawStr(0, 26, "DATA");
|
|
||||||
display.sendBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char buf[8];
|
|
||||||
|
|
||||||
// Крупные цифры температуры.
|
|
||||||
snprintf(buf, sizeof(buf), "%d", (int)(tempC + 0.5f));
|
|
||||||
display.setFont(u8g2_font_logisoso24_tn);
|
|
||||||
display.drawStr(0, 24, buf);
|
|
||||||
const int numWidth = display.getStrWidth(buf);
|
|
||||||
|
|
||||||
// Градус и загрузка — мелким шрифтом справа.
|
|
||||||
display.setFont(u8g2_font_5x7_tf);
|
|
||||||
display.drawStr(numWidth + 2, 10, "\xb0");
|
|
||||||
snprintf(buf, sizeof(buf), "%d%%", loadPct);
|
|
||||||
display.drawStr(numWidth + 2, 24, buf);
|
|
||||||
|
|
||||||
display.sendBuffer();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Написать `CPU_temp.ino` с фиксированной заглушкой**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "ui.h"
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
uiBegin();
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
uiRender(true, 67.0f, 34);
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Собрать и залить**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
"C:/Program Files/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" compile -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default "D:/Soft/Arduino/CPU_temp"
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается успешная сборка, затем `upload -p COM4` с тем же FQBN.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Проверить глазами и поправить вёрстку**
|
|
||||||
|
|
||||||
На экране должно быть `67°` крупно и `34%` мелко, ничего не обрезано по правому краю и не налезает друг на друга. Ширина всего 72 px, поэтому при обрезании — уменьшить крупный шрифт до `u8g2_font_logisoso22_tn` или перенести `34%` на строку графика. Спросить у пользователя, как выглядит, и не считать задачу выполненной без его подтверждения.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: График истории
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `ui.h`, `ui.cpp`
|
|
||||||
- Modify: `CPU_temp.ino`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `uiRender` из Задачи 2
|
|
||||||
- Produces: `void uiPushSample(float tempC)` — вызывается ровно раз в секунду
|
|
||||||
|
|
||||||
- [ ] **Step 1: Добавить объявление в `ui.h`**
|
|
||||||
|
|
||||||
Дописать под `uiBegin()`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Добавляет значение в кольцевой буфер истории. Вызывать раз в секунду
|
|
||||||
// и только при свежих данных, чтобы пауза связи не рисовалась как провал.
|
|
||||||
void uiPushSample(float tempC);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Реализовать буфер и отрисовку графика в `ui.cpp`**
|
|
||||||
|
|
||||||
Шкала фиксированная 30–90 °C — автомасштаб отвергнут в спеке: в простое шум ±1 °C рисовался бы горами.
|
|
||||||
|
|
||||||
Добавить после объявления `display`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
static const uint8_t GRAPH_W = 72; // по пикселю на отсчёт
|
|
||||||
static const uint8_t GRAPH_TOP = 28;
|
|
||||||
static const uint8_t GRAPH_H = 12;
|
|
||||||
static const float SCALE_MIN = 30.0f;
|
|
||||||
static const float SCALE_MAX = 90.0f;
|
|
||||||
|
|
||||||
static uint8_t history[GRAPH_W]; // 0 = нет данных, иначе высота столбика 1..GRAPH_H
|
|
||||||
static uint8_t historyCount = 0;
|
|
||||||
|
|
||||||
void uiPushSample(float tempC) {
|
|
||||||
if (tempC < SCALE_MIN) tempC = SCALE_MIN;
|
|
||||||
if (tempC > SCALE_MAX) tempC = SCALE_MAX;
|
|
||||||
const float norm = (tempC - SCALE_MIN) / (SCALE_MAX - SCALE_MIN);
|
|
||||||
uint8_t h = (uint8_t)(norm * GRAPH_H + 0.5f);
|
|
||||||
if (h < 1) h = 1;
|
|
||||||
|
|
||||||
memmove(history, history + 1, GRAPH_W - 1);
|
|
||||||
history[GRAPH_W - 1] = h;
|
|
||||||
if (historyCount < GRAPH_W) historyCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawGraph() {
|
|
||||||
for (uint8_t x = 0; x < GRAPH_W; x++) {
|
|
||||||
const uint8_t h = history[x];
|
|
||||||
if (h == 0) continue;
|
|
||||||
display.drawVLine(x, GRAPH_TOP + GRAPH_H - h, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Вызвать `drawGraph()` в `uiRender` перед `display.sendBuffer()` в ветке свежих данных. Добавить `#include <string.h>` ради `memmove`.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Подать в заглушку меняющееся значение**
|
|
||||||
|
|
||||||
Заменить `loop` в `CPU_temp.ino`, чтобы график было видно в движении:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
void loop() {
|
|
||||||
static float t = 40.0f;
|
|
||||||
static float step = 2.0f;
|
|
||||||
t += step;
|
|
||||||
if (t > 85.0f || t < 35.0f) step = -step;
|
|
||||||
|
|
||||||
uiPushSample(t);
|
|
||||||
uiRender(true, t, 34);
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Собрать, залить, посмотреть**
|
|
||||||
|
|
||||||
Ожидается: пила, ползущая справа налево, столбики не вылезают за нижний край и не перекрывают цифры. Подтвердить у пользователя.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Приём данных по COM
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `link.h`, `link.cpp`
|
|
||||||
- Modify: `CPU_temp.ino`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `uiPushSample`, `uiRender`
|
|
||||||
- Produces:
|
|
||||||
- `struct CpuStats { float tempC; int loadPct; }`
|
|
||||||
- `void linkBegin()`
|
|
||||||
- `bool linkPoll(CpuStats &out)` — `true` ровно в тот момент, когда разобрана целая валидная строка
|
|
||||||
|
|
||||||
- [ ] **Step 1: Написать `link.h`**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
struct CpuStats {
|
|
||||||
float tempC;
|
|
||||||
int loadPct;
|
|
||||||
};
|
|
||||||
|
|
||||||
void linkBegin();
|
|
||||||
|
|
||||||
// Неблокирующее чтение. Возвращает true, когда собрана и разобрана
|
|
||||||
// целая строка вида "T=67.0;L=34". Неизвестные ключи игнорируются.
|
|
||||||
bool linkPoll(CpuStats &out);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Написать `link.cpp`**
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "link.h"
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
static const size_t BUF_SIZE = 64;
|
|
||||||
static char buf[BUF_SIZE];
|
|
||||||
static size_t len = 0;
|
|
||||||
|
|
||||||
void linkBegin() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool parseLine(char *line, CpuStats &out) {
|
|
||||||
bool haveTemp = false;
|
|
||||||
bool haveLoad = false;
|
|
||||||
CpuStats parsed = {0.0f, 0};
|
|
||||||
|
|
||||||
char *save = nullptr;
|
|
||||||
for (char *tok = strtok_r(line, ";", &save); tok; tok = strtok_r(nullptr, ";", &save)) {
|
|
||||||
char *eq = strchr(tok, '=');
|
|
||||||
if (!eq) continue;
|
|
||||||
*eq = '\0';
|
|
||||||
const char *key = tok;
|
|
||||||
const char *val = eq + 1;
|
|
||||||
|
|
||||||
if (strcmp(key, "T") == 0) {
|
|
||||||
parsed.tempC = atof(val);
|
|
||||||
haveTemp = true;
|
|
||||||
} else if (strcmp(key, "L") == 0) {
|
|
||||||
parsed.loadPct = atoi(val);
|
|
||||||
haveLoad = true;
|
|
||||||
}
|
|
||||||
// прочие ключи игнорируются намеренно: протокол расширяемый
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveTemp || !haveLoad) return false;
|
|
||||||
out = parsed;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool linkPoll(CpuStats &out) {
|
|
||||||
bool got = false;
|
|
||||||
while (Serial.available()) {
|
|
||||||
const char c = (char)Serial.read();
|
|
||||||
if (c == '\r') continue;
|
|
||||||
if (c == '\n') {
|
|
||||||
buf[len] = '\0';
|
|
||||||
if (len > 0 && parseLine(buf, out)) got = true;
|
|
||||||
len = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (len < BUF_SIZE - 1) {
|
|
||||||
buf[len++] = c;
|
|
||||||
} else {
|
|
||||||
len = 0; // строка длиннее буфера — мусор, начинаем заново
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return got;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Собрать всё вместе в `CPU_temp.ino`**
|
|
||||||
|
|
||||||
Отсчёт для графика добавляется раз в секунду и только при свежих данных.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "link.h"
|
|
||||||
#include "ui.h"
|
|
||||||
|
|
||||||
static const uint32_t STALE_MS = 5000; // тайм-аут свежести из спеки
|
|
||||||
static const uint32_t SAMPLE_MS = 1000; // шаг истории
|
|
||||||
|
|
||||||
static CpuStats stats = {0.0f, 0};
|
|
||||||
static uint32_t lastData = 0;
|
|
||||||
static uint32_t lastSample = 0;
|
|
||||||
static bool everGot = false;
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
linkBegin();
|
|
||||||
uiBegin();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
CpuStats fresh;
|
|
||||||
if (linkPoll(fresh)) {
|
|
||||||
stats = fresh;
|
|
||||||
lastData = millis();
|
|
||||||
everGot = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool isFresh = everGot && (millis() - lastData < STALE_MS);
|
|
||||||
|
|
||||||
if (isFresh && millis() - lastSample >= SAMPLE_MS) {
|
|
||||||
lastSample = millis();
|
|
||||||
uiPushSample(stats.tempC);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint32_t lastDraw = 0;
|
|
||||||
if (millis() - lastDraw >= 200) {
|
|
||||||
lastDraw = millis();
|
|
||||||
uiRender(isFresh, stats.tempC, stats.loadPct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Собрать и залить**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
"C:/Program Files/Arduino IDE/resources/app/lib/backend/resources/arduino-cli.exe" compile -b esp32:esp32:esp32c3:CDCOnBoot=cdc,FlashSize=4M,PartitionScheme=default "D:/Soft/Arduino/CPU_temp"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Проверить приём вручную**
|
|
||||||
|
|
||||||
Сразу после заливки на экране должно быть `NO DATA`. Затем отправить строки руками:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$sp = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One'
|
|
||||||
$sp.DtrEnable = $true; $sp.RtsEnable = $false
|
|
||||||
$sp.Open()
|
|
||||||
40..80 | ForEach-Object { $sp.WriteLine("T=$_.0;L=50"); Start-Sleep -Milliseconds 500 }
|
|
||||||
$sp.Close()
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается: цифра растёт с 40 до 80, график ползёт вверх, через 5 секунд после закрытия порта экран показывает `NO DATA`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Установить LibreHardwareMonitor и прочитать сенсоры
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `agent/cpu_temp_agent.ps1` (первая версия — только печать в консоль)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: ничего
|
|
||||||
- Produces: функция `Get-CpuStats`, возвращающая хеш-таблицу `@{ Temp = <double>; Load = <int> }`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Скачать и распаковать LibreHardwareMonitor**
|
|
||||||
|
|
||||||
Нужна сборка **net472**: в системе только Windows PowerShell 5.1 (.NET Framework), сборку net8.0 он не загрузит. Взять последний релиз со страницы <https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases>, файл вида `LibreHardwareMonitor-net472.zip`, распаковать в `D:\Soft\LibreHardwareMonitor`.
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Test-Path 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
|
|
||||||
Test-Path 'D:\Soft\LibreHardwareMonitor\HidSharp.dll'
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается `True` для обоих — `HidSharp.dll` обязателен, это зависимость библиотеки, без неё `Add-Type` пройдёт, а открытие компьютера упадёт.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Проверить чтение сенсоров одной строкой**
|
|
||||||
|
|
||||||
Запустить **в консоли, открытой от имени администратора** — без прав драйвер не загрузится и температура будет пустой:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Add-Type -Path 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
|
|
||||||
$c = New-Object LibreHardwareMonitor.Hardware.Computer
|
|
||||||
$c.IsCpuEnabled = $true
|
|
||||||
$c.Open()
|
|
||||||
$c.Hardware[0].Update()
|
|
||||||
$c.Hardware[0].Sensors | Select-Object SensorType, Name, Value | Format-Table
|
|
||||||
$c.Close()
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается таблица, в которой есть строка `Temperature CPU Package <число около 40–60>`. Если значение пустое — консоль запущена не от администратора. Если имя сенсора отличается (у некоторых CPU это `Core (Tctl/Tdie)`), записать фактическое имя и использовать его дальше.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Написать `agent/cpu_temp_agent.ps1` с выводом в консоль**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
#Requires -RunAsAdministrator
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$LhmPath = 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
|
|
||||||
|
|
||||||
Add-Type -Path $LhmPath
|
|
||||||
$computer = New-Object LibreHardwareMonitor.Hardware.Computer
|
|
||||||
$computer.IsCpuEnabled = $true
|
|
||||||
$computer.Open()
|
|
||||||
|
|
||||||
function Get-CpuStats {
|
|
||||||
foreach ($hw in $computer.Hardware) { $hw.Update() }
|
|
||||||
|
|
||||||
$temp = $null
|
|
||||||
$load = $null
|
|
||||||
foreach ($hw in $computer.Hardware) {
|
|
||||||
foreach ($s in $hw.Sensors) {
|
|
||||||
if ($s.SensorType -eq 'Temperature' -and $s.Name -eq 'CPU Package') { $temp = $s.Value }
|
|
||||||
if ($s.SensorType -eq 'Load' -and $s.Name -eq 'CPU Total') { $load = $s.Value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($null -eq $temp -or $null -eq $load) { return $null }
|
|
||||||
return @{ Temp = [double]$temp; Load = [int][math]::Round($load) }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Форматирование только через InvariantCulture: в русской локали получилось бы
|
|
||||||
# "67,0", и atof на плате прочитал бы 67 вместо 67.0.
|
|
||||||
function Format-StatsLine($stats) {
|
|
||||||
return [string]::Format([cultureinfo]::InvariantCulture, 'T={0:F1};L={1}', $stats.Temp, $stats.Load)
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($true) {
|
|
||||||
$s = Get-CpuStats
|
|
||||||
if ($null -eq $s) { Write-Host 'no sensors'; } else { Write-Host (Format-StatsLine $s) }
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Запустить и проверить формат**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell -ExecutionPolicy Bypass -File 'D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1'
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается поток строк вида `T=47.0;L=12`, раз в секунду. **Обязательно убедиться, что разделитель — точка, а не запятая.** Прервать Ctrl+C.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Передача в COM-порт с переподключением
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `agent/cpu_temp_agent.ps1`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `Get-CpuStats`, `Format-StatsLine` из Задачи 5
|
|
||||||
- Produces: рабочая связка ПК → плата
|
|
||||||
|
|
||||||
- [ ] **Step 1: Добавить поиск порта по VID/PID**
|
|
||||||
|
|
||||||
Номер порта меняется при перетыкании в другой разъём, поэтому ищем устройство, а не COM4. Вставить после `Add-Type`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
function Find-BoardPort {
|
|
||||||
Get-CimInstance Win32_PnPEntity |
|
|
||||||
Where-Object { $_.DeviceID -like 'USB\VID_303A&PID_1001*' -and $_.Name -match '\(COM\d+\)' } |
|
|
||||||
ForEach-Object { if ($_.Name -match '\((COM\d+)\)') { $Matches[1] } } |
|
|
||||||
Select-Object -First 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Проверить поиск порта отдельно**
|
|
||||||
|
|
||||||
Скрипт не подключать (`.`-загрузка запустит бесконечный цикл) — выполнить тело функции в консоли напрямую:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-CimInstance Win32_PnPEntity |
|
|
||||||
Where-Object { $_.DeviceID -like 'USB\VID_303A&PID_1001*' -and $_.Name -match '\(COM\d+\)' } |
|
|
||||||
ForEach-Object { if ($_.Name -match '\((COM\d+)\)') { $Matches[1] } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается `COM4`. Если пусто — плата не воткнута либо перечислилась под другим VID/PID.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Заменить цикл на отправку в порт**
|
|
||||||
|
|
||||||
`RtsEnable = $false` задан явно: одновременное дёрганье DTR и RTS на плате Espressif переводит её в загрузчик.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
function Open-BoardPort {
|
|
||||||
$name = Find-BoardPort
|
|
||||||
if (-not $name) { return $null }
|
|
||||||
$sp = New-Object System.IO.Ports.SerialPort $name, 115200, 'None', 8, 'One'
|
|
||||||
$sp.DtrEnable = $true
|
|
||||||
$sp.RtsEnable = $false
|
|
||||||
$sp.WriteTimeout = 1000
|
|
||||||
$sp.NewLine = "`n"
|
|
||||||
$sp.Open()
|
|
||||||
Write-Host "connected: $name"
|
|
||||||
return $sp
|
|
||||||
}
|
|
||||||
|
|
||||||
$port = $null
|
|
||||||
while ($true) {
|
|
||||||
try {
|
|
||||||
if ($null -eq $port -or -not $port.IsOpen) {
|
|
||||||
if ($null -ne $port) { $port.Dispose(); $port = $null }
|
|
||||||
$port = Open-BoardPort
|
|
||||||
if ($null -eq $port) { Start-Sleep -Seconds 2; continue }
|
|
||||||
}
|
|
||||||
|
|
||||||
$s = Get-CpuStats
|
|
||||||
if ($null -ne $s) { $port.WriteLine((Format-StatsLine $s)) }
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# Порт исчез: перезагрузка платы, перезаливка прошивки, выдёргивание кабеля.
|
|
||||||
Write-Host "port lost: $($_.Exception.Message)"
|
|
||||||
if ($null -ne $port) { try { $port.Dispose() } catch {} ; $port = $null }
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Убрать из цикла `Write-Host` с каждой строкой — иначе в фоновом режиме это лишняя работа.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Проверить связку целиком**
|
|
||||||
|
|
||||||
Запустить скрипт от администратора. Ожидается: на экране платы появляются реальные цифры температуры, график заполняется за ~72 секунды. Нагрузить CPU (например, архиватором) — цифра и график должны поехать вверх.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Проверить переподключение**
|
|
||||||
|
|
||||||
Не останавливая агента, нажать на плате кнопку **RST**. Ожидается: в консоли `port lost: ...`, затем `connected: COM4` в течение нескольких секунд, и данные снова идут. Это же поведение обязано отработать после перезаливки прошивки.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Автозапуск агента
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `agent/README.md`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: рабочий `cpu_temp_agent.ps1`
|
|
||||||
- Produces: задание Планировщика `CPU Temp Agent`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Создать задание в Планировщике**
|
|
||||||
|
|
||||||
Выполнить в консоли администратора:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
|
||||||
-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1"'
|
|
||||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
||||||
$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
||||||
Register-ScheduledTask -TaskName 'CPU Temp Agent' -Action $action -Trigger $trigger -Settings $set -RunLevel Highest -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
`-RunLevel Highest` — это и есть «выполнить с наивысшими правами», без него драйвер LHM не загрузится. `ExecutionTimeLimit` нулевой, иначе Планировщик убьёт агента через 3 дня.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Проверить задание, не перезагружаясь**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Start-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
Start-Sleep -Seconds 5
|
|
||||||
(Get-ScheduledTask -TaskName 'CPU Temp Agent').State
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается `Running`, и на экране платы — живые цифры.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Проверить после перезагрузки**
|
|
||||||
|
|
||||||
Перезагрузить ПК, войти в систему, убедиться, что экран оживает сам в течение ~30 секунд.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Написать `agent/README.md`**
|
|
||||||
|
|
||||||
Должен содержать: требование сборки net472 и почему; необходимость прав администратора и что будет без них; команды регистрации и удаления задания (`Unregister-ScheduledTask -TaskName 'CPU Temp Agent' -Confirm:$false`); **обязательный шаг «остановить задание перед заливкой прошивки»**, иначе `PermissionError` на COM4:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Stop-ScheduledTask -TaskName 'CPU Temp Agent'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Добавить проект в git
|
|
||||||
|
|
||||||
Выполнять **только по явному слову пользователя** — по его решению проект держится локально до первых рабочих результатов.
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `D:/Soft/Arduino/.gitignore`
|
|
||||||
- Create: `README.md` в корне проекта
|
|
||||||
|
|
||||||
- [ ] **Step 1: Добавить в белый список**
|
|
||||||
|
|
||||||
Внести `CPU_temp` в белый список `.gitignore` тем же шаблоном, что применён к соседним отслеживаемым проектам — посмотреть на их записи и повторить форму, не изобретая свою.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Проверить, что попадает в индекс**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git -C "D:/Soft/Arduino" status --short
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидается: файлы `CPU_temp/`, и **ничего** из соседних проектов. В списке не должно быть `D:\Soft\LibreHardwareMonitor` (он вне репозитория) и временных файлов сборки.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Написать `README.md` проекта**
|
|
||||||
|
|
||||||
Схема связки, требования к ПК, порядок установки, ссылка на `agent/README.md` и на спеку.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Коммит**
|
|
||||||
|
|
||||||
Автор задан локально в репозитории как `Vitali <oskarvitalii@gmail.com>` — адрес `vitali@mlmsoft.com` для этого публичного репозитория чужой и попасть в историю не должен.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git -C "D:/Soft/Arduino" add CPU_temp .gitignore
|
|
||||||
git -C "D:/Soft/Arduino" commit -m "feat(cpu_temp): индикатор температуры CPU на ESP32-C3 с OLED"
|
|
||||||
```
|
|
||||||
|
|
||||||
Перед пушем: чистое дерево, показать список уходящих коммитов, поискать секреты в диффе. Пуш — в `dev`, в фоне, повтор при `Failed to authenticate user` — это норма.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Порядок и зависимости
|
|
||||||
|
|
||||||
```
|
|
||||||
Task 1 (пины) ──> Task 2 (экран) ──> Task 3 (график) ──┐
|
|
||||||
├──> Task 6 (связка) ──> Task 7 (автозапуск) ──> Task 8 (git)
|
|
||||||
Task 4 (приём) ────────────────────────────────────────┤
|
|
||||||
Task 5 (агент, консоль) ───────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Задача 4 требует Задачи 3 (вызывает `uiPushSample`). Задачу 5 можно делать параллельно с прошивкой — она ни от чего не зависит.
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# Бэклог
|
|
||||||
|
|
||||||
Идеи и незакрытые хвосты по проекту. Не план работ: сюда складывается то, что
|
|
||||||
решено не делать сейчас, чтобы не потерялось.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Другие показатели и листалка по кнопке BOOT — частично сделано
|
|
||||||
|
|
||||||
**Сделано 2026-09-17:** экран оперативной памяти и переключение кнопкой BOOT.
|
|
||||||
Решения, принятые при реализации, чтобы не пересматривать заново:
|
|
||||||
|
|
||||||
- **Тревога в приоритете.** При перегреве плата принудительно возвращается на
|
|
||||||
экран процессора, и кнопка не работает, пока температура не упадёт. Из трёх
|
|
||||||
вариантов, перечисленных ниже, выбран самый жёсткий: смысл тревоги в том, что
|
|
||||||
её нельзя прощёлкать.
|
|
||||||
- **Выбранный экран не запоминается** при перезагрузке — хранить негде.
|
|
||||||
- **Тревоги по памяти нет:** загрузка ОЗУ под 100 % на этой машине обычное
|
|
||||||
состояние.
|
|
||||||
- **Защита от дребезга** — 50 мс, порог в `config.h`.
|
|
||||||
- **Экран без данных выпадает из листания.** Если агент не шлёт ключ `M`, экран
|
|
||||||
памяти недоступен, и кнопка просто ничего не делает. Это же решение подойдёт
|
|
||||||
для машин без дискретной видеокарты.
|
|
||||||
|
|
||||||
**Осталось:** экраны батареи и сети, а также видеокарта — но у неё свой путь,
|
|
||||||
см. пункт 2.
|
|
||||||
|
|
||||||
**Что хочется.** Показывать не только процессор: видеокарту, оперативную память,
|
|
||||||
диски. Переключать экраны кнопкой **BOOT** на плате — нажал, увидел следующий
|
|
||||||
набор данных.
|
|
||||||
|
|
||||||
**Почему это дёшево со стороны протокола.** Формат `T=67.0;L=34` изначально
|
|
||||||
сделан расширяемым: прошивка молча игнорирует незнакомые ключи. Агент может
|
|
||||||
начать слать `G=61;GL=88;M=73` (видеокарта, её загрузка, память), и уже прошитые
|
|
||||||
платы от этого не сломаются — просто не покажут новое.
|
|
||||||
|
|
||||||
**Что придётся решить при реализации:**
|
|
||||||
|
|
||||||
- ~~**Кнопка BOOT сидит на GPIO9.**~~ Проверено на практике: чтение через
|
|
||||||
`INPUT_PULLUP` в работе никак не мешает загрузке платы.
|
|
||||||
- ~~**Дребезг контактов.**~~ Сделано: 50 мс, `BUTTON_DEBOUNCE_MS`.
|
|
||||||
- **Свой макет под каждый экран.** На 72×40 не получится «то же самое, только
|
|
||||||
другие цифры»: у памяти уместнее проценты и гигабайты, у диска — температура
|
|
||||||
без графика. Каждый экран проектировать отдельно.
|
|
||||||
- **Отдельная история на каждый показатель.** Сейчас кольцевой буфер один и
|
|
||||||
хранит температуру CPU. На каждый график нужен свой буфер: 72 байта на штуку,
|
|
||||||
памяти достаточно.
|
|
||||||
- **Что показывать, если данных нет.** Например, в системе нет дискретной
|
|
||||||
видеокарты — экран GPU надо либо пропускать при листании, либо честно писать,
|
|
||||||
что источника нет.
|
|
||||||
- ~~**Запоминать ли выбранный экран**~~ Решено не запоминать: хранилища нет.
|
|
||||||
- ~~**Тревога при листании.**~~ Решено: принудительно перекидывать на экран
|
|
||||||
процессора и блокировать кнопку.
|
|
||||||
|
|
||||||
**Для видеокарты выбран другой путь** — приоритетный экран без кнопки, см.
|
|
||||||
пункт 2. Листалка остаётся в силе для остального: памяти, батареи, сети.
|
|
||||||
|
|
||||||
### Что фактически доступно на этой машине
|
|
||||||
|
|
||||||
Проверено перебором датчиков через `LibreHardwareMonitorLib` после установки
|
|
||||||
PawnIO (ноутбук с i5-11400H и RTX 3050 Ti):
|
|
||||||
|
|
||||||
| Железо | Датчики | Права |
|
|
||||||
|---|---|---|
|
|
||||||
| Процессор | температура (15 датчиков: пакет, среднее, максимум, по ядрам, запас до предела), мощность | админ |
|
|
||||||
| Процессор | загрузка (общая, по ядрам, по потокам), частоты | обычные |
|
|
||||||
| Видеокарта NVIDIA | температура ядра и горячей точки, загрузка, мощность, напряжение, частоты, память, PCIe | **обычные** |
|
|
||||||
| Встроенная графика Intel | загрузка по блокам (3D, декод видео, копирование) | обычные |
|
|
||||||
| Оперативная память | занято и свободно в ГБ, загрузка в процентах | обычные |
|
|
||||||
| Сеть (5 адаптеров) | скорость приёма и отдачи, объём, утилизация | обычные |
|
|
||||||
| Батарея | заряд, деградация, ток, напряжение, мощность заряда и разряда, ёмкость | обычные |
|
|
||||||
|
|
||||||
Важное следствие: **температура видеокарты не требует прав администратора** —
|
|
||||||
NVIDIA отдаёт её через свой драйвер, ядро для этого не нужно. Админ нужен только
|
|
||||||
процессору, потому что там чтение MSR.
|
|
||||||
|
|
||||||
Чего нет: дисков (раздел не появился даже при включённом `IsStorageEnabled` —
|
|
||||||
вероятно, тоже упирается в права) и датчиков материнской платы с вентиляторами.
|
|
||||||
Для ноутбуков второе обычно: доступ к встроенному контроллеру закрыт.
|
|
||||||
|
|
||||||
### Разобранные кандидаты на экраны
|
|
||||||
|
|
||||||
1. **Видеокарта** — температура и загрузка. Полный аналог текущего экрана,
|
|
||||||
макет переиспользуется целиком. Работает даже без прав администратора.
|
|
||||||
2. **Память** — занято в гигабайтах и процент. Меняется медленно, читается с
|
|
||||||
расстояния, сглаживание не нужно.
|
|
||||||
3. **Батарея** — заряд и мощность разряда в ваттах: сразу видно, сколько ест
|
|
||||||
ноутбук прямо сейчас. Для стационарного ПК бесполезно, экран надо скрывать.
|
|
||||||
4. **Сеть** — скорость приёма и отдачи. Живее всего смотрится на графике, но
|
|
||||||
единицы прыгают от килобайт до мегабайт, и понадобится автоматическая смена
|
|
||||||
масштаба — это единственный кандидат, требующий новой логики отрисовки.
|
|
||||||
|
|
||||||
Порядок реализации по возрастанию стоимости: видеокарта, память, батарея, сеть.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Мониторинг видеокарты: приоритетный экран
|
|
||||||
|
|
||||||
**Выбранный подход.** Не листалка, а **приоритетный экран**: плата сама
|
|
||||||
показывает то, что ближе к своему порогу. Греется видеокарта — показывает её,
|
|
||||||
в простое показывает процессор по умолчанию.
|
|
||||||
|
|
||||||
Обоснование: устройство стоит на столе и смотрится боковым зрением, от него
|
|
||||||
нужен ответ на один вопрос — «всё ли в порядке». Листалка заставляет либо жать
|
|
||||||
кнопку, либо ждать нужного экрана. Значок в углу при этом естественно
|
|
||||||
превращается в указатель, чьи данные показаны, и понадобится второй значок —
|
|
||||||
видеокарты.
|
|
||||||
|
|
||||||
Совмещённый экран отвергнут по измерениям: свободно 17 пикселей по ширине,
|
|
||||||
вторая температура влезет только шрифтом 5×7, и смысл крупной цифры теряется.
|
|
||||||
|
|
||||||
### Сторона агента
|
|
||||||
|
|
||||||
Работа одинакова для любого способа отображения и не ломает совместимость:
|
|
||||||
прошивка игнорирует незнакомые ключи, поэтому агента можно обновить раньше
|
|
||||||
прошивки.
|
|
||||||
|
|
||||||
- включить `IsGpuEnabled`;
|
|
||||||
- брать `Temperature/GPU Core` и `Load/GPU Core`;
|
|
||||||
- добавить в строку ключи `G=` (температура) и `GL=` (загрузка):
|
|
||||||
`T=67.0;L=34;G=61.0;GL=88`;
|
|
||||||
- **опрашивать видеокарту реже процессора**, примерно раз в 5 секунд (см. ниже).
|
|
||||||
|
|
||||||
Права администратора для видеокарты не нужны — NVIDIA отдаёт температуру через
|
|
||||||
свой драйвер, а не через MSR.
|
|
||||||
|
|
||||||
### Сторона прошивки
|
|
||||||
|
|
||||||
- **Свои пороги на каждый показатель.** У процессора 85/82. У ноутбучной
|
|
||||||
RTX 3050 Ti предел около 87, так что пороги будут другими — подобрать,
|
|
||||||
а не копировать.
|
|
||||||
- **Задержка переключения.** Главная ловушка подхода: когда оба показателя
|
|
||||||
близки к своим порогам, экран начнёт скакать между ними каждую секунду. Нужно
|
|
||||||
либо минимальное время удержания экрана (5–10 секунд), либо заметный отрыв
|
|
||||||
по «близости к порогу», при котором происходит переключение.
|
|
||||||
- **Свой буфер истории на каждый показатель** — по 72 байта, памяти достаточно.
|
|
||||||
- **Свой значок** для видеокарты, рядом с существующим значком процессора.
|
|
||||||
- **Устаревание по каждому ключу отдельно.** Если данных по видеокарте нет
|
|
||||||
дольше N секунд, её экран просто не участвует в выборе. Это же снимает вопрос,
|
|
||||||
что делать на машине без дискретной видеокарты: экран не появится никогда.
|
|
||||||
- **Сглаживание.** Температуру видеокарты усреднять так же, как процессорную,
|
|
||||||
но окно считать в замерах, а не в секундах: при опросе раз в 5 секунд окно
|
|
||||||
из 5 замеров даст 25 секунд, что слишком много. Вероятно, хватит 2–3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2a. Сначала проверить: будит ли опрос видеокарту
|
|
||||||
|
|
||||||
**Выполнить до реализации.** У ноутбука переключаемая графика, и в простое
|
|
||||||
дискретная видеокарта уходит в сон. Опрос её датчиков может будить её и мешать
|
|
||||||
энергосбережению — то есть мониторинг начнёт сам расходовать батарею и греть
|
|
||||||
машину. Сейчас она отвечает (39 °C), значит не спит, но в глубоком простое
|
|
||||||
поведение будет другим.
|
|
||||||
|
|
||||||
**Как проверить.** Прямой измеритель уже есть: у батареи читается датчик
|
|
||||||
`Power/Charge/Discharge Rate` в ваттах, и права администратора для него не
|
|
||||||
нужны.
|
|
||||||
|
|
||||||
1. Отключить ноутбук от сети, оставить машину в простое, не трогать ввод.
|
|
||||||
2. Замер А: 10 минут писать мощность разряда **без** опроса видеокарты
|
|
||||||
(`IsGpuEnabled = false`), раз в 10 секунд.
|
|
||||||
3. Замер Б: 10 минут то же самое **с** опросом видеокарты раз в секунду.
|
|
||||||
4. Сравнить средние. Заодно записывать `GpuNvidia Power/GPU Package`: если при
|
|
||||||
опросе он перестаёт падать до нуля — видеокарта не засыпает.
|
|
||||||
|
|
||||||
**Что делать с результатом.** Разница в пределах шума — опрашивать раз в
|
|
||||||
секунду, как процессор. Разница заметная (скажем, больше половины ватта) —
|
|
||||||
оставить опрос раз в 5 секунд или реже и запомнить это решение здесь.
|
|
||||||
|
|
||||||
Оценка: около десяти минут работы плюс двадцать минут ожидания замеров.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Обрезание строки при 100° и 100%
|
|
||||||
|
|
||||||
При трёхзначной температуре и трёхзначной загрузке верхняя строка занимает 79
|
|
||||||
пикселей из 72: `100` — 49 px, зазор 2, `100%` — 28 px. Лишние 7 пикселей
|
|
||||||
уходят за правый край, и загрузка обрезается.
|
|
||||||
|
|
||||||
Дефект существовал с самого начала, проявляется только под предельной нагрузкой.
|
|
||||||
Для i5-11400H это не гипотеза: предельная температура около 100 °C.
|
|
||||||
|
|
||||||
Решение на выбор: при трёхзначной температуре переходить на более мелкий шрифт
|
|
||||||
загрузки либо убирать знак процента. Оценка — около получаса вместе с проверкой.
|
|
||||||
|
|
||||||
Решено оставить как есть: в такой момент экран всё равно мигает тревогой.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Автозапуск не проверен перезагрузкой
|
|
||||||
|
|
||||||
Задание Планировщика `CPU Temp Agent` зарегистрировано с триггером «при входе в
|
|
||||||
систему», запускается вручную и работает. Живой перезагрузкой триггер не
|
|
||||||
испытан.
|
|
||||||
|
|
||||||
Проверится само при следующей перезагрузке: экран должен ожить без участия
|
|
||||||
пользователя в течение примерно полуминуты.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Скрипт заливки
|
|
||||||
|
|
||||||
Цикл «остановить агента → залить прошивку → запустить агента» делается тремя
|
|
||||||
командами вручную. Пока правок мало, это терпимо; если работа по экрану
|
|
||||||
продолжится, стоит свести в `flash.ps1`.
|
|
||||||
|
|
||||||
Причина, по которой шаг вообще нужен: плата прошивается через тот же COM-порт,
|
|
||||||
по которому идут данные, и занятый порт валит заливку с `PermissionError`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Вес обзорного изображения
|
|
||||||
|
|
||||||
`docs/promo.png` весит 2,1 МБ при исходниках примерно на 30 КБ. Сжатие до
|
|
||||||
300–500 КБ не ухудшило бы читаемость, но старый файл всё равно останется в
|
|
||||||
истории git, так что выигрыш только для будущих клонов.
|
|
||||||
|
|
||||||
Решено оставить как есть.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
@@ -1,66 +0,0 @@
|
|||||||
#include "link.h"
|
|
||||||
|
|
||||||
#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(SERIAL_BAUD);
|
|
||||||
len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool parseLine(char *line, CpuStats &out) {
|
|
||||||
bool haveTemp = false;
|
|
||||||
bool haveLoad = false;
|
|
||||||
CpuStats parsed = {0.0f, 0, -1};
|
|
||||||
|
|
||||||
char *save = nullptr;
|
|
||||||
for (char *tok = strtok_r(line, ";", &save); tok; tok = strtok_r(nullptr, ";", &save)) {
|
|
||||||
char *eq = strchr(tok, '=');
|
|
||||||
if (!eq) continue;
|
|
||||||
*eq = '\0';
|
|
||||||
const char *key = tok;
|
|
||||||
const char *val = eq + 1;
|
|
||||||
|
|
||||||
if (strcmp(key, "T") == 0) {
|
|
||||||
parsed.tempC = atof(val);
|
|
||||||
haveTemp = true;
|
|
||||||
} else if (strcmp(key, "L") == 0) {
|
|
||||||
parsed.loadPct = atoi(val);
|
|
||||||
haveLoad = true;
|
|
||||||
} else if (strcmp(key, "M") == 0) {
|
|
||||||
// Память необязательна: старый агент её не шлёт, и это не повод
|
|
||||||
// отбрасывать строку целиком.
|
|
||||||
parsed.memPct = atoi(val);
|
|
||||||
}
|
|
||||||
// прочие ключи игнорируются намеренно: протокол расширяемый
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveTemp || !haveLoad) return false;
|
|
||||||
out = parsed;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool linkPoll(CpuStats &out) {
|
|
||||||
bool got = false;
|
|
||||||
while (Serial.available()) {
|
|
||||||
const char c = (char)Serial.read();
|
|
||||||
if (c == '\r') continue;
|
|
||||||
if (c == '\n') {
|
|
||||||
buf[len] = '\0';
|
|
||||||
if (len > 0 && parseLine(buf, out)) got = true;
|
|
||||||
len = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (len < BUF_SIZE - 1) {
|
|
||||||
buf[len++] = c;
|
|
||||||
} else {
|
|
||||||
len = 0; // строка длиннее буфера — мусор, начинаем заново
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return got;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
struct CpuStats {
|
|
||||||
float tempC;
|
|
||||||
int loadPct;
|
|
||||||
int memPct; // -1 = ключа M в строке не было
|
|
||||||
};
|
|
||||||
|
|
||||||
void linkBegin();
|
|
||||||
|
|
||||||
// Неблокирующее чтение. Возвращает true, когда собрана и разобрана
|
|
||||||
// целая строка вида "T=67.0;L=34;M=97". Неизвестные ключи игнорируются.
|
|
||||||
bool linkPoll(CpuStats &out);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Одноразовый скетч: ищет, на какой паре пинов висит встроенный OLED.
|
|
||||||
// Шелкография подписывает GPIO8/GPIO9 (дефолтные Wire-пины ESP32-C3), но у плат
|
|
||||||
// этого семейства экран обычно разведён на GPIO5/GPIO6. Проверяем перебором.
|
|
||||||
|
|
||||||
#include <Wire.h>
|
|
||||||
|
|
||||||
struct PinPair { uint8_t sda; uint8_t scl; };
|
|
||||||
|
|
||||||
static const PinPair CANDIDATES[] = { {5, 6}, {6, 5}, {8, 9}, {9, 8} };
|
|
||||||
|
|
||||||
static void scanPair(uint8_t sda, uint8_t scl) {
|
|
||||||
Wire.end();
|
|
||||||
if (!Wire.begin(sda, scl)) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: init failed\n", sda, scl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t found = 0;
|
|
||||||
for (uint8_t addr = 1; addr < 127; addr++) {
|
|
||||||
Wire.beginTransmission(addr);
|
|
||||||
if (Wire.endTransmission() == 0) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: device 0x%02X\n", sda, scl, addr);
|
|
||||||
found++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (found == 0) {
|
|
||||||
Serial.printf("SDA=%u SCL=%u: nothing\n", sda, scl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
// Повторяем в цикле: при нативном USB порт переоткрывается после заливки,
|
|
||||||
// и однократный вывод из setup можно не успеть поймать.
|
|
||||||
Serial.println("--- I2C scan ---");
|
|
||||||
for (const PinPair &p : CANDIDATES) {
|
|
||||||
scanPair(p.sda, p.scl);
|
|
||||||
}
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
-155
@@ -1,155 +0,0 @@
|
|||||||
#include "ui.h"
|
|
||||||
|
|
||||||
#include <U8g2lib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#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 SCREEN_W = 72;
|
|
||||||
|
|
||||||
static const uint8_t GRAPH_W = SCREEN_W; // по пикселю на отсчёт: 72 точки = 72 секунды
|
|
||||||
static const uint8_t GRAPH_TOP = 28;
|
|
||||||
static const uint8_t GRAPH_H = 12;
|
|
||||||
|
|
||||||
// Значок в правом верхнем углу. Полоса y=0..10 выбрана не случайно: строка под
|
|
||||||
// цифрами лежит ниже (y=11..24), а сами цифры не дотягиваются сюда даже
|
|
||||||
// трёхзначными (49 px + знак = 56). Значок не перекроет ничего.
|
|
||||||
static const uint8_t ICON_W = 11;
|
|
||||||
static const uint8_t ICON_H = 11;
|
|
||||||
static const uint8_t ICON_X = SCREEN_W - ICON_W;
|
|
||||||
static const uint8_t ICON_Y = 0;
|
|
||||||
|
|
||||||
// 0 = нет данных, иначе высота столбика 1..GRAPH_H
|
|
||||||
static uint8_t cpuHistory[GRAPH_W];
|
|
||||||
static uint8_t ramHistory[GRAPH_W];
|
|
||||||
|
|
||||||
void uiBegin() {
|
|
||||||
display.begin();
|
|
||||||
display.clearBuffer();
|
|
||||||
display.setFont(u8g2_font_6x10_tf);
|
|
||||||
display.drawStr(0, 24, "WAIT");
|
|
||||||
display.sendBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Общая часть для обеих историй: нормировать значение и сдвинуть буфер.
|
|
||||||
static void pushSample(uint8_t *history, float value, float scaleMin, float scaleMax) {
|
|
||||||
if (value < scaleMin) value = scaleMin;
|
|
||||||
if (value > scaleMax) value = scaleMax;
|
|
||||||
|
|
||||||
const float norm = (value - scaleMin) / (scaleMax - scaleMin);
|
|
||||||
uint8_t h = (uint8_t)(norm * GRAPH_H + 0.5f);
|
|
||||||
if (h < 1) h = 1;
|
|
||||||
|
|
||||||
memmove(history, history + 1, GRAPH_W - 1);
|
|
||||||
history[GRAPH_W - 1] = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiPushCpuSample(float tempC) {
|
|
||||||
pushSample(cpuHistory, tempC, SCALE_MIN, SCALE_MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiPushRamSample(int memPct) {
|
|
||||||
// У памяти шкала естественная, подбирать нечего.
|
|
||||||
pushSample(ramHistory, (float)memPct, 0.0f, 100.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawGraph(const uint8_t *history) {
|
|
||||||
for (uint8_t x = 0; x < GRAPH_W; x++) {
|
|
||||||
const uint8_t h = history[x];
|
|
||||||
if (h == 0) continue;
|
|
||||||
display.drawVLine(x, GRAPH_TOP + GRAPH_H - h, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Микросхема: корпус, ядро внутри и по три вывода с каждой стороны.
|
|
||||||
static void drawCpuIcon(uint8_t x, uint8_t y) {
|
|
||||||
display.drawFrame(x + 1, y + 1, ICON_W - 2, ICON_H - 2);
|
|
||||||
display.drawBox(x + 4, y + 4, 3, 3);
|
|
||||||
for (uint8_t i = 0; i < 3; i++) {
|
|
||||||
const uint8_t o = 3 + i * 2;
|
|
||||||
display.drawPixel(x + o, y);
|
|
||||||
display.drawPixel(x + o, y + ICON_H - 1);
|
|
||||||
display.drawPixel(x, y + o);
|
|
||||||
display.drawPixel(x + ICON_W - 1, y + o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Планка памяти: корпус, три микросхемы на ней и контакты снизу.
|
|
||||||
static void drawRamIcon(uint8_t x, uint8_t y) {
|
|
||||||
display.drawFrame(x, y + 1, ICON_W, 7);
|
|
||||||
for (uint8_t i = 0; i < 3; i++) {
|
|
||||||
display.drawBox(x + 2 + i * 3, y + 3, 2, 3);
|
|
||||||
}
|
|
||||||
for (uint8_t i = 0; i < 5; i++) {
|
|
||||||
display.drawPixel(x + 1 + i * 2, y + 9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Негатив всего кадра. Через буфер, а не командой контроллера: так поведение
|
|
||||||
// не зависит от конкретной микросхемы дисплея.
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawNoData() {
|
|
||||||
display.setFont(u8g2_font_6x10_tf);
|
|
||||||
display.drawStr(0, 14, "NO");
|
|
||||||
display.drawStr(0, 26, "DATA");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawCpuScreen(float tempC, int loadPct) {
|
|
||||||
char buf[8];
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "%d", (int)(tempC + 0.5f));
|
|
||||||
display.setFont(u8g2_font_logisoso24_tn);
|
|
||||||
display.drawStr(0, 24, buf);
|
|
||||||
const int numWidth = display.getStrWidth(buf);
|
|
||||||
|
|
||||||
// Градус — надстрочным знаком, вровень с верхом цифр.
|
|
||||||
display.setFont(u8g2_font_5x7_tf);
|
|
||||||
display.drawStr(numWidth + 2, 8, "\xb0");
|
|
||||||
|
|
||||||
// Загрузка — шрифтом вдвое выше прежнего.
|
|
||||||
display.setFont(u8g2_font_7x14_tf);
|
|
||||||
snprintf(buf, sizeof(buf), "%d%%", loadPct);
|
|
||||||
display.drawStr(numWidth + 2, 24, buf);
|
|
||||||
|
|
||||||
drawCpuIcon(ICON_X, ICON_Y);
|
|
||||||
drawGraph(cpuHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawRamScreen(int memPct) {
|
|
||||||
char buf[8];
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "%d", memPct);
|
|
||||||
display.setFont(u8g2_font_logisoso24_tn);
|
|
||||||
display.drawStr(0, 24, buf);
|
|
||||||
const int numWidth = display.getStrWidth(buf);
|
|
||||||
|
|
||||||
display.setFont(u8g2_font_7x14_tf);
|
|
||||||
display.drawStr(numWidth + 2, 24, "%");
|
|
||||||
|
|
||||||
drawRamIcon(ICON_X, ICON_Y);
|
|
||||||
drawGraph(ramHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiRender(UiScreen screen, bool fresh, float tempC, int loadPct, int memPct, bool invert) {
|
|
||||||
display.clearBuffer();
|
|
||||||
|
|
||||||
if (!fresh) {
|
|
||||||
drawNoData();
|
|
||||||
} else if (screen == UI_SCREEN_RAM) {
|
|
||||||
drawRamScreen(memPct);
|
|
||||||
} else {
|
|
||||||
drawCpuScreen(tempC, loadPct);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invert) invertBuffer();
|
|
||||||
|
|
||||||
display.sendBuffer();
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
enum UiScreen : uint8_t {
|
|
||||||
UI_SCREEN_CPU = 0,
|
|
||||||
UI_SCREEN_RAM = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Инициализирует дисплей и показывает заставку ожидания.
|
|
||||||
void uiBegin();
|
|
||||||
|
|
||||||
// Пополняют историю соответствующего графика. Вызывать раз в секунду и только
|
|
||||||
// при свежих данных, чтобы пауза связи не рисовалась как провал.
|
|
||||||
void uiPushCpuSample(float tempC);
|
|
||||||
void uiPushRamSample(int memPct);
|
|
||||||
|
|
||||||
// Перерисовывает экран целиком.
|
|
||||||
// fresh == false - данные устарели, вместо цифр выводится NO DATA.
|
|
||||||
// invert == true - кадр показывается в негативе (фаза мигания тревоги).
|
|
||||||
void uiRender(UiScreen screen, bool fresh, float tempC, int loadPct, int memPct, bool invert);
|
|
||||||
@@ -14,9 +14,3 @@ A collection of Arduino / ESP microcontroller projects.
|
|||||||
panel, and posts events to an HTTPS webhook. Settings live in LittleFS.
|
panel, and posts events to an HTTPS webhook. Settings live in LittleFS.
|
||||||
See its [README](pump_controller_8_2_OLED_DONE/README.md) and
|
See its [README](pump_controller_8_2_OLED_DONE/README.md) and
|
||||||
[MANUAL](pump_controller_8_2_OLED_DONE/MANUAL.md).
|
[MANUAL](pump_controller_8_2_OLED_DONE/MANUAL.md).
|
||||||
|
|
||||||
- **[CPU_temp](CPU_temp/)** — desktop CPU temperature monitor on an ESP32-C3 with
|
|
||||||
a built-in 0.42-inch OLED: a Windows agent reads the sensors and feeds the
|
|
||||||
board over USB serial, which shows the temperature, CPU load and a one-minute
|
|
||||||
history graph, and blinks an overheat alarm. See its
|
|
||||||
[README](CPU_temp/README.md).
|
|
||||||
@@ -60,18 +60,12 @@ GND). **Сирену, лампу или пускатель напрямую к `
|
|||||||
> **Важно: тип модуля должен быть именно active-LOW.** Если поставить модуль с
|
> **Важно: тип модуля должен быть именно active-LOW.** Если поставить модуль с
|
||||||
> активным HIGH, реле будет включено в норме и отпускать при аварии — логика
|
> активным HIGH, реле будет включено в норме и отпускать при аварии — логика
|
||||||
> перевернётся. Пины `D3` и `D4` выбраны именно под active-LOW: они удерживают
|
> перевернётся. Пины `D3` и `D4` выбраны именно под active-LOW: они удерживают
|
||||||
> HIGH во время загрузки, поэтому реле не остаются включёнными на всё время
|
> HIGH во время загрузки платы, поэтому реле молчат при включении питания.
|
||||||
> старта.
|
|
||||||
|
|
||||||
Оговорки, которые лучше знать заранее:
|
Оговорки, которые лучше знать заранее:
|
||||||
|
|
||||||
* **Оба реле кратковременно моргают при каждой перезагрузке прибора** — и при
|
* Реле «Авария» **щёлкает при каждой заливке прошивки** — схема автосброса USB
|
||||||
подаче питания, и при заливке прошивки. Так и есть, это нормально: выходы
|
дёргает `D3`. Это нормально.
|
||||||
сидят на служебных пинах платы, и пока прошивка не запустилась, уровнем на них
|
|
||||||
управляет не она. Автоматика в этот момент ещё не работает, ложных уведомлений
|
|
||||||
моргание не вызывает.
|
|
||||||
* Если моргание окажется недопустимым (например, сирена успевает пискнуть) —
|
|
||||||
ставится RC-цепочка на вход релейного модуля. Прошивку менять не нужно.
|
|
||||||
* Встроенный светодиод платы повторяет состояние реле «Резерв» — удобно для
|
* Встроенный светодиод платы повторяет состояние реле «Резерв» — удобно для
|
||||||
проверки монтажа.
|
проверки монтажа.
|
||||||
* Схема **не отказобезопасная**: если контроллер обесточен, сигнала аварии нет.
|
* Схема **не отказобезопасная**: если контроллер обесточен, сигнала аварии нет.
|
||||||
@@ -149,11 +143,6 @@ ALARM:OFF BACKUP:OFF
|
|||||||
Панель устроена как световое табло: **сверху крупно главный ответ**, ниже —
|
Панель устроена как световое табло: **сверху крупно главный ответ**, ниже —
|
||||||
лампы датчиков и выходов, в самом низу свёрнутые настройки.
|
лампы датчиков и выходов, в самом низу свёрнутые настройки.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Так панель выглядит в норме. Здесь блок настроек раскрыт; обычно он свёрнут
|
|
||||||
в одну строку и не отвлекает.
|
|
||||||
|
|
||||||
### 5.1 Главная плашка
|
### 5.1 Главная плашка
|
||||||
|
|
||||||
| Плашка | Цвет | Что значит |
|
| Плашка | Цвет | Что значит |
|
||||||
@@ -306,7 +295,7 @@ ALARM:OFF BACKUP:OFF
|
|||||||
| Экран замирает на 1-3 с в момент аварии | Так и задумано: отправка уведомления блокирует прибор не дольше 3 секунд. Реле при этом срабатывают сразу, без ожидания сервера |
|
| Экран замирает на 1-3 с в момент аварии | Так и задумано: отправка уведомления блокирует прибор не дольше 3 секунд. Реле при этом срабатывают сразу, без ожидания сервера |
|
||||||
| Панель пишет «Реле включено», но нагрузка не сработала | Проблема в монтаже или в типе модуля. Проверьте, что модуль **active-LOW**, что он питается, и что общий GND соединён |
|
| Панель пишет «Реле включено», но нагрузка не сработала | Проблема в монтаже или в типе модуля. Проверьте, что модуль **active-LOW**, что он питается, и что общий GND соединён |
|
||||||
| Реле сработали наоборот: включены в норме, отпускают при аварии | Стоит модуль с активным HIGH вместо active-LOW. Замените модуль либо потребуйте правку прошивки |
|
| Реле сработали наоборот: включены в норме, отпускают при аварии | Стоит модуль с активным HIGH вместо active-LOW. Замените модуль либо потребуйте правку прошивки |
|
||||||
| Реле моргают при перезагрузке или заливке прошивки | Нормально, см. 2.3. Выходы на служебных пинах платы; пока прошивка не стартовала, уровнем управляет не она. Уведомления при этом не отправляются |
|
| Реле щёлкает при заливке прошивки | Нормально: схема автосброса USB дёргает `D3`. На работу не влияет |
|
||||||
| Имя устройства не сохраняется | В нём есть русские буквы, пробелы или знаки препинания — они отбрасываются. Допустимы только латиница, цифры, `_` и `-` |
|
| Имя устройства не сохраняется | В нём есть русские буквы, пробелы или знаки препинания — они отбрасываются. Допустимы только латиница, цифры, `_` и `-` |
|
||||||
| Задержка не сохраняется | Значение вне диапазона 0…3600 или не целое число — поле игнорируется |
|
| Задержка не сохраняется | Значение вне диапазона 0…3600 или не целое число — поле игнорируется |
|
||||||
| Прибор не стартует после включения | На `D3` или `D4` висит нагрузка, тянущая пин к GND при старте — проверьте релейный модуль и его питание |
|
| Прибор не стартует после включения | На `D3` или `D4` висит нагрузка, тянущая пин к GND при старте — проверьте релейный модуль и его питание |
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
| `README.md` | Техническая документация (этот файл) |
|
| `README.md` | Техническая документация (этот файл) |
|
||||||
| `MANUAL.md` | Руководство пользователя / монтажника |
|
| `MANUAL.md` | Руководство пользователя / монтажника |
|
||||||
| `docs/specs/` | Согласованные проектные решения по крупным изменениям |
|
| `docs/specs/` | Согласованные проектные решения по крупным изменениям |
|
||||||
| `docs/img/` | Снимки экрана для документации |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ Wemos D1 Mini (ESP8266). Питание 5 В по microUSB или 3.3 В на п
|
|||||||
|
|
||||||
Для реле active-LOW безопасное состояние — пин в `HIGH`. Значит выход обязан
|
Для реле active-LOW безопасное состояние — пин в `HIGH`. Значит выход обязан
|
||||||
сидеть на пине, который держит `HIGH` **всю загрузку**, пока `setup()` ещё не
|
сидеть на пине, который держит `HIGH` **всю загрузку**, пока `setup()` ещё не
|
||||||
выполнился, иначе реле останется включённым на всё время старта.
|
выполнился, иначе реле щёлкнет ложной аварией при каждом включении питания.
|
||||||
|
|
||||||
| Пин | GPIO | Уровень при загрузке | Реле active-LOW при загрузке |
|
| Пин | GPIO | Уровень при загрузке | Реле active-LOW при загрузке |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -79,42 +78,12 @@ Wemos D1 Mini (ESP8266). Питание 5 В по microUSB или 3.3 В на п
|
|||||||
void setRelay(int pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); }
|
void setRelay(int pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Что происходит при перезагрузке на самом деле
|
|
||||||
|
|
||||||
Проверено на собранном приборе: **при перезагрузке оба реле кратковременно
|
|
||||||
моргают.** Признано некритичным и оставлено как есть.
|
|
||||||
|
|
||||||
Ранее в этом разделе утверждалось, что реле при загрузке молчат — это оказалось
|
|
||||||
неверно, и формулировка исправлена.
|
|
||||||
|
|
||||||
Наиболее вероятный механизм: оба выхода неизбежно сидят на служебных пинах.
|
|
||||||
`GPIO2` (`D4`) — это ещё и `UART1 TX`, и на нём во время загрузки появляется
|
|
||||||
служебный вывод; `GPIO0` (`D3`) — пин выбора режима загрузки, который дёргается
|
|
||||||
схемой сброса. Пока `setup()` не выполнился, уровнем на них управляет не
|
|
||||||
прошивка. Точную причину не измеряли: осциллографа в проверке не было.
|
|
||||||
|
|
||||||
Часть морганий может приходиться на светодиод релейного модуля, а не на
|
|
||||||
фактическое переключение контакта: короткий импульс успевает зажечь индикатор,
|
|
||||||
но не перебросить якорь. Различить можно тестером на контактах реле, если это
|
|
||||||
когда-нибудь станет важно.
|
|
||||||
|
|
||||||
**Почему решение всё равно верное.** Альтернатива — `D8` — не улучшила бы
|
|
||||||
ситуацию, а ухудшила: он подтянут к `LOW`, и реле было бы уверенно **включено
|
|
||||||
всю загрузку**, а не моргнуло бы. Кратковременное моргание предпочтительнее
|
|
||||||
устойчивого ложного сигнала на несколько секунд.
|
|
||||||
|
|
||||||
**Если моргание когда-нибудь станет проблемой** (например, сирена успевает
|
|
||||||
пискнуть): ставится RC-цепочка на вход релейного модуля, чтобы короткие импульсы
|
|
||||||
не проходили, либо модуль с триггером Шмитта на входе. Прошивку менять не
|
|
||||||
потребуется.
|
|
||||||
|
|
||||||
### 2.4 Прочие особенности ESP8266
|
### 2.4 Прочие особенности ESP8266
|
||||||
|
|
||||||
* `D4` (GPIO2) — встроенный светодиод платы, тоже активен от `LOW`. Реле
|
* `D4` (GPIO2) — встроенный светодиод платы, тоже активен от `LOW`. Реле
|
||||||
«Резерв» получает бесплатную индикацию на плате.
|
«Резерв» получает бесплатную индикацию на плате.
|
||||||
* `D3` (GPIO0) — пин выбора режима загрузки. Схема автосброса USB дёргает его
|
* `D3` (GPIO0) — пин выбора режима загрузки. Схема автосброса USB дёргает его
|
||||||
вниз при заливке скетча, поэтому реле «Авария» щёлкает при каждой прошивке.
|
вниз при заливке скетча, поэтому реле «Авария» щёлкает при каждой прошивке.
|
||||||
Кроме того, оба реле кратковременно моргают при любой перезагрузке (см. 2.3).
|
|
||||||
Косметика, но пугает.
|
Косметика, но пугает.
|
||||||
* `A0` — **только аналоговый вход** (0–3.2 В, без внутренней подтяжки). Ни
|
* `A0` — **только аналоговый вход** (0–3.2 В, без внутренней подтяжки). Ни
|
||||||
цифровым выходом, ни входом с подтяжкой быть не может, поэтому в бюджет
|
цифровым выходом, ни входом с подтяжкой быть не может, поэтому в бюджет
|
||||||
@@ -442,10 +411,6 @@ Wi-Fi настройки из `/settings.json` не затрагивает, и
|
|||||||
|
|
||||||
## 7. Веб-интерфейс
|
## 7. Веб-интерфейс
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*Панель в норме, с раскрытым блоком настроек. Снимок с работающего прибора.*
|
|
||||||
|
|
||||||
Сервер на порту `80`.
|
Сервер на порту `80`.
|
||||||
|
|
||||||
| Маршрут | Метод | Описание |
|
| Маршрут | Метод | Описание |
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
Reference in new issue
Block a user