This week, we are implementing a program suggested by one of our customers: Using the Windows API to adapt the brightness of a monitor depending on the ambient light. Most laptops already do that, but desktops don't because they don't have a light sensor. We are going to use a Yocto-Light-V3 to determine the ambient light.
To implement this small program, we use Windows' monitor configuration API. This API enables you, among other things, to enumerate and modify the brightness of connected monitors. To measure the ambient light, we use a Yocto-Light-V3.
Note: We could have also used a Yocto-Light-V2 or an old Yocto-Light, but the Yocto-Light-V3 is better suited for this kind of scenario. For more details on this topic, you can read this post.
The Windows functions that we are using are directly usable in C++, so we are going to write this small program in C++ and use our C++ library. Note: it is possible to use other languages, like C# or Python, but you have to write wrappers to call the functions of the windows API.
The monitor concept in Windows
To represent all the possible monitor configurations, Windows uses two types of Handles: logical monitor Handles and physical monitor Handles. Logical monitor Handles represent areas that can be used from a software standpoint, while physical monitor Handles represent monitors connected to the computer. The advantage of having both types of Handle is that they can represent any type of multi-screen configuration. What you must remember is that there are one or more "logical" monitors which are made of one or more physical monitors.
Listing the monitors
The functions allowing you to change the monitor brightness, GetMonitorBrightness and SetMonitorBrightness, use physical monitor Handles. We must therefore first build a list of Handles for all the physical monitors.
Unfortunately, we can't directly retrieve this list. We must use the EnumDisplayMonitors function and give it as argument a callback function that is called with the logical monitor Handle as parameter. Only then, we can call the GetNumberOfPhysicalMonitorsFromHMONITOR and GetPhysicalMonitorsFromHMONITOR functions which enable us to retrieve the physical monitor Handles.
Then, for each physical monitor Handle, we call GetMonitorBrightness to retrieve the current configuration of that monitor and we add it to our list of available monitors.
The callback function building the list of available monitors:
{
HANDLE handle;
DWORD minBrightness;
DWORD curBrightness;
DWORD maxBrightness;
} monitor_status;
monitor_status monitors[4];
int nb_usable_monitor = 0;
int CALLBACK MyInfoEnumProc(HMONITOR hMonitor, HDC hdcMonitor,
LPRECT lprcMonitor, LPARAM dwData)
{
PHYSICAL_MONITOR* physical_monitor;
DWORD number_of_physical_monitors;
bool res;
res = GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor,
&number_of_physical_monitors);
if (!res) {
error("GetNumberOfPhysicalMonitorsFromHMONITOR", true);
}
physical_monitor = (PHYSICAL_MONITOR*)malloc(
number_of_physical_monitors * sizeof(PHYSICAL_MONITOR));
res = GetPhysicalMonitorsFromHMONITOR(hMonitor,
number_of_physical_monitors,
physical_monitor);
if (!res) {
error("GetPhysicalMonitorsFromHMONITOR", true);
}
for (DWORD i = 0; i < number_of_physical_monitors; i++) {
monitor_status* p = monitors + nb_usable_monitor;
res = GetMonitorBrightness( physical_monitor[i].hPhysicalMonitor,
&p->minBrightness,
&p->curBrightness,
&p->maxBrightness);
if (!res) {
error("GetMonitorBrightness", false);
continue;
}
p->handle = physical_monitor[i].hPhysicalMonitor;
nb_usable_monitor++;
}
return TRUE;
}
When our program starts, we build the list by calling the EnumDisplayMonitors function, providing it with the callback function as argument.
{
EnumDisplayMonitors(NULL, NULL, MyInfoEnumProc, 0);
..
Initializing the Yoctopuce library
When we have built the monitor list, we must add and initialize the Yoctopuce library to the Visual Studio project. The simplest way is to add the source files to the project and to compile them. We already wrote how to do that in a previous post.
Then, we initialize the library so that it uses the modules connected by USB and we retrieve a pointer to a YLightSensor object, which allows us to interact with the light sensor of the Yocto-Light-V3.
if (YAPI::RegisterHub("usb", errmsg) != YAPI_SUCCESS) {
cerr << TEXT("YAPI::RegisterHub error: ") << errmsg << endl;
return 1;
}
YLightSensor* sensor = YLightSensor::FirstLightSensor();
if (sensor == NULL) {
wcout << "No Yocto-Light connected (check USB cable)" << endl;
return 1;
}
Instead of regularly checking the current ambient light with the object get_currentValue() method, we register a periodic callback called every 10 seconds. The advantage of this solution is that the callback returns the average ambient light for the latest 10 seconds. This automatically smooths sudden light changes.
sensor->registerTimedReportCallback(timedCallback);
The periodic callback iterates on all the monitors that we have detected and adapts the brightness according to the average value of the light sensor. In this example, we simply applied a linear correction based on two reference points (max_lux and min_lux). This solution works correctly in our office, but depending on the location of the Yocto-Light and on the type of lighting of your setup, you may need a more subtle computing formula, for example using a logarithmic scale.
{
double value = measure.get_averageValue();
for (int i = 0; i < nb_usable_monitor; i++) {
int luminosity;
monitor_status* m = monitors + i;
if (value < min_lux) {
luminosity = m->minBrightness;
} else if (value > max_lux) {
luminosity = m->maxBrightness;
} else {
double monitor_range = m->maxBrightness - m->minBrightness;
double nlum = (value - min_lux) * monitor_range / (max_lux - min_lux);
luminosity = (int)(nlum + m->minBrightness + 0.5);
}
if (m->curBrightness != luminosity) {
BOOL res = SetMonitorBrightness(m->handle, luminosity);
if (res) {
m->curBrightness = luminosity;
}
}
}
}
The remainder of the code is a simple never ending loop calling the YAPI::Sleep method and the YAPI::UpdateDeviceList function from time to time.
while (1) {
YAPI::Sleep(1000, errmsg);
if (count++ > 10) {
YAPI::UpdateDeviceList(errmsg);
count = 0;
}
}
YAPI::FreeAPI();
As always, the complete code is available on GitHub:
https://github.com/yoctopuce-examples/MonitorDimmer
Note: We didn't perform exhaustive tests, but this code should work on all Windows computers from Vista onward. Your monitor must support this feature, which is the case for all the HDMI and DisplayPort monitors at our premises, however our old DVI monitors don't seem to work.
Conclusion
As you can see it, the system works correctly.
This feature may be superfluous for office work, but it is very useful if you create advertising billboards which are always on.