Picked up a BenQ RD280UA monitor some time ago. This monitor supports BenQs MoonHalo, Eye Care etc. BenQ offers the Display Pilot 2 software for controlling the monitor from a PC. But this software is unfortunately currently not available on Linux, only on Mac and Windows.
As I suspected that I2C was used for the DDC communication to the monitor and because I had an I2C Driver unit lying around, I took some inspiration from the ddcutil sniffing page to see if there was any I2C traffic flowing when running the Pilot 2 software on a Windows PC. To make the setup a bit easier I obtained a HDMI breakout board from Aliexpress for the purpose.
The breakout board and and the I2C Driver can be seen in the image above. The GND line on the I2C Driver is connected to pin 17 on the HDMI breakout board and the SCL and SDA lines to pin 15 and 16 respectively.
The setup worked very well. The breakout board with the I2C Driver connected caused no issues with the HDMI signal. No problem with 4K and HDR.
After some trials I was able to see some traffic using the I2C Driver capture script by running the command
$ python3 samples/capture.py /dev/ttyUSB0
This gave output in CSV format like this:
STOP,,,
START,WRITE,55,ACK
BYTE,WRITE,81,ACK
BYTE,WRITE,130,ACK
BYTE,WRITE,1,ACK
BYTE,WRITE,217,ACK
BYTE,WRITE,101,ACK
STOP,,,
START,READ,55,ACK
BYTE,READ,110,ACK
BYTE,READ,136,ACK
BYTE,READ,2,ACK
BYTE,READ,0,ACK
Reformatting, using a throwaway script, makes the output bit easier to read:
WRITE 0x37 0x51 0x82 0x01 0xd1 0x6d
READ 0x37 0x6e 0x88 0x02 0x00 0xd1 0x00 0x00 0x02 0x00 0x00 0x67
WRITE 0x37 0x51 0x82 0x01 0x62 0xde
READ 0x37 0x6e 0x88 0x02 0x00 0x62 0x00 0x00 0x32 0x00 0x00 0xe4
WRITE 0x37 0x51 0x82 0x01 0x8d 0x31
READ 0x37 0x6e 0x88 0x02 0x00 0x8d 0x00 0x00 0x02 0x00 0x01 0x3a
WRITE 0x37 0x51 0x84 0x03 0xd7 0x02 0x20 0x4d
WRITE 0x37 0x51 0x84 0x03 0xd7 0x02 0x10 0x7d
WRITE 0x37 0x51 0x82 0x01 0xdc 0x60
READ 0x37 0x6e 0x88 0x02 0x00 0xdc 0x00 0x00 0x32 0x00 0x12 0x48
WRITE 0x37 0x51 0x82 0x01 0xd9 0x65
READ 0x37 0x6e 0x88 0x02 0x00 0xd9 0x00 0x07 0x0a 0x03 0x08 0x6b
WRITE 0x37 0x51 0x82 0x01 0xd7 0x6b
READ 0x37 0x6e 0x88 0x02 0x00 0xd7 0x00 0x02 0x31 0x02 0x10 0x42
WRITE 0x37 0x51 0x82 0x01 0xe2 0x5e
A pattern emerged quite soon. First a WRITE line with 6 bytes
followed by a READ line with 12 bytes. This sequence went on and on
as long as I didn't do anything in the Pilot 2 software. But as soon I
pressed a button, f.ex. the button for turning the MoonHalo light
on, there would be a WRITE line with 8 bytes in the output. This
WRITE line was not followed by a READ line.
Basically it turns out that the 8 byte WRITE lines are commands to
the monitor and the 6 bytes WRITE lines with corresponding READ
lines are "status/query" commands reading the current settings from
the monitor.
The WRITE line for switching the MoonHalo light on and off turned
out to the following two commands:
0x37 0x51 0x84 0x03 0xd7 0x02 0x20 0x4d On
0x37 0x51 0x84 0x03 0xd7 0x02 0x10 0x7d Off
To verify this the i2ctransfer command can be used. To switch the
light on:
$ i2ctransfer -y 12 w7@0x37 0x51 0x84 0x03 0xd7 0x02 0x20 0x4d
and off:
$ i2ctransfer -y 12 w7@0x37 0x51 0x84 0x03 0xd7 0x02 0x10 0x7d
(The i2ctransfer command is typically located in the /usr/sbin/
directory, which might not be present in the PATH environment
variable. In order to be able to run the i2ctransfer command as a
normal user, the user must be a member of the i2c group.)
The -y 12 parameter refers to the I2C bus the monitor is connected
to. This address can be found by running the ddcutil detect command:
ddcutil detect
Display 1
I2C bus: /dev/i2c-12
DRM_connector: card0-DP-2
EDID synopsis:
Mfg id: BNQ - UNK
Model: BenQ RD280UA
...
To see what I2C devices that are connected to this bus:
$ /usr/sbin/i2cdetect -y 12
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: 30 -- -- -- -- -- -- 37 -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- 4a 4b -- -- -- --
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
It turns out the at address 0x37 is the address used for monitor
control. The address is not included in the length of a I2C command,
which is why the length given to i2ctransfer is set to w7@.
Something that can be seen from the
$ i2ctransfer -y 12 w7@0x37 0x51 0x84 0x03 0xd7 0x02 0x10 0x7d
command, is that the last byte must be some sort of checksum. After some
fumbling it turns out that if you take the XOR of all the values you
get the 0x59 value.
0x37 0x51 0x84 0x03 0xd7 0x02 0x10 0x7d => 0x59
And by replacing the last byte with 0x59 you will get 0x7d back again.
This proved to be true for all the collected commands and makes it easy to both calculate and to verify the checksum value.
To calculate the XOR value the calc-crc.py
script can be used. Example:
$ ./scripts/calc-crc.py 0x37 0x51 0x84 0x03 0xd7 0x02 0x20 0x59
Checksum value: 0x4d
The 8 byte long commands follows the same structure (here the command for turning on the MoonHalo light):
| address | command | checksum |
|---|---|---|
| 0x37 | 0x51 0x84 0x03 0xd7 0x02 0x20 | 0x4d |
Below follows an overview of all the found I2C sequences that triggers a change on the monitor. The checksum byte is removed from the entries as this value can be calculated anyway. The organization of the commands follows the same structure as the menu on the monitor when using the monitor controls.
For convenience a full list of commands with checksum added can be seen here.
| Input | |
|---|---|
| 0x37 0x51 0x84 0x03 0x60 0x00 0x13 | USB-C |
| 0x37 0x51 0x84 0x03 0x60 0x00 0x11 | HDMI |
| 0x37 0x51 0x84 0x03 0x60 0x00 0x0f | DP |
| Switch | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd7 0x02 0x20 | On |
| 0x37 0x51 0x84 0x03 0xd7 0x02 0x10 | Off |
| Light Mode | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd7 0x01 0x20 | 270° |
| 0x37 0x51 0x84 0x03 0xd7 0x02 0x20 | 360° |
| Brightness | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd9 0x03 [0x01 ... 0x0a] | 1 ... 10 |
| Color Temperature | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd9 [0x01 ... 0x07] 0x07 | 1 ... 7 |
| Activation Time | |
|---|---|
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x00 | Off |
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x05 | 5 sec |
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x0a | 10 sec |
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x14 | 20 sec |
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x1e | 30 sec |
| 0x37 0x51 0x84 0x03 0xe7 0x00 0x3c | 60 sec |
| Sensor Sensitivity | |
|---|---|
| 0x37 0x51 0x84 0x03 0xe9 0x00 0x01 | Near |
| 0x37 0x51 0x84 0x03 0xe9 0x00 0x02 | Middle |
| 0x37 0x51 0x84 0x03 0xe9 0x00 0x03 | Far |
| Switch | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd1 0x00 0x01 | On |
| 0x37 0x51 0x84 0x03 0xd1 0x00 0x00 | Off |
| 0x37 0x51 0x84 0x03 0xd1 0x00 0x02 | Auto |
| Level | |
|---|---|
| 0x37 0x51 0x84 0x03 0xd0 0x00 [0x01 ... 0x0a] | 1 ... 10 |
| Value | |
|---|---|
| 0x37 0x51 0x84 0x03 0x19 0x00 [0x00 ... 0x05] | 0 ... 5 |
| On/Off | |
|---|---|
| 0x37 0x51 0x84 0x03 0xe2 0x00 0xff | On |
| 0x37 0x51 0x84 0x03 0xe2 0x00 0x00 | Off |
| Light Meter | |
|---|---|
| 0x37 0x51 0x84 0x03 0xe3 0x00 0x01 | On |
| 0x37 0x51 0x84 0x03 0xe3 0x00 0x00 | Off |
| Sensor Sensitivity | |
|---|---|
| 0x37 0x51 0x84 0x03 0xe5 0x00 [0x00 ... 0x0a] | 0 ... 10 |
| Color Weakness | ||
|---|---|---|
| 0x37 0x51 0x84 0x03 0xfd [0x00 ... 0x0a] 0x03 | Red | 0 ... 10 |
| 0x37 0x51 0x84 0x03 0xfd [0x00 ... 0x0a] 0x04 | Green | 0 ... 10 |
For Color Weakness either the Red or the Green scale can be changed. That is if the red color is set to a value greater than zero then the green color will have zero as its value.
| Mode | |
|---|---|
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x31 | Coding - Dark Theme |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x30 | Coding - Light Theme |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x0f | M-Book |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x32 | Cinema |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x1f | ePaper |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x0a | sRGB |
| 0x37 0x51 0x84 0x03 0xdc 0x00 0x12 | User |
For each of the modes the following characteristics can be adjusted, but not all modes supports all characteristics.
| Brightness | |
|---|---|
| 0x37 0x51 0x84 0x03 0x10 0x00 [0x00 ... 0x64] | 0 ... 100 |
| Contrast | |
|---|---|
| 0x37 0x51 0x84 0x03 0x12 0x00 [0x00 ... 0x64] | 0 ... 100 |
| Sharpness | |
|---|---|
| 0x37 0x51 0x84 0x03 0x87 0x00 [0x01 ... 0x0a] | 1 ... 10 |
| Saturation | |
|---|---|
| 0x37 0x51 0x84 0x03 0x8a 0x00 [0x01 ... 0x0a] | 1 ... 10 |
| Gamma | |
|---|---|
| 0x37 0x51 0x84 0x03 0x72 0x00 0x50 | 1 |
| 0x37 0x51 0x84 0x03 0x72 0x00 0x64 | 2 |
| 0x37 0x51 0x84 0x03 0x72 0x00 0x78 | 3 |
| 0x37 0x51 0x84 0x03 0x72 0x00 0x8c | 4 |
| 0x37 0x51 0x84 0x03 0x72 0x00 0xa0 | 5 |
| Color Temperature | |
|---|---|
| 0x37 0x51 0x84 0x03 0x14 0x00 0x05 | Normal |
| 0x37 0x51 0x84 0x03 0x14 0x00 0x08 | Bluish |
| 0x37 0x51 0x84 0x03 0x14 0x00 0x04 | Reddish |
| 0x37 0x51 0x84 0x03 0x14 0x00 0x0b | User Define |
If Color Temperature is set to User Define then the Red, Green and Blue colors can be set explicitly.
| Red | |
|---|---|
| 0x37 0x51 0x84 0x03 0x16 0x00 [0x00 ... 0x64] | 0 ... 100 |
| Green | |
|---|---|
| 0x37 0x51 0x84 0x03 0x18 0x00 [0x00 ... 0x64] | 0 ... 100 |
| Blue | |
|---|---|
| 0x37 0x51 0x84 0x03 0x1a 0x00 [0x00 ... 0x64] | 0 ... 100 |
The following command resets the colors.
| Reset Color |
|---|
| 0x37 0x51 0x84 0x03 0x08 0x00 0x01 |
| Volume Level | |
|---|---|
| 0x37 0x51 0x84 0x03 0x62 0x00 [0x00 ... 0x32] | 0 ... 50 |
The menu for Audio control on the monitor includes in addition the following:
- Mute On/Off
- Audio Scenario (Standard, Dialogue, Music)
Unfortunately the Pilot 2 software don't seems to have options for adjusting these settings.
As mentioned in the introduction the collected traces also includes
WRITE lines that are 6 byte long, together with corresponding READ
lines returning 12 bytes. These WRITE lines reads the current
settings from the monitor.
There are 22 unique WRITE lines that gets repeated for reading the
settings from the monitor. These 22 lines are repeated over and over
again. The 22 lines are:
0x37 0x51 0x82 0x01 0xdc 0x60
0x37 0x51 0x82 0x01 0xd9 0x65
0x37 0x51 0x82 0x01 0xd7 0x6b
0x37 0x51 0x82 0x01 0xe2 0x5e
0x37 0x51 0x82 0x01 0x10 0xac
0x37 0x51 0x82 0x01 0x12 0xae
0x37 0x51 0x82 0x01 0x87 0x3b
0x37 0x51 0x82 0x01 0x14 0xa8
0x37 0x51 0x82 0x01 0x16 0xaa
0x37 0x51 0x82 0x01 0x18 0xa4
0x37 0x51 0x82 0x01 0x1a 0xa6
0x37 0x51 0x82 0x01 0x8a 0x36
0x37 0x51 0x82 0x01 0x72 0xce
0x37 0x51 0x82 0x01 0xfd 0x41
0x37 0x51 0x82 0x01 0xe6 0x5a
0x37 0x51 0x82 0x01 0xe7 0x5b
0x37 0x51 0x82 0x01 0xe9 0x55
0x37 0x51 0x82 0x01 0x60 0xdc
0x37 0x51 0x82 0x01 0x19 0xa5
0x37 0x51 0x82 0x01 0xd1 0x6d
0x37 0x51 0x82 0x01 0x62 0xde
0x37 0x51 0x82 0x01 0x8d 0x31
The last byte in each line is the checksum. The XOR of
a line gives the value 0x59. And by replacing the last
byte with this value you'll get the checksum value back
again.
As seen in the introduction the READ lines are 12 bytes long,
including the first byte which always is 0x37. With the
i2ctransfer command the leading byte is always skipped and only 11
bytes should be read back. Example:
i2ctransfer -y 12 w5@0x37 0x51 0x82 0x01 0x18 0xa4 r11
0x6e 0x88 0x02 0x00 0x18 0x00 0x00 0x64 0x00 0x64 0xac
The XOR of the returned bytes is turns out to always be
0x50. Similar as for commands the responses can be verified against
this value.
After some experimentation running the commands above could indeed be used to read the current settings from the monitor. What you get back from running all the 22 command and reading the response is a 22 by 10 two-dimensional array, excluding the checksum byte from the response, containing the current settings.
By first "dumping" the settings followed by running one of the monitor control commands and then followed by a new dump and comparing the dumps, it was possible to find out which index in the two-dimensional array that represented a specific setting.
Below follows is an overview of what was found. The order is again the same as in the menu displayed on the monitor when using the monitor controls.
| pos | value | |
|---|---|---|
| USB-C | [9][17] | 0x13 |
| HDMI | [9][17] | 0x11 |
| DP | [9][17] | 0x0f |
| pos | value | |||
|---|---|---|---|---|
| MoonHalo | On | [9][2] | 0x20 | |
| Off | [9][2] | 0x10 | ||
| 270° | [8][2] | 0x01 | ||
| 360° | [8][2] | 0x02 | ||
| Brightness | [9][1] | 0x01 ... 0x0e | ||
| Temperature | [8][1] | 0x01 ... 0x07 | ||
| EcoPrivacy | Activation Time | Off | [9][15] | 0x00 |
| 5 Sec | [9][15] | 0x05 | ||
| 10 Sec | [9][15] | 0x0a | ||
| 20 Sec | [9][15] | 0x14 | ||
| 30 Sec | [9][15] | 0x1e | ||
| 60 Sec | [9][15] | 0x3c | ||
| Sensor Sensitivity | Near | [9][16] | 0x01 | |
| Middle | [9][16] | 0x02 | ||
| Far | [9][16] | 0x03 |
| pos | value | |||
|---|---|---|---|---|
| Night Hours Protection | Switch | On | [9][19] | 0x01 |
| Off | [9][19] | 0x00 | ||
| Auto | [9][19] | 0x02 | ||
| Low Blue Light Plus | [9][18] | 0x00 ... 0x05 | ||
| B.I. Gen2 | On | [9][3] | 0xff | |
| Off | [9][3] | 0x00 | ||
| Color Weakness | R | [9][13] | 0x03 | |
| [8][13] | 0x00 ... 0x0a | |||
| G | [9][13] | 0x04 | ||
| [8][13] | 0x00 ... 0x0a |
| pos | value | pos | value | ||
|---|---|---|---|---|---|
| Coding - Dark Theme | [9][0] | 0x31 | |||
| Coding - Light Theme | [9][0] | 0x30 | |||
| M-Book | [9][0] | 0x0f | |||
| Cinema | [9][0] | 0x32 | |||
| ePaper | [9][0] | 0x1f | |||
| sRGB | [9][0] | 0x0a | |||
| User | [9][0] | 0x12 | Brightness | [9][4] | 0x00 ... 0x64 |
| Contrast | [9][5] | 0x00 ... 0x64 | |||
| Sharpness | [9][6] | 0x00 ... 0x0a | |||
| Saturation | [9][11] | 0x01 ... 0x0a | |||
| Gamma 1 | [9][12] | 0x50 | |||
| Gamma 2 | [9][12] | 0x64 | |||
| Gamma 3 | [9][12] | 0x78 | |||
| Gamma 4 | [9][12] | 0x8c | |||
| Gamma 5 | [9][12] | 0x0a | |||
| Temperature (Normal) | [9][7] | 0x05 | |||
| Temperature (Bluish) | [9][7] | 0x08 | |||
| Temperature (Reddish) | [9][7] | 0x04 | |||
| Temperature (User Define) | [9][7] | 0x0b | |||
| Red | [9][8] | 0x00 ... 0x64 | |||
| Green | [9][9] | 0x00 ... 0x64 | |||
| Blue | [9][10] | 0x00 ... 0x64 |
| pos | value | |
|---|---|---|
| Volume | [9][20] | 0x00 ... 0x32 |
| Mute On | [9][21] | 0x01 |
| Mute Off | [9][21] | 0x02 |
Mute on/off was toggled by using the controls on the monitor.
When writing I2C commands to the monitor, ensure that there is a delay between each command sent. Otherwise the command might have no effect, resulting in garbage being read back or overloading the monitor. A suitable delay could be from 200 to 500 msec.
While the monitor settings can be read in an endless loop, it is probably best to stop reading the settings as soon as no commands are being sent to the monitor. For reference the Display Pilot 2 software stops reading the settings when the application is minimized or is out of "focus".
