├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── headers ├── i2c.h ├── ioregistry.h └── utils.h └── sources ├── i2c.m ├── ioregistry.m └── m1ddc.m /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE related files 2 | .idea/ 3 | .vscode/ 4 | 5 | # Default C exclusions 6 | *.o 7 | *.a 8 | 9 | # Project specific files & directories 10 | .objects/ 11 | library/ 12 | m1ddc 13 | 14 | # macOS related files 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 waydabber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # -- VARIABLES 2 | 3 | # Project name 4 | NAME = m1ddc 5 | LIB = lib$(NAME) 6 | 7 | # Compiler 8 | CC = clang 9 | CFLAGS = -Wall -Werror -Wextra -fmodules 10 | CPPFLAGS = -I $(INC_DIR) 11 | DEPFLAGS = -MMD 12 | 13 | # Libraries 14 | LDLIBS = -framework CoreDisplay 15 | 16 | # Commands 17 | RM = rm -f 18 | RMDIR = rm -rf 19 | MKDIR = mkdir -p 20 | MAKE = make -C 21 | AR = ar -rcs 22 | 23 | # Paths 24 | INC_DIR = headers 25 | SRC_DIR = sources 26 | LIB_DIR = library 27 | BIN_DIR = /usr/local/bin 28 | 29 | # Sources & Objects - Binary 30 | SOURCES = i2c \ 31 | ioregistry \ 32 | m1ddc \ 33 | 34 | OBJ_DIR = .objects 35 | OBJECTS = $(patsubst %,$(OBJ_DIR)/%,$(SOURCES:=.o)) 36 | 37 | # Sources & Objects - Library 38 | LIB_SRCS = $(filter-out m1ddc, $(SOURCES)) 39 | LIB_OBJS = $(patsubst %,$(OBJ_DIR)/%,$(LIB_SRCS:=.o)) 40 | LIB_HDRS = $(patsubst %,$(INC_DIR)/%,$(LIB_SRCS:=.h)) 41 | 42 | # -- IMPLICIT RULES / LINKING 43 | 44 | $(OBJ_DIR)/%.o: $(SRC_DIR)/%.m Makefile 45 | @$(CC) -c $< -o $@ $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) 46 | 47 | $(OBJ_DIR): 48 | @$(MKDIR) $(OBJ_DIR) 49 | 50 | $(LIB_DIR): 51 | @$(MKDIR) $(LIB_DIR) 52 | 53 | $(NAME): $(OBJ_DIR) $(OBJECTS) 54 | @$(CC) $(LDLIBS) $(OBJECTS) -o $@ 55 | @printf "Created binary \"$(NAME)\"\n" 56 | 57 | $(LIB).a: $(LIB_DIR) $(OBJ_DIR) $(LIB_OBJS) 58 | @$(AR) $(LIB_DIR)/$@ $(LIB_OBJS) 59 | @printf "Created library \"$(LIB_DIR)/$@\"\n" 60 | 61 | # For each header file, we do the following, using regular expressions: 62 | # - Ignore the #ifndef _FILE/# define _FILE/#endif directives that begin/end the file 63 | # - Extract the #import directives 64 | # - Extract all the remaining # directives 65 | # - Extract the rest: types and functions declarations. 66 | # All the extracted lines are then appendend to the final header file. 67 | $(LIB).h: $(LIB_DIR) $(LIB_HDRS) 68 | @imports=""; \ 69 | directives=""; \ 70 | declarations=""; \ 71 | for file in $(LIB_HDRS); do \ 72 | declarations="$$declarations\n\n/*\n * -- $$(basename $$file .h | tr '[:lower:]' '[:upper:]')\n*/\n"; \ 73 | fileguard=$$(echo "_$$(basename $$file .h | tr '[:lower:]' '[:upper:]')_H"); \ 74 | if [ "$$(head -n 1 $$file)" = "#ifndef $$fileguard" ]; then \ 75 | filecontent=$$(sed -e '1,2d' -e '$$d' $$file); \ 76 | else \ 77 | filecontent=$$(cat $$file); \ 78 | fi; \ 79 | imports="$$imports\n$$(echo "$$filecontent" | grep -E "^#\s*import" | sort | uniq)"; \ 80 | directives="$$directives\n$$(echo "$$filecontent" | grep -E "^#" | grep -vE "^#\s*import" | grep -vE "^#\s*include\s*\".*.h\"$$" )"; \ 81 | declarations="$$declarations\n$$(echo "$$filecontent" | grep -vE "^#" )"; \ 82 | done; \ 83 | guard=$$(echo "_$(LIB)_H" | tr '[:lower:]' '[:upper:]'); \ 84 | printf "#ifndef $$guard\n# define $$guard\n\n" > $(LIB_DIR)/$@; \ 85 | printf "$$directives\n\n" >> $(LIB_DIR)/$@; \ 86 | printf "$$imports\n\n" >> $(LIB_DIR)/$@; \ 87 | printf "$$declarations\n\n" >> $(LIB_DIR)/$@; \ 88 | printf "#endif" >> $(LIB_DIR)/$@; \ 89 | sed -i '' -e '/^$$/N;/^\n$$/D' $(LIB_DIR)/$@; \ 90 | 91 | @printf "Created header \"$(LIB_DIR)/$@\"\n" 92 | 93 | # -- RULES 94 | 95 | .DEFAULT_GOAL := binary 96 | 97 | all: binary lib 98 | 99 | binary: $(NAME) 100 | 101 | lib: $(LIB).a $(LIB).h 102 | 103 | clean: 104 | @if [ -e $(OBJ_DIR) ]; then \ 105 | $(RMDIR) $(OBJ_DIR); \ 106 | printf "Objects deleted\n"; \ 107 | fi; 108 | 109 | fclean: clean 110 | @if [ -e $(NAME) ]; then \ 111 | $(RM) $(NAME); \ 112 | printf "Binary deleted\n"; \ 113 | fi; 114 | @if [ -e $(LIB_DIR) ]; then \ 115 | $(RMDIR) $(LIB_DIR); \ 116 | printf "Library deleted\n"; \ 117 | fi; 118 | 119 | re: fclean all 120 | 121 | install: 122 | /bin/mkdir -p $(BIN_DIR) 123 | sudo /usr/bin/install -s -m 0755 $(NAME) $(BIN_DIR) 124 | 125 | .PHONY: all lib clean fclean re install 126 | 127 | -include $(OBJECTS:.o=.d) 128 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m1ddc 2 | 3 | This little tool controls external displays (connected via USB-C/DisplayPort Alt Mode) using DDC/CI on Apple Silicon Macs. Useful to embed in various scripts. 4 | 5 | For a much more advanced CLI solution check out [BetterDisplay's CLI capabilities](https://github.com/waydabber/BetterDisplay/wiki/Integration-features,-CLI). 6 | 7 | > [!WARNING] 8 | > Please note that this tool does not support the built-in HDMI port of M1 and entry level M2 Macs. This tool does not support Intel Macs. You can use [BetterDisplay](https://github.com/waydabber/BetterDisplay#readme) for free DDC control on all Macs and all ports. 9 | 10 | ## Prerequisites 11 | 12 | > [!NOTE] 13 | > You need `clang` from Apple's Command Line Tools (installs automatically if not present). 14 | 15 | ## Installation 16 | 17 | After download, enter (in Terminal): 18 | ```shell 19 | make 20 | ``` 21 | 22 | You can then run the app by entering: 23 | ```shell 24 | ./m1ddc [options] 25 | ``` 26 | 27 | ## Usage examples 28 | 29 | ```shell 30 | # Sets contrast to 5 on default display 31 | m1ddc set contrast 5 32 | # Returns current luminance ("brightness") on default display 33 | m1ddc get luminance 34 | # Sets red gain to 90 35 | m1ddc set red 90 36 | # Decreases volume by 10 on default display 37 | m1ddc chg volume -10 38 | # Lists displays 39 | m1ddc display list 40 | # Sets volume to 50 on Display 1 41 | m1ddc display 1 set volume 50 42 | # Sets input to DisplayPort 1 on display with UUID '10ACB8A0-0000-0000-1419-0104A2435078' 43 | m1ddc display 10ACB8A0-0000-0000-1419-0104A2435078 set input 15` 44 | ``` 45 | 46 | ## Available commands 47 | 48 | ```shell 49 | set luminance n - Sets luminance (brightness) to n, where n is a number between 0 and the maximum value (usually 100). 50 | contrast n - Sets contrast to n, where n is a number between 0 and the maximum value (usually 100). 51 | (red,green,blue) n - Sets selected color channel gain to n, where n is a number between 0 and the maximum value (usually 100). 52 | volume n - Sets volume to n, where n is a number between 0 and the maximum value (usually 100). 53 | input n - Sets input source to n, common values include: 54 | DisplayPort 1: 15, DisplayPort 2: 16, HDMI 1: 17, HDMI 2: 18, USB-C: 27. 55 | input-alt n - Sets input source to n (using alternate addressing, as used by LG), common values include: 56 | DisplayPort 1: 208, DisplayPort 2: 209, HDMI 1: 144, HDMI 2: 145, USB-C / DP 3: 210. 57 | 58 | mute on - Sets mute on (you can use 1 instead of 'on') 59 | mute off - Sets mute off (you can use 2 instead of 'off') 60 | 61 | pbp n - Switches PIP/PBP on certain Dell screens (e.g. U3421W), possible values: 62 | off: 0, small window: 33, large window: 34, 50/50 split: 36, 26/74 split: 43, 74/26 split: 44. 63 | pbp-input n - Sets second PIP/PBP input on certain Dell screens, possible values: 64 | DisplayPort 1: 15, DisplayPort 2: 16, HDMI 1: 17, HDMI 2: 18. 65 | kvm n - Sets KVM order on certain Dell screens, possible values: TBD. 66 | kvm-switch - Moves KVM to the next device on some Dells. 67 | 68 | get luminance - Returns current luminance (if supported by the display). 69 | contrast - Returns current contrast (if supported by the display). 70 | (red,green,blue) - Returns current color gain (if supported by the display). 71 | volume - Returns current volume (if supported by the display). 72 | 73 | max luminance - Returns maximum luminance (if supported by the display, usually 100). 74 | contrast - Returns maximum contrast (if supported by the display, usually 100). 75 | (red,green,blue) - Returns maximum color gain (if supported by the display, usually 100). 76 | volume - Returns maximum volume (if supported by the display, usually 100). 77 | 78 | chg luminance n - Changes luminance by n and returns the current value (requires current and max reading support). 79 | contrast n - Changes contrast by n and returns the current value (requires current and max reading support). 80 | (red,green,blue) n - Changes color gain by n and returns the current value (requires current and max reading support). 81 | volume n - Changes volume by n and returns the current value (requires current and max reading support). 82 | 83 | display list [detailed] - Lists displays. If `detailed` is provided, prints display extended attributes. 84 | n - Chooses which display to control (use number 1, 2 etc.) 85 | (method=) - Chooses which display to control using the number using a specific identification method. (If not set, it defaults to `uuid`). 86 | Possible values for `method` are: 87 | 'id': 88 | 'uuid': *Default 89 | 'edid': 90 | 'seid': : 91 | 'basic': :: 92 | 'ext': ::::: 93 | 'full': :::::: 94 | ``` 95 | 96 | > [!TIP] 97 | > You can also use 'l', 'v' instead of 'luminance', 'volume' etc. 98 | 99 | 100 | ## Identification methods 101 | 102 | The following display identification methods are supported, and corresponds to the following strings 103 | 104 | |Method|Related display attributes| 105 | |--:|:--| 106 | |`id`|``| 107 | |`uuid`|``| 108 | |`edid`|``| 109 | |`seid`|`:`| 110 | |`basic`|`::`| 111 | |`ext`|`:::::`| 112 | |`full`|`::::::`| 113 | 114 | > [!TIP] 115 | > Corresponding display attributes can be obtained using the `display list detailed` command 116 | 117 | ## Example use in a script 118 | 119 | Check out the following [hammerspoon](https://github.com/Hammerspoon/hammerspoon) script. 120 | 121 | This script allows you to control the volume of your external Display' brightness, contrast and volume via DDC (if you use an M1 Mac) using [m1ddc](https://github.com/waydabber/m1ddc) and also control your Yamaha AV Receiver through network. The script listens to the standard Apple keyboard media keys and shos the standard macOS Brightness and Volume OSDs via uses [showosd](https://github.com/waydabber/showosd) : 122 | 123 | https://gist.github.com/waydabber/3241fc146cef65131a42ce30e4b6eab7 124 | 125 | ## BetterDisplay 126 | 127 | If you like m1ddc, you'll like [BetterDisplay](https://betterdisplay.pro) even better! 128 | 129 | BetterDisplay's CLI documentation: https://github.com/waydabber/BetterDisplay/wiki/Integration-features,-CLI 130 | 131 | If you need a complete Swift implementation for DDC control on Apple Silicon macs, you can take a look at [AppleSiliconDDC](https://github.com/waydabber/AppleSiliconDDC) which is a complete self-contained library I made for BetterDisplay (note: some features and M1 HDMI support is missing from the open source code) and MonitorControl. 132 | 133 | ## Thanks 134 | 135 | Thanks to [@tao-j](https://github.com/tao-j) [@alin23](https://github.com/alin23), [@ybbond](https://github.com/ybbond) 136 | 137 | Enjoy! 138 | -------------------------------------------------------------------------------- /headers/i2c.h: -------------------------------------------------------------------------------- 1 | #ifndef _I2C_H 2 | # define _I2C_H 3 | 4 | # include "ioregistry.h" 5 | 6 | # define DEFAULT_INPUT_ADDRESS 0x51 7 | # define ALTERNATE_INPUT_ADDRESS 0x50 8 | 9 | # define LUMINANCE 0x10 10 | # define CONTRAST 0x12 11 | # define VOLUME 0x62 12 | # define MUTE 0x8D 13 | # define INPUT 0x60 14 | # define INPUT_ALT 0xF4 // Alternate address, used for LG exclusively? 15 | # define STANDBY 0xD6 16 | # define RED 0x16 // VCP Code - Video Gain (Drive): Red 17 | # define GREEN 0x18 // VCP Code - Video Gain (Drive): Green 18 | # define BLUE 0x1A // VCP Code - Video Gain (Drive): Blue 19 | # define PBP_INPUT 0xE8 20 | # define PBP 0xE9 21 | # define KVM 0xE7 22 | 23 | # define DDC_WAIT 10000 // Depending on display this must be set to as high as 50000 24 | # define DDC_ITERATIONS 2 // Depending on display this must be set higher 25 | # define DDC_BUFFER_SIZE 256 26 | 27 | 28 | typedef struct { 29 | UInt8 data[DDC_BUFFER_SIZE]; 30 | UInt8 inputAddr; 31 | } DDCPacket; 32 | 33 | typedef struct { 34 | signed int curValue; 35 | signed int maxValue; 36 | } DDCValue; 37 | 38 | 39 | DDCPacket createDDCPacket(UInt8 attrCode); 40 | 41 | void prepareDDCRead(UInt8 *data); 42 | void prepareDDCWrite(DDCPacket *packet, UInt16 setValue); 43 | 44 | IOReturn performDDCWrite(IOAVServiceRef avService, DDCPacket *packet); 45 | IOReturn performDDCRead(IOAVServiceRef avService, DDCPacket *packet); 46 | 47 | DDCValue convertI2CtoDDC(char *i2cBytes); 48 | 49 | // External functions 50 | 51 | extern IOReturn IOAVServiceReadI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t offset, void *outputBuffer, uint32_t outputBufferSize); 52 | extern IOReturn IOAVServiceWriteI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t dataAddress, void *inputBuffer, uint32_t inputBufferSize); 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /headers/ioregistry.h: -------------------------------------------------------------------------------- 1 | #ifndef _IOREGISTRY_H 2 | # define _IOREGISTRY_H 3 | 4 | # import 5 | 6 | # ifndef MAX_DISPLAYS 7 | # define MAX_DISPLAYS 4 // Set this to 2 or 4 depending on the Apple Silicon Mac you're using 8 | # endif 9 | 10 | # define UUID_SIZE 37 11 | 12 | // IOAVServiceRef is a private class, so we need to define it here 13 | typedef CFTypeRef IOAVServiceRef; 14 | 15 | // Base structure for display infos 16 | typedef struct 17 | { 18 | CGDirectDisplayID id; 19 | io_service_t adapter; 20 | NSString *ioLocation; 21 | NSString *uuid; 22 | NSString *edid; 23 | NSString *productName; 24 | NSString *manufacturer; 25 | NSString *alphNumSerial; 26 | UInt32 serial; 27 | UInt32 model; 28 | UInt32 vendor; 29 | } DisplayInfos; 30 | 31 | CGDisplayCount getOnlineDisplayInfos(DisplayInfos* displayInfos); 32 | DisplayInfos* selectDisplay(DisplayInfos *displays, int connectedDisplays, char *displayIdentifier); 33 | 34 | IOAVServiceRef getDefaultDisplayAVService(); 35 | IOAVServiceRef getDisplayAVService(DisplayInfos* displayInfos); 36 | 37 | // External functions 38 | extern IOAVServiceRef IOAVServiceCreate(CFAllocatorRef allocator); 39 | extern IOAVServiceRef IOAVServiceCreateWithService(CFAllocatorRef allocator, io_service_t service); 40 | extern CFDictionaryRef CoreDisplay_DisplayCreateInfoDictionary(CGDirectDisplayID); 41 | 42 | #endif -------------------------------------------------------------------------------- /headers/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef _UTILS_H_ 2 | # define _UTILS_H_ 3 | 4 | # define STR_EQ(s1, s2) (strcmp(s1, s2) == 0) 5 | 6 | #endif -------------------------------------------------------------------------------- /sources/i2c.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | #include "i2c.h" 4 | #include "utils.h" 5 | 6 | static int getBytesUsed(UInt8* data) { 7 | int bytes = 0; 8 | for (int i = 0; i < (int)sizeof(data); ++i) { 9 | if (data[i] != 0) { 10 | bytes = i + 1; 11 | } 12 | } 13 | return bytes; 14 | } 15 | 16 | // Function to get ready for DDC operations for a specific display attribute 17 | DDCPacket createDDCPacket(UInt8 attrCode) { 18 | DDCPacket packet = {}; 19 | packet.data[2] = attrCode; 20 | packet.inputAddr = packet.data[2] == INPUT_ALT ? ALTERNATE_INPUT_ADDRESS : DEFAULT_INPUT_ADDRESS; 21 | return packet; 22 | } 23 | 24 | // Prepare DDC packet for read 25 | void prepareDDCRead(UInt8* data) { 26 | data[0] = 0x82; 27 | data[1] = 0x01; 28 | data[3] = 0x6e ^ data[0] ^ data[1] ^ data[2] ^ data[3]; 29 | } 30 | 31 | // Prepare DDC packet for write 32 | void prepareDDCWrite(DDCPacket *packet, UInt16 newValue) { 33 | UInt8* data = packet->data; 34 | data[0] = 0x84; 35 | data[1] = 0x03; 36 | data[3] = (newValue) >> 8; 37 | data[4] = newValue & 255; 38 | data[5] = 0x6E ^ packet->inputAddr ^ data[0] ^ data[1] ^ data[2] ^ data[3] ^ data[4]; 39 | } 40 | 41 | 42 | IOReturn performDDCRead(IOAVServiceRef avService, DDCPacket *packet) { 43 | memset(packet->data, 0, sizeof(UInt8) * DDC_BUFFER_SIZE); 44 | usleep(DDC_WAIT); 45 | return IOAVServiceReadI2C(avService, 0x37, packet->inputAddr, packet->data, 12); 46 | } 47 | 48 | IOReturn performDDCWrite(IOAVServiceRef avService, DDCPacket *packet) { 49 | IOReturn ret; 50 | 51 | for (int i = 0; i < DDC_ITERATIONS; ++i) { 52 | usleep(DDC_WAIT); 53 | if ((ret = IOAVServiceWriteI2C(avService, 0x37, packet->inputAddr, packet->data, getBytesUsed(packet->data)))) { 54 | return ret; 55 | } 56 | } 57 | return ret; 58 | } 59 | 60 | 61 | DDCValue convertI2CtoDDC(char *i2cBytes) { 62 | DDCValue displayAttr = {}; 63 | NSData *i2cData = [NSData dataWithBytes:(const void *)i2cBytes length:(NSUInteger)11]; 64 | NSRange maxValueRange = {7, 2}; 65 | uint16_t maxValue = 0; 66 | [[i2cData subdataWithRange:maxValueRange] getBytes:&maxValue length:sizeof(2)]; 67 | displayAttr.maxValue = CFSwapInt16BigToHost(maxValue); 68 | 69 | NSRange curValueRange = {8, 2}; 70 | uint16_t curValue = 0; 71 | [[i2cData subdataWithRange:curValueRange] getBytes:&curValue length:sizeof(2)]; 72 | displayAttr.curValue = CFSwapInt16BigToHost(curValue); 73 | 74 | return displayAttr; 75 | } -------------------------------------------------------------------------------- /sources/ioregistry.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import IOKit; 3 | @import ApplicationServices; 4 | @import CoreGraphics; 5 | 6 | #include "ioregistry.h" 7 | #include "utils.h" 8 | 9 | static CFTypeRef getCFStringRef(io_service_t service, char* key) { 10 | CFStringRef cfstring = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingASCII); 11 | return IORegistryEntrySearchCFProperty(service, kIOServicePlane, cfstring, kCFAllocatorDefault, kIORegistryIterateRecursively); 12 | } 13 | 14 | CGDisplayCount getOnlineDisplayInfos(DisplayInfos* displayInfos) { 15 | // Getting online display list and count 16 | CGDisplayCount screenCount; 17 | CGDirectDisplayID screenList[MAX_DISPLAYS]; 18 | CGGetOnlineDisplayList(MAX_DISPLAYS, screenList, &screenCount); 19 | 20 | // Fetching each display infos from IOKit 21 | for (int i = 0; i < (int)screenCount; i++) { 22 | DisplayInfos *currDisplay = displayInfos + i; 23 | currDisplay->id = screenList[i]; 24 | 25 | // This is a private API, but it's a shortcut to get the system UUID 26 | CFDictionaryRef displayInfos = CoreDisplay_DisplayCreateInfoDictionary(currDisplay->id); 27 | 28 | currDisplay->serial = CGDisplaySerialNumber(currDisplay->id); 29 | currDisplay->model = CGDisplayModelNumber(currDisplay->id); 30 | currDisplay->vendor = CGDisplayVendorNumber(currDisplay->id); 31 | 32 | currDisplay->uuid = CFDictionaryGetValue(displayInfos, CFSTR("kCGDisplayUUID")); 33 | currDisplay->ioLocation = CFDictionaryGetValue(displayInfos, CFSTR("IODisplayLocation")); 34 | 35 | // Retrieving IORegistry entry for display 36 | currDisplay->adapter = IORegistryEntryCopyFromPath(kIOMainPortDefault, (CFStringRef)currDisplay->ioLocation); 37 | if (currDisplay->adapter == MACH_PORT_NULL) { 38 | continue; 39 | } 40 | 41 | // If successful, we can retrieve the EDID UUID, and other display attributes 42 | currDisplay->edid = getCFStringRef(currDisplay->adapter, "EDID UUID"); 43 | CFDictionaryRef displayAttrs = getCFStringRef(currDisplay->adapter, "DisplayAttributes"); 44 | if (displayAttrs) { 45 | NSDictionary* displayAttrsNS = (NSDictionary*)displayAttrs; 46 | NSDictionary* productAttrs = [displayAttrsNS objectForKey:@"ProductAttributes"]; 47 | if (productAttrs) { 48 | currDisplay->productName = [productAttrs objectForKey:@"ProductName"]; 49 | currDisplay->manufacturer = [productAttrs objectForKey:@"ManufacturerID"]; 50 | currDisplay->alphNumSerial = [productAttrs objectForKey:@"AlphanumericSerialNumber"]; 51 | } 52 | } 53 | } 54 | return screenCount; 55 | } 56 | 57 | /* 58 | * Returns display identifier based on identification method 59 | * Allowed methods are: 60 | * - id Display ID "" 61 | * - uuid Display UUID "" 62 | * - edid Display EDID UUID "" 63 | * - seid Display Alphnum SN + EDID UUID ":" 64 | * - basic Match basic identifiers "::" 65 | * - ext Match basic + extended identifiers ":::::" 66 | * - full Match basic + extended + location "::::::" 67 | */ 68 | NSString *getDisplayIdentifier(DisplayInfos *display, char *identificationMethod) { 69 | if (STR_EQ(identificationMethod, "id")) { 70 | return [NSString stringWithFormat:@"%u", display->id]; 71 | } else if (STR_EQ(identificationMethod, "uuid")) { 72 | return display->uuid; 73 | } else if (STR_EQ(identificationMethod, "edid")) { 74 | return display->edid; 75 | } else if (STR_EQ(identificationMethod, "seid")) { 76 | return [NSString stringWithFormat:@"%@:%@", 77 | display->alphNumSerial, 78 | display->edid]; 79 | } else if (STR_EQ(identificationMethod, "basic")) { 80 | return [NSString stringWithFormat:@"%u:%u:%u", 81 | display->vendor, 82 | display->model, 83 | display->serial]; 84 | } else if (STR_EQ(identificationMethod, "ext")) { 85 | return [NSString stringWithFormat:@"%d:%d:%d:%@:%@:%@", 86 | display->vendor, 87 | display->model, 88 | display->serial, 89 | display->manufacturer, 90 | display->alphNumSerial, 91 | display->productName]; 92 | } else if (STR_EQ(identificationMethod, "full")) { 93 | return [NSString stringWithFormat:@"%d:%d:%d:%@:%@:%@:%@", 94 | display->vendor, 95 | display->model, 96 | display->serial, 97 | display->manufacturer, 98 | display->alphNumSerial, 99 | display->productName, 100 | display->ioLocation]; 101 | } 102 | return NULL; 103 | } 104 | 105 | DisplayInfos* selectDisplay(DisplayInfos *displays, int connectedDisplays, char *displayIdentifier) { 106 | 107 | // Checking if display identifier is a display index from the "list" command 108 | char *stop; 109 | long displayNumber = strtol(displayIdentifier, &stop, 10); 110 | if (*stop == '\0') { 111 | return displayNumber <= connectedDisplays ? displays + (displayNumber - 1) : NULL; 112 | } 113 | 114 | // Checking if an identification method is specified, otherwise defaulting to UUID 115 | char *identificationMethod = "uuid"; 116 | char *delimiter = strstr(displayIdentifier, "="); 117 | if (delimiter != NULL) { 118 | // Delimiter should not be at the beginning or end of the string 119 | if (delimiter == displayIdentifier || delimiter == displayIdentifier + strlen(displayIdentifier) - 1) return NULL; 120 | // Splitting display identifier into identification method and value 121 | *delimiter = '\0'; 122 | identificationMethod = displayIdentifier; 123 | displayIdentifier = delimiter + 1; 124 | } 125 | 126 | // Searching for display that matchs the identifier for the given identification method 127 | for (int i = 0; i < connectedDisplays; i++) { 128 | const char *displayValue = getDisplayIdentifier(displays + i, identificationMethod).UTF8String; 129 | if (displayValue != NULL && STR_EQ(displayIdentifier, displayValue)) { 130 | return displays + i; 131 | } 132 | } 133 | return NULL; 134 | } 135 | 136 | static kern_return_t getIORegistryRootIterator(io_iterator_t* iter) { 137 | io_registry_entry_t root = IORegistryGetRootEntry(kIOMainPortDefault); 138 | kern_return_t ret = IORegistryEntryCreateIterator(root, kIOServicePlane, kIORegistryIterateRecursively, iter); 139 | if (ret != KERN_SUCCESS) { 140 | IOObjectRelease(*iter); 141 | } 142 | return ret; 143 | } 144 | 145 | IOAVServiceRef getDefaultDisplayAVService() { 146 | return IOAVServiceCreate(kCFAllocatorDefault); 147 | } 148 | 149 | IOAVServiceRef getDisplayAVService(DisplayInfos* displayInfos) { 150 | 151 | IOAVServiceRef avService = NULL; 152 | io_service_t service = 0; 153 | io_iterator_t iter; 154 | 155 | // Creating IORegistry iterator 156 | if (getIORegistryRootIterator(&iter) != KERN_SUCCESS) { 157 | return NULL; 158 | } 159 | 160 | CFStringRef externalAVServiceLocation = CFStringCreateWithCString(kCFAllocatorDefault, "External", kCFStringEncodingASCII); 161 | 162 | // Iterating through IORegistry 163 | while ((service = IOIteratorNext(iter)) != MACH_PORT_NULL) { 164 | io_string_t servicePath; 165 | IORegistryEntryGetPath(service, kIOServicePlane, servicePath); 166 | // Searching for DCPAVServiceProxy with the same location as the display 167 | if (displayInfos->ioLocation != NULL && STR_EQ(servicePath, displayInfos->ioLocation.UTF8String)) { 168 | while ((service = IOIteratorNext(iter)) != MACH_PORT_NULL) { 169 | io_name_t name; 170 | IORegistryEntryGetName(service, name); 171 | if (STR_EQ(name, "DCPAVServiceProxy")) { 172 | // Creating IOAVServiceRef from DCPAVServiceProxy 173 | avService = IOAVServiceCreateWithService(kCFAllocatorDefault, service); 174 | CFStringRef location = getCFStringRef(service, "Location"); 175 | if (location != NULL && avService != NULL && !CFStringCompare(externalAVServiceLocation, location, 0)) { 176 | return avService; 177 | } 178 | } 179 | } 180 | } 181 | } 182 | return NULL; 183 | } -------------------------------------------------------------------------------- /sources/m1ddc.m: -------------------------------------------------------------------------------- 1 | @import Darwin; 2 | @import Foundation; 3 | @import IOKit; 4 | @import CoreGraphics; 5 | 6 | #include "ioregistry.h" 7 | #include "i2c.h" 8 | #include "utils.h" 9 | 10 | 11 | // -- Generic utility functions 12 | 13 | static void writeToStdOut(NSString *text) { 14 | [text writeToFile:@"/dev/stdout" atomically:NO encoding:NSUTF8StringEncoding error:nil]; 15 | } 16 | 17 | static void printUsage() { 18 | writeToStdOut(@"Controls volume, luminance (brightness), contrast, color gain, input of an external Display connected via USB-C (DisplayPort Alt Mode) over DDC on an Apple Silicon Mac.\n" 19 | "Displays attached via the built-in HDMI port of M1 or entry level M2 Macs are not supported.\n" 20 | "\n" 21 | "Usage examples:\n" 22 | "\n" 23 | " m1ddc set contrast 5 - Sets contrast to 5\n" 24 | " m1ddc get luminance - Returns current luminance\n" 25 | " m1ddc set red 90 - Sets red gain to 90\n" 26 | " m1ddc chg volume -10 - Decreases volume by 10\n" 27 | " m1ddc display list - Lists displays\n" 28 | " m1ddc display 1 set volume 50 - Sets volume to 50 on Display 1\n" 29 | "\n" 30 | "Commands:\n" 31 | "\n" 32 | " set luminance n - Sets luminance (brightness) to n, where n is a number between 0 and the maximum value (usually 100).\n" 33 | " contrast n - Sets contrast to n, where n is a number between 0 and the maximum value (usually 100).\n" 34 | " (red,green,blue) n - Sets selected color channel gain to n, where n is a number between 0 and the maximum value (usually 100).\n" 35 | " volume n - Sets volume to n, where n is a number between 0 and the maximum value (usually 100).\n" 36 | " input n - Sets input source to n, common values include:\n" 37 | " DisplayPort 1: 15, DisplayPort 2: 16, HDMI 1: 17, HDMI 2: 18, USB-C: 27.\n" 38 | " input-alt n - Sets input source to n (using alternate addressing, as used by LG), common values include:\n" 39 | " DisplayPort 1: 208, DisplayPort 2: 209, HDMI 1: 144, HDMI 2: 145, USB-C / DP 3: 210.\n" 40 | "\n" 41 | " mute on - Sets mute on (you can use 1 instead of 'on')\n" 42 | " mute off - Sets mute off (you can use 2 instead of 'off')\n" 43 | "\n" 44 | " pbp n - Switches PIP/PBP on certain Dell screens (e.g. U3421W), possible values:\n" 45 | " off: 0, small window: 33, large window: 34, 50/50 split: 36, 26/74 split: 43, 74/26 split: 44, 2x2: 65.\n" 46 | " pbp-input n - Sets second PIP/PBP input on certain Dell screens, possible values:\n" 47 | " DisplayPort 1: 15, DisplayPort 2: 16, HDMI 1: 17, HDMI 2: 18.\n" 48 | " HDMI 1 and HDMI 2 and DisplayPort 1: 15953.\n" 49 | " kvm n - Sets KVM order on certain Dell screens, possible values:.\n" 50 | " USB1, USB2, USB3, USB4: 1728.\n" 51 | " Set 65280 to move KVM to the next device on some Dells.\n" 52 | "\n" 53 | " get luminance - Returns current luminance (if supported by the display).\n" 54 | " contrast - Returns current contrast (if supported by the display).\n" 55 | " (red,green,blue) - Returns current color gain (if supported by the display).\n" 56 | " volume - Returns current volume (if supported by the display).\n" 57 | "\n" 58 | " max luminance - Returns maximum luminance (if supported by the display, usually 100).\n" 59 | " contrast - Returns maximum contrast (if supported by the display, usually 100).\n" 60 | " (red,green,blue) - Returns maximum color gain (if supported by the display, usually 100).\n" 61 | " volume - Returns maximum volume (if supported by the display, usually 100).\n" 62 | "\n" 63 | " chg luminance n - Changes luminance by n and returns the current value (requires current and max reading support).\n" 64 | " contrast n - Changes contrast by n and returns the current value (requires current and max reading support).\n" 65 | " (red,green,blue) n - Changes color gain by n and returns the current value (requires current and max reading support).\n" 66 | " volume n - Changes volume by n and returns the current value (requires current and max reading support).\n" 67 | "\n" 68 | " display list [detailed] - Lists displays. If `detailed` is provided, prints display extended attributes.\n" 69 | " n - Chooses which display to control (use number 1, 2 etc.)\n" 70 | " (method=) - Chooses which display to control using a specific identification method. (If not set, it defaults to `uuid`).\n" 71 | " Possible values for `method` are:\n" 72 | " 'id': \n" 73 | " 'uuid: *Default\n" 74 | " 'edid': \n" 75 | " 'seid': :\n" 76 | " 'basic': ::\n" 77 | " 'ext': :::::\n" 78 | " 'full': ::::::\n" 79 | "\n" 80 | "Tip: You can also use 'l', 'v' instead of 'luminance', 'volume' etc.\n"); 81 | } 82 | 83 | static void printDisplayInfos(DisplayInfos *display, int nbDisplays, bool detailed) { 84 | for (int i = 0; i < nbDisplays; i++) { 85 | writeToStdOut([NSString stringWithFormat:@"[%i] %@ (%@)\n", (i + 1), (display + i)->productName, (display + i)->uuid]); 86 | if (detailed) { 87 | writeToStdOut([NSString stringWithFormat:@" - Product name: %@\n", (display + i)->productName]); 88 | writeToStdOut([NSString stringWithFormat:@" - Manufacturer: %@\n", (display + i)->manufacturer]); 89 | writeToStdOut([NSString stringWithFormat:@" - AN Serial: %@\n", (display + i)->alphNumSerial]); 90 | writeToStdOut([NSString stringWithFormat:@" - Vendor: %u (0x%04x)\n", (display + i)->vendor, (display + i)->vendor]); 91 | writeToStdOut([NSString stringWithFormat:@" - Model: %u (0x%04x)\n", (display + i)->model, (display + i)->model]); 92 | writeToStdOut([NSString stringWithFormat:@" - Serial: %u (0x%04x)\n", (display + i)->serial, (display + i)->serial]); 93 | writeToStdOut([NSString stringWithFormat:@" - Display ID: %i\n", (display + i)->id]); 94 | writeToStdOut([NSString stringWithFormat:@" - System UUID: %@\n", (display + i)->uuid]); 95 | writeToStdOut([NSString stringWithFormat:@" - EDID UUID: %@\n", (display + i)->edid]); 96 | writeToStdOut([NSString stringWithFormat:@" - IO Location: %@\n", (display + i)->ioLocation]); 97 | writeToStdOut([NSString stringWithFormat:@" - Adapter: %u\n", (display + i)->adapter]); 98 | } 99 | } 100 | } 101 | 102 | // Function to handle the reading operation (get, max, chg) 103 | static DDCValue readingOperation(IOAVServiceRef avService, DDCPacket *packet) { 104 | DDCValue dummyAttr = {-1, -1}; 105 | 106 | prepareDDCRead(packet->data); 107 | 108 | IOReturn err = performDDCWrite(avService, packet); 109 | if (err) { 110 | writeToStdOut([NSString stringWithFormat:@"DDC communication failure: %s\n", mach_error_string(err)]); 111 | return dummyAttr; 112 | } 113 | 114 | DDCPacket readPacket = {}; 115 | readPacket.inputAddr = packet->inputAddr; 116 | 117 | err = performDDCRead(avService, &readPacket); 118 | if (err) { 119 | writeToStdOut([NSString stringWithFormat:@"DDC communication failure: %s\n", mach_error_string(err)]); 120 | return dummyAttr; 121 | } 122 | 123 | return convertI2CtoDDC((char *)readPacket.data); 124 | } 125 | 126 | // Function to handle the writing operation (set, chg) 127 | static int writingOperation(IOAVServiceRef avService, DDCPacket *packet, UInt16 newValue) { 128 | 129 | prepareDDCWrite(packet, newValue); 130 | 131 | IOReturn err = performDDCWrite(avService, packet); 132 | if (err) { 133 | writeToStdOut([NSString stringWithFormat:@"DDC communication failure: %s\n", mach_error_string(err)]); 134 | return 1; 135 | } 136 | return 0; 137 | } 138 | 139 | static UInt8 attrCodeFromCommand(char *command) { 140 | if (STR_EQ(command, "luminance") || STR_EQ(command, "l")) { return LUMINANCE; } 141 | else if (STR_EQ(command, "contrast") || STR_EQ(command, "c")) { return CONTRAST; } 142 | else if (STR_EQ(command, "volume") || STR_EQ(command, "v")) { return VOLUME; } 143 | else if (STR_EQ(command, "mute") || STR_EQ(command, "m")) { return MUTE; } 144 | else if (STR_EQ(command, "input") || STR_EQ(command, "i")) { return INPUT; } 145 | else if (STR_EQ(command, "input-alt") || STR_EQ(command, "I")) { return INPUT_ALT; } 146 | else if (STR_EQ(command, "standby") || STR_EQ(command, "s")) { return STANDBY; } 147 | else if (STR_EQ(command, "red") || STR_EQ(command, "r")) { return RED; } 148 | else if (STR_EQ(command, "green") || STR_EQ(command, "g")) { return GREEN; } 149 | else if (STR_EQ(command, "blue") || STR_EQ(command, "b")) { return BLUE; } 150 | else if (STR_EQ(command, "pbp") || STR_EQ(command, "p")) { return PBP; } 151 | else if (STR_EQ(command, "pbp-input") || STR_EQ(command, "pi")) { return PBP_INPUT; } 152 | else if (STR_EQ(command, "kvm") || STR_EQ(command, "k")) { return KVM; } 153 | return 0x00; 154 | } 155 | 156 | static UInt16 computeAttributeValue(char *command, char *arg, DDCValue displayAttr) { 157 | int newValue; 158 | 159 | if (STR_EQ(arg, "on") ) { newValue = 1; } 160 | else if (STR_EQ(arg, "off") ) { newValue = 2; } 161 | else { newValue = atoi(arg); } 162 | 163 | if (STR_EQ(command, "chg")) { 164 | newValue = displayAttr.curValue + newValue; 165 | if (newValue < 0 ) { newValue = 0; } 166 | if (newValue > displayAttr.maxValue ) { newValue = displayAttr.maxValue; } 167 | } 168 | 169 | return (UInt16)newValue; 170 | } 171 | 172 | int main(int argc, char** argv) { 173 | 174 | bool verbose = false; 175 | argv += 1; 176 | argc -= 1; 177 | 178 | if (argc < 2) { 179 | printUsage(); 180 | return argc && STR_EQ(argv[0], "help") ? 1 : 0; 181 | } 182 | 183 | if (STR_EQ(argv[0], "-v") || STR_EQ(argv[0], "--verbose")) { 184 | argv += 1; 185 | argc -= 1; 186 | verbose = true; 187 | } 188 | 189 | DisplayInfos displayInfos[MAX_DISPLAYS]; 190 | DisplayInfos *selectedDisplay = NULL; 191 | 192 | // Display lister and selection 193 | if (STR_EQ(argv[0], "display")) { 194 | 195 | int connectedDisplays = getOnlineDisplayInfos(displayInfos); 196 | if (connectedDisplays == 0) { 197 | writeToStdOut(@"No external display found, aborting"); 198 | return EXIT_FAILURE; 199 | } 200 | 201 | // Printing out display list 202 | if (STR_EQ(argv[1], "list") || STR_EQ(argv[1], "l")) { 203 | printDisplayInfos(displayInfos, connectedDisplays, (argc >= 3 && (STR_EQ(argv[2], "detailed") || STR_EQ(argv[2], "d")))); 204 | return EXIT_SUCCESS; 205 | } 206 | 207 | // Selecting display 208 | selectedDisplay = selectDisplay(displayInfos, connectedDisplays, argv[1]); 209 | if (selectedDisplay == NULL) { 210 | writeToStdOut(@"The specified display does not exist. Use 'display list' to list displays and use it's number (1, 2...) or its UUID to specify display!\n"); 211 | return EXIT_FAILURE; 212 | } 213 | 214 | argv += 2; 215 | argc -= 2; 216 | } 217 | 218 | IOAVServiceRef avService; 219 | 220 | // If there is no display selected, we'll use the default display 221 | if (selectedDisplay == NULL) { 222 | selectedDisplay = displayInfos; 223 | avService = getDefaultDisplayAVService(); 224 | } else { 225 | avService = getDisplayAVService(selectedDisplay); 226 | } 227 | 228 | if (avService == NULL) { 229 | writeToStdOut(@"Could not find a suitable external display.\n"); 230 | return EXIT_FAILURE; 231 | } 232 | 233 | if (argc < 2) { 234 | writeToStdOut(@"Missing parameter! Enter 'm1ddc help' for help!\n"); 235 | return EXIT_FAILURE; 236 | } 237 | 238 | if (verbose) { 239 | writeToStdOut([NSString stringWithFormat:@"Using display: %@ [%@]\n", selectedDisplay->productName, selectedDisplay->uuid]); 240 | } 241 | 242 | DDCValue displayAttr = {-1, -1}; 243 | UInt8 attrCode = attrCodeFromCommand(argv[1]); 244 | DDCPacket packet = createDDCPacket(attrCode); 245 | 246 | // Checking that packet.data[2] is not 0 (invalid command) 247 | if (packet.data[2] == 0) { 248 | writeToStdOut(@"Invalid command! Enter 'm1ddc help' for help!\n"); 249 | return EXIT_FAILURE; 250 | } 251 | 252 | // Reading current 253 | if (!STR_EQ(argv[0], "set")) { 254 | displayAttr = readingOperation(avService, &packet); 255 | if (displayAttr.curValue == -1) { 256 | return EXIT_FAILURE; 257 | } 258 | } 259 | 260 | if (STR_EQ(argv[0], "get")) { 261 | writeToStdOut([NSString stringWithFormat:@"%i\n", displayAttr.curValue]); 262 | return EXIT_SUCCESS; 263 | } 264 | 265 | if (STR_EQ(argv[0], "max")) { 266 | writeToStdOut([NSString stringWithFormat:@"%i\n", displayAttr.maxValue]); 267 | return EXIT_SUCCESS; 268 | } 269 | 270 | if (STR_EQ(argv[0], "set") || STR_EQ(argv[0], "chg") ) { 271 | if (argc < 3) { 272 | writeToStdOut(@"Missing value! Enter 'm1ddc help' for help!\n"); 273 | return EXIT_FAILURE; 274 | } 275 | 276 | UInt16 writeValue = computeAttributeValue(argv[0], argv[2], displayAttr); 277 | 278 | if (writingOperation(avService, &packet, writeValue)) { 279 | return EXIT_FAILURE; 280 | } 281 | writeToStdOut([NSString stringWithFormat:@"Writing %i\n", writeValue]); 282 | return EXIT_SUCCESS; 283 | } 284 | writeToStdOut(@"Use 'set', 'get', 'max', 'chg' as first parameter! Enter 'm1ddc help' for help!\n"); 285 | return EXIT_FAILURE; 286 | } --------------------------------------------------------------------------------