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>
This commit is contained in:
1 parent
ed4dad0a52
commit
59848698ff
12 files changed
+1473
No files matched your search
@@ -0,0 +1,86 @@
|
||||
# Агент CPU Temp
|
||||
|
||||
Читает температуру и загрузку процессора и раз в секунду отдаёт их плате
|
||||
ESP32-C3 по USB-COM строкой вида `T=67.0;L=34`.
|
||||
|
||||
## Что нужно установить
|
||||
|
||||
### 1. LibreHardwareMonitor
|
||||
|
||||
Скачать со <https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases>
|
||||
архив **`LibreHardwareMonitor.zip`** и распаковать в `D:\Soft\LibreHardwareMonitor`.
|
||||
|
||||
Не брать `LibreHardwareMonitor.NET.10.zip`: это сборка под .NET 10, а Windows
|
||||
PowerShell 5.1 работает на .NET Framework и такую сборку не загрузит.
|
||||
|
||||
Само приложение запускать не нужно — используются только
|
||||
`LibreHardwareMonitorLib.dll` и лежащий рядом `HidSharp.dll`.
|
||||
|
||||
### 2. Драйвер PawnIO
|
||||
|
||||
Скачать со <https://github.com/namazso/PawnIO.Setup/releases> файл
|
||||
`PawnIO_setup.exe` и запустить **без аргументов**: установщик не принимает
|
||||
никаких ключей, и на `/S` или `/quiet` отвечает кодом 87.
|
||||
|
||||
Зачем: с версии 0.9.5 LibreHardwareMonitor читает MSR-регистры процессора через
|
||||
PawnIO, а не через WinRing0, и в архив с программой этот драйвер не входит.
|
||||
|
||||
**Без PawnIO загрузка CPU читается, а все температуры пустые** — даже из консоли
|
||||
администратора. Выглядит как нехватка прав, но причина другая.
|
||||
|
||||
## Запуск вручную
|
||||
|
||||
Нужны права администратора: без них драйвер не открывается.
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File "D:\Soft\Arduino\CPU_temp\agent\cpu_temp_agent.ps1" -Echo
|
||||
```
|
||||
|
||||
Ключ `-Echo` печатает каждую отправленную строку — только для проверки.
|
||||
|
||||
При старте агент сам проверяет, доступна ли температура, и при отказе называет
|
||||
причину, а не крутит молча пустой цикл.
|
||||
|
||||
## Автозапуск
|
||||
|
||||
Задание Планировщика `CPU Temp Agent`: триггер «при входе в систему», наивысшие
|
||||
права, скрытое окно.
|
||||
|
||||
Регистрация (из консоли администратора):
|
||||
|
||||
```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` обязателен. Нулевой `ExecutionTimeLimit` нужен, иначе
|
||||
Планировщик убьёт агента через трое суток.
|
||||
|
||||
Удаление:
|
||||
|
||||
```powershell
|
||||
Unregister-ScheduledTask -TaskName 'CPU Temp Agent' -Confirm:$false
|
||||
```
|
||||
|
||||
## Перед заливкой прошивки — остановить агента
|
||||
|
||||
Плата прошивается через тот же COM-порт, по которому идут данные. Пока агент
|
||||
держит порт, заливка падает с `PermissionError`.
|
||||
|
||||
```powershell
|
||||
Stop-ScheduledTask -TaskName 'CPU Temp Agent'
|
||||
```
|
||||
|
||||
После заливки задание можно запустить снова: `Start-ScheduledTask -TaskName 'CPU Temp Agent'`.
|
||||
|
||||
## Замечания по коду
|
||||
|
||||
- Файл `cpu_temp_agent.ps1` сохранён в **UTF-8 с BOM**. Без BOM PowerShell 5.1
|
||||
читает его как ANSI, кириллица в комментариях превращается в мусор, и скрипт
|
||||
перестаёт разбираться с невнятными ошибками про скобки.
|
||||
- Номер COM-порта не задан жёстко: плата ищется по `VID_303A&PID_1001`, потому
|
||||
что номер меняется при перетыкании в другой разъём.
|
||||
- Числа форматируются через `InvariantCulture`: в русской локали иначе вышло бы
|
||||
`T=67,0`, и `atof` на плате прочитал бы `67`, потеряв дробную часть.
|
||||
@@ -0,0 +1,132 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Читает температуру и загрузку CPU и раз в секунду отдаёт их плате ESP32-C3
|
||||
по USB-COM в виде строки "T=67.0;L=34".
|
||||
|
||||
.NOTES
|
||||
Требует прав администратора: без них драйвер LibreHardwareMonitor не
|
||||
загружается, и температуры приходят пустыми (загрузка при этом читается —
|
||||
отсюда ложное ощущение, что всё работает).
|
||||
#>
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Печатать каждую отправленную строку в консоль (для отладки).
|
||||
[switch]$Echo
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$LhmPath = 'D:\Soft\LibreHardwareMonitor\LibreHardwareMonitorLib.dll'
|
||||
|
||||
if (-not (Test-Path $LhmPath)) {
|
||||
throw "Не найден $LhmPath. Скачайте LibreHardwareMonitor (архив LibreHardwareMonitor.zip, не .NET.10) и распакуйте в D:\Soft\LibreHardwareMonitor"
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
# Возвращаем обе величины даже неполными: вызывающий код должен уметь
|
||||
# сказать, какая именно отсутствует, а не валить всё в одно "нет данных".
|
||||
return @{
|
||||
Temp = $temp
|
||||
Load = $load
|
||||
}
|
||||
}
|
||||
|
||||
# Температура читается только через драйвер ядра. У LibreHardwareMonitor 0.9.5+
|
||||
# это PawnIO, который ставится отдельным установщиком и в архив с программой не
|
||||
# входит. Без него загрузка читается (она из счётчиков Windows), а температура —
|
||||
# нет. Права администратора этого не заменяют.
|
||||
function Assert-SensorsAvailable {
|
||||
$probe = Get-CpuStats
|
||||
if ($null -eq $probe.Temp) {
|
||||
$hasPawnIo = (Get-CimInstance Win32_SystemDriver | Where-Object { $_.Name -match 'PawnIO' }) -ne $null
|
||||
Write-Host 'Температура CPU недоступна.' -ForegroundColor Red
|
||||
if (-not $hasPawnIo) {
|
||||
Write-Host 'Причина: не установлен драйвер PawnIO (https://pawnio.eu).' -ForegroundColor Red
|
||||
} else {
|
||||
Write-Host 'Драйвер PawnIO найден, но значение пустое — запущено без прав администратора?' -ForegroundColor Red
|
||||
}
|
||||
throw 'Нет источника температуры'
|
||||
}
|
||||
if ($null -eq $probe.Load) { throw 'Недоступна загрузка CPU (сенсор "CPU Total" не найден)' }
|
||||
}
|
||||
|
||||
# Форматирование только через InvariantCulture: в русской локали вышло бы
|
||||
# "T=67,0", и atof на плате прочитал бы 67, молча потеряв дробную часть.
|
||||
function Format-StatsLine($stats) {
|
||||
return [string]::Format([cultureinfo]::InvariantCulture, 'T={0:F1};L={1}', [double]$stats.Temp, [int][math]::Round([double]$stats.Load))
|
||||
}
|
||||
|
||||
# Номер порта не хардкодим: он меняется при перетыкании в другой разъём.
|
||||
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
|
||||
}
|
||||
|
||||
function Open-BoardPort {
|
||||
$name = Find-BoardPort
|
||||
if (-not $name) { return $null }
|
||||
|
||||
$sp = New-Object System.IO.Ports.SerialPort $name, 115200, 'None', 8, 'One'
|
||||
# RTS явно выключен: одновременное дёрганье DTR и RTS переводит плату в загрузчик.
|
||||
$sp.DtrEnable = $true
|
||||
$sp.RtsEnable = $false
|
||||
$sp.WriteTimeout = 1000
|
||||
$sp.NewLine = "`n"
|
||||
$sp.Open()
|
||||
Write-Host "connected: $name"
|
||||
return $sp
|
||||
}
|
||||
|
||||
# Падаем сразу и с внятной причиной, а не крутим бесконечный цикл молча.
|
||||
Assert-SensorsAvailable
|
||||
|
||||
$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 }
|
||||
}
|
||||
|
||||
$stats = Get-CpuStats
|
||||
if ($null -ne $stats.Temp -and $null -ne $stats.Load) {
|
||||
$line = Format-StatsLine $stats
|
||||
$port.WriteLine($line)
|
||||
if ($Echo) { Write-Host $line }
|
||||
}
|
||||
elseif ($Echo) {
|
||||
Write-Host "пропуск: Temp=$($stats.Temp) Load=$($stats.Load)"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user