├── .gitignore ├── rustfmt.toml ├── tests ├── edid │ └── dell.edid.bin └── integration_tests.rs ├── src ├── iokit │ ├── mod.rs │ ├── errors.rs │ ├── display.rs │ ├── wrappers.rs │ └── io2c_interface.rs ├── lib.rs ├── error.rs ├── intel.rs ├── monitor.rs └── arm.rs ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── docs.yml ├── Cargo.toml ├── LICENSE ├── examples └── list.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | rust-toolchain -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | style_edition = "2021" 3 | use_try_shorthand = true 4 | -------------------------------------------------------------------------------- /tests/edid/dell.edid.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haimgel/ddc-macos-rs/HEAD/tests/edid/dell.edid.bin -------------------------------------------------------------------------------- /src/iokit/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub(crate) mod errors; 3 | mod display; 4 | mod io2c_interface; 5 | mod wrappers; 6 | 7 | pub(crate) use display::*; 8 | pub(crate) use io2c_interface::*; 9 | pub(crate) use wrappers::*; 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build_and_test: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build 14 | run: cargo build --all-targets --verbose 15 | - name: Run tests 16 | run: cargo test --verbose 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | release: 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Release 16 | env: 17 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 18 | run: cargo publish --verbose 19 | -------------------------------------------------------------------------------- /src/iokit/errors.rs: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/svartalf/rust-battery/blob/master/battery/src/platform/darwin/iokit/errors.rs 2 | 3 | #[macro_export] 4 | macro_rules! r#kern_try { 5 | ($expr:expr) => { 6 | match $expr { 7 | mach2::kern_return::KERN_SUCCESS => (), 8 | err_code => return ::std::result::Result::Err(::std::io::Error::from_raw_os_error(err_code).into()), 9 | } 10 | }; 11 | ($expr:expr,) => { 12 | r#kern_try!($expr) 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | deploy: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build docs 17 | run: | 18 | cargo doc --lib --no-deps 19 | echo "" > target/doc/index.html 20 | - name: Deploy docs 21 | uses: peaceiris/actions-gh-pages@v4 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./target/doc 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://haimgel.github.io/ddc-macos-rs/")] 2 | 3 | //! Implementation of DDC/CI traits on MacOS. 4 | //! 5 | //! # Example 6 | //! 7 | //! ```rust,no_run 8 | //! extern crate ddc; 9 | //! extern crate ddc_macos; 10 | //! 11 | //! # fn main() { 12 | //! use ddc::Ddc; 13 | //! use ddc_macos::Monitor; 14 | //! 15 | //! for mut ddc in Monitor::enumerate().unwrap() { 16 | //! let input = ddc.get_vcp_feature(0x60).unwrap(); 17 | //! println!("Current input: {:04x}", input.value()); 18 | //! } 19 | //! # } 20 | //! ``` 21 | 22 | mod arm; 23 | mod error; 24 | mod intel; 25 | mod iokit; 26 | mod monitor; 27 | 28 | pub use error::*; 29 | pub use monitor::*; 30 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | extern crate ddc_macos; 2 | use ddc::Ddc; 3 | 4 | #[test] 5 | #[ignore] 6 | /// Test getting current monitor inputs, this would fail on CI. 7 | fn test_get_vcp_feature() { 8 | let mut monitors = ddc_macos::Monitor::enumerate().unwrap(); 9 | assert_ne!(monitors.len(), 0); 10 | 11 | for monitor in monitors.iter_mut() { 12 | let input = monitor.get_vcp_feature(0x60); 13 | assert!(input.is_ok()); 14 | } 15 | } 16 | 17 | #[test] 18 | #[ignore] 19 | /// Test monitor description. Not on CI, no monitors there. 20 | fn test_description() { 21 | let monitors = ddc_macos::Monitor::enumerate().unwrap(); 22 | let monitor = monitors.first().unwrap(); 23 | let description = monitor.description(); 24 | assert!(description.len() > 0); 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ddc-macos" 3 | version = "0.2.2" 4 | authors = ["Haim Gelfenbeyn "] 5 | description = "DDC/CI monitor control on MacOS" 6 | documentation = "https://haimgel.github.io/ddc-macos-rs/ddc_macos" 7 | readme = "README.md" 8 | repository = "https://github.com/haimgel/ddc-macos-rs" 9 | license = "MIT" 10 | keywords = ["ddc", "mccs", "vcp", "vesa", "macos"] 11 | categories = ["hardware-support", "os::macos-apis"] 12 | edition = "2021" 13 | 14 | [dependencies] 15 | core-foundation = "0.10" 16 | core-foundation-sys = "0.8" 17 | core-graphics = "0.24" 18 | # ddc must stay on "0.2" till ddc-hi is also updated 19 | ddc = "0.2" 20 | io-kit-sys = "0.4" 21 | mach2 = "0.4" 22 | thiserror = "1.0" 23 | 24 | [dev-dependencies] 25 | edid-rs = "0.1" 26 | nom = "7.1" 27 | 28 | [badges] 29 | maintenance = { status = "actively-developed" } 30 | -------------------------------------------------------------------------------- /src/iokit/display.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals, unused)] 2 | 3 | /// Selective translation of IOKit/graphics/IOGraphicsLib.h 4 | use core_foundation::dictionary::CFDictionaryRef; 5 | use core_graphics::display::CGDirectDisplayID; 6 | use io_kit_sys::types::{io_service_t, IOOptionBits}; 7 | use mach2::port::mach_port_t; 8 | 9 | pub const kIODisplayMatchingInfo: IOOptionBits = 0x00000100; 10 | pub const kIODisplayOnlyPreferredName: IOOptionBits = 0x00000200; 11 | pub const kIODisplayNoProductName: IOOptionBits = 0x00000400; 12 | 13 | extern "C" { 14 | // For some reason, this is missing from io_kit_sys 15 | pub static kIOMainPortDefault: mach_port_t; 16 | #[link(name = "IOKit", kind = "framework")] 17 | pub fn IODisplayCreateInfoDictionary(framebuffer: io_service_t, options: IOOptionBits) -> CFDictionaryRef; 18 | 19 | #[link(name = "CoreDisplay", kind = "framework")] 20 | // Creates a display info dictionary for a specified display ID 21 | pub fn CoreDisplay_DisplayCreateInfoDictionary(display_id: CGDirectDisplayID) -> CFDictionaryRef; 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Haim Gelfenbeyn 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 | -------------------------------------------------------------------------------- /examples/list.rs: -------------------------------------------------------------------------------- 1 | extern crate ddc; 2 | extern crate ddc_macos; 3 | 4 | use ddc::Ddc; 5 | use ddc_macos::Monitor; 6 | 7 | fn main() { 8 | let monitors = Monitor::enumerate().expect("Could not enumerate external monitors"); 9 | 10 | if monitors.is_empty() { 11 | println!("No external monitors found"); 12 | return; 13 | } 14 | 15 | for mut monitor in monitors { 16 | println!("Monitor"); 17 | println!("\tDescription: {}", monitor.description()); 18 | if let Some(desc) = monitor.product_name() { 19 | println!("\tProduct Name: {}", desc); 20 | } 21 | if let Some(number) = monitor.serial_number() { 22 | println!("\tSerial Number: {}", number); 23 | } 24 | if let Ok(input) = monitor.get_vcp_feature(0x60) { 25 | println!("\tCurrent input: {:04x}", input.value()); 26 | } 27 | if let Some(data) = monitor.edid() { 28 | let mut cursor = std::io::Cursor::new(&data); 29 | let mut reader = edid_rs::Reader::new(&mut cursor); 30 | match edid_rs::EDID::parse(&mut reader) { 31 | Ok(edid) => println!("\tEDID Info: {:?}", edid), 32 | _ => println!("\tCould not parse provided EDID information"), 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build-badge][]][build] 2 | [![cargo-badge][]][cargo] 3 | [![docs-badge][]][docs] 4 | [![license-badge][]][license] 5 | 6 | # ddc-macos 7 | 8 | `ddc-macos` implements the [`ddc`](https://crates.io/crates/ddc) traits for MacOS. 9 | 10 | ## Examples 11 | You can list external monitors and their description using the provided example using: 12 | 13 | `cargo run --example list` 14 | 15 | ## [Documentation][docs] 16 | 17 | See the [documentation][docs] for up to date information. 18 | 19 | ## Thanks 20 | 21 | The code in this crate is heavily inspired by the excellent work done by [ddcctl](https://github.com/kfix/ddcctl) and 22 | [ExternalDisplayBrightness](https://github.com/fnesveda/ExternalDisplayBrightness) projects authors. 23 | 24 | [build]: https://github.com/haimgel/ddc-macos-rs/actions?query=workflow%3Abuild 25 | [build-badge]: https://github.com/haimgel/ddc-macos-rs/workflows/build/badge.svg?style=flat-square 26 | [cargo]: https://crates.io/crates/ddc-macos 27 | [cargo-badge]: https://img.shields.io/crates/v/ddc-macos.svg?style=flat-square 28 | [docs]: http://haimgel.github.io/ddc-macos-rs/ 29 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 30 | [license]: https://github.com/haimgel/ddc-macos-rs/blob/master/LICENSE 31 | [license-badge]: https://img.shields.io/github/license/haimgel/ddc-macos-rs 32 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use core_graphics::base::CGError; 2 | use ddc::ErrorCode; 3 | use io_kit_sys::ret::kIOReturnSuccess; 4 | use mach2::kern_return::{kern_return_t, KERN_FAILURE}; 5 | use thiserror::Error; 6 | 7 | /// An error that can occur during DDC/CI communication with a monitor 8 | #[derive(Error, Debug)] 9 | pub enum Error { 10 | /// Core Graphics errors 11 | #[error("Core Graphics error: {0}")] 12 | CoreGraphics(CGError), 13 | /// Kernel I/O errors 14 | #[error("MacOS kernel I/O error: {0}")] 15 | Io(kern_return_t), 16 | /// DDC/CI errors 17 | #[error("DDC/CI error: {0}")] 18 | Ddc(ErrorCode), 19 | /// Service not found 20 | #[error("Service not found")] 21 | ServiceNotFound, 22 | /// Display location not found 23 | #[error("Display location not found")] 24 | DisplayLocationNotFound, 25 | } 26 | 27 | pub fn verify_io(result: kern_return_t) -> Result<(), Error> { 28 | if result == kIOReturnSuccess { 29 | Ok(()) 30 | } else { 31 | Err(Error::Io(result)) 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(error: std::io::Error) -> Self { 37 | Error::Io(error.raw_os_error().unwrap_or(KERN_FAILURE)) 38 | } 39 | } 40 | 41 | impl From for Error { 42 | fn from(error: ErrorCode) -> Self { 43 | Error::Ddc(error) 44 | } 45 | } 46 | 47 | impl From for Error { 48 | fn from(error: CGError) -> Self { 49 | Error::CoreGraphics(error) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/iokit/wrappers.rs: -------------------------------------------------------------------------------- 1 | use crate::iokit::display::kIOMainPortDefault; 2 | use core_foundation::base::{kCFAllocatorDefault, CFType, TCFType}; 3 | use core_foundation::dictionary::{CFDictionary, CFMutableDictionary, CFMutableDictionaryRef}; 4 | use core_foundation::string::CFString; 5 | use io_kit_sys::keys::kIOServicePlane; 6 | use io_kit_sys::types::{io_iterator_t, io_object_t}; 7 | use io_kit_sys::{ 8 | kIOMasterPortDefault, kIORegistryIterateRecursively, IOIteratorNext, IOObjectRelease, 9 | IORegistryEntryCreateCFProperties, IORegistryEntryCreateIterator, IORegistryGetRootEntry, 10 | IOServiceGetMatchingServices, IOServiceMatching, IOServiceNameMatching, 11 | }; 12 | use std::ops::{Deref, DerefMut}; 13 | 14 | #[derive(Debug)] 15 | pub struct IoObject(io_object_t); 16 | 17 | impl IoObject { 18 | /// Returns typed dictionary with this object properties. 19 | pub fn properties(&self) -> Result, std::io::Error> { 20 | unsafe { 21 | let mut props = std::ptr::null_mut(); 22 | kern_try!(IORegistryEntryCreateCFProperties( 23 | self.0, 24 | &mut props, 25 | kCFAllocatorDefault as _, 26 | 0 27 | )); 28 | Ok(CFMutableDictionary::wrap_under_create_rule(props as _).to_immutable()) 29 | } 30 | } 31 | } 32 | 33 | impl From for IoObject { 34 | fn from(object: io_object_t) -> Self { 35 | Self(object) 36 | } 37 | } 38 | 39 | impl From<&IoObject> for io_object_t { 40 | fn from(val: &IoObject) -> io_object_t { 41 | val.0 42 | } 43 | } 44 | 45 | impl Drop for IoObject { 46 | fn drop(&mut self) { 47 | unsafe { IOObjectRelease(self.0) }; 48 | } 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct IoIterator(io_iterator_t); 53 | 54 | impl IoIterator { 55 | pub fn for_service_names(name: &str) -> Option { 56 | let c_name = std::ffi::CString::new(name).ok()?; 57 | let dict = unsafe { IOServiceNameMatching(c_name.as_ptr()) }; 58 | Self::matching_services(dict as _).ok() 59 | } 60 | 61 | pub fn for_services(name: &str) -> Option { 62 | let c_name = std::ffi::CString::new(name).ok()?; 63 | let dict = unsafe { IOServiceMatching(c_name.as_ptr()) }; 64 | Self::matching_services(dict as _).ok() 65 | } 66 | 67 | fn matching_services(dict: CFMutableDictionaryRef) -> Result { 68 | let mut iter: io_iterator_t = 0; 69 | unsafe { 70 | kern_try!(IOServiceGetMatchingServices(kIOMasterPortDefault, dict as _, &mut iter)); 71 | } 72 | Ok(Self(iter)) 73 | } 74 | 75 | pub fn root() -> Result { 76 | let mut iter: io_iterator_t = 0; 77 | unsafe { 78 | let root = IORegistryGetRootEntry(kIOMainPortDefault); 79 | kern_try!(IORegistryEntryCreateIterator( 80 | root, 81 | kIOServicePlane, 82 | kIORegistryIterateRecursively, 83 | &mut iter 84 | )); 85 | }; 86 | Ok(Self(iter)) 87 | } 88 | } 89 | 90 | impl Deref for IoIterator { 91 | type Target = io_iterator_t; 92 | fn deref(&self) -> &Self::Target { 93 | &self.0 94 | } 95 | } 96 | 97 | impl DerefMut for IoIterator { 98 | fn deref_mut(&mut self) -> &mut Self::Target { 99 | &mut self.0 100 | } 101 | } 102 | 103 | impl Iterator for IoIterator { 104 | type Item = IoObject; 105 | 106 | fn next(&mut self) -> Option { 107 | match unsafe { IOIteratorNext(self.0) } { 108 | 0 => None, 109 | io_object => Some(IoObject(io_object)), 110 | } 111 | } 112 | } 113 | 114 | impl Drop for IoIterator { 115 | fn drop(&mut self) { 116 | unsafe { IOObjectRelease(self.0) }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/iokit/io2c_interface.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_upper_case_globals, non_snake_case, unused)] 2 | 3 | /// Translation of IOKit/i2c/IOI2CInterface.h 4 | extern crate io_kit_sys; 5 | extern crate mach2; 6 | 7 | use crate::iokit::wrappers::IoObject; 8 | use io_kit_sys::ret::IOReturn; 9 | use io_kit_sys::types::{io_service_t, IOItemCount, IOOptionBits}; 10 | use mach2::vm_types::{mach_vm_address_t, mach_vm_size_t, vm_address_t}; 11 | use std::os::raw::c_uint; 12 | 13 | /// IOI2CRequest.sendTransactionType, IOI2CRequest.replyTransactionType 14 | pub const kIOI2CNoTransactionType: c_uint = 0; 15 | pub const kIOI2CSimpleTransactionType: c_uint = 1; 16 | pub const kIOI2CDDCciReplyTransactionType: c_uint = 2; 17 | pub const kIOI2CCombinedTransactionType: c_uint = 3; 18 | pub const kIOI2CDisplayPortNativeTransactionType: c_uint = 4; 19 | 20 | pub type IOI2CRequestCompletion = ::std::option::Option; 21 | 22 | #[repr(C, packed(4))] 23 | #[derive(Debug, Copy, Clone)] 24 | /// A structure defining an I2C bus transaction 25 | pub struct IOI2CRequest { 26 | pub sendTransactionType: IOOptionBits, 27 | pub replyTransactionType: IOOptionBits, 28 | pub sendAddress: u32, 29 | pub replyAddress: u32, 30 | pub sendSubAddress: u8, 31 | pub replySubAddress: u8, 32 | pub __reservedA: [u8; 2usize], 33 | pub minReplyDelay: u64, 34 | pub result: IOReturn, 35 | pub commFlags: IOOptionBits, 36 | pub __padA: u32, 37 | pub sendBytes: u32, 38 | pub __reservedB: [u32; 2usize], 39 | pub __padB: u32, 40 | pub replyBytes: u32, 41 | pub completion: IOI2CRequestCompletion, 42 | pub sendBuffer: vm_address_t, 43 | pub replyBuffer: vm_address_t, 44 | pub __reservedC: [u32; 10usize], 45 | } 46 | 47 | /// struct IOI2CConnect is opaque 48 | #[repr(C)] 49 | pub struct IOI2CConnect { 50 | _opaque: [u8; 0], 51 | } 52 | type IOI2CConnectRef = *mut IOI2CConnect; 53 | 54 | extern "C" { 55 | #[link(name = "IOKit", kind = "framework")] 56 | 57 | /// Returns a count of I2C interfaces available associated with an IOFramebuffer instance 58 | pub fn IOFBGetI2CInterfaceCount(framebuffer: io_service_t, count: *mut IOItemCount) -> IOReturn; 59 | 60 | /// Returns an instance of an I2C bus interface, associated with an IOFramebuffer instance / bus index pair 61 | pub fn IOFBCopyI2CInterfaceForBus( 62 | framebuffer: io_service_t, 63 | bus: IOOptionBits, 64 | interface: *mut io_service_t, 65 | ) -> IOReturn; 66 | 67 | /// Opens an instance of an I2C bus interface, allowing I2C requests to be made 68 | pub fn IOI2CInterfaceOpen( 69 | interface: io_service_t, 70 | options: IOOptionBits, 71 | connect: *mut IOI2CConnectRef, 72 | ) -> IOReturn; 73 | 74 | /// Closes an IOI2CConnectRef 75 | pub fn IOI2CInterfaceClose(connect: IOI2CConnectRef, options: IOOptionBits) -> IOReturn; 76 | 77 | /// Carries out the I2C transaction specified by an IOI2CRequest structure 78 | pub fn IOI2CSendRequest(connect: IOI2CConnectRef, options: IOOptionBits, request: *mut IOI2CRequest) -> IOReturn; 79 | } 80 | 81 | pub(crate) struct IoI2CInterfaceConnection(IOI2CConnectRef); 82 | 83 | impl IoI2CInterfaceConnection { 84 | pub fn new(interface: &IoObject) -> Result { 85 | let mut handle = std::ptr::null_mut(); 86 | unsafe { 87 | kern_try!(IOI2CInterfaceOpen(interface.into(), 0, &mut handle)); 88 | } 89 | Ok(Self(handle)) 90 | } 91 | 92 | pub fn send_request(&self, request: *mut IOI2CRequest) -> Result<(), std::io::Error> { 93 | unsafe { 94 | kern_try!(IOI2CSendRequest(self.0, 0, request)); 95 | kern_try!((*request).result); 96 | } 97 | Ok(()) 98 | } 99 | } 100 | 101 | impl Drop for IoI2CInterfaceConnection { 102 | fn drop(&mut self) { 103 | unsafe { 104 | IOI2CInterfaceClose(self.0, 0); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/intel.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{verify_io, Error}; 2 | use crate::iokit::{kIODisplayOnlyPreferredName, kIOI2CNoTransactionType, IODisplayCreateInfoDictionary}; 3 | use crate::iokit::{ 4 | kIOI2CDDCciReplyTransactionType, kIOI2CSimpleTransactionType, IOFBCopyI2CInterfaceForBus, IOFBGetI2CInterfaceCount, 5 | IOI2CRequest, IoI2CInterfaceConnection, 6 | }; 7 | use crate::iokit::{IoIterator, IoObject}; 8 | use core_foundation::base::{CFType, TCFType}; 9 | use core_foundation::dictionary::CFDictionary; 10 | use core_foundation::number::CFNumber; 11 | use core_foundation::string::CFString; 12 | use core_foundation_sys::base::kCFAllocatorDefault; 13 | use core_graphics::display::CGDisplay; 14 | use ddc::SUB_ADDRESS_DDC_CI; 15 | use io_kit_sys::ret::kIOReturnSuccess; 16 | use io_kit_sys::types::{io_service_t, IOItemCount}; 17 | use io_kit_sys::IORegistryEntryCreateCFProperties; 18 | use mach2::kern_return::KERN_FAILURE; 19 | use std::time::Duration; 20 | 21 | pub(crate) fn execute<'a>( 22 | service: &IoObject, 23 | i2c_address: u16, 24 | request_data: &[u8], 25 | out: &'a mut [u8], 26 | response_delay: Duration, 27 | ) -> Result<&'a mut [u8], crate::error::Error> { 28 | let mut request: IOI2CRequest = unsafe { std::mem::zeroed() }; 29 | 30 | request.commFlags = 0; 31 | request.sendAddress = (i2c_address << 1) as u32; 32 | request.sendTransactionType = kIOI2CSimpleTransactionType; 33 | request.sendBuffer = &request_data as *const _ as usize; 34 | request.sendBytes = request_data.len() as u32; 35 | request.minReplyDelay = response_delay.as_nanos() as u64; 36 | request.result = -1; 37 | 38 | request.replyTransactionType = if out.is_empty() { 39 | kIOI2CNoTransactionType 40 | } else { 41 | unsafe { get_supported_transaction_type().unwrap_or(kIOI2CNoTransactionType) } 42 | }; 43 | request.replyAddress = ((i2c_address << 1) | 1) as u32; 44 | request.replySubAddress = SUB_ADDRESS_DDC_CI; 45 | 46 | request.replyBuffer = &out as *const _ as usize; 47 | request.replyBytes = out.len() as u32; 48 | 49 | unsafe { 50 | send_request(service, &mut request)?; 51 | } 52 | if request.replyTransactionType != kIOI2CNoTransactionType { 53 | Ok(&mut [0u8; 0]) 54 | } else { 55 | Ok(out) 56 | } 57 | } 58 | 59 | fn display_info_dict(frame_buffer: &IoObject) -> Option> { 60 | unsafe { 61 | let info = IODisplayCreateInfoDictionary(frame_buffer.into(), kIODisplayOnlyPreferredName).as_ref()?; 62 | Some(CFDictionary::::wrap_under_create_rule(info)) 63 | } 64 | } 65 | 66 | /// Get supported I2C / DDC transaction types 67 | /// DDCciReply is what we want, but Simple will also work 68 | unsafe fn get_supported_transaction_type() -> Option { 69 | let transaction_types_key = CFString::from_static_string("IOI2CTransactionTypes"); 70 | 71 | for io_service in IoIterator::for_service_names("IOFramebufferI2CInterface")? { 72 | let mut service_properties = std::ptr::null_mut(); 73 | if IORegistryEntryCreateCFProperties( 74 | (&io_service).into(), 75 | &mut service_properties, 76 | kCFAllocatorDefault as _, 77 | 0, 78 | ) == kIOReturnSuccess 79 | { 80 | let info = CFDictionary::::wrap_under_create_rule(service_properties as _); 81 | let transaction_types = info.find(&transaction_types_key)?.downcast::()?.to_i64()?; 82 | if ((1 << kIOI2CDDCciReplyTransactionType) & transaction_types) != 0 { 83 | return Some(kIOI2CDDCciReplyTransactionType); 84 | } else if ((1 << kIOI2CSimpleTransactionType) & transaction_types) != 0 { 85 | return Some(kIOI2CSimpleTransactionType); 86 | } 87 | } 88 | } 89 | None 90 | } 91 | 92 | /// Finds if a framebuffer that matches display 93 | fn framebuffer_port_matches_display(port: &IoObject, display: CGDisplay) -> Option<()> { 94 | let mut bus_count: IOItemCount = 0; 95 | unsafe { 96 | IOFBGetI2CInterfaceCount(port.into(), &mut bus_count); 97 | } 98 | if bus_count == 0 { 99 | return None; 100 | }; 101 | 102 | let info = display_info_dict(port)?; 103 | 104 | let display_vendor_key = CFString::from_static_string("DisplayVendorID"); 105 | let display_product_key = CFString::from_static_string("DisplayProductID"); 106 | let display_serial_key = CFString::from_static_string("DisplaySerialNumber"); 107 | 108 | let display_vendor = info.find(&display_vendor_key)?.downcast::()?.to_i64()? as u32; 109 | let display_product = info.find(&display_product_key)?.downcast::()?.to_i64()? as u32; 110 | // Display serial number is not always present. If it's not there, default to zero 111 | // (to match what CGDisplay.serial_number() returns 112 | let display_serial = info 113 | .find(&display_serial_key) 114 | .and_then(|x| x.downcast::()) 115 | .and_then(|x| x.to_i32()) 116 | .map_or(0, |x| x as u32); 117 | 118 | if display_vendor == display.vendor_number() 119 | && display_product == display.model_number() 120 | && display_serial == display.serial_number() 121 | { 122 | Some(()) 123 | } else { 124 | None 125 | } 126 | } 127 | 128 | /// Gets the framebuffer port for a display 129 | pub(crate) fn get_io_framebuffer_port(display: CGDisplay) -> Option { 130 | if display.is_builtin() { 131 | return None; 132 | } 133 | IoIterator::for_services("IOFramebuffer")? 134 | .find(|framebuffer| framebuffer_port_matches_display(framebuffer, display).is_some()) 135 | } 136 | 137 | /// send an I2C request to a display 138 | unsafe fn send_request( 139 | service: &IoObject, 140 | request: &mut IOI2CRequest, 141 | // post_request_delay: u32, 142 | ) -> Result<(), Error> { 143 | let mut bus_count = 0; 144 | let mut result: Result<(), Error> = Err(Error::Io(KERN_FAILURE)); 145 | verify_io(IOFBGetI2CInterfaceCount(service.into(), &mut bus_count))?; 146 | for bus in 0..bus_count { 147 | let mut interface: io_service_t = 0; 148 | if IOFBCopyI2CInterfaceForBus(service.into(), bus, &mut interface) == kIOReturnSuccess { 149 | let interface = IoObject::from(interface); 150 | result = IoI2CInterfaceConnection::new(&interface) 151 | .and_then(|connection| connection.send_request(request)) 152 | .map_err(From::from); 153 | if result.is_ok() { 154 | break; 155 | } 156 | } 157 | } 158 | result 159 | } 160 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | use crate::error::Error; 4 | use crate::iokit::CoreDisplay_DisplayCreateInfoDictionary; 5 | use crate::iokit::IoObject; 6 | use crate::{arm, intel}; 7 | use core_foundation::base::{CFType, TCFType}; 8 | use core_foundation::data::CFData; 9 | use core_foundation::dictionary::CFDictionary; 10 | use core_foundation::string::{CFString, CFStringRef}; 11 | use core_graphics::display::CGDisplay; 12 | use ddc::{ 13 | DdcCommand, DdcCommandMarker, DdcCommandRaw, DdcCommandRawMarker, DdcHost, Delay, ErrorCode, I2C_ADDRESS_DDC_CI, 14 | SUB_ADDRESS_DDC_CI, 15 | }; 16 | use std::time::Duration; 17 | use std::{fmt, iter}; 18 | 19 | /// DDC access method for a monitor 20 | #[derive(Debug)] 21 | enum MonitorService { 22 | Intel(IoObject), 23 | Arm(arm::IOAVService), 24 | } 25 | 26 | /// A handle to an attached monitor that allows the use of DDC/CI operations. 27 | #[derive(Debug)] 28 | pub struct Monitor { 29 | monitor: CGDisplay, 30 | service: MonitorService, 31 | i2c_address: u16, 32 | delay: Delay, 33 | } 34 | 35 | impl fmt::Display for Monitor { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | write!(f, "{}", self.description()) 38 | } 39 | } 40 | 41 | impl Monitor { 42 | /// Create a new monitor from the specified handle. 43 | fn new(monitor: CGDisplay, service: MonitorService, i2c_address: u16) -> Self { 44 | Monitor { 45 | monitor, 46 | service, 47 | i2c_address, 48 | delay: Default::default(), 49 | } 50 | } 51 | 52 | /// Enumerate all connected physical monitors returning [Vec] 53 | pub fn enumerate() -> Result, Error> { 54 | let monitors = CGDisplay::active_displays() 55 | .map_err(Error::from)? 56 | .into_iter() 57 | .filter_map(|display_id| { 58 | let display = CGDisplay::new(display_id); 59 | return if let Some(service) = intel::get_io_framebuffer_port(display) { 60 | Some(Self::new(display, MonitorService::Intel(service), I2C_ADDRESS_DDC_CI)) 61 | } else if let Ok((service, i2c_address)) = arm::get_display_av_service(display) { 62 | Some(Self::new(display, MonitorService::Arm(service), i2c_address)) 63 | } else { 64 | None 65 | }; 66 | }) 67 | .collect(); 68 | Ok(monitors) 69 | } 70 | 71 | /// Physical monitor description string. If it cannot get the product's name it will use 72 | /// the vendor number and model number to form a description 73 | pub fn description(&self) -> String { 74 | self.product_name().unwrap_or(format!( 75 | "{:04x}:{:04x}", 76 | self.monitor.vendor_number(), 77 | self.monitor.model_number() 78 | )) 79 | } 80 | 81 | /// Serial number for this [Monitor] 82 | pub fn serial_number(&self) -> Option { 83 | let serial = self.monitor.serial_number(); 84 | match serial { 85 | 0 => None, 86 | _ => Some(format!("{}", serial)), 87 | } 88 | } 89 | 90 | /// Product name for this [Monitor], if available 91 | pub fn product_name(&self) -> Option { 92 | let info: CFDictionary = 93 | unsafe { CFDictionary::wrap_under_create_rule(CoreDisplay_DisplayCreateInfoDictionary(self.monitor.id)) }; 94 | 95 | let display_product_name_key = CFString::from_static_string("DisplayProductName"); 96 | let display_product_names_dict = info.find(&display_product_name_key)?.downcast::()?; 97 | let (_, localized_product_names) = display_product_names_dict.get_keys_and_values(); 98 | localized_product_names 99 | .first() 100 | .map(|name| unsafe { CFString::wrap_under_get_rule(*name as CFStringRef) }.to_string()) 101 | } 102 | 103 | /// Returns Extended display identification data (EDID) for this [Monitor] as raw bytes data 104 | pub fn edid(&self) -> Option> { 105 | let info: CFDictionary = 106 | unsafe { CFDictionary::wrap_under_create_rule(CoreDisplay_DisplayCreateInfoDictionary(self.monitor.id)) }; 107 | let display_product_name_key = CFString::from_static_string("IODisplayEDIDOriginal"); 108 | let edid_data = info.find(&display_product_name_key)?.downcast::()?; 109 | Some(edid_data.bytes().into()) 110 | } 111 | 112 | /// CoreGraphics display handle for this monitor 113 | pub fn handle(&self) -> CGDisplay { 114 | self.monitor 115 | } 116 | 117 | fn encode_command<'a>(&self, data: &[u8], packet: &'a mut [u8]) -> &'a [u8] { 118 | packet[0] = SUB_ADDRESS_DDC_CI; 119 | packet[1] = 0x80 | data.len() as u8; 120 | packet[2..2 + data.len()].copy_from_slice(data); 121 | packet[2 + data.len()] = 122 | Self::checksum(iter::once((self.i2c_address as u8) << 1).chain(packet[..2 + data.len()].iter().cloned())); 123 | &packet[..3 + data.len()] 124 | } 125 | 126 | fn decode_response<'a>(&self, response: &'a mut [u8]) -> Result<&'a mut [u8], crate::error::Error> { 127 | if response.is_empty() { 128 | return Ok(response); 129 | }; 130 | let len = (response[1] & 0x7f) as usize; 131 | if len + 2 >= response.len() { 132 | return Err(Error::Ddc(ErrorCode::InvalidLength)); 133 | } 134 | let checksum = Self::checksum( 135 | iter::once(((self.i2c_address << 1) | 1) as u8) 136 | .chain(iter::once(SUB_ADDRESS_DDC_CI)) 137 | .chain(response[1..2 + len].iter().cloned()), 138 | ); 139 | if response[2 + len] != checksum { 140 | return Err(Error::Ddc(ErrorCode::InvalidChecksum)); 141 | } 142 | Ok(&mut response[2..2 + len]) 143 | } 144 | } 145 | 146 | impl DdcHost for Monitor { 147 | type Error = Error; 148 | 149 | fn sleep(&mut self) { 150 | self.delay.sleep() 151 | } 152 | } 153 | 154 | impl DdcCommandRaw for Monitor { 155 | fn execute_raw<'a>( 156 | &mut self, 157 | data: &[u8], 158 | out: &'a mut [u8], 159 | response_delay: Duration, 160 | ) -> Result<&'a mut [u8], Self::Error> { 161 | assert!(data.len() <= 36); 162 | let mut packet = [0u8; 36 + 3]; 163 | let packet = self.encode_command(data, &mut packet); 164 | let response = match &self.service { 165 | MonitorService::Intel(service) => intel::execute(service, self.i2c_address, packet, out, response_delay), 166 | MonitorService::Arm(service) => arm::execute(service, self.i2c_address, packet, out, response_delay), 167 | }?; 168 | self.decode_response(response) 169 | } 170 | } 171 | 172 | impl DdcCommandMarker for Monitor {} 173 | 174 | impl DdcCommandRawMarker for Monitor { 175 | fn set_sleep_delay(&mut self, delay: Delay) { 176 | self.delay = delay; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/arm.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::error::Error::{DisplayLocationNotFound, ServiceNotFound}; 3 | use crate::iokit::IoIterator; 4 | use crate::iokit::{CoreDisplay_DisplayCreateInfoDictionary, IoObject}; 5 | use crate::{kern_try, verify_io}; 6 | use core_foundation::base::{CFType, TCFType}; 7 | use core_foundation::dictionary::CFDictionary; 8 | use core_foundation::string::CFString; 9 | use core_foundation_sys::base::{kCFAllocatorDefault, CFAllocatorRef, CFTypeRef, OSStatus}; 10 | use core_graphics::display::CGDisplay; 11 | use ddc::{I2C_ADDRESS_DDC_CI, SUB_ADDRESS_DDC_CI}; 12 | use io_kit_sys::keys::kIOServicePlane; 13 | use io_kit_sys::types::{io_object_t, io_registry_entry_t}; 14 | use io_kit_sys::{ 15 | kIORegistryIterateRecursively, IORegistryEntryCreateCFProperty, IORegistryEntryGetName, 16 | IORegistryEntryGetParentEntry, IORegistryEntryGetPath, 17 | }; 18 | use mach2::kern_return::KERN_SUCCESS; 19 | use std::ffi::CStr; 20 | use std::os::raw::{c_uint, c_void}; 21 | use std::time::Duration; 22 | 23 | pub type IOAVService = CFTypeRef; 24 | 25 | pub(crate) fn execute<'a>( 26 | service: &IOAVService, 27 | i2c_address: u16, 28 | request_data: &[u8], 29 | out: &'a mut [u8], 30 | response_delay: Duration, 31 | ) -> Result<&'a mut [u8], crate::error::Error> { 32 | unsafe { 33 | verify_io(IOAVServiceWriteI2C( 34 | *service, 35 | i2c_address as _, // I2C_ADDRESS_DDC_CI as u32, 36 | SUB_ADDRESS_DDC_CI as _, 37 | // Skip the first byte, which is the I2C address, which this API does not need 38 | request_data[1..].as_ptr() as _, 39 | (request_data.len() - 1) as _, // command_length as u32 + 3, 40 | ))?; 41 | }; 42 | if !out.is_empty() { 43 | std::thread::sleep(response_delay); 44 | unsafe { 45 | verify_io(IOAVServiceReadI2C( 46 | *service, 47 | i2c_address as _, // I2C_ADDRESS_DDC_CI as u32, 48 | 0, 49 | out.as_ptr() as _, 50 | out.len() as u32, 51 | ))?; 52 | }; 53 | Ok(out) 54 | } else { 55 | Ok(&mut [0u8; 0]) 56 | } 57 | } 58 | 59 | /// Returns an AVService and its DDC I2C address for a given display 60 | pub(crate) fn get_display_av_service(display: CGDisplay) -> Result<(IOAVService, u16), Error> { 61 | if display.is_builtin() { 62 | return Err(ServiceNotFound); 63 | } 64 | let display_infos: CFDictionary = 65 | unsafe { CFDictionary::wrap_under_create_rule(CoreDisplay_DisplayCreateInfoDictionary(display.id)) }; 66 | let location = display_infos 67 | .find(CFString::from_static_string("IODisplayLocation")) 68 | .ok_or(DisplayLocationNotFound)? 69 | .downcast::() 70 | .ok_or(DisplayLocationNotFound)? 71 | .to_string(); 72 | let external_location = CFString::from_static_string("External").into_CFType(); 73 | 74 | let mut iter = IoIterator::root()?; 75 | while let Some(service) = iter.next() { 76 | if let Ok(registry_location) = get_service_registry_entry_path((&service).into()) { 77 | if registry_location == location { 78 | while let Some(service) = iter.next() { 79 | if get_service_registry_entry_name((&service).into())? == "DCPAVServiceProxy" { 80 | let av_service = unsafe { IOAVServiceCreateWithService(kCFAllocatorDefault, (&service).into()) }; 81 | let loc_ref = unsafe { 82 | IORegistryEntryCreateCFProperty( 83 | (&service).into(), 84 | CFString::from_static_string("Location").as_concrete_TypeRef(), 85 | kCFAllocatorDefault, 86 | kIORegistryIterateRecursively, 87 | ) 88 | }; 89 | if !loc_ref.is_null() { 90 | let loc_ref = unsafe { CFType::wrap_under_create_rule(loc_ref) }; 91 | if !av_service.is_null() && (loc_ref == external_location) { 92 | return Ok((av_service, i2c_address(service))); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | Err(ServiceNotFound) 101 | } 102 | 103 | const I2C_ADDRESS_DDC_CI_MDCP29XX: u16 = 0xB7; 104 | 105 | /// Returns the I2C chip address for a given service 106 | fn i2c_address(service: IoObject) -> u16 { 107 | // M1 Macs use a non-standard chip address on their builtin HDMI ports: they are behind a 108 | // MDCP29xx DisplayPort to HDMI bridge chip, and it needs a different I2C slave address: 109 | // not a standard 0x37 but 0xB7. 110 | let mut parent: io_registry_entry_t = 0; 111 | unsafe { 112 | if IORegistryEntryGetParentEntry((&service).into(), kIOServicePlane, &mut parent) != KERN_SUCCESS { 113 | return I2C_ADDRESS_DDC_CI; 114 | } 115 | } 116 | let class_ref = unsafe { 117 | IORegistryEntryCreateCFProperty( 118 | parent, 119 | CFString::from_static_string("EPICProviderClass").as_concrete_TypeRef(), 120 | kCFAllocatorDefault, 121 | kIORegistryIterateRecursively, 122 | ) 123 | }; 124 | if class_ref.is_null() { 125 | return I2C_ADDRESS_DDC_CI; 126 | } 127 | let mcdp29xx = CFString::from_static_string("AppleDCPMCDP29XX").into_CFType(); 128 | let class_ref = unsafe { CFType::wrap_under_create_rule(class_ref) }; 129 | if class_ref == mcdp29xx { 130 | I2C_ADDRESS_DDC_CI_MDCP29XX 131 | } else { 132 | I2C_ADDRESS_DDC_CI 133 | } 134 | } 135 | 136 | fn get_service_registry_entry_path(entry: io_registry_entry_t) -> Result { 137 | let mut path_buffer = [0_i8; 1024]; 138 | unsafe { 139 | kern_try!(IORegistryEntryGetPath(entry, kIOServicePlane, path_buffer.as_mut_ptr())); 140 | Ok(CStr::from_ptr(path_buffer.as_ptr()).to_string_lossy().into_owned()) 141 | } 142 | } 143 | 144 | fn get_service_registry_entry_name(entry: io_registry_entry_t) -> Result { 145 | let mut name = [0; 128]; 146 | unsafe { 147 | kern_try!(IORegistryEntryGetName(entry, name.as_mut_ptr())); 148 | Ok(CStr::from_ptr(name.as_ptr()).to_string_lossy().into_owned()) 149 | } 150 | } 151 | 152 | #[link(name = "CoreDisplay", kind = "framework")] 153 | extern "C" { 154 | // Creates an IOAVService from an existing I/O Kit service 155 | fn IOAVServiceCreateWithService(allocator: CFAllocatorRef, service: io_object_t) -> IOAVService; 156 | 157 | // Reads data over I2C from the specified IOAVService 158 | fn IOAVServiceReadI2C( 159 | service: IOAVService, 160 | chip_address: c_uint, 161 | offset: c_uint, 162 | output_buffer: *mut c_void, 163 | output_buffer_size: c_uint, 164 | ) -> OSStatus; 165 | 166 | // Writes data over I2C to the specified IOAVService 167 | fn IOAVServiceWriteI2C( 168 | service: IOAVService, 169 | chip_address: c_uint, 170 | data_address: c_uint, 171 | input_buffer: *const c_void, 172 | input_buffer_size: c_uint, 173 | ) -> OSStatus; 174 | } 175 | 176 | --------------------------------------------------------------------------------