-
-
Save wadimw/4ac972d07ed1f3b6f22a101375ecac41 to your computer and use it in GitHub Desktop.
| #!/usr/local/bin/python3 | |
| # For Gigabyte M27Q KVM connected over USB-C | |
| # | |
| # Reads brightness value of builtin display from system, adapts it (Lunar-style) and sends over USB command to external display | |
| # | |
| # You need PyUSB and PyObjC | |
| # | |
| # Not much testing was done so far, only on MBP A1990 15" 2019 i7 555X | |
| # | |
| # Works only on MacOS, but set_brightness_M27Q() can be used on any platform that supports PyUSB. | |
| from sys import platform | |
| from time import sleep | |
| import m27q | |
| import builtinbrt | |
| # https://github.com/alin23/Lunar/blob/master/Lunar/Data/Util.swift | |
| def map_number(number, from_low, from_high, to_low, to_high): | |
| if number >= from_high: | |
| return to_high | |
| elif number <= from_low: | |
| return to_low | |
| elif to_low < to_high: | |
| diff = to_high - to_low | |
| from_diff = from_high - from_low | |
| return (number - from_low) * diff / from_diff + to_low | |
| else: | |
| diff = to_high - to_low | |
| from_diff = from_high - from_low | |
| return (number - from_low) * diff / from_diff + to_low | |
| # Algorithm copied over from Lunar | |
| # https://www.desmos.com/calculator/zciiqhtnov | |
| # https://github.com/alin23/Lunar/blob/master/Lunar/Data/Display.swift | |
| def adapt_brightness(old_brightness): | |
| # Adjust parameters here | |
| int_clip_min = 35 # 0-100 | |
| int_clip_max = 95 # 0-100 | |
| ext_brt_min = 0 # 0-100 | |
| ext_brt_max = 100 # 0-100 | |
| offset = -50 # -100-100 | |
| # Float division | |
| old_brightness = float(old_brightness) | |
| # Clipping | |
| old_brightness = map_number(old_brightness, int_clip_min, int_clip_max, 0, 100) | |
| # Curve factor | |
| factor = 1-(float(offset)/100) | |
| new_brightness = ((((old_brightness/100)*(ext_brt_max-ext_brt_min)+ext_brt_min)/100)**factor)*100 | |
| new_brightness = int(round(new_brightness)) | |
| return max(min(100, new_brightness), 0) | |
| def main(): | |
| if platform != "darwin": | |
| raise Exception("This script works only on MacOS.") | |
| else: | |
| try: | |
| builtin = builtinbrt.BuiltinBrightness() | |
| stored_brightness = None | |
| while True: | |
| # Get builtin display brightness | |
| builtin_brightness = builtin.get_brightness() | |
| # Calculate value for external display | |
| new_brightness = adapt_brightness(builtin_brightness) | |
| print("Builtin:", builtin_brightness, "M27Q:", new_brightness, "was:", stored_brightness) | |
| with m27q.MonitorControl() as external: | |
| if new_brightness != stored_brightness: | |
| external.transition_brightness(new_brightness) | |
| stored_brightness = new_brightness | |
| sleep(3) | |
| except IOError as e: | |
| import sys | |
| t, v, tb = sys.exc_info() | |
| print(v) | |
| # Wait a minute so that launchd won't try to revive process non-stop | |
| sleep(60) | |
| raise e.with_traceback(tb) | |
| if __name__ == "__main__": | |
| main() |
| # Builtin Brightness for MacOS | |
| import objc | |
| import CoreFoundation | |
| from Foundation import NSBundle | |
| IOKIT_FRAMEWORK = "com.apple.framework.IOKit" | |
| DISPLAY_CONNECT = b"IODisplayConnect" | |
| class BuiltinBrightness: | |
| # Import IOKit functions only once | |
| _iokit_imported = False | |
| # Load functions used in fetching brightness | |
| @staticmethod | |
| def import_iokit(): | |
| if not BuiltinBrightness._iokit_imported: | |
| iokit = NSBundle.bundleWithIdentifier_(IOKIT_FRAMEWORK) | |
| functions = [ | |
| ("IOServiceGetMatchingService", b"II@"), | |
| ("IOServiceMatching", b"@*"), | |
| ("IODisplayGetFloatParameter", b"iII@o^f"), | |
| ("IOObjectRelease", b"iI") | |
| ] | |
| variables = [ | |
| ("kIOMasterPortDefault", b"I"), | |
| # ("kIODisplayBrightnessKey", b"*") # "brightness", had some trouble loading it so using string literal instead | |
| ] | |
| objc.loadBundleFunctions(iokit, globals(), functions) | |
| objc.loadBundleVariables(iokit, globals(), variables) | |
| globals()['kIODisplayBrightnessKey'] = "brightness" | |
| BuiltinBrightness._iokit_imported = True | |
| # Make sure necessary functions were imported | |
| def __init__(self): | |
| BuiltinBrightness.import_iokit() | |
| # Read brightness level of builtin (first) display | |
| def get_brightness(self) -> int: | |
| # Get first available display IOService | |
| service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(DISPLAY_CONNECT)) | |
| if not service: | |
| raise IOError("No IODisplayConnect services found") | |
| (error, brightness) = IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey, None) | |
| if error: | |
| raise IOError(f"Couldn't get parameter {kIODisplayBrightnessKey} from service {service}, error {error}") | |
| error = IOObjectRelease(service) | |
| if error: | |
| raise IOError(f"Failed to release IOService {service}") | |
| return int(brightness*100) |
| # For Gigabyte M27Q KVM connected over USB-C | |
| # | |
| # Recreates messages captured with Wireshark from OSD Sidekick on Windows. | |
| # Requires PyUSB. | |
| # Further testing should be done. | |
| # Code based mostly on https://www.linuxvoice.com/drive-it-yourself-usb-car-6/ | |
| # and https://github.com/pyusb/pyusb/blob/master/docs/tutorial.rst | |
| # | |
| # Can be used on any platform that supports PyUSB. | |
| # | |
| # OSD getter, setter and KVM toggle based on @P403n1x87 findings (https://github.com/P403n1x87/m27q) | |
| import usb.core | |
| import usb.util | |
| import typing as t | |
| from time import sleep | |
| class MonitorControl: | |
| def __init__(self): | |
| self._VID=0x2109 # (VIA Labs, Inc.) | |
| self._PID=0x8883 # USB Billboard Device | |
| self._dev=None | |
| self._usb_delay = 50/1000 # 50 ms sleep after every usb op | |
| self._min_brightness = 0 | |
| self._max_brightness = 100 | |
| self._min_volume = 0 | |
| self._max_volume = 100 | |
| # Find USB device, set config | |
| def __enter__(self): | |
| # Find device | |
| self._dev=usb.core.find(idVendor=self._VID, idProduct=self._PID) | |
| if self._dev is None: | |
| raise IOError(f"Device VID_{self._VID}&PID_{self._PID} not found") | |
| # Deach kernel driver | |
| self._had_driver = False | |
| try: | |
| if self._dev.is_kernel_driver_active(0): | |
| self._dev.detach_kernel_driver(0) | |
| self._had_driver = True | |
| except Exception as e: | |
| pass | |
| # Set config (1 as discovered with Wireshark) | |
| self._dev.set_configuration(1) | |
| return self | |
| # Optionally reattach kernel driver | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| # Reattach kernel driver | |
| if self._had_driver: | |
| self._dev.attach_kernel_driver(0) | |
| # Release device | |
| usb.util.dispose_resources(self._dev) | |
| def usb_write(self, b_request: int, w_value: int, w_index: int, message: bytes): | |
| bm_request_type = 0x40 | |
| if not self._dev.ctrl_transfer(bm_request_type, b_request, w_value, w_index, message) == len(message): | |
| raise IOError("Transferred message length mismatch") | |
| sleep(self._usb_delay) | |
| def usb_read(self, b_request: int, w_value: int, w_index: int, msg_length: int): | |
| bm_request_type = 0xC0 | |
| data = self._dev.ctrl_transfer(bm_request_type, b_request, w_value, w_index, msg_length) | |
| sleep(self._usb_delay) | |
| return data | |
| def get_osd(self, data: t.List[int]): | |
| self.usb_write( | |
| b_request=178, | |
| w_value=0, | |
| w_index=0, | |
| message=bytearray([0x6E, 0x51, 0x81 + len(data), 0x01]) + bytearray(data), | |
| ) | |
| data = self.usb_read(b_request=162, w_value=0, w_index=111, msg_length=12) | |
| return data[10] | |
| def set_osd(self, data: bytearray): | |
| self.usb_write( | |
| b_request=178, | |
| w_value=0, | |
| w_index=0, | |
| message=bytearray([0x6E, 0x51, 0x81 + len(data), 0x03] + data), | |
| ) | |
| def set_brightness(self, brightness: int): | |
| self.set_osd( | |
| [ | |
| 0x10, | |
| 0x00, | |
| max(self._min_brightness, min(self._max_brightness, brightness)), | |
| ] | |
| ) | |
| def get_brightness(self): | |
| return self.get_osd([0x10]) | |
| def transition_brightness(self, to_brightness: int, step: int = 3): | |
| current_brightness = self.get_brightness() | |
| diff = abs(to_brightness - current_brightness) | |
| if current_brightness <= to_brightness: | |
| step = 1 * step # increase | |
| else: | |
| step = -1 * step # decrease | |
| while diff >= abs(step): | |
| current_brightness += step | |
| self.set_brightness(current_brightness) | |
| diff -= abs(step) | |
| # Set one last time | |
| if current_brightness != to_brightness: | |
| self.set_brightness(to_brightness) | |
| def get_kvm_status(self): | |
| return self.get_osd([224, 105]) | |
| def set_kvm_status(self, status): | |
| self.set_osd([224, 105, status]) | |
| def toggle_kvm(self): | |
| self.set_kvm_status(1 - self.get_kvm_status()) | |
If you're on M1 Mac, this script won't work: IODisplayConnect is not supported on M1 Macs. I managed to make it work by executing shell command here though: - https://developer.apple.com/forums/thread/666383?answerId=663154022#663154022 (it's although it's buggy and not ideal solution)
@jaknas I guess You could try to use the private CoreDisplay API that's mentioned in the topic You linked - that's what Lunar seems to use https://github.com/alin23/Lunar/search?q=DisplayServicesGetBrightness
@jaknas I guess You could try to use the private CoreDisplay API that's mentioned in the topic You linked - that's what Lunar seems to use github.com/alin23/Lunar/search?q=DisplayServicesGetBrightness
@wadimw Thanks, I managed to get it working - I forked the gist here: https://gist.github.com/jaknas/82db1f3814a265b4399c1985394c29c1 with changes to support M1 mac. I'm a python noob so main goal was to make it working.
Although I noticed that I often get ValueError: brightness out of bounds (0-100), got 107 error, probably still need some debugging and then tweaking.
@kelvie did you make it work on M32U? Looks like my M32Q works the same.
@s4ndm4ns https://github.com/kelvie/gbmonctl works on the M32U yes.
@s4ndm4ns https://github.com/kelvie/gbmonctl works on the M32U yes.
no windows :(
You can try using my script on windows:
https://github.com/WildFireFlum/gbmonitor/tree/main
@jaknas I guess You could try to use the private CoreDisplay API that's mentioned in the topic You linked - that's what Lunar seems to use github.com/alin23/Lunar/search?q=DisplayServicesGetBrightness
@wadimw Thanks, I managed to get it working - I forked the gist here: https://gist.github.com/jaknas/82db1f3814a265b4399c1985394c29c1 with changes to support M1 mac. I'm a python noob so main goal was to make it working.
Although I noticed that I often get
ValueError: brightness out of bounds (0-100), got 107error, probably still need some debugging and then tweaking.
Guys, i will be brutally honest i don't know a shit about using Python Scripts, so can i ask for your help? I've installed Python 3 alreadym i'm on M1 Pro with Sonoma.
I downloaded the scripts, but don't really know if I should run all of them or just one is enough? I did try to run each but didnt work like that - "Run Module". Got errors only...I would like to make this autostarting in the background.
Thats what i got:
Python 3.12.2 (v3.12.2:6abddd9f6a, Feb 6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license()" for more information.
= RESTART: /Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/builtinbrt.py
= RESTART: /Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py
Builtin: 43 M27Q: 5 was: None
Traceback (most recent call last):
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 88, in
main()
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 73, in main
with m27q.MonitorControl() as external:
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/m27q.py", line 30, in enter
self._dev=usb.core.find(idVendor=self._VID, idProduct=self._PID)
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/usb/core.py", line 1309, in find
raise NoBackendError('No backend available')
usb.core.NoBackendError: No backend available
========== RESTART: /Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/m27q.py =========
= RESTART: /Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py
Builtin: 43 M27Q: 5 was: None
Traceback (most recent call last):
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 88, in
main()
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 73, in main
with m27q.MonitorControl() as external:
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/m27q.py", line 30, in enter
self._dev=usb.core.find(idVendor=self._VID, idProduct=self._PID)
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/usb/core.py", line 1309, in find
raise NoBackendError('No backend available')
usb.core.NoBackendError: No backend available
You need to install the hidapi library for this to work.
While I'm not a mac user, there are instructions in the repository I linked,
you can either install it using howebrew via
brew install hidapi
or build it yourself (which will probably take you longer)
https://github.com/libusb/hidapi#mac
If you are looking for installation instruction for homebrew, I think this post might help you:
https://stackoverflow.com/a/67271753
Thanks but didn't help. I used pip3 cmd for that: "pip3 install hidapi"
Also using PIP3 i installed: PyUSB and PyObjC - which is required for this to work?
Either way it still gives the same error:
Python 3.12.2 (v3.12.2:6abddd9f6a, Feb 6 2024, 17:02:06) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license()" for more information.
= RESTART: /Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py
Builtin: 58 M27Q: 24 was: None
Traceback (most recent call last):
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 88, in
main()
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/adaptMonitorBrightness-M27Q.py", line 73, in main
with m27q.MonitorControl() as external:
File "/Users/adammichalski/Downloads/82db1f3814a265b4399c1985394c29c1-daca4d8bc40bb62bacbbfe59f2f570ff443eee14/m27q.py", line 30, in enter
self._dev=usb.core.find(idVendor=self._VID, idProduct=self._PID)
File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/usb/core.py", line 1309, in find
raise NoBackendError('No backend available')
usb.core.NoBackendError: No backend available
I was referring to using this script (my version):
https://github.com/WildFireFlum/gbmonitor/tree/main
which relies on using hidapi.
It is not enough to install the python bindings for this library, you'll need to install the library itself as well.
I built a small menu bar UI app to control basic M27Q settings based on this script if anyone is interested.
https://github.com/mithwick93/Gigabyte-M27Q-Settings-Controller
I built a small menu bar UI app to control basic M27Q settings based on this script if anyone is interested.
https://github.com/mithwick93/Gigabyte-M27Q-Settings-Controller
@mithwick93 this is awesome! Would be cool if you could add multi-monitor support, I run a pair of M27Qs.
Just disassembled the OSD Sidekick tool and found this
public ushort GetCurrentBC() => this.GetOSD(2, new byte[3] { (byte) 224, (byte) 105, (byte) 0 }); . . . public void SetB() => this.SetOSD(3, new byte[3] { (byte) 224, (byte) 105, (byte) 0 }); . . . public void SetC() => this.SetOSD(3, new byte[3] { (byte) 224, (byte) 105, (byte) 1 });I think this means that the message will look like this for setting
message = bytearray([0x6e, 0x51, 0x84, 0x03, 224, 105, 0 if USB_B else 1]WDYT?
EDIT I can confirm that it works 🙂
Did you use IDA for the disassembly?
I've had a rough time trying to wrap my head around the Wireshark usb packets and think a product wide dump would be very useful.


Haven't tested this yet, but I have the same issue (DDC/CI commands not working over USB-C DisplayPort). Thanks for sharing this.