├── .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 |
--------------------------------------------------------------------------------