|
@@ -0,0 +1,176 @@
|
|
1
|
+title: AutoBrightness
|
|
2
|
+description: USB ambient light sensor for DDC/CI backlight control
|
|
3
|
+parent: projects
|
|
4
|
+git: https://git.xythobuz.de/thomas/AutoBrightness
|
|
5
|
+github: https://github.com/xythobuz/AutoBrightness
|
|
6
|
+date: 2024-09-07
|
|
7
|
+comments: true
|
|
8
|
+---
|
|
9
|
+
|
|
10
|
+One of my two ~10 year old 23" 1080p main displays died recently.
|
|
11
|
+So I finally upgraded to two used 27" 1440p displays.
|
|
12
|
+These are now the first displays on my desktop that allow adjustments of the backlight from software.
|
|
13
|
+So I tried to find out how to go about that.
|
|
14
|
+
|
|
15
|
+Turns out on laptops both the display backlight intensity and the ambient light sensors are controlled via ACPI, with proper kernel support already available (see eg. the [Arch Wiki](https://wiki.archlinux.org/title/Backlight)).
|
|
16
|
+
|
|
17
|
+But on desktops no standard for ambient light sensors seems to be established.
|
|
18
|
+Instead of ACPI, the backlight of some desktop monitors can be controlled using [DDC/CI](https://en.wikipedia.org/wiki/Display_Data_Channel#DDC/CI).
|
|
19
|
+
|
|
20
|
+There are some projects, both [hardware](https://www.yoctopuce.com/EN/products/usb-environmental-sensors/yocto-light-v3) and [software](https://github.com/FedeDP/Clight), available for this already.
|
|
21
|
+But as usual I decided to make my own.
|
|
22
|
+
|
|
23
|
+## Prototype Hardware
|
|
24
|
+
|
|
25
|
+Initially I tought I could just go the most simple route and use an LDR on the ADC of an MCU.
|
|
26
|
+I already had a [Digispark Rev. 3 clone](https://www.az-delivery.de/en/products/digispark-board), LDR, resistor and potentiometer on hand.
|
|
27
|
+
|
|
28
|
+But deep down I already knew this would not be good.
|
|
29
|
+
|
|
30
|
+<!--%
|
|
31
|
+lightgallery([
|
|
32
|
+ [ "img/autobrightness_ldr_1.jpg", "Front view of AutoBrightness prototype" ],
|
|
33
|
+ [ "img/autobrightness_ldr_2.jpg", "Back view of AutoBrightness prototype" ],
|
|
34
|
+])
|
|
35
|
+%-->
|
|
36
|
+
|
|
37
|
+The range of LDRs is far too big to easily measure the human eye dynamic range with an ADC.
|
|
38
|
+You can extend the range by switching different resistor values into your voltage divider using GPIOs, but I didn't want to go that far.
|
|
39
|
+Instead I added a 1M potentiometer to manually adjust the measurement range.
|
|
40
|
+
|
|
41
|
+This gave me an opportunity to play around with integer low pass filters using bit shifts, as described [here](https://www.infineon.com/dgdl/Infineon-AN2099_PSoC_1_PSoC_3_PSoC_4_and_PSoC_5LP_Single_Pole_Infinite_Impulse_Response_%28IIR%29_Filters-ApplicationNotes-v11_00-EN.pdf?fileId=8ac78c8c7cdc391c017d072cde6e51bd) (which I got from [here](https://stackoverflow.com/a/38927630)).
|
|
42
|
+
|
|
43
|
+So as suspected, the resulting values were not able to measure both a dark room at night and a sunny day.
|
|
44
|
+
|
|
45
|
+## Proper Hardware
|
|
46
|
+
|
|
47
|
+To get usable values I had to use a "real" sensor.
|
|
48
|
+
|
|
49
|
+The final hardware is just a [Digispark Rev. 3 clone](https://www.az-delivery.de/en/products/digispark-board) with a [GY-302 breakout board (for the BH1750 sensor)](https://www.az-delivery.de/en/products/gy-302-bh1750-lichtsensor-lichtstaerke-modul-fuer-arduino-und-raspberry-pi) connected to it.
|
|
50
|
+
|
|
51
|
+<!--%
|
|
52
|
+lightgallery([
|
|
53
|
+ [ "img/autobrightness_pcb_1.jpg", "Front view of AutoBrightness device" ],
|
|
54
|
+ [ "img/autobrightness_pcb_2.jpg", "Back view of AutoBrightness device" ],
|
|
55
|
+])
|
|
56
|
+%-->
|
|
57
|
+
|
|
58
|
+The [BH1750](https://www.mouser.com/datasheet/2/348/bh1750fvi-e-186247.pdf) is a nice small ambient light sensor and very easy to use.
|
|
59
|
+This is literally the whole driver I wrote.
|
|
60
|
+
|
|
61
|
+<pre class="sh_c">
|
|
62
|
+void luxInit(void) {
|
|
63
|
+ twiWrite(LUX_ADDR, OP_POWER_ON); // reset registers
|
|
64
|
+ twiWrite(LUX_ADDR, OP_CONT_0_5X); // continuous measurement at 0.5lx resolution
|
|
65
|
+}
|
|
66
|
+
|
|
67
|
+uint16_t luxGet(void) {
|
|
68
|
+ uint16_t val = twiRead(LUX_ADDR); // read measurement
|
|
69
|
+ return val;
|
|
70
|
+}
|
|
71
|
+</pre>
|
|
72
|
+
|
|
73
|
+## USB Communication
|
|
74
|
+
|
|
75
|
+The Digispark has the USB D+ and D- signals directly connected to GPIOs of the AtTiny85.
|
|
76
|
+So the USB protocol is bit-banged using the [V-USB library](https://github.com/obdev/v-usb).
|
|
77
|
+Because I did not use the Arduino Cores already available, I had to do some [fiddling](https://git.xythobuz.de/thomas/AutoBrightness/commit/d50da00006edd87d9363d83befc8eb5bc9274fb5) to configure the library properly for this device.
|
|
78
|
+
|
|
79
|
+The code is based on the [custom-class example](https://github.com/obdev/v-usb/tree/master/examples/custom-class) from V-USB.
|
|
80
|
+This abuses USB control transfers to transmit data.
|
|
81
|
+
|
|
82
|
+On the PC side I'm using [PyUSB](https://github.com/pyusb/pyusb) instead of going to libusb directly, as in the example.
|
|
83
|
+
|
|
84
|
+<pre class="sh_python">
|
|
85
|
+CUSTOM_RQ_GET = 2 # get ldr value
|
|
86
|
+
|
|
87
|
+def is_target_device(dev):
|
|
88
|
+ if dev.manufacturer == "xythobuz.de" and dev.product == "AutoBrightness":
|
|
89
|
+ return True
|
|
90
|
+ return False
|
|
91
|
+
|
|
92
|
+dev = usb.core.find(idVendor=0x16c0, idProduct=0x05dc, custom_match=is_target_device)
|
|
93
|
+dev.set_configuration()
|
|
94
|
+
|
|
95
|
+r = dev.ctrl_transfer(usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_IN, CUSTOM_RQ_GET, 0, 0, 2)
|
|
96
|
+val = int.from_bytes(r, "little")
|
|
97
|
+</pre>
|
|
98
|
+
|
|
99
|
+To run this without root permissions you need to add a udev rule (in eg. `/etc/udev/rules.d/49-autobrightness.rules`).
|
|
100
|
+
|
|
101
|
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05dc", ATTRS{manufacturer}=="xythobuz.de", ATTRS{product}=="AutoBrightness", MODE:="0666"
|
|
102
|
+
|
|
103
|
+I'm using the shared V-USB vendor and product IDs, so I [have to](https://github.com/obdev/v-usb/blob/master/usbdrv/USB-IDs-for-free.txt) always do the matching using my manufacturer and product strings as well.
|
|
104
|
+
|
|
105
|
+## Prototype Client
|
|
106
|
+
|
|
107
|
+With the hardware side out of the way the next step was adjusting the display brightness.
|
|
108
|
+I made a [short prototype](https://git.xythobuz.de/thomas/AutoBrightness/commit/6fcab3b981bb5705028e1dd0f3b52e4eed609253) using [ddcutil](https://www.ddcutil.com/) to set the values.
|
|
109
|
+
|
|
110
|
+To calculate the resulting values I made some measurements at midday (~500 lux) and night (~50 lux).
|
|
111
|
+And I thought about my habits (the MSI display seems ~10% brighter than the HP).
|
|
112
|
+
|
|
113
|
+<pre class="sh_python">
|
|
114
|
+c_in = 0.6, -60.0, # in_a, in_b
|
|
115
|
+calibration = {
|
|
116
|
+ "HPN:HP 27xq:CNK1072BJY": [
|
|
117
|
+ 1.0, 30.0, # out_a, out_b
|
|
118
|
+ ],
|
|
119
|
+
|
|
120
|
+ "MSI:MSI G27CQ4:": [
|
|
121
|
+ 1.0, 20.0, # out_a, out_b
|
|
122
|
+ ],
|
|
123
|
+}
|
|
124
|
+
|
|
125
|
+def cal(v, c):
|
|
126
|
+ # out = out_b + out_a * in_a * max(0, in_b + in)
|
|
127
|
+ return c[1] + c[0] * c_in[0] * max(0, c_in[1] + v)
|
|
128
|
+</pre>
|
|
129
|
+
|
|
130
|
+This simple formula gives surprisingly good results.
|
|
131
|
+To avoid noticable noisy changes I do some simple low-pass filtering of the sensor values.
|
|
132
|
+
|
|
133
|
+<pre class="sh_python">
|
|
134
|
+filter_fact = 0.9
|
|
135
|
+
|
|
136
|
+def filter_lux(old, new):
|
|
137
|
+ return (old * filter_fact) + (new * (1.0 - filter_fact))
|
|
138
|
+</pre>
|
|
139
|
+
|
|
140
|
+All this just runs once per second.
|
|
141
|
+
|
|
142
|
+Unfortunately, using ddcutil to adjust the brightness causes a noticable stutter of the whole system each time the value is changed.
|
|
143
|
+So this is not a good long-term solution.
|
|
144
|
+
|
|
145
|
+## Proper Client
|
|
146
|
+
|
|
147
|
+My initial idea for the client was to use the ambient light sensor to also "calibrate" the two displays to each other.
|
|
148
|
+To do this, a white square could be shown on both screens.
|
|
149
|
+Then the sensor can be placed in front of each display to measure their brightness "ramps".
|
|
150
|
+This could then give the `calibration` dictionary shown above.
|
|
151
|
+
|
|
152
|
+To determine the `c_in` values the room brightness has to be measured at day and night.
|
|
153
|
+
|
|
154
|
+This should automate the process I've done manually to determine the calibration values.
|
|
155
|
+
|
|
156
|
+But as you may have noticed, I'm more the prototype kind of guy and don't really do finished products on here.
|
|
157
|
+So...
|
|
158
|
+
|
|
159
|
+**To Do** 😅
|
|
160
|
+
|
|
161
|
+## License
|
|
162
|
+<a class="anchor" name="license"></a>
|
|
163
|
+
|
|
164
|
+The [AutoBrightness project](https://git.xythobuz.de/thomas/AutoBrightness) is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
|
165
|
+
|
|
166
|
+ This program is free software: you can redistribute it and/or modify
|
|
167
|
+ it under the terms of the GNU General Public License as published by
|
|
168
|
+ the Free Software Foundation, either version 3 of the License, or
|
|
169
|
+ (at your option) any later version.
|
|
170
|
+
|
|
171
|
+ This program is distributed in the hope that it will be useful,
|
|
172
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
173
|
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
174
|
+ GNU General Public License for more details.
|
|
175
|
+
|
|
176
|
+ See <http://www.gnu.org/licenses/>.
|