-
Notifications
You must be signed in to change notification settings - Fork 4
/
vdu_controls.py
executable file
·8965 lines (7686 loc) · 481 KB
/
vdu_controls.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python3
"""
vdu_controls - a DDC control panel for monitors
===============================================
A control panel for DisplayPort, DVI, HDMI, or USB connected VDUs (*Visual Display Units*).
Synopsis:
=========
vdu_controls
[--help|-h] [--about] [--detailed-help]
[--show {brightness,contrast,audio-volume,input-source,power-mode,osd-language}]
[--hide {brightness,contrast,audio-volume,input-source,power-mode,osd-language}]
[--enable-vcp-code vcp_code] [--schedule|--no-schedule]
[--location latitude,longitude] [--weather|--no-weather]
[--lux-options|--no-lux-options] [--translations|--no-translations]
[--splash|--no-splash] [--system-tray|--no-system-tray]
[--monochrome-tray|--no-monochrome-tray] [--mono-light-tray|--no-mono-light-tray]
[--hide-on-focus-out|--no-hide-on-focus-out] [--smart-window|--no-smart-window]
[--syslog|--no-syslog] [--debug|--no-debug] [--warnings|--no-warnings]
[--sleep-multiplier multiplier] [--ddcutil-extra-args 'extra args']
[--dbus-client|--no-dbus-client]
[--protect-nvram|--no-protect-nvram]
[--create-config-files] [--install] [--uninstall]
Optional arguments:
-------------------
Arguments supplied on the command line override config file equivalent settings.
-h, --help show this help message and exit
--detailed-help full help in Markdown format
--about info about vdu_controls
--show control_name
show specified control only, may be specified multiple times
--hide control_name
hide/disable a control, may be specified multiple times
--enable-vcp-code vcp_code
enable a control for a vcp-code unavailable via hide/show,
may be specified multiple times
--system-tray|--no-system-tray
start up as an entry in the system tray.
``--no-system-tray`` is the default.
--location latitude,longitude
local latitude and longitude for triggering presets
by solar elevation
--translations|--no-translations
enable/disable language translations.
``--no-translations`` is the default.
--schedule|--no-schedule
enable/disable preset scheduling. ``--schedule`` is the default.
--weather|--no-weather
enable/disable weather lookups. ``--weather`` is the default.
--lux-options|--no-lux-options
enable/disable ambient light metering options.
``--lux-options`` is the default.
--debug|--no-debug
enable/disable additional debug information.
``--no-debug`` is the default.
--warnings--no-warnings
popup a warning when a VDU lacks an enabled control.
``--no-warnings`` is the default.
--syslog|-no-syslog
divert diagnostic output to the syslog (journald).
``--no-syslog`` is the default.
--hide-on-focus-out|--no-hide-on-focus-out
minimize the main window automatically on focus out
``--no-hide-on-focus-out`` is the default.
--splash|--no-splash
show the splash screen. ``--splash`` is the default.
--monochrome-tray|--no-monochrome-tray
monochrome dark themed system-tray.
``--no-monochrome-tray`` is the default.
--mono-light-tray|--no-mono-light-tray
monochrome themed system-tray.
``--no-mono-light-tray`` is the default.
--smart-window|--no-smart-window
smart main window placement and geometry.
``--smart-window`` is the default.
--sleep-multiplier set the default ddcutil sleep multiplier
protocol reliability multiplier for ddcutil (typically
0.1 .. 2.0, default is 1.0)
--ddcutil-extra-args extra arguments to pass to ddcutil (enclosed in single quotes)
--dbus-client|--no-dbus-client
use the D-Bus ddcutil-service instead of the ddcutil command
``--dbus-client`` is the default
--protect-nvram|--no-protect-nvram
alter options and defaults to minimize VDU NVRAM writes
--create-config-files
if they do not exist, create template config INI files
in $HOME/.config/vdu_controls/
--install installs the vdu_controls in the current user's path and
desktop application menu.
--uninstall uninstalls the vdu_controls application menu file and
script for the current user.
Description
===========
``vdu_controls`` is a control-panel for DisplayPort, DVI, HDMI, or USB connected VDUs. Out of the
box, ``vdu_controls`` offers a subset of controls including brightness, contrast and audio
controls. Additional controls can be enabled via the ``Settings`` dialog.
``vdu_controls`` interacts with VDUs by using ``ddcutil`` to issue standard VESA
*Virtual Control Panel* (*VCP*) commands via the VESA *Display Data Channel* (*DDC*).
``Ddcutil`` provides a robust interface that is tolerant of the vagaries of the many OEM DDC
implementations.
From ``vdu_controls 2.0`` onward, ``vdu_controls`` defaults to using the ``D-Bus ddcutil-service``.
Should the ``ddcutil-service`` be unavailable, ``vdu_controls`` will fall back to running the
``ddcutil`` command to perform each request.
The UI's look-and-feel adjusts itself for dark and light desktop themes. The application may
optionally run in the system tray of KDE, Deepin, GNOME, and Xfce (and possibly others).
The UI provides an optional ``ambient-light slider`` for simultaneously adjusting
all VDUs according to custom per-VDU ambient lux/brightness profiles. Options are included
for automated adjustment by accessing a hardware light-meters, webcams, or other devices.
Named ``Preset`` configurations can be saved and recalled. For example, presets may be created
for night, day, photography, movies, and so forth. Presets can be triggered by specific ambient
light levels, scheduled according to local solar elevation, vetoed by local weather conditions,
or activated by UNIX signals.
From any UI window, `F1` accesses help, and `F10` accesses the main-menu. The main-menu is
also available via the hamburger-menu, and also via the right-mouse button in either the
main-window or the system-tray icon. The main-menu has `ALT-key` shortcuts for all menu items
(subject to sufficient letters being available to distinguish all user defined presets).
For further information, including screenshots, see https://github.com/digitaltrails/vdu_controls .
The long term affects of repeatably rewriting a VDUs setting are not well understood, but some
concerns have been expressed. See **LIMITATIONS** for further details.
Configuration
=============
Configuration changes can be made via the ``Settings`` dialog or by editing the config-files.
Settings Menu and Config files
------------------------------
The ``Settings`` dialog features a tab for editing common/default settings as well as
tabs specific to each VDU. The config files are named according to the following scheme:
- Application wide default config: ``$HOME/.config/vdu_controls/vdu_controls.conf``
- VDU model and serial number config: ``$HOME/.config/vdu_controls/<model>_<serial|display_num>.conf``
- VDU model only config: ``$HOME/.config/vdu_controls/<model>.conf``
The VDU-specific config files can be used to:
- Correct manufacturer built-in metadata.
- Customise which controls are to be provided for each VDU.
- Set optimal ``ddcutil`` DDC parameters for each VDU.
The config files are in INI-format divided into a number of sections as outlined below::
[vdu-controls-globals]
# The vdu-controls-globals section is only required in $HOME/.config/vdu_controls/vdu_controls.conf
system-tray-enabled = yes|no
splash-screen-enabled = yes|no
translations-enabled = yes|no
weather-enabled = yes|no
schedule-enabled = yes|no
lux-options-enabled = yes|no
warnings-enabled = yes|no
debug-enabled = yes|no
syslog-enabled = yes|no
[vdu-controls-widgets]
# Yes/no for each of the control options that vdu_controls normally provides by default.
brightness = yes|no
contrast = yes|no
audio-volume = yes|no
audio-mute = yes|no
audio-treble = yes|no
audio-bass = yes|no
audio-mic-volume = yes|no
input-source = yes|no
power-mode = yes|no
osd-language = yes|no
# Enable ddcutil supported codes not enabled in vdu_controls by default, CSV list of two-digit hex values.
enable-vcp-codes = NN, NN, NN
[ddcutil-parameters]
# Useful values appear to be >=0.1
sleep-multiplier = 0.5
[ddcutil-capabilities]
# The (possibly edited) output from "ddcutil --display N capabilities" with leading spaces retained.
capabilities-override =
Config files can only be used to enable and alter definitions of VCP codes supported by ``ddcutil``.
Unsupported manufacturer specific features should only be experimented with caution, some
may have irreversible consequences, including bricking the hardware.
As well as using the ``Settings``, config files may also be created by the command line option::
vdu_controls --create-config-files
which will create initial templates based on the currently connected VDUs.
The config files are completely optional, they need not be used if the default options are found to be
adequate.
Adding value restrictions to a VDU's capabilities override
----------------------------------------------------------
In some cases, a VDU's DDC reported minimums and maximums may be incorrect or overstated. Within
vdu_controls this can be corrected by overriding the DDC reported range. For example, perhaps a VDU
reports it supports a brightness range of 0 to 100, but in fact only practically supports 20 to 90.
This can be corrected by bringing up the VDU's settings tab and editing the text in
the **capabilities override**:
1. Open the *Settings* tab for the VDU, navigate to the "capabilities override* field
2. locate the feature, in this example, the brightness,
3. add a **Values:** **min..max** specification to line the following the feature definition,
4. save the changes.
For the brightness example the completed edit would look like::
Feature: 10 (Brightness)
Values: 20..80
The vdu_controls slider for that value will now be restricted to the specified range.
Adding a refresh/reload requirement to a VDU's capabilities override
--------------------------------------------------------------------
Altering the values of some VCP codes may result in a cascade of changes to other
codes. For example, changing a VCP value for *Picture Mode* might result in changes
to several VCP-code features, including brightness, contrast, and others. Exactly
which codes have these kinds of side effects isn't indicated in the metadata
obtained from each VDU, however vdu_controls supports adding *refresh* annotations
to the feature-names within the **capabilities override**. For example::
Feature: 15 (Picture Mode)
Can be annotated with::
Feature: 15 (Picture Mode *refresh*)
With this annotation, when ever *Picture Mode* is altered, vdu_controls will
reload all configuration files and refresh all control values from the VDUs.
Presets
-------
A custom named preset can be used to save the current VDU settings for later recall. Any number of
presets can be created for different lighting conditions or different applications, for example:
*Night*, *Day*, *Overcast*, *Sunny*, *Photography*, and *Video*. Each presets can be assigned a
name and icon.
The ``Presets`` item in ``main-menu`` will bring up a ``Presets`` dialog for managing and
applying presets. The ``main-menu`` also includes a shortcut for applying each existing presets.
Any small SVG or PNG can be assigned as a preset's icon. Monochrome SVG icons that conform to the
Plasma color conventions will be automatically inverted if the desktop them is changed from dark to
light. If a preset lacks an icon, an icon will be created from its initials (of its first and last
words). A starter set of icons is included in ``/usr/share/vdu_controls/icons/``.
Any time the current VDUs settings match those of a preset, the preset's name and icon will
automatically show in the window-title, tray tooltip, tray icon.
Presets may be set to transition immediately (the default); gradually on schedule (solar elevation);
or gradually always (when triggered by schedule, main-menu, or UNIX signal). The speed of
transition is determined by how quickly each VDU can respond to adjustment. During a transition,
the transition will be abandoned if the controls involved in the transition are altered by any other
activity.
Each preset is stored in config directory as: ``$HOME/.config/vdu_controls/Preset_<preset_name>.conf``
Preset files are saved in INI-file format for ease of editing. Each preset file contains a
section for each connected VDU, for example::
[preset]
icon = /usr/share/icons/breeze/status/16/cloudstatus.svg
solar-elevation = eastern-sky 40
transition-type = scheduled
transition-step-interval-seconds = 5
[HP_ZR24w_CNT008]
brightness = 50
osd-language = 02
[LG_HDR_4K_89765]
brightness = 13
audio-speaker-volume = 16
When creating a preset file, you may select which controls to save for each VDU. For example,
you might create a preset that includes the brightness, but not the contrast or audio-volume.
Keeping the included controls to a minimum speeds up the transition and reduces the chances of the
VDU failing to keep up with the associated stream of DDC commands.
While using the GUI to create or edit a preset, activation of scheduled presets and adjustments due
to light-metering are blocked until editing is complete.
Presets - VDU initialization-presets
------------------------------------
For a VDU named `abc` with a serial number `xyz`, if a preset named `abx xyz` exists, that
preset will be restored at startup or when ever the VDU is subsequently detected.
This feature is designed to restore settings that cannot be saved in the VDU’s NVRAM
or for VDUs where the NVRAM capacity has been exhausted.
Presets - solar elevation triggers
----------------------------------
A preset may be set to automatically trigger when the sun rises to a specified elevation. The idea
being to allow a preset to trigger relative to dawn or dusk, or when the sun rises above some
surrounding terrain (the time of which will vary as the seasons change).
If a preset has an elevation, the preset will be triggered each day at a time calculated according
to the latitude and longitude specified by in the ``vdu-controls-globals`` ``location`` option.
By choosing an appropriate ``solar-elevation`` a preset may be confined to specific times of the
year. For example, a preset with a positive solar elevation will not trigger at mid-winter in the
Arctic circle (because the sun never gets that high). Any preset may be manually invoked
regardless of its specified solar elevations.
To assign a trigger, use the Preset Dialog to set a preset's ``solar-elevation``. A solar elevation
may range from -19 degrees in the eastern sky (morning/ascending) to -19 degrees in the western sky
(afternoon/descending), with a maximum nearing 90 degrees at midday.
On any given day, the Preset Dialog may be used to temporarily override any trigger, in which case
the trigger is suspended until the following day. For example, a user might choose to disable
a trigger intended for the brightest part of the day if the day is particularly dull.
At startup ``vdu_controls`` will restore the most recent preset that would have been triggered for
this day (if any). For example, say a user has ``vdu_controls`` set to run at login, and they've
also set a preset to trigger at dawn, but they don't actually log in until just after dawn, the
overdue dawn preset will be triggered at login.
Presets - Smooth Transitions
----------------------------
In order to minimize writes to VDU NVRAM, the smooth transition of presets is deprecated
and is now normally disabled. Transitions can be enabled by disabling `protect-nvram`
in _Settings_.
A preset may be set to ``Transition Smoothly``, in which case changes to controls continuous-value
slider controls such as brightness and contrast will be stepped by one until the final values are
reached. Any non-continuous values will be set after all continuous values have reached their
final values, for example, if input-source is included in a preset, it will be restored at the end.
The Preset Dialog includes a combo-box for defining when to apply transitions to a preset:
- ``None`` - change immediately;
- ``On schedule`` - slowly change according to a solar elevation trigger;
- ``On signal`` - slowly change on the appropriate UNIX signal;
- ``On menu`` - slowly change when selected in the main-menu;
Normally a transition single-steps the controls as quickly as possible. In practice this means each
step takes one or more seconds and increases linearly depending on the number of VDUs and number of
controls being altered. The Presets Dialog includes a ``Transition Step seconds`` control that can
be used to increase the step interval and extend a transition over a longer period of time.
If any transitioning controls change independently of the transition, the transition will cease. In
that manner a transition can be abandoned by dragging a slider or choosing a different preset.
Presets - supplementary weather requirements
--------------------------------------------
A solar elevation trigger can have a weather requirement which will be checked against the weather
reported by https://wttr.in.
By default, there are three possible weather requirements: ``good``, ``bad``, and ``all weather``.
Each requirement is defined by a file containing a list of WWO (https://www.worldweatheronline.com)
weather codes, one per line. The three default requirements are contained in the files
``$HOME/.config/vdu_controls/{good,bad,all}.weather``. Additional weather requirements can be
created by using a text editor to create further files. The ``all.weather`` file exists primarily
as a convenient resource that lists all possible codes.
Because reported current weather conditions may be inaccurate or out of date, it's best to use
weather requirements as a coarse measure. Going beyond good and bad may not be very practical.
What's possible might depend on you local weather conditions.
To ensure ``wttr.in`` supplies the weather for your location, please ensure that ``Settings``
``Location`` includes a place-name suffix. The ``Settings`` ``Location`` ``Detect`` button has been
enhanced to fill out a place-name for you. Should ``wttr.in`` not recognise a place-name, the
place-name can be manually edited to something more suitable. The nearest big city or an
airport-code will do, for example: LHR, LAX, JFK. You can use a web browser to test a place-name,
for example: https://wttr.in/JFK
When weather requirements are in use, ``vdu_controls`` will check that the coordinates in
``Settings`` ``Location`` are a reasonable match for those returned from ``wttr.in``, a warning will
be issued if they are more than 200 km (124 miles) apart.
If the place-name is left blank, the ``wttr.in`` server will try to guess you location from your
external IP address. The guess may not be accurate and may vary over time.
Presets - remote control
------------------------
Scripts may use UNIX/Linux signals may be used to instruct a running ``vdu_controls`` to invoke a
preset or to initiate "Refresh settings from monitors". Signals in the range 40 to 55 correspond to
first to last presets (if any are defined). Additionally, SIGHUP can be used to initiate "Refresh
settings from monitors". For example:
Identify the running vdu_controls (assuming it is installed as /usr/bin/vdu_controls)::
ps axwww | grep '[/]usr/bin/vdu_controls'
Combine this with kill to trigger a preset change::
kill -40 $(ps axwww | grep '[/]usr/bin/vdu_controls' | awk '{print $1}')
kill -41 $(ps axwww | grep '[/]usr/bin/vdu_controls' | awk '{print $1}')
Or if some other process has changed a VDUs settings, trigger vdu_controls to update its UI::
kill -HUP $(ps axwww | grep '[/]usr/bin/vdu_controls' | awk '{print $1}')
Any other signals will be handled normally (in many cases they will result in process termination).
Ambient Light Levels and Light/Lux Metering
-------------------------------------------
The default UI includes an ``ambient-light slider`` which will simultaneously adjust all VDUs
according to custom per-VDU lux/brightness profiles. As well as indicating the ambient light
level manually via the slider, ``vdu_controls`` can be configured to periodically read from a
hardware lux metering device and adjust brightness automatically. The Lux-Dialog provides
controls for setting up light metering and VDU lux/brightness profiles. If ambient light level
controls are not required, the Settings Dialog includes an option to disable and hide them.
As well as the manual-slider, a metering device may be a serial-device, a UNIX FIFO (named-pipe),
or an executable (script or program):
- A serial-device must periodically supply one floating-point lux-value
terminated by a carriage-return newline.
- A FIFO must periodically supply one floating-point lux-value terminated by a newline.
- An executable must supply one floating-point lux-value reading terminated by a newline each time
it is run.
Possible hardware devices include:
- An Arduino with a GY-30/BH1750 lux meter writing to a usb serial-port.
- A webcam periodically sampled to produce approximate lux values. Values
might be estimated by analysing image content or image settings that
contribute to exposure, such ISO values, apertures, and shutter speed.
Further information on various lux metering options, as well as instructions for constructing and
programming an Arduino with a GY-30/BH1750, can be found at:
https://github.com/digitaltrails/vdu_controls/blob/master/Lux-metering.md
Example scripts for mapping a webcam's average-brightness to approximate lux values are included in
``/usr/share/vdu_controls/sample-scripts/`` or they can also be downloaded from the following
location:
https://github.com/digitaltrails/vdu_controls/tree/master/sample-scripts.
The examples include ``vlux_meter.py``, a beta-release Qt-GUI python-script that meters from a
webcam and writes to a FIFO (`$HOME/.cache/vlux_fifo`). Controls are included for mapping
image-brightness to lux mappings, and for defining a crop from which to sample brightness values.
The script optionally runs in the system-tray.
The examples may require customising for your own webcam and lighting conditions.
Lux Metering and brightness transitions
---------------------------------------
Due to VDU hardware and DDC protocol limitations, gradual transitions from one brightness level to
another are likely to be noticeable and potentially annoying. As well as being annoying
excessive stepping may eat into VDU NVRAM lifespan.
The auto-brightness adjustment feature includes several measures to reduce the number of
changes passed to the VDU:
- Lux/Brightness Profiles may be altered for local conditions so that
brightness levels remain constant over set ranges of lux values (night, day, and so forth).
- Adjustments are only made at intervals of one or more minutes (default is 10 minutes).
- The adjustment task passes lux values through a smoothing low-pass filter.
- A VDU brightness profile may optionally be set to stair-step with no interpolation
of intermediate values.
When ambient light conditions are fluctuating, for example, due to passing clouds, automatic adjust
can be manually suspended. The main-panel, main-menu, and light-metering dialog each contain controls for
toggling Auto/Manual. Additionally, moving the manual lux-slider turns off automatic adjustment.
The Light-metering dialog includes an option to enable auto-brightness interpolation. This option
will enable the calculation of values between steps in the profiles. In order to avoid small
fluctuating changes, interpolation won't result in brightness changes less than 10%. During
interpolation, if a lux value is found to be in proximity to any attached preset, the preset
values will be preferred over interpolated ones.
Light/Lux Metering and Presets
-------------------------------
The Light-Metering Dialog includes the ability to set a Preset to trigger at a lux value. This feature
is accessed by hovering under the bottom axis of the Lux Profile Chart.
When a preset is tied to a lux value, the preset's VDU brightness values become fixed points on the
Lux Profile Chart. When the specified metered lux value is achieved, the stepping process will
restore the preset's brightness values and then trigger the full restoration of the preset. This
ordering of events reduces the likelihood of metered-stepping, and preset-restoration from clashing.
A preset that does not include a VDU's brightness may be attached to a lux point to restore one or
more non-brightness controls. For example, on reaching a particular lux level, an attached preset
might restore a contrast setting.
If a preset is attached to a lux value and then detached, the preset's profile points will be
converted to normal (editable) profile points. Attach/detach is a quick way to copy VDU brightness
values from presets if you don't want to permanently attach them.
If you utilise light-metered auto-brightness and preset-scheduling together, their combined effects
may conflict. For example, a scheduled preset may set a reduced brightness, but soon after,
light-metering might increase it. If you wish to use the two together, design your lux/brightness
profile steps to match the brightness levels of specific presets, for example, a full-sun preset and
the matching step in a lux/brightness Profile might both be assigned the same brightness level.
Lux Metering Internal Parameters
--------------------------------
The following internal constants can be altered by manually editing
`~/.config/vdu_controls/AutoLux.conf`. They guide the various metering and auto-adjustment
heuristics::
[lux-meter]
# How many times per minute to sample from the Lux meter (for auto-adjustment)
samples-per-minute=3
# How many samples to include in the smoothing process
smoother-n=5
# How heavily should past values smooth the present value (smaller = more smoothing)
# See: https://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter
smoother-alpha=0.5
# If an interpolated value yields a change in brightness, how big should the change
# be to trigger an actual VDU change in brightness? Also determines how close
# an interpolated value needs to be to an attached preset's brightness in order
# to prefer triggering the preset over applying the interpolated value.
interpolation-sensitivity-percent=10
# Jump brightness in one step up to this maximum, after which transition in steps.
max-brightness-jump=100
Improving Response Time: Dynamic Optimization and Sleep Multipliers
-------------------------------------------------------------------
If you are using ``ddcutil`` version 2.0 or greater, ``vdu_controls`` will default to using the
``ddcutil`` *dynamic sleep optimiser*. The optimiser automatically tunes and caches VDU specific
timings when ever ``ddcutil`` is run. Any reliability-issues or errors may be automatically
resolved as the optimiser refines it's cached timings. Should problems persist, the
optimiser can be disabled by adding `--disable-dynamic-sleep` to the **ddcutil extra arguments** in
the **Settings Dialog** (either globally on the **vdu_controls tab** or selectively under each VDU's
tab). If dynamic sleep is disabled, multipliers can then be manually assigned. The optimiser's
heuristics continue to be refined, it may be that some issues may be resolved by moving to a more
recent version of ``libddcutil/ddcutil``.
For versions of ``ddcutil`` prior to 2.0, you can manually set the ``vdu_control``
``sleep-multiplier`` passed to ``ddcutil``. A sleep multiplier less than one will speed up the i2c
protocol interactions at the risk of increased protocol errors. The default sleep multiplier of 1.0
has to be quite conservative, many VDUs can cope with smaller multipliers. A bit of experimentation
with multiplier values may greatly speed up responsiveness. In a multi-VDU setup individual sleep
multipliers can be configured (see previous section).
Improving Response Time and Reliability: Connections and Controls
-----------------------------------------------------------------
``DDC/I2C`` is not a totally reliable form of communication. VDUs may vary in their responsiveness
and compliance. GPUs, GPU drivers, and types of connection may affect the reliability. Both ddcutil
and vdu_controls attempt to manage the reliability by using repetition and by adjusting timings.
If you have the choice, a ``DisplayPort`` to ``DisplayPort`` connection may be more reliable than
``DVI`` or ``HDMI``.
Reducing the number of enabled controls can speed up initialization, decrease the refresh time, and
reduce the time taken to restore presets.
There's plenty of useful info for getting the best out of ``ddcutil`` at https://www.ddcutil.com/.
Limitations
===========
Repeatably altering VDU settings might affect VDU lifespan, exhausting the NVRAM write
cycles, stressing the VDU power-supply, or increasing panel burn-in.
That said, ``vdu_controls`` does include a number of features that can be used
to reduce the overall frequency of adjustments to acceptable levels.
+ Inbuilt mitigations:
+ Slider and spin-box controls only update the VDU when adjustments become slow or stop (when
no change occurs in 0.5 seconds).
+ Preset restoration only updates the VDU values that differ from its current values.
+ Transitioning smoothly has been disabled by default and deprecated for version 2.1.0 onward.
+ Automatic ambient brightness adjustment only triggers a change when the proposed brightness
differs from the current brightness by at least 10%.
+ Electable mitigations:
+ Choose to restore pre-prepared 'presets' instead of dragging sliders.
+ Refrain from adding transitions to `presets`.
+ If using the ambient-light brightness response curves, tune the settings and
curves to minimize frequent small changes.
+ If using a light-meter, disengage metered automatic adjustment when faced with
rapidly fluctuating levels of ambient brightness.
+ Consider adjusting the ambient lighting instead of the VDU.
+ Monitoring to assist with making adjustments:
+ Hovering over a VDU name in the main window reveals a popup that includes
the number of VCP (NVRAM) writes.
+ The bottom of the About-dialog shows the same numbers. They update dynamically.
Other concerns
--------------
The power-supplies in some older VDUs may buzz/squeel audibly when the brightness is
turned way down. This may not be a major issue because, in normal surroundings,
older VDUs are often not usable below about 85-90% brightness.
Going beyond the standard DDC features by attempting to experiment with hidden
or undocumented features or values has the potential to make irreversible changes.
Some controls change the number of connected devices (for example, some VDUs support a power-off
command). If such controls are used, ``vdu_controls`` will detect the change and will reconfigure
the controls for the new situation (for example, DDC VDU 2 may now be DDC VDU 1). If you change
settings independently of ``vdu_controls``, for example, by using a VDU's physical controls, the
``vdu_controls`` UI includes a refresh button to force it to assess the new configuration.
Some VDU settings may disable or enable other settings in the VDU. For example, setting a VDU to a
specific picture-profile might result in the contrast-control being disabled, but ``vdu_controls``
will not be aware of the restriction resulting in its contrast-control erring or appearing to do
nothing.
If your VDUs support *picture-modes*, altering any controls in vdu_controls will most likely
result in the picture-mode being customised. For example, say you have selected the
VDU's *Vivid* picture-mode, if you use vdu_controls to change the brightness, it's likely
that this will now become the brightness for *Vivid* until the VDU is reset to its defaults.
To avoid confusion, it may be advisable to stick to one picture-mode for use with vdu_controls,
preserving the others unaltered.
The ``ddcutil-service`` has some ability to signal hot-plug events, such as connecting
a new VDU or powering one down. Not all GPU-drivers support an efficient means of event detection,
as a result there may be a delay of many seconds before ``ddcutil-service`` passes these events
to ``vdu_controls``.
Builtin laptop displays normally don't implement DDC and those displays are not supported, but a
laptop's externally connected VDUs are likely to be controllable.
Examples
========
vdu_controls
All default controls.
vdu_controls --show brightness --show contrast
Specified controls only:
vdu_controls --hide contrast --hide audio-volume
All default controls except for those to be hidden.
vdu_controls --system-tray --no-splash --show brightness --show audio-volume
Start as a system tray entry without showing the splash-screen.
vdu_controls --create-config-files --system-tray --no-splash --show brightness --show audio-volume
Create template config files in $HOME/.config/vdu_controls/ that include the other settings.
vdu_controls --enable-vcp-code 63 --enable-vcp-code 93 --warnings --debug
All default controls, plus controls for VCP_CODE 63 and 93, show any warnings, output debugging info.
This script often refers to displays and monitors as VDUs in order to disambiguate the noun/verb
duality of "display" and "monitor"
Prerequisites
=============
Described for OpenSUSE, similar for other distros:
Software::
zypper install python3 python3-qt5 noto-sans-math-fonts noto-sans-symbols2-fonts
zypper install ddcutil
zypper install libddcutil ddcutil-service # optional, but recommended if available
If you wish to use a serial-port lux metering device, the ``pyserial`` module is a runtime requirement.
Get ddcutil working first. Check that the detect command detects your VDUs without issuing any
errors:
ddcutil detect
Read ddcutil documentation concerning config of i2c_dev with nvidia GPUs. Detailed ddcutil info
at https://www.ddcutil.com/
Environment
===========
LC_ALL, LANG, LANGUAGE
These variables specify the locale for language translations and units of distance.
LC_ALL is used by python, LANGUAGE is used by Qt. Normally, they should all have the same
value, for example: ``Da_DK``. For these to have any effect on language, ``Settings``
``Translations Enabled`` must also be enabled.
VDU_CONTROLS_UI_IDLE_SECS
The length of pause in slider or spin-box control motion that triggers commit of
the controls value to the VDU. This is a precautionary throttle in case frequently
updating a VDU might shorten its lifespan. The default is 0.5 seconds.
VDU_CONTROLS_IPINFO_URL
Override the default ip-address to location service URL (``https://ipinfo.io/json``).
VDU_CONTROLS_WTTR_URL
Override the default weather service URL (``https://wttr.in``).
VDU_CONTROLS_WEATHER_KM
Override the default maximum permissible spherical distance (in kilometres)
between the ``Settings`` ``Location`` and ``wttr.in`` reported location (``200 km``, 124 miles).
VDU_CONTROLS_DDCUTIL_ARGS
Add to the list of arguments passed to each exec of ddcutil.
VDU_CONTROLS_DDCUTIL_RETRIES
Set the number of times to repeat a ddcutil getvcp or setvcp before returning an error.
VDU_CONTROLS_DEVELOPER
Changes some search paths to be more convenient in a development
scenario. (``no`` or yes)
VDU_CONTROLS_DBUS_TIMEOUT_MILLIS
Dbus call wait timeout. Default is 10000, 10 seconds.
Files
=====
$HOME/.config/vdu_controls/
Location for config files, Presets, and other persistent data.
$HOME/.config/vdu_controls/tray_icon.svg
If present, this file is the preferred source for the system-tray icon. It can be used if the normal
icon conflicts with the desktop theme. If the ``Settings`` ``monochrome-tray``
and ``mono-light-tray`` are enabled, they are applied to the file when it is read.
$HOME/.config/vdu_controls/translations/
Location for user supplied translations.
$HOME/.config/vdu_controls.qt.state/
Location for Qt/desktop state such as the past window sizes and locations.
/usr/share/vdu_controls
Location for system-wide icons, sample-scripts, and translations.
Reporting Bugs
==============
https://github.com/digitaltrails/vdu_controls/issues
GNU License
===========
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see https://www.gnu.org/licenses/.
"""
# vdu_controls Copyright (C) 2021 Michael Hamilton
from __future__ import annotations
import argparse
import base64
import configparser
import glob
import inspect
import io
import json
import locale
import math
import os
import pathlib
import queue
import random
import re
import select
import signal
import socket
import stat
import subprocess
import sys
import syslog
import termios
import textwrap
import threading
import time
import traceback
import urllib.request
from abc import abstractmethod
from ast import literal_eval
from collections import namedtuple
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from enum import Enum, IntFlag
from functools import partial
from importlib import import_module
from pathlib import Path
from threading import Lock
from typing import List, Tuple, Mapping, Type, Dict, Callable, Any, NewType
from urllib.error import URLError
from PyQt5 import QtCore
from PyQt5 import QtNetwork
from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal, QProcess, QRegExp, QPoint, QObject, QEvent, \
QSettings, QSize, QTimer, QTranslator, QLocale, QT_TR_NOOP, QVariant, pyqtSlot, QMetaType, QDir
from PyQt5.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage, QDBusArgument, QDBusVariant
from PyQt5.QtGui import QPixmap, QIcon, QCursor, QImage, QPainter, QRegExpValidator, \
QPalette, QGuiApplication, QColor, QValidator, QPen, QFont, QFontMetrics, QMouseEvent, QResizeEvent, QKeySequence, QPolygon
from PyQt5.QtSvg import QSvgWidget, QSvgRenderer
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QSlider, QMessageBox, QLineEdit, QLabel, \
QSplashScreen, QPushButton, QProgressBar, QComboBox, QSystemTrayIcon, QMenu, QStyle, QTextEdit, QDialog, QTabWidget, \
QCheckBox, QPlainTextEdit, QGridLayout, QSizePolicy, QAction, QMainWindow, QToolBar, QToolButton, QFileDialog, \
QWidgetItem, QScrollArea, QGroupBox, QFrame, QSplitter, QSpinBox, QDoubleSpinBox, QInputDialog, QStatusBar, qApp, QShortcut, \
QDesktopWidget, QSpacerItem
APPNAME = "VDU Controls"
VDU_CONTROLS_VERSION = '2.1.3'
VDU_CONTROLS_VERSION_TUPLE = tuple(int(i) for i in VDU_CONTROLS_VERSION.split('.'))
assert sys.version_info >= (3, 8), f'{APPNAME} utilises python version 3.8 or greater (your python is {sys.version}).'
WESTERN_SKY = 'western-sky'
EASTERN_SKY = 'eastern-sky'
IP_ADDRESS_INFO_URL = os.getenv('VDU_CONTROLS_IPINFO_URL', default='https://ipinfo.io/json')
WEATHER_FORECAST_URL = os.getenv('VDU_CONTROLS_WTTR_URL', default='https://wttr.in')
TESTING_TIME_ZONE = os.getenv('VDU_CONTROLS_TEST_TIME_ZONE') # for example 'Europe/Berlin' 'Asia/Shanghai'
TIME_CLOCK_SYMBOL = '\u25F4' # WHITE CIRCLE WITH UPPER LEFT QUADRANT
WEATHER_RESTRICTION_SYMBOL = '\u2614' # UMBRELLA WITH RAIN DROPS
TOO_HIGH_SYMBOL = '\u29BB' # CIRCLE WITH SUPERIMPOSED X
DEGREE_SYMBOL = '\u00B0' # DEGREE SIGN
SUN_SYMBOL = '\u2600' # BLACK SUN WITH RAYS
WEST_ELEVATION_SYMBOL = '\u29A9' # MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND RIGHT
EAST_ELEVATION_SYMBOL = '\u29A8' # MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND LEFT
TIMER_RUNNING_SYMBOL = '\u23F3' # HOURGLASS WITH FLOWING SAND
WEATHER_CANCELLATION_SYMBOL = '\u2744' # SNOWFLAKE
SKIPPED_SYMBOL = '\u2718' # HEAVY BALLOT X
SUCCESS_SYMBOL = '\u2714' # CHECKMARK
PRESET_APP_SEPARATOR_SYMBOL = '\u2014' # EM DASH
MENU_SYMBOL = '\u2630' # TRIGRAM FOR HEAVEN - hamburger menu
TRANSITION_SYMBOL = '\u25b9' # WHITE RIGHT POINTING SMALL TRIANGLE
TRANSITION_ALWAYS_SYMBOL = '\u25b8\u2732' # BLACK RIGHT POINTING SMALL TRIANGLE + OPEN CENTER ASTERIX
PROCESSING_LUX_SYMBOL = '\u25b9' * 3 # WHITE RIGHT POINTING SMALL TRIANGLE * 3
SIGNAL_SYMBOL = '\u26a1' # HIGH VOLTAGE - lightning bolt
ERROR_SYMBOL = '\u274e' # NEGATIVE SQUARED CROSS MARK
WARNING_SYMBOL = '\u26a0' # WARNING SIGN
ALMOST_EQUAL_SYMBOL = '\u2248' # ALMOST EQUAL TO
SMOOTHING_SYMBOL = '\u21dd' # RIGHT POINTING SQUIGGLY ARROW
STEPPING_SYMBOL = '\u279f' # DASHED TRIANGLE-HEADED RIGHTWARDS ARROW
RAISED_HAND_SYMBOL = '\u270b' # RAISED HAND
RIGHT_POINTER_WHITE = '\u25B9' # WHITE RIGHT-POINTING SMALL TRIANGLE
RIGHT_POINTER_BLACK = '\u25B8' # BLACK RIGHT-POINTING SMALL TRIANGLE
MENU_ACTIVE_PRESET_SYMBOL = '\u25c2' # BLACK LEFT-POINTING SMALL TRIANGLE
SET_VCP_SYMBOL = "\u25B7" # WHITE RIGHT-POINTING TRIANGLE
SolarElevationKey = namedtuple('SolarElevationKey', ['direction', 'elevation'])
SolarElevationData = namedtuple('SolarElevationData', ['azimuth', 'zenith', 'when'])
Shortcut = namedtuple('Shortcut', ['letter', 'annotated_word'])
gui_thread: QThread | None = None
def is_running_in_gui_thread() -> bool:
return QThread.currentThread() == gui_thread
def zoned_now(rounded_to_minute: bool = False) -> datetime:
now = datetime.now().astimezone()
if TESTING_TIME_ZONE is not None: # This is a testing only path that requires python > 3.8
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo(TESTING_TIME_ZONE)) # for testing scheduling
return (now + timedelta(seconds=30)).replace(second=0, microsecond=0) if rounded_to_minute else now
def format_solar_elevation_abbreviation(elevation: SolarElevationKey) -> str:
direction_char = EAST_ELEVATION_SYMBOL if elevation.direction == EASTERN_SKY else WEST_ELEVATION_SYMBOL
return f"{SUN_SYMBOL} {direction_char} {elevation.elevation}{DEGREE_SYMBOL}"
def format_solar_elevation_description(elevation: SolarElevationKey) -> str:
# Note - repeating the constants here to force them to be included by pylupdate5 internationalisation
direction_text = tr('eastern-sky') if elevation.direction == EASTERN_SKY else tr('western-sky')
return f"{direction_text} {elevation.elevation}{DEGREE_SYMBOL}"
def format_solar_elevation_ini_text(elevation: SolarElevationKey | None) -> str:
return f"{elevation.direction} {elevation.elevation}" if elevation is not None else ''
def parse_solar_elevation_ini_text(ini_text: str) -> SolarElevationKey:
parts = ini_text.strip().split()
if len(parts) != 2:
raise ValueError(f"Invalid SolarElevation: '{ini_text}'")
if parts[0] not in [EASTERN_SKY, WESTERN_SKY]:
raise ValueError(f"Invalid value for SolarElevation direction: '{parts[0]}'")
solar_elevation = SolarElevationKey(parts[0], int(parts[1]))
return solar_elevation
def proper_name(*args) -> str:
return re.sub(r'[^A-Za-z0-9._-]', '_', '_'.join([arg.strip() for arg in args]))
def tr(source_text: str, disambiguation: str | None = None) -> str:
"""
This function is named tr() so that it matches what pylupdate5 is looking for.
If this method is ever renamed to something other than tr(), then you must
pass -ts-function=new_name to pylupdate5.
For future internationalization:
1) Generate template file from this code, for example for French:
ALWAYS BACKUP THE CURRENT .ts FILE BEFORE RUNNING AN UPDATE - it can go wrong!
pylupdate5 vdu_controls.py -ts translations/fr_FR.ts
where translations is a subdirectory of your current working directory.
2) Edit that using a text editor or the linguist-qt5 utility.
If using an editor, remove the 'type="unfinished"' as you complete each entry.
3) Convert the .ts to a binary .qm file
lrelease-qt5 translations/fr_FR.ts
mkdir -p $HOME/.config/vdu_controls/translations/
translations/fr_FR.qm $HOME/.config/vdu_controls/translations/
4) Test using by setting LC_ALL for python and LANGUAGE for Qt
LC_ALL=fr_FR LANGUAGE=fr_FR python3 vdu_controls.py
At startup the app will log several messages as it searches for translation files.
5) Completed .qm files can reside in $HOME/.config/vdu_controls/translations/
or /user/share/vdu_controls/translations/
"""
# If the source .ts file is newer, we load messages from the XML into ts_translations
# and use the most recent translations. Using the .ts files in production may be a good
# way to allow the users to help themselves.
if ts_translations:
if source_text in ts_translations:
return ts_translations[source_text]
# the context @default is what is generated by pylupdate5 by default
return QCoreApplication.translate('@default', source_text, disambiguation=disambiguation)
def translate_option(option_text) -> str:
# We can't be sure of the case in capability descriptions retrieved from the monitors.
# If there is no direct translation, we try canonical version of the name (all lowercase with '-' replaced with ' ').
if (translation := tr(option_text)) != option_text: # Probably a command line option
return translation
canonical = option_text.lower().replace('-', ' ')
return tr(canonical)
ABOUT_TEXT = f"""
<b>vdu_controls version {VDU_CONTROLS_VERSION}</b>
<p>
A virtual control panel for external Visual Display Units.
<p>
Visit <a href="https://github.com/digitaltrails/vdu_controls">https://github.com/digitaltrails/vdu_controls</a> for
more details.
<p>
Release notes: <a href="https://github.com/digitaltrails/vdu_controls/releases/tag/v{VDU_CONTROLS_VERSION}">
v{VDU_CONTROLS_VERSION}.</a>
<p>
<hr>
<small>
<b>vdu_controls Copyright (C) 2021 Michael Hamilton</b>
<br><br>
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, version 3.
<br><br>
<bold>
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
</bold>
<br><br>
You should have received a copy of the GNU General Public License along
with this program. If not, see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.
</small>
<hr>
<p><p>
<quote>
<small>
Vdu_controls relies on <a href="https://www.ddcutil.com/">ddcutil</a>, a robust interface to DDC capable VDUs.
<br>
At your request, your geographic location may be retrieved from <a href="{IP_ADDRESS_INFO_URL}">{IP_ADDRESS_INFO_URL}</a>.
<br>
At your request, weather for your location may be retrieved from <a href="{WEATHER_FORECAST_URL}">{WEATHER_FORECAST_URL}</a>.
</small>
</quote>
"""
RELEASE_ANNOUNCEMENT = """
<h3>{WELCOME}</h3>
{NOTE}<br>
<a href="https://github.com/digitaltrails/vdu_controls/releases/tag/v{VERSION}">
https://github.com/digitaltrails/vdu_controls/releases/tag/v{VERSION}</a>