Adapter automatiquement la luminosité d'un écran

Adapter automatiquement la luminosité d'un écran

Cette semaine, nous allons réaliser un programme qui nous a été suggéré par un client : Utiliser l'API de Windows pour adapter la luminosité de l'écran en fonction de la lumière. La plupart des laptops le font déjà mais pas les ordinateurs de bureau, car ils ne possèdent pas de capteur de luminosité. Nous allons utiliser un Yocto-Light-V3 pour déterminer la luminosité ambiante.



Pour réaliser ce petit programme, nous allons utiliser l'API de configuration du moniteur de Windows. Cette API permet entre autre d’énumérer et de modifier la luminosité des écrans connectés, et pour mensurer la luminosité ambiante nous allons utiliser un Yocto-Light-V3.

Note: Il est aussi possible d’utiliser un Yocto-Light-V2 ou un vieux Yocto-Light, mais le Yocto-Light-V3 est mieux adapté à ce genre de scénario. Pour plus de détails à ce sujet, vous pouvez lire cet article.

Les fonctions Windows que nous allons utiliser sont directement utilisable en C++, nous allons donc écrire ce petit programme en C++ et utiliser notre librairie C++. Note: il est possible d'utiliser d'autre langages, comme C# ou Python, mais il faut écrire des wrappers pour appeler les fonctions de l'API windows.

Le concept d'écran dans Windows


Pour représenter toutes les configurations d'écran possibles, Windows utilise deux types de Handles: les Handles d'écrans logiques et les Handles d'écrans physiques. Les Handles d'écrans logiques représentent les zones qui sont utilisables d'un point de vue logiciel, alors que les Handles d'écrans physiques représentent les écrans qui sont connectés au PC. L’intérêt d'avoir ces deux types d'Handle est qu'ils permettent de représenter n'importe quelle configuration multi-écrans. Ce qu'il faut retenir, c'est qu'il y a un ou plusieurs écrans "logiques" qui sont composés d'un ou plusieurs écrans physiques.

Énumérer les écrans


Les fonctions qui permettent de changer la luminosité de l'écran, GetMonitorBrightness et SetMonitorBrightness, utilisent les Handle d'écrans physiques. Il faut donc en premier lieu construire une liste de Handle de tous les écrans physiques.

Malheureusement, il n'y a pas de moyen de récupérer directement cette liste. Il faut utiliser la fonction EnumDisplayMonitors et lui passer en argument une fonction de callback qui va être appelée avec le Handle de d'écran logique en paramètre. Ensuite seulement, on peut appeler les fonctions GetNumberOfPhysicalMonitorsFromHMONITOR et GetPhysicalMonitorsFromHMONITOR qui permettent de récupérer les Handles des écrans physiques.

Ensuite, pour chaque Handle d'écran physique, on appelle GetMonitorBrightness pour récupérer la configuration actuelle de cet écran et on l'ajoute à notre liste d'écrans utilisables.

La fonction de callback qui construit la liste d'écrans utilisables.

typedef struct
{
  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;
}



Au démarrage de notre programme, on construit la liste en appelant la fonction EnumDisplayMonitors en passant en argument la fonction de callback.

int main(int argc, char* argv[])
{
  EnumDisplayMonitors(NULL, NULL, MyInfoEnumProc, 0);
  ..



Initialiser la librairie Yoctopuce


Après avoir construit la liste d'écrans, il faut ajouter et initialiser la librairie Yoctopuce au projet Visual Studio. Le plus simple est d'ajouter les fichiers sources au projet et de les compiler. Nous avons déjà écrit comment le faire dans un précédent article.

Ensuite, on initialise la librairie pour qu'elle utilise les modules qui sont branchés en USB et on récupère un pointeur sur un objet YLightSensor qui permet d'interagir avec le capteur de luminosité du Yocto-Light-V3.

// Sets up the API to use local USB devices
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;
}



Au lieu de périodiquement vérifier la luminosité courante à l'aide de la méthode get_currentValue() de l'objet, nous allons enregistrer un callback périodique qui sera appelé toutes les 10 secondes. L’intérêt de cette solution est que le callback sera appelé avec la luminosité moyenne pour les 10 dernière secondes. Cela permet de lisser automatiquement les variations brusques de luminosité.


sensor->setReportFrequency("6/m");
sensor->registerTimedReportCallback(timedCallback);




Le callback périodique itère sur tous les écrans que nous avons détectés et adapte la luminosité de ce dernier en fonction de la valeur moyenne du capteur de luminosité. Dans cet exemple, nous avons simplement appliqué une correction linéaire à partir de deux points de référence (max_lux et min_lux). Cette solution fonctionne correctement dans nos bureaux, mais suivant l'exposition du Yocto-Light et le type d’éclairage de votre installation, il pourrait être nécessaire d'avoir une formule de calcul plus subtile, par exemple, en utilisant une échelle logarithmique.

void timedCallback(YLightSensor* func, YMeasure measure)
{
  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;
      }
    }
  }
}



Le reste du code est une simple boucle sans fin qui appelle la méthode YAPI::Sleep et la fonction YAPI::UpdateDeviceList de temps en temps.

int count = 0;
while (1) {
  YAPI::Sleep(1000, errmsg);
  if (count++ > 10) {
      YAPI::UpdateDeviceList(errmsg);
      count = 0;
  }
}
YAPI::FreeAPI();



Comme toujours, le code complet de l'application est disponible sur GitHub:
https://github.com/yoctopuce-examples/MonitorDimmer

Note: Nous n'avons pas fait de test très exhaustif, mais ce code devrait fonctionner sur touts les ordinateurs Windows depuis Vista. Il faut que votre écran supporte cette fonctionnalité, ce qui est le cas sur tous les écrans HDMI et DisplayPort que nous avons dans nos bureaux, par contre nos vieux écrans DVI ne semblent pas fonctionner.

Conclusion


Comme vous pouvez le voir, le système fonctionne correctement.
Comme vous pouvez le voir, le système fonctionne correctement.




Cette fonctionnalité peut sembler superflue pour de la bureautique, mais elle est très utile si vous réalisez des panneaux d'affichage publicitaire qui sont allumés en permanence.

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.