Плата принимает по 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>
31 KiB
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
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 можно не успеть поймать.
#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: Собрать
"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: Залить
"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: Прочитать вывод
$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
#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, это частый источник ошибок.
#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с фиксированной заглушкой
#include "ui.h"
void setup() {
Serial.begin(115200);
uiBegin();
delay(1000);
}
void loop() {
uiRender(true, 67.0f, 34);
delay(1000);
}
- Step 4: Собрать и залить
"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():
// Добавляет значение в кольцевой буфер истории. Вызывать раз в секунду
// и только при свежих данных, чтобы пауза связи не рисовалась как провал.
void uiPushSample(float tempC);
- Step 2: Реализовать буфер и отрисовку графика в
ui.cpp
Шкала фиксированная 30–90 °C — автомасштаб отвергнут в спеке: в простое шум ±1 °C рисовался бы горами.
Добавить после объявления display:
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, чтобы график было видно в движении:
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
#pragma once
struct CpuStats {
float tempC;
int loadPct;
};
void linkBegin();
// Неблокирующее чтение. Возвращает true, когда собрана и разобрана
// целая строка вида "T=67.0;L=34". Неизвестные ключи игнорируются.
bool linkPoll(CpuStats &out);
- Step 2: Написать
link.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
Отсчёт для графика добавляется раз в секунду и только при свежих данных.
#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: Собрать и залить
"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. Затем отправить строки руками:
$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.
Проверка:
Test-Path 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
Test-Path 'D:\Soft\LibreHardwareMonitor\HidSharp.dll'
Ожидается True для обоих — HidSharp.dll обязателен, это зависимость библиотеки, без неё Add-Type пройдёт, а открытие компьютера упадёт.
- Step 2: Проверить чтение сенсоров одной строкой
Запустить в консоли, открытой от имени администратора — без прав драйвер не загрузится и температура будет пустой:
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с выводом в консоль
#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 -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:
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: Проверить поиск порта отдельно
Скрипт не подключать (.-загрузка запустит бесконечный цикл) — выполнить тело функции в консоли напрямую:
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 переводит её в загрузчик.
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: Создать задание в Планировщике
Выполнить в консоли администратора:
$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: Проверить задание, не перезагружаясь
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:
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: Проверить, что попадает в индекс
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 для этого публичного репозитория чужой и попасть в историю не должен.
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 можно делать параллельно с прошивкой — она ни от чего не зависит.