# 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 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 // Инициализирует дисплей и показывает заставку ожидания. 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 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 ` ради `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 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 = ; Load = }` - [ ] **Step 1: Скачать и распаковать LibreHardwareMonitor** Нужна сборка **net472**: в системе только Windows PowerShell 5.1 (.NET Framework), сборку net8.0 он не загрузит. Взять последний релиз со страницы , файл вида `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 ` — адрес `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 можно делать параллельно с прошивкой — она ни от чего не зависит.