Browse Source

add AutoBrightness page

Thomas B 3 months ago
parent
commit
090f76d3ea

+ 176
- 0
input/projects/auto_brightness.md View File

@@ -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/>.

BIN
static/img/autobrightness_ldr_1.jpg View File


BIN
static/img/autobrightness_ldr_1_small.jpg View File


BIN
static/img/autobrightness_ldr_2.jpg View File


BIN
static/img/autobrightness_ldr_2_small.jpg View File


BIN
static/img/autobrightness_pcb_1.jpg View File


BIN
static/img/autobrightness_pcb_1_small.jpg View File


BIN
static/img/autobrightness_pcb_2.jpg View File


BIN
static/img/autobrightness_pcb_2_small.jpg View File


Loading…
Cancel
Save