docs: переписать README как пошаговую инструкцию по повторению

Прежний README описывал устройство проекта, но по нему нельзя было
собрать всё с нуля. Теперь это инструкция: что установить, какие пункты
меню выставить, как определить пины дисплея, как поставить агента и
автозапуск, как проверить без агента.

Отдельно вынесены грабли, на которые мы наступили: USB CDC On Boot по
умолчанию выключен, LibreHardwareMonitor требует PawnIO, разделитель
дробной части должен быть точкой, .ps1 нужен UTF-8 с BOM, порт надо
освобождать перед заливкой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
oskarvitaliiandClaude Opus 5 committed 2026-09-14 22:40:25 +07:00
1 parent bd0a87ea49
commit 03db58473c
1 file changed
+306 -69
+306 -69
View File
@@ -1,107 +1,344 @@
# CPU Temp — индикатор температуры процессора на ESP32-C3
Плата ESP32-C3 со встроенным OLED 0.42″ (72×40) стоит на столе и показывает
температуру процессора компьютера: крупная цифра, загрузка CPU рядом и график
за последнюю минуту.
Небольшая плата ESP32-C3 со встроенным OLED-экраном 0.42″ стоит на столе и
показывает температуру процессора компьютера: крупная цифра, загрузка CPU рядом
и график за последнюю минуту. При перегреве экран мигает негативом.
```
LibreHardwareMonitorLib.dll ──> PowerShell-агент ──COM (USB CDC)──> ESP32-C3 ──I2C──> OLED 72×40
(через драйвер PawnIO) раз в секунду "T=67.0;L=34" U8g2
```
Плата ничего не измеряет сама: она принимает готовые строки и рисует их. Всё
чтение железа происходит на ПК.
**Плата ничего не измеряет сама.** Она принимает готовые строки по USB и рисует
их. Всё чтение датчиков происходит на компьютере — иначе никак: температура CPU
доступна только через драйвер уровня ядра.
## Состав
Подключение — по 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 Настройки платы
Подключить плату кабелем. В меню **Инструменты** выставить:
| Пункт меню | Значение |
|---|---|
| `config.h` | **все настройки**: пины, пороги тревоги, шкала графика, тайминги |
| `CPU_temp.ino` | основной скетч: свежесть данных, гистерезис тревоги, светодиод |
| `link.h` / `link.cpp` | неблокирующее чтение строки из Serial и её разбор |
| `ui.h` / `ui.cpp` | отрисовка через U8g2 и кольцевой буфер истории |
| `tools/i2c_scan/` | одноразовый скетч: поиск пинов встроенного дисплея |
| `agent/` | агент для ПК, см. [agent/README.md](agent/README.md) |
| `docs/` | спецификация и план реализации |
| Плата | **ESP32C3 Dev Module** |
| **USB CDC On Boot** | **Enabled** ← обязательно |
| Flash Size | **4MB (32Mb)** |
| Partition Scheme | Default 4MB with spiffs |
| Upload Speed | 921600 |
| Порт | тот, что появился после подключения платы |
## Железо
Два пункта критичны:
| Параметр | Значение |
|---|---|
| Плата | ESP32-C3 (QFN32), ревизия v0.4, встроенный OLED 0.42″ |
| Флеш | 4 МБ |
| Дисплей | SSD1306 72×40, I2C, адрес `0x3C`, **SDA = GPIO5, SCL = GPIO6** |
| Подключение к ПК | нативный USB, `VID_303A&PID_1001` |
- **USB CDC On Boot = Enabled.** По умолчанию стоит `Disabled`, и тогда `Serial`
уходит в аппаратный UART вместо USB. Прошивка соберётся и зальётся, но данные
до платы доходить не будут, а на экране навсегда останется `NO DATA`.
- **Flash Size должен совпадать с физическим объёмом флеша.** У этой платы 4 МБ.
Посмотреть фактический объём можно через **Инструменты → Get Board Info** или
в выводе утилиты прошивки. При несовпадении разделы уезжают за границу
микросхемы.
Пины дисплея определены сканером `tools/i2c_scan`, а не по шелкографии: подписи
`GPIO8/GPIO9` на плате относятся к дефолтным Wire-пинам чипа, а не к разводке
экрана.
Если номер 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 .
```
`CDCOnBoot=cdc` обязателен: по умолчанию в ядре стоит `Disabled`, и тогда
`Serial` уходит в аппаратный UART вместо USB, а данные до платы не доходят.
Вместо `COM4` подставить свой порт.
**Перед заливкой остановить агента** — он держит тот же COM-порт:
---
## Часть 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
T=48.0;L=14
```
Одновременно на экране платы появится температура. Остановить — 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 — его тоже надо закрыть.
Одна строка в секунду, `\n` в конце:
---
## Настройка
Всё, что имеет смысл менять, собрано в `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 | светодиод |
| `LED_ACTIVE_LEVEL` | `LOW` | уровень, при котором светодиод горит |
| `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 рисовались бы во
всю высоту экрана и выглядели тревожно, хотя ничего не происходит.
---
## Что показывает экран
| Состояние | Экран | Светодиод GPIO8 |
|---|---|---|
| Норма | температура, загрузка, график | погашен |
| Перегрев | то же, мигает негативом | мигает синхронно с экраном |
| Нет данных дольше 5 с | `NO DATA` | горит ровно |
В режиме `NO DATA` история графика не пополняется — иначе обрыв связи выглядел бы
как резкое падение температуры.
---
## Протокол обмена
Агент шлёт плате по одной строке в секунду, в конце — перевод строки:
```
T=67.0;L=34
```
`T` — температура CPU Package в °C, `L` — общая загрузка CPU в процентах.
Неизвестные ключи прошивка игнорирует, поэтому протокол можно расширять
(например, `G=` для GPU), не трогая старую прошивку.
Если валидная строка не приходила дольше 5 секунд, на экране появляется
`NO DATA`, и история перестаёт пополняться — чтобы пауза связи не выглядела
как падение температуры.
## Экран
Шкала графика фиксированная, 30–90 °C. Автомасштаб сознательно не используется:
при нём шум ±1 °C в простое рисовался бы горами и создавал ложную тревогу.
Ориентация задаётся константой `DISPLAY_ROTATION` в `config.h`: `U8G2_R0` —
как разведено на плате, `U8G2_R2` — поворот на 180°.
## Тревога по перегреву
Порогов два, `ALARM_ON_C` и `ALARM_OFF_C` (по умолчанию 85 и 82 °C). Тревога
включается на верхнем и отпускает только на нижнем — с одним порогом она
дёргалась бы каждую секунду, пока температура топчется на границе.
В тревоге экран мигает негативом, а светодиод на GPIO8 мигает с ним в такт
(общая фаза, период `ALARM_BLINK_MS`).
Светодиод по состояниям:
| Состояние | Светодиод |
| Ключ | Смысл |
|---|---|
| Норма | погашен |
| Тревога | мигает синхронно с экраном |
| `NO DATA` | горит ровно |
| `T` | температура CPU Package, °C |
| `L` | общая загрузка CPU, % |
Светодиод зажигается **низким** уровнем (`LED_ACTIVE_LEVEL = LOW`, определено
опытом). Попутно это удобно: в обычном режиме пин удерживается высоким, а GPIO8
— пин начальной загрузки, которому высокий уровень и нужен.
Неизвестные ключи прошивка молча игнорирует, поэтому протокол расширяем: можно
добавить, например, `G=` для видеокарты, не трогая уже прошитые платы.
Проверять тревогу можно, не трогая пороги: остановить агента и подать значения
вручную.
**Разделитель дробной части — только точка.** Агент форматирует числа через
`InvariantCulture`: в русской локали иначе вышло бы `T=67,0`, и разбор на плате
прочитал бы `67`, молча потеряв дробную часть.
## Требования к ПК
Плату можно проверить без агента, отправив строки вручную. Порт при этом должен
быть свободен, то есть агента надо остановить:
Windows, LibreHardwareMonitor и драйвер PawnIO — порядок установки и разбор
подводных камней в [agent/README.md](agent/README.md).
```
$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/` | спецификация и план реализации |
Разделение простое: `link` ничего не знает об экране, `ui` не знает, откуда
взялись данные, а логика состояний живёт в скетче.
## Расход ресурсов
Агент занимает около 70 МБ оперативной памяти и со временем не растёт. Почти всё
это — обвязка Windows PowerShell и .NET Framework; сам цикл держит в памяти
несколько чисел.
Прошивка занимает 26% флеша платы и 4% её оперативной памяти.