65.9K
CodeProject 正在变化。 阅读更多。
Home

CPU温度、风扇转速等应用,第二部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2018年10月7日

CPOL

17分钟阅读

viewsIcon

13818

downloadIcon

702

应用以监控系统的传感器,并增加了附加功能

引言

在上一篇文章《CPU温度、风扇转速等应用》中,我开发了一个极简的应用,用于充分利用OpenHardwareMonitorLib.dll(此处简称OHM)来监控温度、风扇转速等。它包含设置最大值和最小值阈值的功能,当阈值被超出时,会发出警报,提示某个组件的运行超出了其可接受的范围。正如我在那里提到的,该应用曾提醒我,我的CPU风扇出现间歇性运行,可能正在损坏。我也想到,可能不是风扇损坏,而是主板内置的风扇转速传感器出了问题,这大大增加了我的恐惧感。我真不想换主板。

为了诊断情况,我想查看一段时间内报告的风扇转速图。考虑到我可能需要查看几个小时的数据,每秒采样一次(每小时3600次采样),我需要能够通过鼠标选择区域来放大图表数据。微软的图表控件包含一个缩放功能,但它极其不直观、有bug且严重不足。

我偶尔也会收到一个或多个传感器的警报。如果我不在房间里听到潜艇下潜般的刺耳警报声,等我走到电脑前时,警报已经结束了,我也不知道是哪个传感器触发了它。因此,除了能够跟踪和绘制传感器历史记录的能力外,我决定添加一个自动警报日志,记录警报事件。

由于其中任何一个都是一项重大工程,而这两项加在一起就是两倍的工程量,我认为最好将它们的添加内容放在另一篇文章中。因此,有了**第二部分**。

Using the Code

该代码是作为Visual Studio 2015的独立项目实现的。它包含OpenHardwareMonitorLib.dll版本0.8.0 Beta。您应该能够下载代码文件,解压缩,将其加载到VS中,然后编译并执行。

您也可以从bin/Release目录中复制相关的可执行文件和DLL文件并使用它们,但我确实认为您必须安装相应的.NET运行时库。

要了解该应用的基本功能和配置方法,请回到上一篇文章《CPU温度、风扇转速等应用》。在那里,我描述了所有基本功能和配置设置,而在这篇文章中,我将只讨论这里开发的新功能。

  1. 传感器历史记录的后台记录和跟踪
  2. 存储超出可接受阈值的传感器的警报日志,以及
  3. 为传感器小部件添加视觉指示器,以警告传感器已触发警报

附加主菜单项

为了适应新功能,我在主上下文菜单的View(视图)项下添加了两个子项:History(历史)和Alarm Log(警报日志),如下图所示。

 

跟踪传感器历史

当您选择View>History菜单项时,会打开“History Tracking”(历史跟踪)对话框,如果当前没有正在跟踪的传感器,则它会显示为已打开选择窗格的状态,如下图所示。

选择窗格位于右侧,其中包含一个复选框,对应于正在监控的每个传感器——这些传感器在之前的Select Sensors(选择传感器)对话框中已被选中。此时,您只需执行以下操作:

  1. 勾选要跟踪的传感器对应的复选框,
  2. 右键单击选择窗格,然后单击Close Selection(关闭选择)菜单项。

在下面的图片中,我勾选了所有“Temperature”(温度)和“Fan”(风扇)传感器,然后关闭了选择窗格。

当已选择要跟踪的传感器时,对话框也会这样显示。

请注意,每种传感器类型都有一个图表,同一类型的传感器都显示在同一张图上。另外请注意,所有X轴的值都对齐了——稍后会详细介绍如何做到这一点。

默认情况下,“Update Continuously”(持续更新)框是勾选的,这意味着在OHM数据树发送的每个计时器滴答(参见CPU温度、风扇转速等应用)期间,任何新可用数据都将被添加到图表中。如果您想冻结图表显示,请取消选中此框。在后台,应用程序将在每个计时器滴答时继续累积新数据,但图表在您重新勾选该框之前不会改变。

内存需求相当简单。每个数据对包含一个double类型的时间值和一个float类型的传感器值,总共12个字节。在这种情况下,我们监控六个传感器,每秒更新一次(每小时3600次更新),所以:

(6个传感器) x (12字节/采样) x (3600次采样/小时) = 259,200字节/小时

除以1024,得到253 KB/小时。我的电脑通常全天开着,这意味着12小时后,应用程序需要略多于3MB的内存来维护所有数据,这对于现在的任何电脑来说都不是一个非常严重的要求。

此计算未包括包含已存储数据的列表的一些少量开销。

放大细节

在上一张图中,我正在放大查看特定细节。这是通过按住Ctrl键并用鼠标左键单击并拖动虚线缩放矩形以围绕感兴趣的区域完成的。请注意,我已经拖动了缩放矩形以同时包含两个图表,从而同时放大两者,并保持X轴的比例和值对齐。结果如下所示:

只有缩放矩形与内部绘图区域部分重叠的图表才会放大。例如,如果显示了三个图表,而缩放矩形仅与其中两个重叠,则只有这两个图表会放大。放大图表后,您可以再次放大,然后再次放大,在每个缩放级别上显示更精细的数据细节。

请注意,当我右键单击图表时出现的上下文菜单。显示的三个项目的用途如下:

  • Expand(展开)将隐藏所有其他图表,并将当前图表展开以填充整个图表面板。当图表以这种方式展开时,“Expand”菜单项将被“View All”(查看全部)替换,以便您能够恢复显示所有图表。
  • Zoom Out(缩小)仅在图表被缩放到较窄的数据范围时出现。选择它会将图表完全缩小到全视图。
  • Select Sensors(选择传感器)将打开选择传感器窗格,允许您更改正在跟踪的传感器。请注意,如果更改了选择,关闭选择窗格时,所有先前历史数据都会丢失,跟踪历史将重新开始。

组织多个图表

图表窗格和选择窗格是包含在顶层面板控件内的面板控件,分别命名为pnlChartspnlSelectionpnlChartsDocking属性设置为Fill(填充),而pnlSelectionDocking属性设置为Right(右侧)。在窗体构建期间,每个传感器*类型*的标签以及每个传感器的复选框会被添加到pnlSelection中,然后调整其宽度以刚好容纳标签和复选框,并留出10像素的填充。这是通过以下代码完成的:

//Set the selection panel's width so it is just wide enough 
//to display all check boxes with 10 pixels of padding
Control.ControlCollection cc = pnlSelection.Controls;
int xMax = 0;
for (int i = 0; i < cc.Count; i++)
    if (cc[i].Right > xMax)
        xMax = cc[i].Right;
pnlSelection.Width = xMax + 10;

这使得当选择面板可见时,图表可以占据最大的空间。选择面板的代码通过将其Visible属性设置为truefalse来打开或关闭。

当所有图表都可见时,它们的Docking属性设置为None(无),并且在pnlChartsResize事件期间,通过调整它们的大小来为它们提供相等的垂直空间,如下所示:

private void pnlCharts_Resize(object sender, EventArgs e)
{
    //If all charts are showing, then charts[0] is visible and its Docking property is None.
    //Alternatively, if charts[0] is not visible, 
    //or its Docking property is set to Fill, then one
    //chart has been Expanded and its Docking property is set to Fill, 
    //so exit without sizing the charts.
    if (charts == null || charts.Length == 0 || 
        !charts[0].Visible || charts[0].Dock == DockStyle.Fill)
        return;

    Rectangle ca = pnlCharts.ClientRectangle;
    int chartH = ca.Height / charts.Length;
    for (int i = 0; i < charts.Length; i++)
    {
        charts[i].Left = 0;
        charts[i].Top = i * chartH;
        charts[i].Width = ca.Width;
        charts[i].Height = chartH;
    }
    //Do this in case rounding error left us 1 or more pixels short of phlCharts' entire height
    charts[charts.Length - 1].Height = ca.Height - charts[charts.Length - 1].Top;
}

当选择了一个图表的Expand上下文菜单项时,该图表被赋予Docking属性Fill,其他图表的Visible属性设置为false,并且代码在不修改图表大小的情况下退出Resize事件。

为了使所有图表的水平轴值对齐,在SetupCharts()中创建图表时,会执行以下代码片段:

charts[i].ChartAreas[0].Position.Auto = false;
charts[i].ChartAreas[0].Position.X = 8;
charts[i].ChartAreas[0].Position.Y = 10;
charts[i].ChartAreas[0].Position.Width = 80;
charts[i].ChartAreas[0].Position.Height = 80;
charts[i].ChartAreas[0].InnerPlotPosition.Auto = false;
charts[i].ChartAreas[0].InnerPlotPosition.X = 10;
charts[i].ChartAreas[0].InnerPlotPosition.Y = 10;
charts[i].ChartAreas[0].InnerPlotPosition.Width = 80;
charts[i].ChartAreas[0].InnerPlotPosition.Height = 80;

ChartArea对象的PositionInnerPlotPosition属性是ElementPosition对象,接受的值范围是从(0,0)到(100,100)。这段代码禁用自动定位,并将ChartArea定位在Chart内,将InnerPlotPosition定位在ChartArea内,均为各自的百分比。由于图表始终具有相同的宽度,因此它们的X轴能够很好地对齐。

我曾考虑过设置一个更强大的算法来最大化内部绘图区域的宽度。这需要先确定Y轴标签、Y轴值和图例所需的宽度,然后相应地设置ChartAreaInnerPlotPosition。但我没有时间去做,而且这个更简单的算法已经做得很好,所以我不知道我是否会最终实现它。

带有平滑缩放和漂亮圆数的图表

我多次尝试使用MS Chart的内置缩放功能,但我始终无法理解它的工作逻辑。缩放矩形似乎会吸附到最近的区间。有时,它会坚持覆盖图表的整个宽度或高度,并且当它这样做时,它只在一个维度上缩放。因此,在经历了相当大的挫折后,我早就放弃了它,并实现了自己的功能。

MS Chart的另一个问题是,当涉及到轴缩放和显示的网格线时,如果您将其交给图表控件处理,您的图表轴将显示为不规则的区间,并且没有接近圆数的数值。

要了解我在这款应用中如何实现平滑缩放漂亮圆数,请参阅我之前的文章《MS Chart的平滑缩放与圆数》。

捕获警报事件

为了捕获警报事件以供日后审查,我创建了容器类AlarmEvent——我知道,我不是一个善于起好听名字的人。它存储了一个事件的几个数据项,如下图所示的代码片段:

public enum AlarmStatus { Silent, Sounding, StartDelay, StopDelay, Aborted }

public class AlarmEvent
{
    public DateTime? triggeredDT = null;            //DateTime the event was initially triggered
    public DateTime? startOfStopDelayDT = null;     //DateTime a triggered alarm first 
                                                    //returned to operating within acceptable limits
    public DateTime? clearedDT = null;              //DateTime the triggered event cleared
    public string DisplayName = ""; //May not be unique
    public string sName = "";       //Sensor name, may not be unique
    public string id = "";          //Always unique on a particular system
    public SensorType sType;        //Sensor type
    public float? alarmMin;         //Minimum alarm threshold (nullable)
    public float? alarmMax;         //Maximum alarm threshold (nullable)
    public AlarmStatus alarmStatus;

    //List of (DateTime, Value, AlarmStatus) triplet for each timer tick during
    //which the sensor's value dropped outside of the alarm limits
    public List<AlarmDetail> details = new List<AlarmDetail>(64);
    
    ...
}

当传感器开始超出其可接受的限制时,警报被*触发*;当它恢复正常运行时,警报被*清除*。JLDProbeII的配置包含一个falseAlarmDelay变量,可以在Configure Sensors(配置传感器)对话框的Font & Misc.(字体与杂项)选项卡上设置。如果其值大于零,则在警报触发后,传感器小部件会等待该时间量,然后闪烁红色并发出警报。falseAlarmDelay的默认值为3.0秒。这可以防止短暂的数据峰值触发一两秒的警报,这被认为是虚假警报。

startOfStopDelayDT时间用于在警报清除时实现相同的延迟。如果警报已经持续一段时间,当传感器恢复到其可接受的限制范围内运行时,警报不会被视为清除,直到它已经这样持续了至少falseAlarmDelay秒。这可以防止虚假停止,即正在响起的警报短暂地恢复到正常运行状态。

AlarmStatus枚举与falseAlarmDelaystartOfStopDelayDT结合使用,以控制触发和清除警报背后的逻辑。一旦警报开始超出其限制,在每次计时器滴答时,DateTime、传感器ValueAlarmStatus都会存储在details列表中。这包括虚假警报事件。

请注意,您可以在Configure Sensors(配置传感器)对话框中通过复选框指定JLDProbeII是否会保存和存储虚假警报事件。

JLDProbeIIProbeForm.cs中维护两个警报列表,定义为:

public List<AlarmEvent> alarmEventsCurrent = new List<AlarmEvent>();
public List<AlarmEvent> alarmEventsPast;

当前警报事件是指应用程序启动以来发生的事件。过去的警报事件是指在JLDProbeII当前实例启动之前触发的事件,并存储在日志文件中。当应用程序退出时,它会自动将所有当前和过去的警报事件保存到一个名为“JLDProbeII_Alarm.log”的日志文件中,该文件存储在可执行文件所在的文件夹中,通常是“bin\Release”文件夹。

要查看警报事件列表,请右键单击探测器窗体上的任意位置,然后选择View>Alarm Log(视图>警报日志)菜单项以打开以下对话框:

过去的警报事件以黑色显示。当前警报事件以蓝色显示。两者中,虚假警报事件的持续时间以红色表示。如果与警报事件关联的传感器id与正在监控的任何传感器都不匹配,则其名称将以StrikeOut(删除线)字体显示。如果某个曾触发警报的传感器在此对话框打开之前在Select Sensors(选择传感器)对话框中被取消选择,或者日志文件是从另一台具有不同传感器id的计算机复制过来的,就可能发生这种情况。

该对话框显示每个触发事件的日期和时间。对于发生在之前日期的事件,同时显示日期和时间;而对于发生在打开对话框当天发生的事件,则仅显示时间。它还显示持续时间,在Configure Sensors(配置传感器)对话框中为每个传感器设置的警报限制,以及从AlarmEvent对象中的details列表中获取的事件期间发生的最大和最小传感器值。如果您想查看存储在details列表中的数据,请选择感兴趣的警报事件,右键单击ListView,然后选择Export to csv(导出到csv)。您可以指定一个文件名,应用程序会将事件导出到一个制表符分隔的CSV文本文件,该文件可以在Microsoft Excel®中打开。

请注意,只有持续时间小于falseAlarmDelay的事件才会被标记为虚假。这意味着如果您将falseAlarmDelay设置为零,将不会有任何警报事件被标记为虚假。在上图中,请仔细查看选中传感器正下方的两个“GPU Core”(GPU核心)的警报事件。第一个持续时间为2.026秒,被标记为虚假。第二个持续时间较短,为1.003秒,但未被标记为虚假。这是因为在这两个事件之间,我将falseAlarmDelay从3.0秒更改为0.0秒。

在此对话框中,您还可以右键单击并删除选定的事件。如果您通过单击Ok(确定)按钮关闭对话框,则会永久删除这些事件。如果您单击Cancel(取消)按钮,则会忽略删除。

如果您想在自己的系统上模拟警报,请为几个传感器设置最大警报阈值,在ProbeConfig.cs的顶部,将#define SimulateAlarmsNo更改为#define SimulateAlarms,然后重新编译并运行应用程序。在运行的前几分钟,它将模拟几种类型的警报。但请务必将其改回,否则您将始终收到这些模拟警报。

迷你控件警报指示器

为了让用户了解警报状态,我实现了一个MiniAlarmControl类来在每个传感器上显示警报状态。它包含一个警报铃铛的图像,如果没有发生警报,铃铛是绿色的;如果触发了警报,铃铛是红色的。两个MiniAlarmControl绘制在每个SensorWidget的表面上,左边一个用于过去事件,右边一个用于当前事件。迷你控件显示如下图所示:

用户可以在Configure Sensors(配置传感器)对话框中选择迷你控件的位置。在上图的示例中,从左到右,迷你控件的位置设置为None(无)、Top(顶部)、Middle(中间)和Bottom(底部)。

迷你控件还在每个铃铛上方叠加了一个透明的Label控件。当鼠标光标悬停在警报铃铛上时,标签会提供一个弹出提示,显示事件的状态,如上图中从右边数第二个的图像所示。Label控件也使得捕获鼠标事件变得容易。双击红色或绿色的铃铛,将打开Alarm Log(警报日志)对话框,仅显示该传感器的警报事件。

我本可以使用PictureBox控件代替Label控件,然后只需要将其放置在SensorWidget上,设置其Image属性和工具提示即可。它将处理绘图以及工具提示的弹出和鼠标事件的捕获。但是,PictureBox控件在图像绘图方法上缺乏灵活性,而且由于我使用的是64x64像素的位图,我希望控制插值方法。因此,铃铛通过以下方式绘制到SensorWidget的表面上,在其OnPaint方法中:

if (miniControlPosition != MiniControlPosition.None)
{
    e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
    for (int i = 0; i < miniAlarmControls.Length; i++)
        e.Graphics.DrawImage(miniAlarmControls[i].alarmImage, miniAlarmControls[i].ClientRectF);
}

HighQualityBilinear插值模式下绘制时,缩小后的图像具有更好的质量。

这些迷你控件允许用户立即查看传感器是否触发了任何警报事件。

风扇诊断:我的有问题风扇

我使用新的历史跟踪功能来分析我在《CPU温度、风扇转速等应用》一文中讨论的那个表现不佳的CPU风扇的问题。我首先注意到的是,它的转速有时会瞬时飙升到50,000或60,000转。没有一台电脑风扇会如此之快地旋转,除非它由一架拥有2000马力、劳斯莱斯V-12航空发动机驱动,比如在第二次世界大战期间产量最大、战略上最重要的英国单座战斗机“喷火”上使用的劳斯莱斯梅林发动机——我扯得有点远了,是不是?但我的电脑并没有冒出烟雾,所以我想那一定是误读。

我换了一个我放在那里的旧风扇,这个风扇在我的车库里已经放了大约十年了。本文前面显示的CPU风扇曲线是用那个较新的风扇安装后记录的。但请再看看那些曲线。那个风扇仍然偶尔会飙升到6000转以上,我知道它不可能达到那个速度。而且它还会在短暂的瞬间降到零。但总的来说,它似乎还在工作,我已经设置了100转的最低警报阈值,以防它停机。请记住,当falseAlarmDelay为3.0秒时,除非风扇连续3秒保持在该阈值以下,否则警报不会触发。我已经将应用程序配置为在我系统上捕获虚假警报事件。

所以,现在我知道有两个不同的风扇在发生奇怪的事情,但第二个风扇的症状明显减轻,所以很可能是风扇导致了问题,而不是主板传感器——呼,希望我躲过了一劫。但为了绝对确定,我买了一个新风扇并安装了它。这是新风扇的一些跟踪历史:

这个风扇最高可以运行到1200转。偶尔会超出这个值,但幅度不大,所以我对几个略高的值并不在意,它们很可能是误读。我不得不仔细考虑风扇转速短暂降至零的瞬间。我的华硕P6T主板的BIOS包含根据CPU温度控制CPU风扇转速的算法。华硕不公布他们使用的风扇转速与温度曲线的细节,但当CPU温度升高时,它显然会提高风扇转速。由于我的CPU大部分时间运行得相当凉爽(<50°C [122°F]),我假设BIOS算法告诉风扇以低于其额定最低值(400转)的速度运行,所以它会短暂地降至零。我可以接受这一点。但我已经为我系统中的所有风扇设置了最低警报,以及3.0秒的falseAlarmDelay,这样我就知道一个风扇是否完全停止了。

有趣的是,当我安装新风扇时,在启动系统时出现了一个“CPU Fan Error”(CPU风扇错误)的提示,这意味着风扇没有运行。我打开机箱并目视确认风扇确实在运行,所以我忽略了错误,让系统完成了启动过程。再次查看上面的曲线,答案很明显:我的系统运行得*太凉了*——无意双关。典型的CPU风扇转速约为700转,但当我第一次启动系统且CPU处于室温时,BIOS算法可能会让它运行在最低值以下,所以它会完全停止,我就会收到启动错误。这里的信息是,*最低额定风扇转速*也很重要。

所以问题解决了,没有把责任推给主板——呼!

我希望这能说明这款应用如何用于诊断和保护您的系统。

待办事项

  • 如果OHM将来实现了设置风扇速度的功能,我需要将其整合到这款应用中。

历史

  • 2018.10.06:首次实现和发布
  • 2019.02.20:修复了一些bug;参见ChangeLog.txt
© . All rights reserved.