Files
Arduino/CPU_temp/docs/2026-09-14-cpu-temp-plan.md
T
oskarvitaliiandClaude Opus 5 59848698ff feat(cpu_temp): индикатор температуры CPU на ESP32-C3 с OLED
Плата принимает по USB-CDC строки "T=67.0;L=34" от агента на ПК и рисует
крупную цифру, загрузку и график за последнюю минуту на экране 72x40.

Пины дисплея (SDA=5, SCL=6) определены сканером, а не по шелкографии:
подписи GPIO8/GPIO9 на плате - это дефолтные Wire-пины чипа.

Агент читает сенсоры через LibreHardwareMonitorLib. С версии 0.9.5 она
требует отдельно установленный драйвер PawnIO: без него загрузка CPU
читается, а температуры пусты даже под администратором.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:42:24 +07:00

756 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 можно делать параллельно с прошивкой — она ни от чего не зависит.