Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

App for CPU Temps, Fan Speeds, etc.

4.98/5 (27 votes)
20 Feb 2019CPOL15 min read 36.2K   1.7K  
Minimalist app to monitor a system's sensors

Introduction

In a previous article, Computer Temperatures, Fan Speeds, etc., I discussed methods to fully exploit OpenHardwareMonitorLib.dll (referred to here as OHM) to monitor temperatures, fan speeds, etc. As I mentioned there, I have a homebuilt desktop with an ASUS P6T motherboard for which ASUS no longer supports their excellent monitoring application PC Probe II. And I've found other apps to be quirky or inflexible.

I decided to structure this app, JLDProbe II, using small widgets, one for each sensor. The user can select which sensors to monitor, set up alarms that are triggered if a sensor value falls outside minimum or maximum values, and configure the widgets with a considerable amount of flexibility. Below are a couple of screenshots of the app docked in the lower-right corner of one of my screens:

Image 1

On the left, no alarms have been triggered. On the right, the temperature for CPU Core 1 has exceeded the alarm threshold—I had to set the threshold low to get it to trigger for this demo. The offending widget flashes red, and optionally, a warning sound is emitted.

Using the Code

The code is implemented as a Visual Studio 2015 self-contained project. It includes OpenHardwareMonitorLib.dll version 0.8.0 Beta. You should be able to download the code file, unzip it, load it into VS, and compile and execute it.

You can also copy the salient executables and DLLs out of the bin/Release directory and use them, though I do believe you must install the appropriate .NET Runtime Libraries.

Implementation

To understand classes like ohmDataTree, ohmHwNode and ohmSensor, refer to my earlier article Computer Temperatures, Fan Speeds, etc..

The key to the implementation of JLDProbe II is the main probe form, which is rather unimaginably named ProbeForm, and a custom drawn UserControl named SensorWidget, for which I used the same lack of imagination when naming. There is also a container class named ProbeConfig, which maintains the overall probe configuration, including layout, positioning, and a list of selected sensors to monitor.

Sensor Widgets

The SensorWidget class maintains objects and variables for:

  • Sizing and display of an individual widget
  • Manual positioning of the widget within the probe form—more on that later
  • Maximum and minimum alarm thresholds
  • ID and SensorType as read by OHM
  • Linear index of the sensor as it is encountered when walking the ohmDataTree
  • A pointer to the widget's ohmSensor instance
  • A pointer to the main configuration contained in an instance of ProbeConfig—more on that later
  • A delegate used to notify the main probe form when the widget has been dragged and dropped into a different position during manual positioning.

SensorWidget also implements methods DrawRoundedRectangle and FillRoundedRectangle—I kludged them together—as well as the OnPaint override in which it paints itself. The code for those is listed here:

C#
private void DrawRoundedRectangle(Graphics gr, Pen p, Rectangle r, float cornerRy)
{
    float cornerDy = 2 * cornerRy;
    float cornerRx = gr.DpiX * cornerRy / gr.DpiY;
    float cornerDx = 2 * cornerRx;
    gr.DrawArc(p, r.Left, r.Top, cornerDx, cornerDy, 180, 90);
    gr.DrawArc(p, r.Left + r.Width - cornerDx, r.Top, cornerDx, cornerDy, 270, 90);
    gr.DrawArc(p, r.Left, r.Top + r.Height - cornerDy, cornerDx, cornerDy, 90, 90);
    gr.DrawArc(p, r.Left + r.Width - cornerDx, r.Top + r.Height - cornerDy, cornerDx, cornerDy, 0, 90);
    gr.DrawLine(p, r.Left + cornerRx, r.Top, r.Right - cornerRx, r.Top);
    gr.DrawLine(p, r.Left + cornerRx, r.Bottom, r.Right - cornerRx, r.Bottom);
    gr.DrawLine(p, r.Left, r.Top + cornerRy, r.Left, r.Bottom - cornerRy);
    gr.DrawLine(p, r.Right, r.Top + cornerRy, r.Right, r.Bottom - cornerRy);
}

private void FillRoundedRectangle(Graphics gr, Brush br, Rectangle r, float cornerRy)
{
    float cornerDy = 2 * cornerRy;
    float cornerRx = gr.DpiX * cornerRy / gr.DpiY;
    float cornerDx = 2 * cornerRx;
    gr.FillEllipse(br, r.Left, r.Top, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left + r.Width - cornerDx, r.Top, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left, r.Top + r.Height - cornerDy, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left + r.Width - cornerDx, r.Top + r.Height - cornerDy, cornerDx, cornerDy);
    int icRx = Convert.ToInt32(cornerRx);
    int icRy = Convert.ToInt32(cornerRy);
    r.Inflate(-icRx, 0);
    gr.FillRectangle(br, r);
    r.Inflate(+icRx, -icRy);
    gr.FillRectangle(br, r);
}

protected override void OnPaint(PaintEventArgs e)
{
    //base.OnPaint(e);

    //This can happen when manually setting context menu in method setContextMenu()
    if (cornerRad == 0)
        return;

    Rectangle cRect = this.ClientRectangle;
    cRect.Width--; //must do this so dark line is included on bottom and right
    cRect.Height--;

    //Paint the widget background Light Gray with a black border
    FillRoundedRectangle(e.Graphics, cScheme.BackNameBrush, cRect, cornerRad);
    DrawRoundedRectangle(e.Graphics, Pens.Black, cRect, cornerRad);
    if (this.Font.Size >= 16)
    {
        //Add a second dark line around the widget
        Rectangle smallerR = cRect;
        smallerR.Inflate(-1, -1);
        DrawRoundedRectangle(e.Graphics, Pens.Black, smallerR, cornerRad - 1);
    }

    //Get the rectangle for the Value string
    Rectangle valueRect = new Rectangle( Convert.ToInt32(gap), 
        Convert.ToInt32(nameY + sizeName.Height + gap), 
        Convert.ToInt32(this.Width - 2 * gap), Convert.ToInt32(sizeValue.Height));

    //If flashing, paint the background red, otherwise use bvBrush
    if (FlashRed)
        FillRoundedRectangle(e.Graphics, Brushes.Red, valueRect, cornerRad);
    else
        FillRoundedRectangle(e.Graphics, cScheme.BackValueBrush, valueRect, cornerRad);
    //Add a black border
    DrawRoundedRectangle(e.Graphics, Pens.Black, valueRect, cornerRad);
    if (this.Font.Size >= 16)
    {
        //Add a second dark line around the Value panel
        Rectangle smallerR = valueRect;
        smallerR.Inflate(-1, -1);
        DrawRoundedRectangle(e.Graphics, Pens.Black, smallerR, cornerRad - 1);
    }

    //Paint the strings
    e.Graphics.DrawString(displayName, nameFont, cScheme.ForeNameBrush, 
                         (this.Width - sizeName.Width) / 2, nameY);

    if (ohms == null)
    {
        //If no ohmSensor found, paint red X in the middle of the value panel
        int L = valueRect.Left;
        int R = valueRect.Right;
        int T = valueRect.Top;
        int B = valueRect.Bottom;
        for (int i = -1; i < 2; i++)
        {
            e.Graphics.DrawLine(Pens.Red, L, T + i, R, B + i);
            e.Graphics.DrawLine(Pens.Red, L, B + i, R, T + i);
        }
    }
    else if (FlashRed)//If flashing, paint the Value string white, otherwise, use the fvBrush
        e.Graphics.DrawString(stValue, this.Font, Brushes.White, 
                             (this.Width - sizeValue.Width) / 2, nameY + sizeName.Height + gap);
    else
        e.Graphics.DrawString(stValue, this.Font, cScheme.ForeValueBrush, 
                             (this.Width - sizeValue.Width) / 2, nameY + sizeName.Height + gap);
}

To allow the user to drag the form on the screen, SensorWidget overrides the WndProc method in the following way:

C#
protected override void WndProc(ref Message m)
{
    const int WM_NCHITTEST = 0x0084;
    const int HTTRANSPARENT = (-1);

    if (m.Msg == WM_NCHITTEST && mousePassThrough)
        m.Result = (IntPtr)HTTRANSPARENT;
    else
        base.WndProc(ref m);
}

This produces hit-test transparency so mouse events are ignored by the widget and passed to the parent form. The Boolean variable mousePassThrough is set to false to override this behavior when dragging and manually positioning an individual widget on the form, or in the Configure Sensors dialog box. OnMouseDown, OnMouseMove and OnMouseUp are overridden to provide the drag-and-drop behavior during such an override.

SensorWidget also exposes the Boolean property AutoSizeWandH. The widget maintains a number of dimensional variables to position and size the widget elements during painting. If AutoSizeWandH is true, the widget also adjusts its Width and Height to the minimum necessary to accommodate the sensor Name and Value strings.

The main probe displays the widgets by first setting AutoSizeWandH to true for all widgets. It then determines the width and height of the largest widgets, sets AutoSizeWandH to false, and manually sets the width and height of all widgets to the maximums. This sets all widgets to a uniform size so they line up nicely on the form.

The sensor widget gets much of its appearance from an instance of the container class SensorWidgetColorScheme, which is implemented as follows:

C#
public class SensorWidgetColorScheme
{
    public bool byType = true;
    private Color fnColor, bnColor, fvColor, bvColor;
    private SolidBrush fvBrush, bvBrush; //Used for font and background colors in the Value pane. 
    private SolidBrush fnBrush, bnBrush; //Used for font and background colors in the Name pane.
    
    ...
    ...
    
    public SolidBrush ForeNameBrush { get { return fnBrush; } }
    public SolidBrush BackNameBrush { get { return bnBrush; } }

    public SolidBrush ForeValueBrush { get { return fvBrush; } }
    public SolidBrush BackValueBrush { get { return bvBrush; } }

    public Color NameFontColor
    {
        get { return fnColor; }
        set { fnColor = value; UpdateBrushes(); }
    }
    public Color NameBackgroundColor
    {
        get { return bnColor; }
        set { bnColor = value; UpdateBrushes(); }
    }
    public Color ValueFontColor
    {
        get { return fvColor; }
        set { fvColor = value; UpdateBrushes(); }
    }
    public Color ValueBackgroundColor
    {
        get { return bvColor; }
        set { bvColor = value; UpdateBrushes(); }
    }
}

SensorWidgetColorScheme maintains colors and brushes for:

  • Sensor name font color
  • Sensor name background color
  • Sensor value font color
  • Sensor value background color

These colors can be set in the Configure Sensors dialog box to provide:

  • a set of colors for all sensors of a particular type (Voltage, Temperature, Fan, etc.), and/or
  • specific colors for an individual sensor.

More on that later.

Selecting Sensors

To get started, you have to select which sensors to monitor.

Note that if you run the application with my configuration files, unless you have a system exactly like mine, you'll get a lot of big, red Xs in the value panel of the widgets because the application could not detect sensors that match the IDs my system. For more information on this, see the section on Incorrect ID or Sensor Failure.

When the application starts, if there is no configuration file, or no sensors have been selected, the Select Sensors dialog will open automatically. Alternatively, at any time you want to change the selected sensors, right click on the form or on any widget, and the following context menu appears:

Image 2

You can select "Select Sensors…" to change your selection. The Select Sensors dialog looks like this:

Image 3

Select the sensors you want to monitor by checking the boxes on the left, and click Ok. If you haven't previously configured the layout, it will default to Horizontal—see below.

Configure Sensors & ProbeConfig

ProbeConfig is a container class that maintains a list of sensors to monitor, which is a selected subset of the available sensors on the system. It also maintains variables for arranging and laying out the sensor widgets. There are four basic types of layout, Vertical, Horizontal, Block and Manual. Vertical, Horizontal and Block are illustrated below:

Image 4

All three are shown docked in the lower-right corner of one of my screens. When laying out the widgets in these three configurations, the application places the widgets into a grid in the order in which they appear in the list in the ProbeConfig class. You can change that order in the Configure Sensors dialog, which looks like this:

Image 5

In this dialog, you can:

  • Give the sensor a recognizable display name by selecting a sensor, then clicking once more on its Display Name and editing the value
  • Arrange the ordering of the sensors alphabetically, or inverse alphabetically, by clicking on any of the headings
  • Arrange the ordering of the sensors by dragging and dropping an individual sensor to a different position in the list.
  • Select the sensor Layout as Horizontal, Vertical, Block or Manual
  • Set the number of rows for Horizontal layout, or columns for Vertical layout.
  • Set minimum and/or maximum alarm thresholds for the selected sensor by entering a value in the appropriate text box and pressing the Set Min/Max button.
  • Select the sound you want to hear when an alarm occurs, including no sound at all
  • Test various alarm sounds

On the Font & Misc. tab, you can

  • choose how you want the probe form to be docked, Upper-Left, Upper-Center, Upper-Right, etc.
  • specify a timer interval for sensor value updates, and
  • select font name, size and style.

To embed font selection in the dialog, I had to create a FontPicker custom control that mimics the Windows font dialog. Note that font size determines widget size, and therefore the size of the form on your screen.

Color Schemes are covered in a separate section immediately following this.

While the Configure Sensors dialog is open, any changes made to the configuration are immediately implemented in the form so you see what it's going to look like. If you like the changes, press OK to save them, or press Cancel to return to the configuration that was in place when the dialog was first opened.

Color Schemes

The figure below illustrates the Color Schemes page of the Configure Sensors dialog:

Image 6

On the left is a classic ColorPicker, with four radio buttons added to select which color is modified by the picker—here, the user is modifying the background color for the sensor name. On the right, the radio buttons at the top indicate that the user has chosen to configure color schemes by sensor type (Voltage, Clock, Temperature, etc.).

Note the selection rectangle around the Fan widget, which indicates that its colors will be modified by the color picker. When the dialog box is first open, or when switching between configuring schemes By Type or Individually, no color widget is selected, so changes in the color picker have no effect. To select a color widget, merely left-click on the widget.

In this case, any changes in the color picker will immediately be reflected in the background of the sensor name for:

  1. the Fan color widget, and
  2. any Fan sensors being monitored in the actual JLDProbe II form.

If you have set up a color scheme for a particular type, and you want to duplicate it on another type, you can do so by simply left-clicking on the one you want to duplicate, then drag it and drop it on top of the other. In the image below, the Clock color scheme is about to be dropped on, and duplicated in, the Power color scheme.

Image 7

To set colors for an individual sensor that is being monitored, click the radio button labeled Individually in the top-right of the form. For my configuration, the form initially changes to look like this:

Image 8

The actual sensors being monitored by JLDProbe II are now displayed on the right, and their color schemes can be modified in the same way as the sensor type color schemes. Notice that the color schemes setup for Voltage, Clock, Temperature and Fan types have been carried over to the individual sensors being monitored.

I'm having some trouble with my CPU Fan—which I discuss in more detail a little further on—so to make it easy to keep a close eye on it, I selected that individual sensor, and gave it a bright red background color, as in the figure below:

Image 9

I also changed color for the Name Font to bright white. The final configuration looks like this:

Image 10

A bit gaudy, but effective for demonstration purposes.

ByType versus Individually

JLDProbe II maintains an array of color schemes for sensor types in a simple array declared in the following way:

C#
public SensorWidgetColorScheme[] sensorTypeColorSchemes; 

The size of the array is equal to the length of the SensorType enumeration in OHM, plus one extra to maintain the default color scheme, which is hard coded to produce the scheme visible in the first image in this article. Each of the sensor monitoring widgets in the probe form does not get its own copy of the Sensor Type scheme, but is merely given a pointer to the appropriate color scheme in sensorTypeColorSchemes.

Note that each of the monitored sensors in the probe form defaults to the color scheme By Type. But if, in the Configure Sensors dialog, its scheme is individually modified, it is given its own copy of the new color scheme. In this way, Individually takes precedence over By Type.

If a monitored sensor's color scheme has been set individually, you can make it revert to a By Type scheme by right-clicking on it in the Configure Sensors dialog, and selecting the menu item "Revert to Color Scheme By Type."

Manual Widget Positioning

Manual layout allows you to position the widgets however you choose. Here is a manual layout of the same widgets where I randomly placed them, and floated the entire form in the middle of my screen:

Image 11

In manual layout, the application ignores the order in which the widgets appear in the list, and places them based on individual column and row assignments.

To manually layout the widgets, right-click on any widget and select Position Widgets. The form will be expanded, and appear something like that on the left in the figure below:

Image 12

Now left-click on any widget and drag it to a position in the form. During manual positioning, the Boolean variable mousePassThrough is set to false to allow dragging and dropping of individual widgets. When you drop it, the widget will snap into an integral column/row position. Note that the application will not allow you to drop one widget on top of another.

The figure on the right above is the same form after several widgets have been dragged and dropped into some random positions. When you have the widgets positioned to your liking, right-click on the form and select Accept Positioning, which for this example produces a form with a transparent background that looks something like this:

Image 13

In all four layouts, while not manually positioning widgets on the form, if the user left-clicks on a widget and drags it, all of the other widgets will be dragged with it because you're actually dragging the entire form, even though the background of the form is transparent.

Miscellaneous Menu Items

Right-click on a widget and select View>Grow to increase the font size by 25%, or View>Shrink to decrease it by 20%. This is a quick way of changing the size of the widgets and the form.

Select View>Hide to dock the form in the System Tray.

Quit exits the application and saves the configuration to a file in the folder where the executable is located. Note that it only saves the configuration if the configuration has changed.

About displays the JLDProbe II and OHM versions.

Configuration File

The application saves all configuration information to a file named JLDProbeII.cfg in the folder where the executable is located. It only saves the configuration if it has been changed while the application is running. Such information should probably be saved to the Windows Registry, but for the purposes of this article, I chose not to do that.

Running at Startup

Since the sensor data is only valid when the application is run with Administrative Privileges, the only way I could find to do that at startup in Windows 10 is by scheduling it as a task at log-in. To do so:

  1. Open the Task Scheduler.
  2. On the General tab, give the task a Name and Description, and check the box "Run with highest privileges".
  3. On the Triggers tab, click on the New button, and in the dialog that appears, in the combo box for "Begin the task", select "At log on"—I haven't tried "At startup", but that will probably work as well.
  4. On the Actions tab, click on the New button, and in the dialog that appears, for the "Action" combo select "Start a program", then hit the Browse button, browse to and select executable "JLDProbe II.exe" in the bin/Release folder, or whatever folder in which you've placed it.
  5. On the Conditions tab, uncheck everything.
  6. On the Settings tab uncheck "Stop the task if it runs longer than:"
  7. Click Ok, and you're done.

If you don't set it up this way, and for example, simply place a link to the executable in the Windows startup folder, your log-in process will freeze up while waiting for you to act on the User Access Control (UAC) prompt.

If anyone has a better way of starting it with Administrative Privileges at startup or log-in, let me know.

Incorrect ID or Sensor Failure

I have a fan that appears to be failing; after all, it is seven or eight years old. Right now, it's operating sporadically. But it's in my configuration as one of the sensors I want to monitor. If the fan isn't running when the application starts, the application will read its ID from the configuration file, but fail to find it in the ohmDataTree. Any sensors for which this occurs are displayed with a big red X in the value panel, as in this figure:

Image 14

When that happens, right-click on the offending widget, and the menu item "Error Details..." will now appear. Select it and a dialog will provide the sensor's Display Name, Sensor Type, and ID to help you troubleshoot the problem. The dialog also gives you the option of having that sensor removed from the configuration.

Just keep in mind that if you get the same error message for a particular sensor, especially for something with moving parts like a fan, it may be failing and need replacement

Also keep in mind that if you run JLDProbe II with my configuration files, the application won't bomb, but it is unlikely your sensors have the same IDs as mine, so you'll get a lot of those big red X's..

To Do

Add the ability to:

  1. background record, track and graph a sensor's history,
  2. store an alarm log for sensors that operate outside acceptable thresholds, and
  3. display visual indicators on the sensor widgets to warn that a sensor has triggered an alarm.

These are now complete and are available in a separate article: App for CPU Temps, Fan Speeds, etc., Part II.

History

  • 2018.07.27
    • First implementation and publication
  • 2018.07.28
    • Fixed problem with images not showing
  • 2018.08.10
    • Added widget color customization in the Configure Sensors dialog.
    • Created FontPicker ColorPicker controls to embed font and color selection in the Configure Sensors dialog.
    • Added a 3-second delay before an alarm is sounded: the sensor must exceed alarm threshold continuously for three seconds before the alarm is sounded. This prevents a momentary spike in signal from sounding the alarm on a false positive.
  • 2018.10.11
  • 2019.02.20
    • Fixed a few minor bugs

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)