├── .gitignore ├── scripts └── ddcctl.sh ├── .github ├── workflows │ └── release-workflow.yml └── ISSUE_TEMPLATE │ └── ISSUE_REPORT.md ├── Makefile ├── README.md ├── src ├── DDC.h ├── DDC.c └── ddcctl.m └── ddcctl.xcodeproj └── project.pbxproj /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin/ 3 | build/ 4 | *.xcworkspace/ 5 | xc*data/ 6 | -------------------------------------------------------------------------------- /scripts/ddcctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #tweak OSX display monitors' brightness to a given scheme, increment, or based on the current local time 3 | 4 | hp="ddcctl -d 1" 5 | len="ddcctl -d 2" 6 | 7 | dim() { 8 | $hp -b 42 -c 26 9 | $len -b 4 -c 9 10 | } 11 | 12 | bright() { 13 | $hp -b 100 -c 75 14 | $len -b 85 -c 80 15 | } 16 | 17 | up() { 18 | $hp -b 20+ -c 12+ 19 | $len -b 15+ -c 12+ 20 | } 21 | 22 | down() { 23 | $hp -b 20- -c 12- 24 | $len -b 15- -c 12- 25 | } 26 | 27 | case "$1" in 28 | dim|bright|up|down) $1;; 29 | *) #no scheme given, match local Hour of Day 30 | HoD=$(date +%k) #hour of day 31 | let "night = (( $HoD < 7 || $HoD > 18 ))" #daytime is 7a-7p 32 | (($night)) && dim || bright 33 | ;; 34 | esac 35 | -------------------------------------------------------------------------------- /.github/workflows/release-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Create a GitHub release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build: 9 | name: Create a GitHub Release 10 | runs-on: macos-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Build 15 | run: | 16 | make 17 | zip ddcctl.zip bin/release/ddcctl 18 | - name: Get tag 19 | id: get_tag 20 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 21 | - name: Create GitHub Release 22 | id: create_release 23 | uses: actions/create-release@v1 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | tag_name: ${{ github.ref }} 28 | release_name: Release ${{ github.ref }} 29 | body: ${{ steps.get_tag.outputs.VERSION }} release of `ddcctl`. 30 | draft: false 31 | prerelease: false 32 | - name: Upload GitHub Release Assets 33 | id: upload-release-asset 34 | uses: actions/upload-release-asset@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | upload_url: ${{ steps.create_release.outputs.upload_url }} 39 | asset_path: ./ddcctl.zip 40 | asset_name: ddcctl_binaries.zip 41 | asset_content_type: application/zip 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/bin/make -f 2 | 3 | ## Reply Transaction Type 4 | ## uncomment one only one 5 | ## comment both to use automatic detection 6 | #CCFLAGS += -DTT_DDC 7 | #CCFLAGS += -DTT_SIMPLE 8 | 9 | ## Uncomment to use an external app 'OSDisplay' to have a BezelUI-like OSD 10 | ## provided by https://github.com/zulu-entertainment/OSDisplay 11 | #CCFLAGS += -DOSD 12 | 13 | INSTALL_DIR = /usr/local/bin 14 | SOURCE_DIR = ./src 15 | 16 | ifneq "$(strip $(filter debug, $(MAKECMDGOALS)))" "" 17 | CCFLAGS += -DDEBUG 18 | BUILD_DIR = ./build/debug 19 | PRODUCT_DIR = ./bin/debug 20 | else 21 | CCFLAGS += -O3 22 | BUILD_DIR = ./build/release 23 | PRODUCT_DIR = ./bin/release 24 | endif 25 | 26 | all: clean $(PRODUCT_DIR)/ddcctl 27 | 28 | debug: clean 29 | 30 | $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.c 31 | @mkdir -p $(@D) 32 | $(CC) -Wall $(CCFLAGS) -c -o $@ $< 33 | 34 | $(PRODUCT_DIR)/ddcctl: $(BUILD_DIR)/DDC.o 35 | @mkdir -p $(@D) 36 | $(CC) -Wall $(CCFLAGS) -o $@ -lobjc -framework IOKit -framework AppKit -framework Foundation $< $(SOURCE_DIR)/$(@F).m 37 | 38 | install: $(PRODUCT_DIR)/ddcctl 39 | install $(PRODUCT_DIR)/ddcctl $(INSTALL_DIR) 40 | 41 | clean: 42 | $(RM) $(BUILD_DIR)/*.o $(PRODUCT_DIR)/ddcctl 43 | 44 | framebuffers: 45 | ioreg -c IOFramebuffer -k IOFBI2CInterfaceIDs -b -f -l -r -d 1 46 | 47 | displaylist: 48 | ioreg -c IODisplayConnect -b -f -r -l -i -d 2 49 | 50 | .PHONY: all debug clean install displaylist 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ISSUE_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Issue 3 | about: Guidelines for reporting issues with ddcctl 4 | --- 5 | 6 | Sending just an error message is not enough! 7 | Requirements for submitters 8 | -- 9 | Errors are a warning for you (the unknown monitor & Mac owner) and 10 | do not in-and-of-themselves mean there is a bug in `ddcctl`. 11 | 12 | You must include pertinent information on your monitors, Macintosh, and macOS, or else 13 | your issue will get an `incomplete` tag. 14 | 15 | Making & running a debug build (`make debug`) and reproducing your issue to provide 16 | detailed output for the report is highly encouraged! 17 | 18 | Known issues 19 | -- 20 | I _will_ close reports about these issues out-of-hand: 21 | 22 | ### __MY HACKINTOSH __: 23 | You're on your own with Hackintoshes. 24 | 25 | ### __YOUR MONITOR MAY NOT CORRECTLY SUPPORT MUCH OF DDC__ 26 | The DDC standard is very loosely implemented by monitor manufacturers beyond sleeping the display. 27 | * This is because Windows doesn't use brightness sensors to dim screens like OSX does —via USB, not DDC! 28 | * Adjusting brightness, contrast, and super-awesome-multimedia-frobber-mode may not be possible. 29 | 30 | ### __YOUR MONITOR MIGHT FREEZE__ when making settings, especially the non-brightness/contrast ones. 31 | * Power cycle the monitor. 32 | * You just have to trial-and-error what works for your hardware. 33 | 34 | Practical advice 35 | -- 36 | VGA cables seem to wreak havoc with DDC comms. 37 | Use DVI/DisplayPort/Thunderbolt if you can. 38 | 39 | Please consider that there is no team working on `ddcctl`, it is a fun-time project 40 | that has long-since been considered "finished". 41 | 42 | Bad, incomplete, or *lazy* reports and non-bugs are not fun to work on 43 | so I *will* be cranky towards their reporters who didn't heed these instructions. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddcctl: DDC monitor controls for the OSX command line 2 | 3 | Adjust your external monitors' built-in controls from the OSX shell: 4 | 5 | - brightness 6 | - contrast 7 | - PBP mode 8 | 9 | And _possibly_ (if your monitor firmware is well implemented): 10 | 11 | - input source 12 | - built-in speaker volume 13 | - on/off/standby 14 | - rgb colors 15 | - color presets 16 | - reset 17 | 18 | # Added three flags: 19 | 20 | | VCP Code | flag | args | description | 21 | | ---------- | ----------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------- | 22 | | 233 (0xE9) | -pbp | Value (0-255) | This either enables or disables PBP mode. On my machine, 0 is off and 36 is on | 23 | | 232 (0xE8) | -pbp-screen | Value (0-255) | Selects the input, uses same values as Input Sources below | 24 | | N/A | -vcp | VCP code (0-255), Value (0-255) | Sets the value a VCP code. Example `./ddcctl -d 1 -vcp 233 36`. This would set the value of VCP code 233 to 36. | 25 | 26 | # PBP Mode 27 | 28 | This is a fork I made to try and enable DPB mode (and select input) on a Dell U4919DW. 29 | And it works :) 30 | 31 | # How to Find your -pbp flag? 32 | 33 | **Only use these steps if using `-pbp 36` doesnt work.** 34 | 35 | 1. Nagivate to the directory where ddcctl is. 36 | 2. Ensure PBP mode is not on. 37 | 3. Run `./ddcctl -d 1 -D > normal.txt` - this will dump all the VCP codes into a text file 38 | 4. Once it has stopped running enable PBP mode on your monitor, and hook up a 2nd device - make sure the 2nd device does not go to sleep 39 | 5. On the first device in the same directory run `./ddcctl -d 1 -D > pbp.txt` and wait for it to complete 40 | 6. Go to [diffchecker](https://www.diffchecker.com/). 41 | 7. Paste each of the respective files content into each side and press find differences 42 | 8. It will show the VCP Codes that changed, i.e. one of these will be responsible for enabling PBP mode. 43 | 9. Copy these codes and their respective values when PBP mode is enabled/disabled. 44 | 10. Using the `-vcp` flag ddcctl allows you to set values for specific VCP codes. It takes two arguments. 45 | - The VCP code to change 46 | - The value to set that VCP code to 47 | 11. A command would look somethlike this: `./ddcctl -d 1 -vcp 233 36`. The first value being the VCP code in decmimal and the second being the value to change it to. 48 | 12. Using this command change the value of each VCP code from the list in step 9 one by one, till one of them enables PBP mode. Ensure that you change back any flags that do not enable PBP mode, BEFORE MOVIGING ON TO THE NEXT FLAG This means there won't be unintended changes 49 | 50 | ## Build from Source 51 | 52 | - install Xcode 53 | - run `make` 54 | 55 | # Usage 56 | 57 | Run `ddcctl -h` for some options. 58 | [ddcctl.sh](/scripts/ddcctl.sh) is a script I use to control two PC monitors plugged into my Mac Mini. 59 | You can point Alfred, ControlPlane, or Karabiner at it to quickly switch presets. 60 | 61 | # Input Sources 62 | 63 | When setting input source, refer to the table below to determine which value to use. 64 | For example, to set your first display to HDMI: `ddcctl -d 1 -i 17`. 65 | 66 | | Input Source | Value | 67 | | ------------------------------- | ----- | 68 | | VGA-1 | 1 | 69 | | VGA-2 | 2 | 70 | | DVI-1 | 3 | 71 | | DVI-2 | 4 | 72 | | Composite video 1 | 5 | 73 | | Composite video 2 | 6 | 74 | | S-Video-1 | 7 | 75 | | S-Video-2 | 8 | 76 | | Tuner-1 | 9 | 77 | | Tuner-2 | 10 | 78 | | Tuner-3 | 11 | 79 | | Component video (YPrPb/YCrCb) 1 | 12 | 80 | | Component video (YPrPb/YCrCb) 2 | 13 | 81 | | Component video (YPrPb/YCrCb) 3 | 14 | 82 | | DisplayPort-1 | 15 | 83 | | DisplayPort-2 | 16 | 84 | | HDMI-1 | 17 | 85 | | HDMI-2 | 18 | 86 | | USB-C | 27 | 87 | 88 | # Credits 89 | 90 | `ddcctl.m` sprang from a [forum thread](https://www.tonymacx86.com/threads/controlling-your-monitor-with-osx-ddc-panel.90077/page-6#post-795208) on the TonyMac-x86 boards. 91 | 92 | `DDC.c` originated from [jontaylor/DDC-CI-Tools-for-OS-X](https://github.com/jontaylor/DDC-CI-Tools-for-OS-X), but was reworked by others on the forums. 93 | 94 | A few forks have also backported patches, which is _nice_ :ok_hand:. 95 | -------------------------------------------------------------------------------- /src/DDC.h: -------------------------------------------------------------------------------- 1 | // 2 | // DDC.h 3 | // DDC Panel 4 | // 5 | // Created by Jonathan Taylor on 7/10/09. 6 | // See ftp://ftp.cis.nctu.edu.tw/pub/csie/Software/X11/private/VeSaSpEcS/VESA_Document_Center_Monitor_Interface/mccsV3.pdf 7 | // See http://read.pudn.com/downloads110/ebook/456020/E-EDID%20Standard.pdf 8 | // See ftp://ftp.cis.nctu.edu.tw/pub/csie/Software/X11/private/VeSaSpEcS/VESA_Document_Center_Monitor_Interface/EEDIDrAr2.pdf 9 | // 10 | 11 | #ifndef DDC_Panel_DDC_h 12 | #define DDC_Panel_DDC_h 13 | 14 | #include 15 | 16 | #define RESET 0x04 17 | #define RESET_BRIGHTNESS_AND_CONTRAST 0x05 18 | #define RESET_GEOMETRY 0x06 19 | #define RESET_COLOR 0x08 20 | #define BRIGHTNESS 0x10 //OK 21 | #define CONTRAST 0x12 //OK 22 | #define COLOR_PRESET_A 0x14 // dell u2515h -> Presets: 4 = 5000K, 5 = 6500K, 6 = 7500K, 8 = 9300K, 9 = 10000K, 11 = 5700K, 12 = Custom Color 23 | #define RED_GAIN 0x16 24 | #define GREEN_GAIN 0x18 25 | #define BLUE_GAIN 0x1A 26 | #define AUTO_SIZE_CENTER 0x1E 27 | #define WIDTH 0x22 28 | #define HEIGHT 0x32 29 | #define VERTICAL_POS 0x30 30 | #define HORIZONTAL_POS 0x20 31 | #define PINCUSHION_AMP 0x24 32 | #define PINCUSHION_PHASE 0x42 33 | #define KEYSTONE_BALANCE 0x40 34 | #define PINCUSHION_BALANCE 0x26 35 | #define TOP_PINCUSHION_AMP 0x46 36 | #define TOP_PINCUSHION_BALANCE 0x48 37 | #define BOTTOM_PINCUSHION_AMP 0x4A 38 | #define BOTTOM_PINCUSHION_BALANCE 0x4C 39 | #define VERTICAL_LINEARITY 0x3A 40 | #define VERTICAL_LINEARITY_BALANCE 0x3C 41 | #define HORIZONTAL_STATIC_CONVERGENCE 0x28 42 | #define VERTICAL_STATIC_CONVERGENCE 0x28 43 | #define MOIRE_CANCEL 0x56 44 | #define INPUT_SOURCE 0x60 45 | #define AUDIO_SPEAKER_VOLUME 0x62 46 | #define RED_BLACK_LEVEL 0x6C 47 | #define GREEN_BLACK_LEVEL 0x6E 48 | #define BLUE_BLACK_LEVEL 0x70 49 | #define ORIENTATION 0xAA 50 | #define AUDIO_MUTE 0x8D 51 | #define SETTINGS 0xB0 //unsure on this one 52 | #define ON_SCREEN_DISPLAY 0xCA // read only -> returns '1' (OSD closed) or '2' (OSD active) 53 | #define OSD_LANGUAGE 0xCC 54 | #define DPMS 0xD6 55 | #define COLOR_PRESET_B 0xDC // dell u2515h -> Presets: 0 = Standard, 2 = Multimedia, 3 = Movie, 5 = Game 56 | #define VCP_VERSION 0xDF 57 | #define COLOR_PRESET_C 0xE0 // dell u2515h -> Brightness on/off (0 or 1) 58 | #define POWER_CONTROL 0xE1 59 | #define TOP_LEFT_SCREEN_PURITY 0xE8 60 | #define TOP_RIGHT_SCREEN_PURITY 0xE9 61 | #define BOTTOM_LEFT_SCREEN_PURITY 0xE8 62 | #define BOTTOM_RIGHT_SCREEN_PURITY 0xEB 63 | #define PBP 0xE9 64 | #define PBP_SCREEN 0xE8 65 | 66 | 67 | struct DDCWriteCommand 68 | { 69 | UInt8 control_id; 70 | UInt8 new_value; 71 | }; 72 | 73 | struct DDCReadCommand 74 | { 75 | UInt8 control_id; 76 | bool success; 77 | UInt8 max_value; 78 | UInt8 current_value; 79 | }; 80 | 81 | struct EDID { 82 | UInt64 header : 64; 83 | UInt8 : 1; 84 | UInt16 eisaid :15; 85 | UInt16 productcode : 16; 86 | UInt32 serial : 32; 87 | UInt8 week : 8; 88 | UInt8 year : 8; 89 | UInt8 versionmajor : 8; 90 | UInt8 versionminor : 8; 91 | union videoinput { 92 | struct digitalinput { 93 | UInt8 type : 1; 94 | UInt8 : 6; 95 | UInt8 dfp : 1; 96 | } digital; 97 | struct analoginput { 98 | UInt8 type : 1; 99 | UInt8 synclevels : 2; 100 | UInt8 pedestal : 1; 101 | UInt8 separate : 1; 102 | UInt8 composite : 1; 103 | UInt8 green : 1; 104 | UInt8 serrated : 1; 105 | } analog; 106 | } videoinput; 107 | UInt8 maxh : 8; 108 | UInt8 maxv : 8; 109 | UInt8 gamma : 8; 110 | UInt8 standby : 1; 111 | UInt8 suspend : 1; 112 | UInt8 activeoff : 1; 113 | UInt8 displaytype: 2; 114 | UInt8 srgb : 1; 115 | UInt8 preferredtiming : 1; 116 | UInt8 gtf : 1; 117 | UInt8 redxlsb : 2; 118 | UInt8 redylsb : 2; 119 | UInt8 greenxlsb : 2; 120 | UInt8 greenylsb : 2; 121 | UInt8 bluexlsb : 2; 122 | UInt8 blueylsb : 2; 123 | UInt8 whitexlsb : 2; 124 | UInt8 whiteylsb : 2; 125 | UInt8 redxmsb : 8; 126 | UInt8 redymsb : 8; 127 | UInt8 greenxmsb : 8; 128 | UInt8 greenymsb : 8; 129 | UInt8 bluexmsb : 8; 130 | UInt8 blueymsb : 8; 131 | UInt8 whitexmsb : 8; 132 | UInt8 whiteymsb : 8; 133 | UInt8 t720x400a70 : 1; 134 | UInt8 t720x400a88 : 1; 135 | UInt8 t640x480a60 : 1; 136 | UInt8 t640x480a67 : 1; 137 | UInt8 t640x480a72 : 1; 138 | UInt8 t640x480a75 : 1; 139 | UInt8 t800x600a56 : 1; 140 | UInt8 t800x600a60 : 1; 141 | UInt8 t800x600a72 : 1; 142 | UInt8 t800x600a75 : 1; 143 | UInt8 t832x624a75 : 1; 144 | UInt8 t1024x768a87 : 1; 145 | UInt8 t1024x768a60 : 1; 146 | UInt8 t1024x768a72 : 1; 147 | UInt8 t1024x768a75 : 1; 148 | UInt8 t1280x1024a75 : 1; 149 | UInt8 t1152x870a75 : 1; 150 | UInt8 othermodes : 7; 151 | struct timing { 152 | UInt8 xresolution : 8; 153 | UInt8 ratio : 2; 154 | UInt8 vertical : 6; 155 | } timing1; 156 | struct timing timing2; 157 | struct timing timing3; 158 | struct timing timing4; 159 | struct timing timing5; 160 | struct timing timing6; 161 | struct timing timing7; 162 | struct timing timing8; 163 | union descriptor { 164 | struct timingdetail { 165 | UInt16 clock : 16; 166 | UInt8 hactivelsb : 8; 167 | UInt8 hblankinglsb : 8; 168 | UInt8 hactivemsb : 4; 169 | UInt8 hblankingmsb : 4; 170 | UInt8 vactivelsb : 8; 171 | UInt8 vblankinglsb : 8; 172 | UInt8 vactivemsb : 4; 173 | UInt8 vblankingmsb : 4; 174 | UInt8 hsyncoffsetlsb : 8; 175 | UInt8 hsyncpulselsb : 8; 176 | UInt8 vsyncoffsetlsb : 4; 177 | UInt8 vsyncpulselsb : 4; 178 | UInt8 hsyncoffsetmsb : 2; 179 | UInt8 hsyncpulsemsb : 2; 180 | UInt8 vsyncoffsetmsb : 2; 181 | UInt8 vsyncpulsemsb : 2; 182 | UInt8 hsizelsb : 8; 183 | UInt8 vsizelsb : 8; 184 | UInt8 hsizemsb : 4; 185 | UInt8 vsizemsb : 4; 186 | UInt8 hborder : 8; 187 | UInt8 vborder : 8; 188 | UInt8 interlaced : 1; 189 | UInt8 stereo : 2; 190 | UInt8 synctype : 2; 191 | UInt8 vsyncpol_serrated: 1; 192 | UInt8 hsyncpol_syncall: 1; 193 | UInt8 twowaystereo : 1; 194 | } timing; 195 | struct text { 196 | UInt32 : 24; 197 | UInt8 type : 8; 198 | UInt8 : 8; 199 | char data[13]; 200 | } text; 201 | struct __attribute__ ((packed)) rangelimits { 202 | UInt64 header : 40; 203 | UInt8 minvfield : 8; 204 | UInt8 minhfield : 8; 205 | UInt8 minhline : 8; 206 | UInt8 minvline : 8; 207 | UInt8 maxclock : 8; 208 | UInt8 extended : 8; 209 | UInt8 : 8; 210 | UInt8 startfreq : 8; 211 | UInt8 cvalue : 8; 212 | UInt16 mvalue : 16; 213 | UInt8 kvalue : 8; 214 | UInt8 jvalue : 8; 215 | } range; 216 | struct __attribute__ ((packed)) whitepoint { 217 | UInt64 header : 40; 218 | UInt8 index : 8; 219 | UInt8 : 4; 220 | UInt8 whitexlsb : 2; 221 | UInt8 whiteylsb : 2; 222 | UInt8 whitexmsb : 8; 223 | UInt8 whiteymsb : 8; 224 | UInt8 gamma : 8; 225 | UInt8 index2 : 8; 226 | UInt8 : 4; 227 | UInt8 whitexlsb2 : 2; 228 | UInt8 whiteylsb2 : 2; 229 | UInt8 whitexmsb2 : 8; 230 | UInt8 whiteymsb2 : 8; 231 | UInt8 gamma2 : 8; 232 | UInt32 : 24; 233 | } whitepoint; 234 | } descriptors[4]; 235 | UInt8 extensions : 8; 236 | UInt8 checksum : 8; 237 | }; 238 | 239 | long DDCDelayBase; 240 | long DDCDelay(io_service_t framebuffer); 241 | bool DDCWrite(io_service_t framebuffer, struct DDCWriteCommand *write); 242 | bool DDCRead(io_service_t framebuffer, struct DDCReadCommand *read); 243 | bool EDIDTest(io_service_t framebuffer, struct EDID *edid); 244 | UInt32 SupportedTransactionType(void); 245 | io_service_t IOFramebufferPortFromCGDisplayID(CGDirectDisplayID displayID, CFStringRef displayLocation); 246 | #endif 247 | -------------------------------------------------------------------------------- /ddcctl.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8FB2F43F253CA13D0005A241 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FB2F41B253C9F8F0005A241 /* IOKit.framework */; }; 11 | 8FB2F440253CA1480005A241 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FB2F41F253C9F990005A241 /* Foundation.framework */; }; 12 | 8FB2F441253CA15D0005A241 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FB2F41D253C9F930005A241 /* AppKit.framework */; }; 13 | 8FB2F44A253CA1960005A241 /* ddcctl.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB2F40C253C9DA20005A241 /* ddcctl.m */; }; 14 | 8FB2F44B253CA1960005A241 /* DDC.h in Sources */ = {isa = PBXBuildFile; fileRef = 8FB2F40E253C9DA20005A241 /* DDC.h */; }; 15 | 8FB2F44C253CA1960005A241 /* DDC.c in Sources */ = {isa = PBXBuildFile; fileRef = 8FB2F410253C9DA20005A241 /* DDC.c */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 8FB2F426253CA0B90005A241 /* CopyFiles */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = /usr/share/man/man1/; 23 | dstSubfolderSpec = 0; 24 | files = ( 25 | ); 26 | runOnlyForDeploymentPostprocessing = 1; 27 | }; 28 | /* End PBXCopyFilesBuildPhase section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 8FB2F40C253C9DA20005A241 /* ddcctl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ddcctl.m; sourceTree = ""; }; 32 | 8FB2F40E253C9DA20005A241 /* DDC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DDC.h; sourceTree = ""; }; 33 | 8FB2F410253C9DA20005A241 /* DDC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DDC.c; sourceTree = ""; }; 34 | 8FB2F41B253C9F8F0005A241 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 35 | 8FB2F41D253C9F930005A241 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 36 | 8FB2F41F253C9F990005A241 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 37 | 8FB2F428253CA0B90005A241 /* ddcctl */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = ddcctl; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 8FB2F425253CA0B90005A241 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 8FB2F440253CA1480005A241 /* Foundation.framework in Frameworks */, 46 | 8FB2F441253CA15D0005A241 /* AppKit.framework in Frameworks */, 47 | 8FB2F43F253CA13D0005A241 /* IOKit.framework in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 8FB2F3F6253C9D750005A241 = { 55 | isa = PBXGroup; 56 | children = ( 57 | 8FB2F40B253C9DA20005A241 /* src */, 58 | 8FB2F400253C9D750005A241 /* Products */, 59 | 8FB2F41A253C9F8E0005A241 /* Frameworks */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 8FB2F400253C9D750005A241 /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 8FB2F428253CA0B90005A241 /* ddcctl */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 8FB2F40B253C9DA20005A241 /* src */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 8FB2F40C253C9DA20005A241 /* ddcctl.m */, 75 | 8FB2F40E253C9DA20005A241 /* DDC.h */, 76 | 8FB2F410253C9DA20005A241 /* DDC.c */, 77 | ); 78 | path = src; 79 | sourceTree = ""; 80 | }; 81 | 8FB2F41A253C9F8E0005A241 /* Frameworks */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 8FB2F41F253C9F990005A241 /* Foundation.framework */, 85 | 8FB2F41D253C9F930005A241 /* AppKit.framework */, 86 | 8FB2F41B253C9F8F0005A241 /* IOKit.framework */, 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 8FB2F427253CA0B90005A241 /* ddcctl */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 8FB2F42C253CA0B90005A241 /* Build configuration list for PBXNativeTarget "ddcctl" */; 97 | buildPhases = ( 98 | 8FB2F424253CA0B90005A241 /* Sources */, 99 | 8FB2F425253CA0B90005A241 /* Frameworks */, 100 | 8FB2F426253CA0B90005A241 /* CopyFiles */, 101 | 8FB2F459253CA6F80005A241 /* ShellScript */, 102 | ); 103 | buildRules = ( 104 | ); 105 | dependencies = ( 106 | ); 107 | name = ddcctl; 108 | productName = "ddcctl"; 109 | productReference = 8FB2F428253CA0B90005A241 /* ddcctl */; 110 | productType = "com.apple.product-type.tool"; 111 | }; 112 | /* End PBXNativeTarget section */ 113 | 114 | /* Begin PBXProject section */ 115 | 8FB2F3F7253C9D750005A241 /* Project object */ = { 116 | isa = PBXProject; 117 | attributes = { 118 | LastUpgradeCheck = 1200; 119 | TargetAttributes = { 120 | 8FB2F427253CA0B90005A241 = { 121 | CreatedOnToolsVersion = 12.0.1; 122 | }; 123 | }; 124 | }; 125 | buildConfigurationList = 8FB2F3FA253C9D750005A241 /* Build configuration list for PBXProject "ddcctl" */; 126 | compatibilityVersion = "Xcode 9.3"; 127 | developmentRegion = en; 128 | hasScannedForEncodings = 0; 129 | knownRegions = ( 130 | en, 131 | Base, 132 | ); 133 | mainGroup = 8FB2F3F6253C9D750005A241; 134 | productRefGroup = 8FB2F400253C9D750005A241 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 8FB2F427253CA0B90005A241 /* ddcctl */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXShellScriptBuildPhase section */ 144 | 8FB2F459253CA6F80005A241 /* ShellScript */ = { 145 | isa = PBXShellScriptBuildPhase; 146 | buildActionMask = 8; 147 | files = ( 148 | ); 149 | inputFileListPaths = ( 150 | ); 151 | inputPaths = ( 152 | ); 153 | outputFileListPaths = ( 154 | ); 155 | outputPaths = ( 156 | ); 157 | runOnlyForDeploymentPostprocessing = 1; 158 | shellPath = /bin/sh; 159 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\ncd \"$INSTALL_DIR\"\nln -s \"$TARGET_NAME\" \"$PROJECT_NAME\"\n"; 160 | }; 161 | /* End PBXShellScriptBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | 8FB2F424253CA0B90005A241 /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 8FB2F44A253CA1960005A241 /* ddcctl.m in Sources */, 169 | 8FB2F44B253CA1960005A241 /* DDC.h in Sources */, 170 | 8FB2F44C253CA1960005A241 /* DDC.c in Sources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXSourcesBuildPhase section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 8FB2F404253C9D750005A241 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_ENABLE_OBJC_WEAK = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 204 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 205 | CLANG_WARN_STRICT_PROTOTYPES = YES; 206 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 207 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 208 | CLANG_WARN_UNREACHABLE_CODE = YES; 209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 210 | COPY_PHASE_STRIP = NO; 211 | DEBUG_INFORMATION_FORMAT = dwarf; 212 | ENABLE_STRICT_OBJC_MSGSEND = YES; 213 | ENABLE_TESTABILITY = YES; 214 | GCC_C_LANGUAGE_STANDARD = gnu11; 215 | GCC_DYNAMIC_NO_PIC = NO; 216 | GCC_NO_COMMON_BLOCKS = YES; 217 | GCC_OPTIMIZATION_LEVEL = 0; 218 | GCC_PREPROCESSOR_DEFINITIONS = ( 219 | "DEBUG=1", 220 | "$(inherited)", 221 | ); 222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 223 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 226 | GCC_WARN_UNUSED_FUNCTION = YES; 227 | GCC_WARN_UNUSED_VARIABLE = YES; 228 | INSTALL_PATH = /bin; 229 | MACOSX_DEPLOYMENT_TARGET = 10.15; 230 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 231 | MTL_FAST_MATH = YES; 232 | ONLY_ACTIVE_ARCH = YES; 233 | OTHER_CFLAGS = ( 234 | "-Wall", 235 | "-Wextra", 236 | ); 237 | SDKROOT = macosx; 238 | SYMROOT = "$(SRCROOT)/build"; 239 | }; 240 | name = Debug; 241 | }; 242 | 8FB2F405253C9D750005A241 /* Release */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_ENABLE_OBJC_WEAK = YES; 253 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 254 | CLANG_WARN_BOOL_CONVERSION = YES; 255 | CLANG_WARN_COMMA = YES; 256 | CLANG_WARN_CONSTANT_CONVERSION = YES; 257 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 258 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 259 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 260 | CLANG_WARN_EMPTY_BODY = YES; 261 | CLANG_WARN_ENUM_CONVERSION = YES; 262 | CLANG_WARN_INFINITE_RECURSION = YES; 263 | CLANG_WARN_INT_CONVERSION = YES; 264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | COPY_PHASE_STRIP = NO; 276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 277 | DEPLOYMENT_LOCATION = YES; 278 | DEPLOYMENT_POSTPROCESSING = YES; 279 | DSTROOT = "$(HOME)"; 280 | ENABLE_NS_ASSERTIONS = NO; 281 | ENABLE_STRICT_OBJC_MSGSEND = YES; 282 | GCC_C_LANGUAGE_STANDARD = gnu11; 283 | GCC_NO_COMMON_BLOCKS = YES; 284 | GCC_OPTIMIZATION_LEVEL = 3; 285 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 286 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 287 | GCC_WARN_UNDECLARED_SELECTOR = YES; 288 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 289 | GCC_WARN_UNUSED_FUNCTION = YES; 290 | GCC_WARN_UNUSED_VARIABLE = YES; 291 | INSTALL_PATH = /bin; 292 | MACOSX_DEPLOYMENT_TARGET = 10.15; 293 | MTL_ENABLE_DEBUG_INFO = NO; 294 | MTL_FAST_MATH = YES; 295 | ONLY_ACTIVE_ARCH = YES; 296 | OTHER_CFLAGS = ( 297 | "-Wall", 298 | "-Wextra", 299 | ); 300 | SDKROOT = macosx; 301 | SYMROOT = "$(SRCROOT)/build"; 302 | }; 303 | name = Release; 304 | }; 305 | 8FB2F42D253CA0B90005A241 /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | CODE_SIGN_STYLE = Automatic; 309 | OTHER_CFLAGS = ( 310 | "-Wall", 311 | "-Wextra", 312 | ); 313 | PRODUCT_NAME = "$(TARGET_NAME)"; 314 | }; 315 | name = Debug; 316 | }; 317 | 8FB2F42E253CA0B90005A241 /* Release */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | CODE_SIGN_STYLE = Automatic; 321 | OTHER_CFLAGS = ( 322 | "-Wall", 323 | "-Wextra", 324 | ); 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | }; 327 | name = Release; 328 | }; 329 | /* End XCBuildConfiguration section */ 330 | 331 | /* Begin XCConfigurationList section */ 332 | 8FB2F3FA253C9D750005A241 /* Build configuration list for PBXProject "ddcctl" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | 8FB2F404253C9D750005A241 /* Debug */, 336 | 8FB2F405253C9D750005A241 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | 8FB2F42C253CA0B90005A241 /* Build configuration list for PBXNativeTarget "ddcctl" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 8FB2F42D253CA0B90005A241 /* Debug */, 345 | 8FB2F42E253CA0B90005A241 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | /* End XCConfigurationList section */ 351 | }; 352 | rootObject = 8FB2F3F7253C9D750005A241 /* Project object */; 353 | } 354 | -------------------------------------------------------------------------------- /src/DDC.c: -------------------------------------------------------------------------------- 1 | // 2 | // DDC.c 3 | // DDC Panel 4 | // 5 | // Created by Jonathan Taylor on 7/10/09. 6 | // See http://github.com/jontaylor/DDC-CI-Tools-for-OS-X 7 | // 8 | 9 | #include 10 | #include 11 | #include 12 | #include "DDC.h" 13 | 14 | #ifndef kMaxRequests 15 | #define kMaxRequests 10 16 | #endif 17 | 18 | #ifndef _IOKIT_IOFRAMEBUFFER_H 19 | #define kIOFBDependentIDKey "IOFBDependentID" 20 | #define kIOFBDependentIndexKey "IOFBDependentIndex" 21 | #endif 22 | 23 | long DDCDelayBase = 1; // nanoseconds 24 | 25 | /* 26 | 27 | Iterate IOreg's device tree to find the IOFramebuffer mach service port that corresponds to a given CGDisplayID && IOReg path 28 | based on: https://github.com/glfw/glfw/pull/192/files 29 | */ 30 | io_service_t IOFramebufferPortFromCGDisplayID(CGDirectDisplayID displayID, CFStringRef displayLocation) 31 | { 32 | io_iterator_t iter; 33 | io_service_t serv, servicePort = 0; 34 | 35 | kern_return_t err = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(IOFRAMEBUFFER_CONFORMSTO), &iter); 36 | 37 | if (err != KERN_SUCCESS) 38 | return 0; 39 | 40 | // now recurse the IOReg tree 41 | while ((serv = IOIteratorNext(iter)) != MACH_PORT_NULL) 42 | { 43 | CFDictionaryRef info; 44 | CFIndex vendorID = 0, productID = 0, serialNumber = 0; 45 | CFNumberRef vendorIDRef, productIDRef, serialNumberRef; 46 | CFStringRef location = CFSTR(""); 47 | #ifdef DEBUG 48 | CFIndex dependID = 0, dependIndex = 0; 49 | CFNumberRef dependIDRef, dependIndexRef; 50 | io_name_t name; 51 | CFStringRef serial = CFSTR(""); 52 | 53 | // get metadata from IOreg node 54 | IORegistryEntryGetName(serv, name); 55 | info = IODisplayCreateInfoDictionary(serv, kIODisplayOnlyPreferredName); 56 | 57 | CFStringRef ioRegPath = IORegistryEntryCopyPath(serv, kIOServicePlane); 58 | // just location/../../ (-- /display0/AppleDisplay) 59 | 60 | // https://stackoverflow.com/a/48450870/3878712 61 | CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayID); 62 | CFStringRef uuidStr = CFUUIDCreateString(NULL, uuid); 63 | #endif 64 | Boolean success = 0; 65 | 66 | info = IODisplayCreateInfoDictionary(serv, kIODisplayOnlyPreferredName); // kIODisplayMatchingInfo 67 | 68 | /* When assigning a display ID, Quartz considers the following parameters:Vendor, Model, Serial Number and Position in the I/O Kit registry */ 69 | // http://opensource.apple.com//source/IOGraphics/IOGraphics-179.2/IOGraphicsFamily/IOKit/graphics/IOGraphicsTypes.h 70 | CFStringRef locationRef = CFDictionaryGetValue(info, CFSTR(kIODisplayLocationKey)); 71 | if (locationRef) location = CFStringCreateCopy(NULL, locationRef); 72 | #ifdef DEBUG 73 | CFStringRef serialRef = CFDictionaryGetValue(info, CFSTR(kDisplaySerialString)); 74 | if (serialRef) serial = CFStringCreateCopy(NULL, serialRef); 75 | 76 | if ((dependIDRef = CFDictionaryGetValue(info, CFSTR(kIOFBDependentIDKey)))) 77 | CFNumberGetValue(dependIDRef, kCFNumberCFIndexType, &dependID); 78 | if ((dependIndexRef = CFDictionaryGetValue(info, CFSTR(kIOFBDependentIndexKey)))) 79 | CFNumberGetValue(dependIndexRef, kCFNumberCFIndexType, &dependIndex); 80 | #endif 81 | if (CFDictionaryGetValueIfPresent(info, CFSTR(kDisplayVendorID), (const void**)&vendorIDRef)) 82 | success = CFNumberGetValue(vendorIDRef, kCFNumberCFIndexType, &vendorID); 83 | 84 | if (CFDictionaryGetValueIfPresent(info, CFSTR(kDisplayProductID), (const void**)&productIDRef)) 85 | success &= CFNumberGetValue(productIDRef, kCFNumberCFIndexType, &productID); 86 | 87 | IOItemCount busCount; 88 | IOFBGetI2CInterfaceCount(serv, &busCount); 89 | 90 | if (!success || busCount < 1 || CGDisplayIsBuiltin(displayID)) { 91 | // this does not seem to be a DDC-enabled display, skip it 92 | #ifdef DEBUG 93 | CFRelease(location); 94 | CFRelease(serial); 95 | #endif 96 | CFRelease(info); 97 | continue; 98 | } 99 | // kAppleDisplayTypeKey -- if this is an Apple display, can use IODisplay func to change brightness: http://stackoverflow.com/a/32691700/3878712 100 | 101 | if (CFDictionaryGetValueIfPresent(info, CFSTR(kDisplaySerialNumber), (const void**)&serialNumberRef)) 102 | CFNumberGetValue(serialNumberRef, kCFNumberCFIndexType, &serialNumber); 103 | #ifdef DEBUG 104 | printf("I: Attempting match for %s's IODisplayPort @ %s...\n", 105 | CFStringGetCStringPtr(uuidStr, kCFStringEncodingUTF8), CFStringGetCStringPtr(location, kCFStringEncodingUTF8)); 106 | #endif 107 | if (displayLocation) { 108 | // we were provided with a specific ioreg location we suspect the targeted framebuffer to be attached to... 109 | if (!CFStringHasPrefix(location, displayLocation)) { 110 | // this framebuffer doesn't reside within the provided location 111 | CFRelease(info); 112 | continue; 113 | } 114 | } 115 | // compare IOreg's metadata to CGDisplay's metadata to infer if the IOReg's I2C monitor is the display for the given NSScreen.displayID 116 | if (CGDisplayVendorNumber(displayID) != (UInt32)vendorID || 117 | CGDisplayModelNumber(displayID) != (UInt32)productID || 118 | CGDisplaySerialNumber(displayID) != (UInt32)serialNumber) // SN is zero in lots of cases, so duplicate-monitors can confuse us :-/ 119 | { 120 | #ifdef DEBUG 121 | CFRelease(location); 122 | CFRelease(serial); 123 | #endif 124 | CFRelease(info); 125 | continue; 126 | } 127 | 128 | #ifdef DEBUG 129 | // considering this IOFramebuffer as the match for the CGDisplay, dump out its information 130 | // compare with `make displaylist` 131 | printf("\nFramebuffer: %s\n", name); 132 | printf("%s\n", CFStringGetCStringPtr(ioRegPath, kCFStringEncodingUTF8)); 133 | printf("%s\n", CFStringGetCStringPtr(location, kCFStringEncodingUTF8)); 134 | printf("VN:%ld PN:%ld SN:%ld", vendorID, productID, serialNumber); 135 | printf(" UN:%d", CGDisplayUnitNumber(displayID)); 136 | printf(" IN:%d", iter); 137 | printf(" depID:%ld depIdx:%ld", dependID, dependIndex); 138 | printf(" Serial:%s\n\n", CFStringGetCStringPtr(serial, kCFStringEncodingUTF8)); 139 | CFRelease(location); 140 | CFRelease(serial); 141 | #endif 142 | servicePort = serv; 143 | CFRelease(info); 144 | break; 145 | } 146 | 147 | IOObjectRelease(iter); 148 | return servicePort; 149 | } 150 | 151 | dispatch_semaphore_t I2CRequestQueue(io_service_t i2c_device_id) { 152 | static UInt64 queueCount = 0; 153 | static struct ReqQueue {uint32_t id; dispatch_semaphore_t queue;} *queues = NULL; 154 | dispatch_semaphore_t queue = NULL; 155 | if (!queues) 156 | queues = calloc(50, sizeof(*queues)); //FIXME: specify 157 | UInt64 i = 0; 158 | while (i < queueCount) 159 | if (queues[i].id == i2c_device_id) 160 | break; 161 | else 162 | i++; 163 | if (queues[i].id == i2c_device_id) 164 | queue = queues[i].queue; 165 | else 166 | queues[queueCount++] = (struct ReqQueue){i2c_device_id, (queue = dispatch_semaphore_create(1))}; 167 | return queue; 168 | } 169 | 170 | bool FramebufferI2CRequest(io_service_t framebuffer, IOI2CRequest *request) { 171 | dispatch_semaphore_t queue = I2CRequestQueue(framebuffer); 172 | dispatch_semaphore_wait(queue, DISPATCH_TIME_FOREVER); 173 | bool result = false; 174 | IOItemCount busCount; 175 | if (IOFBGetI2CInterfaceCount(framebuffer, &busCount) == KERN_SUCCESS) { 176 | IOOptionBits bus = 0; 177 | while (bus < busCount) { 178 | io_service_t interface; 179 | if (IOFBCopyI2CInterfaceForBus(framebuffer, bus++, &interface) != KERN_SUCCESS) 180 | continue; 181 | 182 | IOI2CConnectRef connect; 183 | if (IOI2CInterfaceOpen(interface, kNilOptions, &connect) == KERN_SUCCESS) { 184 | result = (IOI2CSendRequest(connect, kNilOptions, request) == KERN_SUCCESS); 185 | IOI2CInterfaceClose(connect, kNilOptions); 186 | } 187 | IOObjectRelease(interface); 188 | if (result) break; 189 | } 190 | } 191 | if (request->replyTransactionType == kIOI2CNoTransactionType) 192 | usleep(20000); 193 | dispatch_semaphore_signal(queue); 194 | return result && request->result == KERN_SUCCESS; 195 | } 196 | 197 | long DDCDelay(io_service_t framebuffer) { 198 | // Certain displays / graphics cards require a long-enough delay to yield a response to DDC commands 199 | // Relying on retry will not help if the delay is too short. 200 | // kernel panics are possible if value is wrong 201 | // https://developer.apple.com/documentation/iokit/ioi2crequest/1410394-minreplydelay?language=objc 202 | 203 | CFStringRef ioRegPath = IORegistryEntryCopyPath(framebuffer, kIOServicePlane); 204 | if (CFStringFind(ioRegPath, CFSTR("/AMD"), kCFCompareCaseInsensitive).location != kCFNotFound) { 205 | return DDCDelayBase + 30000000; // Team Red needs more time, as usual! 206 | } 207 | return DDCDelayBase; 208 | } 209 | 210 | bool DDCWrite(io_service_t framebuffer, struct DDCWriteCommand *write) { 211 | IOI2CRequest request; 212 | UInt8 data[128]; 213 | 214 | bzero( &request, sizeof(request)); 215 | 216 | request.commFlags = 0; 217 | 218 | request.sendAddress = 0x6E; 219 | request.sendTransactionType = kIOI2CSimpleTransactionType; 220 | request.sendBuffer = (vm_address_t) &data[0]; 221 | request.sendBytes = 7; 222 | 223 | data[0] = 0x51; 224 | data[1] = 0x84; 225 | data[2] = 0x03; 226 | data[3] = write->control_id; 227 | data[4] = (write->new_value) >> 8; 228 | data[5] = write->new_value & 255; 229 | data[6] = 0x6E ^ data[0] ^ data[1] ^ data[2] ^ data[3]^ data[4] ^ data[5]; 230 | 231 | request.replyTransactionType = kIOI2CNoTransactionType; 232 | request.replyBytes = 0; 233 | 234 | bool result = FramebufferI2CRequest(framebuffer, &request); 235 | return result; 236 | } 237 | 238 | bool DDCRead(io_service_t framebuffer, struct DDCReadCommand *read) { 239 | IOI2CRequest request; 240 | UInt8 reply_data[11] = {}; 241 | bool result = false; 242 | UInt8 data[128]; 243 | 244 | long reply_timeout = DDCDelay(framebuffer) * kNanosecondScale; 245 | 246 | for (int i=1; i<=kMaxRequests; i++) { 247 | bzero(&request, sizeof(request)); 248 | 249 | request.commFlags = 0; 250 | request.sendAddress = 0x6E; 251 | request.sendTransactionType = kIOI2CSimpleTransactionType; 252 | request.sendBuffer = (vm_address_t) &data[0]; 253 | request.sendBytes = 5; 254 | request.minReplyDelay = reply_timeout; 255 | data[0] = 0x51; 256 | data[1] = 0x82; 257 | data[2] = 0x01; 258 | data[3] = read->control_id; 259 | data[4] = 0x6E ^ data[0] ^ data[1] ^ data[2] ^ data[3]; 260 | #ifdef TT_SIMPLE 261 | request.replyTransactionType = kIOI2CSimpleTransactionType; 262 | #elif defined TT_DDC 263 | request.replyTransactionType = kIOI2CDDCciReplyTransactionType; 264 | #else 265 | request.replyTransactionType = SupportedTransactionType(); 266 | #endif 267 | request.replyAddress = 0x6F; 268 | request.replySubAddress = 0x51; 269 | 270 | request.replyBuffer = (vm_address_t) reply_data; 271 | request.replyBytes = sizeof(reply_data); 272 | 273 | result = FramebufferI2CRequest(framebuffer, &request); 274 | result = (result && reply_data[0] == request.sendAddress && reply_data[2] == 0x2 && reply_data[4] == read->control_id && reply_data[10] == (request.replyAddress ^ request.replySubAddress ^ reply_data[1] ^ reply_data[2] ^ reply_data[3] ^ reply_data[4] ^ reply_data[5] ^ reply_data[6] ^ reply_data[7] ^ reply_data[8] ^ reply_data[9])); 275 | 276 | if (result) { // checksum is ok 277 | if (i > 1) { 278 | printf("D: Tries required to get data: %d (%ldns reply-timeout)\n", i, reply_timeout); 279 | } 280 | break; 281 | } 282 | 283 | if (request.result == kIOReturnUnsupportedMode) 284 | printf("E: Unsupported Transaction Type! \n"); 285 | 286 | // reset values and return 0, if data reading fails 287 | if (i >= kMaxRequests) { 288 | read->success = false; 289 | read->max_value = 0; 290 | read->current_value = 0; 291 | printf("E: No data after %d tries! (%ldns reply-timeout)\n", i, reply_timeout); 292 | return 0; 293 | } 294 | 295 | usleep(40000); // 40msec -> See DDC/CI Vesa Standard - 4.4.1 Communication Error Recovery 296 | } 297 | read->success = true; 298 | read->max_value = reply_data[7]; 299 | read->current_value = reply_data[9]; 300 | return result; 301 | } 302 | 303 | UInt32 SupportedTransactionType() { 304 | /* 305 | With my setup (Intel HD4600 via displaylink to 'DELL U2515H') the original app failed to read ddc and freezes my system. 306 | This happens because AppleIntelFramebuffer do not support kIOI2CDDCciReplyTransactionType. 307 | So this version comes with a reworked ddc read function to detect the correct TransactionType. 308 | --SamanVDR 2016 309 | */ 310 | 311 | kern_return_t kr; 312 | io_iterator_t io_objects; 313 | io_service_t io_service; 314 | 315 | kr = IOServiceGetMatchingServices(kIOMasterPortDefault, 316 | IOServiceNameMatching("IOFramebufferI2CInterface"), &io_objects); 317 | 318 | if (kr != KERN_SUCCESS) { 319 | printf("E: Fatal - No matching service! \n"); 320 | return 0; 321 | } 322 | 323 | UInt32 supportedType = 0; 324 | 325 | while((io_service = IOIteratorNext(io_objects)) != MACH_PORT_NULL) 326 | { 327 | CFMutableDictionaryRef service_properties; 328 | CFIndex types = 0; 329 | CFNumberRef typesRef; 330 | 331 | kr = IORegistryEntryCreateCFProperties(io_service, &service_properties, kCFAllocatorDefault, kNilOptions); 332 | if (kr == KERN_SUCCESS) 333 | { 334 | if (CFDictionaryGetValueIfPresent(service_properties, CFSTR(kIOI2CTransactionTypesKey), (const void**)&typesRef)) 335 | CFNumberGetValue(typesRef, kCFNumberCFIndexType, &types); 336 | 337 | /* 338 | We want DDCciReply but Simple is better than No-thing. 339 | Combined and DisplayPortNative are not useful in our case. 340 | */ 341 | if (types) { 342 | #ifdef DEBUG 343 | printf("\nD: IOI2CTransactionTypes: 0x%02lx (%ld)\n", types, types); 344 | 345 | // kIOI2CNoTransactionType = 0 346 | if ( 0 == ((1 << kIOI2CNoTransactionType) & (UInt64)types)) { 347 | printf("E: IOI2CNoTransactionType unsupported \n"); 348 | } else { 349 | printf("D: IOI2CNoTransactionType supported \n"); 350 | supportedType = kIOI2CNoTransactionType; 351 | } 352 | 353 | // kIOI2CSimpleTransactionType = 1 354 | if ( 0 == ((1 << kIOI2CSimpleTransactionType) & (UInt64)types)) { 355 | printf("E: IOI2CSimpleTransactionType unsupported \n"); 356 | } else { 357 | printf("D: IOI2CSimpleTransactionType supported \n"); 358 | supportedType = kIOI2CSimpleTransactionType; 359 | } 360 | 361 | // kIOI2CDDCciReplyTransactionType = 2 362 | if ( 0 == ((1 << kIOI2CDDCciReplyTransactionType) & (UInt64)types)) { 363 | printf("E: IOI2CDDCciReplyTransactionType unsupported \n"); 364 | } else { 365 | printf("D: IOI2CDDCciReplyTransactionType supported \n"); 366 | supportedType = kIOI2CDDCciReplyTransactionType; 367 | } 368 | 369 | // kIOI2CCombinedTransactionType = 3 370 | if ( 0 == ((1 << kIOI2CCombinedTransactionType) & (UInt64)types)) { 371 | printf("E: IOI2CCombinedTransactionType unsupported \n"); 372 | } else { 373 | printf("D: IOI2CCombinedTransactionType supported \n"); 374 | //supportedType = kIOI2CCombinedTransactionType; 375 | } 376 | 377 | // kIOI2CDisplayPortNativeTransactionType = 4 378 | if ( 0 == ((1 << kIOI2CDisplayPortNativeTransactionType) & (UInt64)types)) { 379 | printf("E: IOI2CDisplayPortNativeTransactionType unsupported\n"); 380 | } else { 381 | printf("D: IOI2CDisplayPortNativeTransactionType supported \n"); 382 | //supportedType = kIOI2CDisplayPortNativeTransactionType; 383 | // http://hackipedia.org/Hardware/video/connectors/DisplayPort/VESA%20DisplayPort%20Standard%20v1.1a.pdf 384 | // http://www.electronic-products-design.com/geek-area/displays/display-port 385 | } 386 | #else 387 | // kIOI2CSimpleTransactionType = 1 388 | if ( 0 != ((1 << kIOI2CSimpleTransactionType) & (UInt64)types)) { 389 | supportedType = kIOI2CSimpleTransactionType; 390 | } 391 | 392 | // kIOI2CDDCciReplyTransactionType = 2 393 | if ( 0 != ((1 << kIOI2CDDCciReplyTransactionType) & (UInt64)types)) { 394 | supportedType = kIOI2CDDCciReplyTransactionType; 395 | } 396 | #endif 397 | } else printf("E: Fatal - No supported Transaction Types! \n"); 398 | 399 | CFRelease(service_properties); 400 | } 401 | 402 | IOObjectRelease(io_service); 403 | 404 | // Mac OS offers three framebuffer devices, but we can leave here 405 | if (supportedType > 0) return supportedType; 406 | } 407 | 408 | return supportedType; 409 | } 410 | 411 | 412 | bool EDIDTest(io_service_t framebuffer, struct EDID *edid) { 413 | IOI2CRequest request = {}; 414 | /*! from https://opensource.apple.com/source/IOGraphics/IOGraphics-513.1/IOGraphicsFamily/IOKit/i2c/IOI2CInterface.h.auto.html 415 | * not in https://developer.apple.com/reference/kernel/1659924-ioi2cinterface.h/ioi2crequest?changes=latest_beta&language=objc 416 | * @struct IOI2CRequest 417 | * @abstract A structure defining an I2C bus transaction. 418 | * @discussion This structure is used to request an I2C transaction consisting of a send (write) to and reply (read) from a device, either of which is optional, to be carried out atomically on an I2C bus. 419 | * @field __reservedA Set to zero. 420 | * @field result The result of the transaction. Common errors are kIOReturnNoDevice if there is no device responding at the given address, kIOReturnUnsupportedMode if the type of transaction is unsupported on the requested bus. 421 | * @field completion A completion routine to be executed when the request completes. If NULL is passed, the request is synchronous, otherwise it may execute asynchronously. 422 | * @field commFlags Flags that modify the I2C transaction type. The following flags are defined:
423 | * kIOI2CUseSubAddressCommFlag Transaction includes a subaddress.
424 | * @field minReplyDelay Minimum delay as absolute time between send and reply transactions. 425 | * @field sendAddress I2C address to write. 426 | * @field sendSubAddress I2C subaddress to write. 427 | * @field __reservedB Set to zero. 428 | * @field sendTransactionType The following types of transaction are defined for the send part of the request:
429 | * kIOI2CNoTransactionType No send transaction to perform.
430 | * kIOI2CSimpleTransactionType Simple I2C message.
431 | * kIOI2CCombinedTransactionType Combined format I2C R/~W transaction.
432 | * @field sendBuffer Pointer to the send buffer. 433 | * @field sendBytes Number of bytes to send. Set to actual bytes sent on completion of the request. 434 | * @field replyAddress I2C Address from which to read. 435 | * @field replySubAddress I2C Address from which to read. 436 | * @field __reservedC Set to zero. 437 | * @field replyTransactionType The following types of transaction are defined for the reply part of the request:
438 | * kIOI2CNoTransactionType No reply transaction to perform.
439 | * kIOI2CSimpleTransactionType Simple I2C message.
440 | * kIOI2CDDCciReplyTransactionType DDC/ci message (with embedded length). See VESA DDC/ci specification.
441 | * kIOI2CCombinedTransactionType Combined format I2C R/~W transaction.
442 | * @field replyBuffer Pointer to the reply buffer. 443 | * @field replyBytes Max bytes to reply (size of replyBuffer). Set to actual bytes received on completion of the request. 444 | * @field __reservedD Set to zero. 445 | */ 446 | 447 | UInt8 data[128] = {}; 448 | request.sendAddress = 0xA0; 449 | request.sendTransactionType = kIOI2CSimpleTransactionType; 450 | request.sendBuffer = (vm_address_t) data; 451 | request.sendBytes = 0x01; 452 | data[0] = 0x00; 453 | request.replyAddress = 0xA1; 454 | request.replyTransactionType = kIOI2CSimpleTransactionType; 455 | request.replyBuffer = (vm_address_t) data; 456 | request.replyBytes = sizeof(data); 457 | if (!FramebufferI2CRequest(framebuffer, &request)) return false; 458 | if (edid) memcpy(edid, &data, 128); 459 | UInt32 i = 0; 460 | UInt8 sum = 0; 461 | while (i < request.replyBytes) { 462 | if (i % 128 == 0) { 463 | if (sum) break; 464 | sum = 0; 465 | } 466 | sum += data[i++]; 467 | } 468 | return !sum; 469 | } 470 | -------------------------------------------------------------------------------- /src/ddcctl.m: -------------------------------------------------------------------------------- 1 | // 2 | // ddcctl.m 3 | // query and control monitors through their on-wire data channels and OSD microcontrollers 4 | // http://en.wikipedia.org/wiki/Display_Data_Channel#DDC.2FCI 5 | // http://en.wikipedia.org/wiki/Monitor_Control_Command_Set 6 | // 7 | // Copyright Joey Korkames 2016 http://github.com/kfix 8 | // Licensed under GPLv3, full text at http://www.gnu.org/licenses/gpl-3.0.txt 9 | 10 | // Now using argv[] instead of user-defaults to handle commandline arguments. 11 | // Added optional use of an external app 'OSDisplay' to have a BezelUI like OSD. 12 | // Have fun! Marc (Saman-VDR) 2016 13 | 14 | #ifdef DEBUG 15 | #define MyLog NSLog 16 | #else 17 | #define MyLog(...) (void)printf("%s\n",[[NSString stringWithFormat:__VA_ARGS__] UTF8String]) 18 | #endif 19 | 20 | #import 21 | #import 22 | #import "DDC.h" 23 | 24 | #ifdef BLACKLIST 25 | NSUserDefaults *defaults; 26 | int blacklistedDeviceWithNumber; 27 | #endif 28 | #ifdef OSD 29 | bool useOsd; 30 | #endif 31 | 32 | extern io_service_t CGDisplayIOServicePort(CGDirectDisplayID display) __attribute__((weak_import)); 33 | 34 | extern long DDCDelayBase; 35 | 36 | NSString *EDIDString(char *string) 37 | { 38 | NSString *temp = [[NSString alloc] initWithBytes:string length:13 encoding:NSASCIIStringEncoding]; 39 | return ([temp rangeOfString:@"\n"].location != NSNotFound) ? [[temp componentsSeparatedByString:@"\n"] objectAtIndex:0] : temp; 40 | } 41 | 42 | NSString *getDisplayDeviceLocation(CGDirectDisplayID cdisplay) 43 | { 44 | // FIXME: scraping prefs files is vulnerable to use of stale data? 45 | // TODO: try shelling `system_profiler SPDisplaysDataType -xml` to get "_spdisplays_displayPath" keys 46 | // this seems to use private routines in: 47 | // /System/Library/SystemProfiler/SPDisplaysReporter.spreporter/Contents/MacOS/SPDisplaysReporter 48 | 49 | // get the WindowServer's table of DisplayIds -> IODisplays 50 | NSString *wsPrefs = @"/Library/Preferences/com.apple.windowserver.plist"; 51 | NSDictionary *wsDict = [NSDictionary dictionaryWithContentsOfFile:wsPrefs]; 52 | if (!wsDict) 53 | { 54 | MyLog(@"E: Failed to parse WindowServer's preferences! (%@)", wsPrefs); 55 | return NULL; 56 | } 57 | 58 | NSArray *wsDisplaySets = [wsDict valueForKey:@"DisplayAnyUserSets"]; 59 | if (!wsDisplaySets) 60 | { 61 | MyLog(@"E: Failed to get 'DisplayAnyUserSets' key from WindowServer's preferences! (%@)", wsPrefs); 62 | return NULL; 63 | } 64 | 65 | // $ PlistBuddy -c "Print DisplayAnyUserSets:0:0:IODisplayLocation" -c "Print DisplayAnyUserSets:0:0:DisplayID" /Library/Preferences/com.apple.windowserver.plist 66 | // > IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/PEG0@1/IOPP/GFX0@0/ATY,Longavi@0/AMDFramebufferVIB 67 | // > 69733382 68 | for (NSArray *displaySet in wsDisplaySets) { 69 | for (NSDictionary *display in displaySet) { 70 | if ([[display valueForKey:@"DisplayID"] integerValue] == cdisplay) { 71 | return [display valueForKey:@"IODisplayLocation"]; // kIODisplayLocationKey 72 | } 73 | } 74 | } 75 | 76 | MyLog(@"E: Failed to find display in WindowServer's preferences! (%@)", wsPrefs); 77 | return NULL; 78 | } 79 | 80 | /* Get current value for control from display */ 81 | uint getControl(CGDirectDisplayID cdisplay, uint control_id) 82 | { 83 | struct DDCReadCommand command; 84 | command.control_id = control_id; 85 | command.max_value = 0; 86 | command.current_value = 0; 87 | MyLog(@"D: querying VCP control: #%u =?", command.control_id); 88 | 89 | if (!DDCRead(cdisplay, &command)) { 90 | MyLog(@"E: DDC send command failed!"); 91 | MyLog(@"E: VCP control #%u (0x%02hhx) = current: %u, max: %u", command.control_id, command.control_id, command.current_value, command.max_value); 92 | } else { 93 | MyLog(@"I: VCP control #%u (0x%02hhx) = current: %u, max: %u", command.control_id, command.control_id, command.current_value, command.max_value); 94 | } 95 | return command.current_value; 96 | } 97 | 98 | /* Set new value for control from display */ 99 | void setControl(io_service_t framebuffer, uint control_id, uint new_value) 100 | { 101 | struct DDCWriteCommand command; 102 | command.control_id = control_id; 103 | command.new_value = new_value; 104 | 105 | MyLog(@"D: setting VCP control #%u => %u", command.control_id, command.new_value); 106 | if (!DDCWrite(framebuffer, &command)){ 107 | MyLog(@"E: Failed to send DDC command!"); 108 | } 109 | 110 | 111 | 112 | #ifdef OSD 113 | if (useOsd) { 114 | NSString *OSDisplay = @"/Applications/OSDisplay.app/Contents/MacOS/OSDisplay"; 115 | switch (control_id) { 116 | case 16: 117 | [NSTask launchedTaskWithLaunchPath:OSDisplay 118 | arguments:[NSArray arrayWithObjects: 119 | @"-l", [NSString stringWithFormat:@"%u", new_value], 120 | @"-i", @"brightness", nil]]; 121 | break; 122 | 123 | case 18: 124 | [NSTask launchedTaskWithLaunchPath:OSDisplay 125 | arguments:[NSArray arrayWithObjects: 126 | @"-l", [NSString stringWithFormat:@"%u", new_value], 127 | @"-i", @"contrast", nil]]; 128 | break; 129 | 130 | default: 131 | break; 132 | } 133 | } 134 | #endif 135 | } 136 | 137 | /* Get current value to Set relative value for control from display */ 138 | void getSetControl(io_service_t framebuffer, uint control_id, NSString *new_value, NSString *operator) 139 | { 140 | struct DDCReadCommand command; 141 | command.control_id = control_id; 142 | command.max_value = 0; 143 | command.current_value = 0; 144 | 145 | // read 146 | MyLog(@"D: querying VCP control: #%u =?", command.control_id); 147 | 148 | if (!DDCRead(framebuffer, &command)) { 149 | MyLog(@"E: DDC send command failed!"); 150 | MyLog(@"E: VCP control #%u (0x%02hhx) = current: %u, max: %u", command.control_id, command.control_id, command.current_value, command.max_value); 151 | } else { 152 | MyLog(@"I: VCP control #%u (0x%02hhx) = current: %u, max: %u", command.control_id, command.control_id, command.current_value, command.max_value); 153 | } 154 | 155 | // calculate 156 | NSString *formula = [NSString stringWithFormat:@"%u %@ %@", command.current_value, operator, new_value]; 157 | NSExpression *exp = [NSExpression expressionWithFormat:formula]; 158 | NSNumber *set_value = [exp expressionValueWithObject:nil context:nil]; 159 | 160 | // validate and write 161 | int clamped_value = MIN(MAX(set_value.intValue, 0), command.max_value); 162 | MyLog(@"D: relative setting: %@ = %d (clamped to 0, %d)", formula, clamped_value, command.max_value); 163 | setControl(framebuffer, control_id, (uint) clamped_value); 164 | } 165 | 166 | /* Main function */ 167 | int main(int argc, const char * argv[]) 168 | { 169 | 170 | @autoreleasepool { 171 | 172 | NSPointerArray *_displayIDs = [NSPointerArray pointerArrayWithOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsIntegerPersonality]; 173 | 174 | for (NSScreen *screen in NSScreen.screens) 175 | { 176 | NSDictionary *description = [screen deviceDescription]; 177 | if ([description objectForKey:@"NSDeviceIsScreen"]) { 178 | CGDirectDisplayID screenNumber = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]; 179 | if (CGDisplayIsBuiltin(screenNumber)) continue; // ignore MacBook screens because the lid can be closed and they don't use DDC. 180 | // https://stackoverflow.com/a/48450870/3878712 181 | CFUUIDRef screenUUID = CGDisplayCreateUUIDFromDisplayID(screenNumber); 182 | CFStringRef screenUUIDstr = CFUUIDCreateString(NULL, screenUUID); 183 | [_displayIDs addPointer:(void *)(UInt64)screenNumber]; 184 | NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; 185 | CGSize displayPhysicalSize = CGDisplayScreenSize(screenNumber); // dspPhySz only valid if EDID present! 186 | float displayScale = [screen backingScaleFactor]; 187 | double rotation = CGDisplayRotation(screenNumber); 188 | if (displayScale > 1) { 189 | MyLog(@"D: CGDisplay %@ dispID(#%u) (%.0fx%.0f %g°) HiDPI", 190 | screenUUIDstr, 191 | screenNumber, 192 | displayPixelSize.width, 193 | displayPixelSize.height, 194 | rotation); 195 | } 196 | else { 197 | MyLog(@"D: CGDisplay %@ dispID(#%u) (%.0fx%.0f %g°) %0.2f DPI", 198 | screenUUIDstr, 199 | screenNumber, 200 | displayPixelSize.width, 201 | displayPixelSize.height, 202 | rotation, 203 | (displayPixelSize.width / displayPhysicalSize.width) * 25.4f); // there being 25.4 mm in an inch 204 | } 205 | 206 | #ifdef DEBUG 207 | NSString *devLoc = getDisplayDeviceLocation(screenNumber); 208 | if (devLoc) { 209 | MyLog(@"D: -> location %@", devLoc); 210 | } 211 | #endif 212 | } 213 | } 214 | MyLog(@"I: found %lu external display%@", [_displayIDs count], [_displayIDs count] > 1 ? @"s" : @""); 215 | 216 | 217 | // Defaults 218 | NSString *screenName = @""; 219 | NSUInteger displayId = -1; 220 | NSUInteger command_interval = 100000; 221 | BOOL dump_values = NO; 222 | 223 | NSString *HelpString = @"Usage:\n" 224 | @"ddcctl \t-d <1-..> [display#]\n" 225 | @"\t-w <0-..> [delay in usecs between settings]\n" 226 | @"\t-W <0-..> [timeout in nanosecs for replies]\n" 227 | @"\n" 228 | @"----- Basic settings -----\n" 229 | @"\t-b <1-..> [brightness]\n" 230 | @"\t-c <1-..> [contrast]\n" 231 | @"\t-rbc [reset brightness and contrast]\n" 232 | #ifdef OSD 233 | @"\t-O [osd: needs external app 'OSDisplay']\n" 234 | #endif 235 | @"\n" 236 | @"----- Settings that don\'t always work -----\n" 237 | @"\t-m <1|2> [mute speaker OFF/ON]\n" 238 | @"\t-v <1-254> [speaker volume]\n" 239 | @"\t-i <1-18> [select input source]\n" 240 | @"\t-p <1|2-5> [power on | standby/off]\n" 241 | @"\t-o [read-only orientation]\n" 242 | @"\n" 243 | @"----- Settings (testing) -----\n" 244 | @"\t-rg <1-..> [red gain]\n" 245 | @"\t-gg <1-..> [green gain]\n" 246 | @"\t-bg <1-..> [blue gain]\n" 247 | @"\t-rrgb [reset color]\n" 248 | @"\n" 249 | @"----- Setting grammar -----\n" 250 | @"\t-X ? (query value of setting X)\n" 251 | @"\t-X NN (put setting X to NN)\n" 252 | @"\t-X - (decrease setting X by NN)\n" 253 | @"\t-X + (increase setting X by NN)"; 254 | 255 | 256 | // Commandline Arguments 257 | NSMutableDictionary *actions = [[NSMutableDictionary alloc] init]; 258 | 259 | for (int i=1; i= argc) break; 264 | displayId = atoi(argv[i]); 265 | } 266 | 267 | else if (!strcmp(argv[i], "-vcp")) { 268 | i++; 269 | if (i >= argc) break; 270 | [actions setObject:@[[[NSString alloc] initWithUTF8String:argv[i]], [[NSString alloc] initWithUTF8String:argv[i+1]]] forKey:@"vcp"]; 271 | i++; 272 | } 273 | 274 | else if (!strcmp(argv[i], "-pbp")) { 275 | i++; 276 | if (i >= argc) break; 277 | [actions setObject:@[@PBP, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"pbp"]; 278 | } 279 | 280 | else if (!strcmp(argv[i], "-pbp-screen")) { 281 | i++; 282 | if (i >= argc) break; 283 | [actions setObject:@[@PBP_SCREEN, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"pbp-screen"]; 284 | } 285 | 286 | else if (!strcmp(argv[i], "-b")) { 287 | i++; 288 | if (i >= argc) break; 289 | [actions setObject:@[@BRIGHTNESS, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"b"]; 290 | } 291 | 292 | else if (!strcmp(argv[i], "-c")) { 293 | i++; 294 | if (i >= argc) break; 295 | [actions setObject:@[@CONTRAST, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"c"]; 296 | } 297 | 298 | else if (!strcmp(argv[i], "-rbc")) { 299 | [actions setObject:@[@RESET_BRIGHTNESS_AND_CONTRAST, @"1"] forKey:@"rbc"]; 300 | } 301 | 302 | else if (!strcmp(argv[i], "-rg")) { 303 | i++; 304 | if (i >= argc) break; 305 | [actions setObject:@[@RED_GAIN, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"rg"]; 306 | } 307 | 308 | else if (!strcmp(argv[i], "-gg")) { 309 | i++; 310 | if (i >= argc) break; 311 | [actions setObject:@[@GREEN_GAIN, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"gg"]; 312 | } 313 | 314 | else if (!strcmp(argv[i], "-bg")) { 315 | i++; 316 | if (i >= argc) break; 317 | [actions setObject:@[@BLUE_GAIN, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"bg"]; 318 | } 319 | 320 | else if (!strcmp(argv[i], "-rrgb")) { 321 | [actions setObject:@[@RESET_COLOR, @"1"] forKey:@"rrgb"]; 322 | } 323 | 324 | else if (!strcmp(argv[i], "-D")) { 325 | dump_values = YES; 326 | } 327 | 328 | else if (!strcmp(argv[i], "-p")) { 329 | i++; 330 | if (i >= argc) break; 331 | [actions setObject:@[@DPMS, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"p"]; 332 | } 333 | 334 | else if (!strcmp(argv[i], "-o")) { // read only 335 | [actions setObject:@[@ORIENTATION, @"?"] forKey:@"o"]; 336 | } 337 | 338 | else if (!strcmp(argv[i], "-osd")) { // read only - returns '1' (OSD closed) or '2' (OSD active) 339 | [actions setObject:@[@ON_SCREEN_DISPLAY, @"?"] forKey:@"osd"]; 340 | } 341 | 342 | else if (!strcmp(argv[i], "-lang")) { // read only 343 | [actions setObject:@[@OSD_LANGUAGE, @"?"] forKey:@"lang"]; 344 | } 345 | 346 | else if (!strcmp(argv[i], "-reset")) { 347 | [actions setObject:@[@RESET, @"1"] forKey:@"reset"]; 348 | } 349 | 350 | else if (!strcmp(argv[i], "-preset_a")) { 351 | i++; 352 | if (i >= argc) break; 353 | [actions setObject:@[@COLOR_PRESET_A, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"preset_a"]; 354 | } 355 | 356 | else if (!strcmp(argv[i], "-preset_b")) { 357 | i++; 358 | if (i >= argc) break; 359 | [actions setObject:@[@COLOR_PRESET_B, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"preset_b"]; 360 | } 361 | 362 | else if (!strcmp(argv[i], "-preset_c")) { 363 | i++; 364 | if (i >= argc) break; 365 | [actions setObject:@[@COLOR_PRESET_C, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"preset_c"]; 366 | } 367 | 368 | else if (!strcmp(argv[i], "-i")) { 369 | i++; 370 | if (i >= argc) break; 371 | [actions setObject:@[@INPUT_SOURCE, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"i"]; 372 | } 373 | 374 | 375 | else if (!strcmp(argv[i], "-m")) { 376 | i++; 377 | if (i >= argc) break; 378 | [actions setObject:@[@AUDIO_MUTE, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"m"]; 379 | } 380 | 381 | else if (!strcmp(argv[i], "-v")) { 382 | i++; 383 | if (i >= argc) break; 384 | [actions setObject:@[@AUDIO_SPEAKER_VOLUME, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"v"]; 385 | } 386 | 387 | else if (!strcmp(argv[i], "-w")) { 388 | i++; 389 | if (i >= argc) break; 390 | command_interval = atoi(argv[i]); 391 | } 392 | 393 | else if (!strcmp(argv[i], "-W")) { 394 | i++; 395 | if (i >= argc) break; 396 | DDCDelayBase = atoi(argv[i]); 397 | } 398 | 399 | #ifdef OSD 400 | else if (!strcmp(argv[i], "-O")) { 401 | useOsd = YES; 402 | } 403 | #endif 404 | #ifdef TEST 405 | else if (!strcmp(argv[i], "-test")) { 406 | i++; 407 | if (i >= argc) break; 408 | NSString *test = [[NSString alloc] initWithUTF8String:argv[i]]; 409 | i++; 410 | if (i >= argc) break; 411 | [actions setObject:@[test, [[NSString alloc] initWithUTF8String:argv[i]]] forKey:@"test"]; 412 | NSLog(@"TEST: %@ %@", test, [[NSString alloc] initWithUTF8String:argv[i]]); 413 | } 414 | #endif 415 | else if (!strcmp(argv[i], "-h")) { 416 | NSLog(@"ddcctl 0.1x - %@", HelpString); 417 | return 0; 418 | } 419 | 420 | else { 421 | NSLog(@"Unknown argument: %@", [[NSString alloc] initWithUTF8String:argv[i]]); 422 | return -1; 423 | } 424 | } 425 | 426 | if (0 >= displayId || displayId > [_displayIDs count]) { 427 | // no display id given, nothing left to do! 428 | NSLog(@"%@", HelpString); 429 | exit(1); 430 | } 431 | 432 | CGDirectDisplayID cdisplay = (CGDirectDisplayID)[_displayIDs pointerAtIndex:displayId - 1]; 433 | 434 | // find & grab the IOFramebuffer for the display, the IOFB is where DDC/I2C commands are sent 435 | io_service_t framebuffer = 0; 436 | NSString *devLoc = getDisplayDeviceLocation(cdisplay); 437 | #pragma clang diagnostic push 438 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 439 | if (CGDisplayIOServicePort != NULL) { 440 | // legacy API call to get the IOFB's service port, was deprecated after macOS 10.9: 441 | // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/Quartz_Services_Ref/index.html#//apple_ref/c/func/CGDisplayIOServicePort 442 | framebuffer = CGDisplayIOServicePort(cdisplay); 443 | #pragma clang diagnostic pop 444 | } 445 | 446 | if (! framebuffer && devLoc) { 447 | // a devLoc is required because, without that IOReg path, this func is prone to always match the 1st device of a monitor-pair (#17) 448 | framebuffer = IOFramebufferPortFromCGDisplayID(cdisplay, (__bridge CFStringRef)devLoc); 449 | } 450 | 451 | if (! framebuffer) { 452 | MyLog(@"E: Failed to acquire framebuffer device for display"); 453 | return -1; 454 | } 455 | 456 | MyLog(@"I: polling EDID for #%lu (ID %u => %@)", displayId, cdisplay, devLoc); 457 | 458 | struct EDID edid = {}; 459 | if (EDIDTest(framebuffer, &edid)) { 460 | for (union descriptor *des = edid.descriptors; des < edid.descriptors + sizeof(edid.descriptors) / sizeof(edid.descriptors[0]); des++) { 461 | switch (des->text.type) 462 | { 463 | case 0xFF: 464 | MyLog(@"I: got edid.serial: %@", EDIDString(des->text.data)); 465 | break; 466 | case 0xFC: 467 | screenName = EDIDString(des->text.data); 468 | MyLog(@"I: got edid.name: %@", screenName); 469 | break; 470 | } 471 | } 472 | } else { 473 | MyLog(@"E: Failed to poll display!"); 474 | IOObjectRelease(framebuffer); 475 | return -1; 476 | } 477 | 478 | // Debugging 479 | if (dump_values) { 480 | for (uint i=0x00; i<=255; i++) { 481 | getControl(framebuffer, i); 482 | usleep(command_interval); 483 | } 484 | } 485 | 486 | // Actions 487 | [actions enumerateKeysAndObjectsUsingBlock:^(id argname, NSArray* valueArray, BOOL *stop) { 488 | NSInteger control_id = [valueArray[0] intValue]; 489 | NSString *argval = valueArray[1]; 490 | MyLog(@"D: action: %@: %@", argname, argval); 491 | if (control_id > -1) { 492 | // this is a valid monitor control 493 | NSString *argval_num = [argval stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-+"]]; // look for relative setting ops 494 | if ([argval hasPrefix:@"+"] || [argval hasPrefix:@"-"]) { // +/-NN relative 495 | getSetControl(framebuffer, control_id, argval_num, [argval substringToIndex:1]); 496 | } else if ([argval hasSuffix:@"+"] || [argval hasSuffix:@"-"]) { // NN+/- relative 497 | // read, calculate, then write 498 | getSetControl(framebuffer, control_id, argval_num, [argval substringFromIndex:argval.length - 1]); 499 | } else if ([argval hasPrefix:@"?"]) { 500 | // read current setting 501 | getControl(framebuffer, control_id); 502 | } else if (argval_num == argval) { 503 | // write fixed setting 504 | setControl(framebuffer, control_id, [argval intValue]); 505 | } 506 | } 507 | usleep(command_interval); // stagger comms to these wimpy I2C mcu's 508 | }]; 509 | // done with all actions, release display's framebuffer 510 | IOObjectRelease(framebuffer); 511 | } // -autoreleasepool 512 | return 0; 513 | } // -main 514 | --------------------------------------------------------------------------------