# CPU Temp — индикатор температуры процессора на ESP32-C3 Небольшая плата 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 доступна только через драйвер уровня ядра. ![Обзор проекта](docs/promo.png) *Две поправки к изображению: экран на этой плате 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 Скачать с и установить. Проект собран на версии 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 Скачать со страницы релизов архив **`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 Скачать со страницы файл `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 — его тоже надо закрыть. --- ## Настройка Всё, что имеет смысл менять, собрано в `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` | уровень, при котором светодиод горит | | `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 рисовались бы во всю высоту экрана и выглядели тревожно, хотя ничего не происходит. --- ## Что показывает экран | Состояние | Экран | Светодиод GPIO8 | |---|---|---| | Норма | температура, загрузка, график | погашен | | Перегрев | то же, мигает негативом | мигает синхронно с экраном | | Нет данных дольше 5 с | `NO DATA` | горит ровно | В режиме `NO DATA` история графика не пополняется — иначе обрыв связи выглядел бы как резкое падение температуры. --- ## Протокол обмена Агент шлёт плате по одной строке в секунду, в конце — перевод строки: ``` T=67.0;L=34;M=97 ``` | Ключ | Смысл | |---|---| | `T` | температура CPU Package, °C | | `L` | общая загрузка CPU, % | | `M` | загрузка оперативной памяти, % | Ключ `M` агент шлёт, а прошивка пока **не показывает** — на экране 72×40 нет места для третьего показателя. Это и есть демонстрация расширяемости: агента обновили, платы не трогали, ничего не сломалось. Берётся он у железа с именем `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% её оперативной памяти.