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

31 KiB
Raw Blame History

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 можно делать параллельно с прошивкой — она ни от чего не зависит.