2020 年 11 月一个慵懒的夜晚,我看到蒂姆库克如何发布 CPU 比最新 16 英寸 MacBook 更快的无风扇 MacBook Air,而我的工作提供的 15 英寸 2019 年 MacBook Pro 正在慢慢地煎炸我的腿,并以其恒定的速度烦扰我的妻子风扇噪音。我不得不把手放在那台机器上。我也有借口我的应用程序的用户无法再控制他们的显示器亮度,所以我可以很容易地在我的脑海中证明这笔费用是合理的。所以我明白了!长时间的延误和错综复杂的交付计划,因为生活在罗马尼亚这样的国家意味着苹果所有产品的价格都非常高。这已经开始听起来像那些关于看到 M1 有多棒的快乐故事,但事实远非如此。这是一个关于获得 M1 如何让我辞掉工作的故事,我的头撞在无数墙壁上以找出对它的监视器支持,并将开源应用程序变成我不需要“真正的工作”就可以真正生活的东西。我开发了一个名为 Lunar 的应用程序,它可以通过 Mac GPU 发送 DDC 命令来调整显示器的实际亮度、对比度和音量。在英特尔 Mac 上,这非常有效,因为 macOS 有一些私有 API 来查找监视器的帧缓冲区,通过 I²C 向其发送数据,最重要的是,有人已经在这个 ddcctl 实用程序中完成了解决这个问题的困难部分。
M1 Mac 带有不同的内核,与 iOS 非常相似。以前的 API 不再在 M1 GPU 上工作,IOFramebuffer 现在是 IOMobileFramebuffer 并且 IOI2C* 函数没有做任何事情。突然之间,我收到了无数关于 Lunar 如何在 macOS Big Sur 上不再工作的电子邮件、Twitter DM 和 GitHub 问题(大多数 M1 用户认为操作系统升级导致了这种情况,而忽略了他们现在正在使用的事实) Mac 上从未见过的硬件和固件)这对我来说也是一个现实检查。没有分析,我不知道 Lunar 有这么多活跃用户! #define BRIGHTNESS_CONTROL_ID 0x10 UInt8 亮度 = 75 ; // 75% 亮度 IOI2CRequest 请求; bzero ( & request , sizeof ( request ));要求 。 commFlags = 0 ;要求 。发送地址 = 0x6E ;要求 。 sendTransactionType = kIOI2CSimpleTransactionType ;要求 。发送字节 = 7 ; UInt8 数据 [256];要求 。 sendBuffer = ( vm_address_t ) & 数据 [ 0 ];数据[0]=0x51;数据 [1] = 0x84;数据[2]=0x03;数据 [3] = BRIGHTNESS_CONTROL_ID ;数据 [4] = 亮度 >> 8 ;数据 [5] = 亮度 & 255;数据[6]=0x6E^数据[0]^数据[1]^数据[2]^数据[3]^数据[4]^数据[5];要求 。回复交易类型 = kIOI2CNoTransactionType ;要求 。回复字节 = 0 ; io_service_t 帧缓冲区 = 0 ; CGSServiceForDisplayNumber ( displayID , & framebuffer ); io_service_t 接口; if (IOFBCopyI2CInterfaceForBus (framebuffer, bus ++, & interface) != KERN_SUCCESS) return ; IOI2CConnectRef 连接; if ( IOI2CInterfaceOpen ( interface , kNilOptions , & connect ) == KERN_SUCCESS ) { IOI2CSendRequest (connect , kNilOptions , request ); IOI2CInterfaceClose ( connect , kNilOptions ); IOObjectRelease(接口);那是十一月的最后一天。冬天已经来了。天很冷,离我家不到 10 公里,你可以在白雪皑皑的森林中散步。但我很幸运,因为我拥有可信赖的 2019 款 MacBook Pro,可以在我编写代码时保持双手温暖,这些代码在我的日常工作中将在不到 6 个月的时间内过时。
就在白天变成晚上的时候,送货员打电话给我说要买一台笔记本电脑:花费高达 7 个初级开发人员月薪的定制配置 M1 MacBook Pro 到货了!将笔记本电脑充电至 100% 后,我开始安装庞大的 Brewfile,并将其放在电池上作为实验。与此同时,我继续在 2019 款 MacBook 上工作,因为当截止日期很紧时,我的白天工作也是夜班工作。在我睡觉之前,我想测试 Lunar 只是为了了解 M1 上会发生什么。我通过 Rosetta 启动它,应用程序窗口按预期显示,每个 UI 交互都正常工作,但 DDC 没有响应。监视器没有受到任何控制。我只是希望这是一个简单的解决方法,然后就去睡觉了。所以事实证明 M1 上的 I/O 结构非常不同(与以前的 Mac 相比,更类似于 iPhone 和 iPad)。没有我们可以调用 IOFBCopyI2CInterfaceForBus 的 IOFramebuffer。现在有一个 IOMobileFramebuffer 代替它,它没有从中获取 I²C 总线的等效功能。经过几天对 I/O 注册表的筛选,试图找到一种将 I²C 数据发送到监视器的方法后,我放弃了并试图找到一种解决方法。我意识到如果 Lunar 无法正常工作,我就无法工作。我回去做我在拿到显示器的第一天必须做的仪式,但对 DDC 一无所知:每天晚上我都会注意到眼睛疲劳和轻微的头痛,因为显示器以其令人难以置信的明亮 LED 背光使我失明(可能还有就是我已经到了工作的第 10 个小时,但谁重要)
担心要在最后期限前完成多少工作 尝试重新开始工作,但现在我在显示器上看不到任何东西,因为它的亮度和对比度从前一天晚上设置为几乎为 0 QuickShade使用具有可调不透明度的黑色覆盖层来降低图像亮度。它可以在任何 Mac 上运行,因为它不依赖于某些私有 API 来更改显示器的亮度。 QuickShade 通过使用全屏点击黑色窗口使图像变暗来模拟较低的亮度,该窗口根据亮度滑块更改其不透明度。显示器的 LED 背光及其 OSD 中的亮度值保持不变。这绝不是对 QuickShade 的坏批评。这是一个简单的实用程序,可以很好地完成其工作。有些人甚至没有注意到叠加和实际亮度调整之间的区别,因此 QuickShade 可能是他们更好的选择。我想,这不是 Lunar 打算做的,模拟亮度。但与此同时,很多用户都依赖这个应用程序,如果它至少可以做到这一点,人们就会更快乐一点。所以我开始研究人眼如何感知图像的亮度,并阅读了太多关于 Gamma 因子的内容。这是一篇关于这个主题的非常好的文章:每个编码员应该了解的关于 Gamma 的内容
我注意到 macOS 有一个非常简单的方法来控制 Gamma 参数,所以我说为什么不呢?。让我们尝试使用 Gamma 表来实现亮度和对比度近似: let minGamma = 0.0 let gamma = mapNumber ( BrightPercent , fromLow : 0.0 , fromHigh : 1.0 , toLow : 0.3 , toHigh : 1.0 ) let contrast = mapNumber ( powf (contrast.Percent ), , fromLow : 0 , fromHigh : 1.0 , toLow : - 0.2 , toHigh : 0.2 ) CGSetDisplayTransferByFormula ( displayID , minGamma , gamma , gamma + contrast , // 红色伽马 minGamma , 伽马 , 伽马 + 最小对比度 , // 伽马绿色, 伽马最小对比度gamma + contrast // blue gamma ) 当然,这需要数周的重构,因为该应用程序并非旨在支持多种设置亮度的方式(因为它通常发生在每个单人黑客项目中)。还有很多意想不到的问题,比如,为什么应用 gamma 值需要超过 5 秒? ლ(╹◡╹ლ) 似乎只有在下一次重绘屏幕时伽马变化才变得可见。而且由于我使用的是 MacBook 的内置显示器来编写代码,而显示器仅用于观察亮度变化,因此它只会在我太不耐烦并愤怒地将光标移到显示器上时才会更新。现在如何强制屏幕重绘以使伽马更改立即应用? (甚至可能在亮度值之间平滑过渡)当发生伽马过渡时,月球会闪烁一个黄点,以强制屏幕重绘
现在我准备发布一个新版本的 Lunar,将 Gamma 近似值作为 M1 的后备。但碰巧的是,一位特定用户给我发送了一封电子邮件,说明他如何设法从连接到 HDMI 输入的 Raspberry Pi 更改显示器的亮度,而活动输入仍设置为 MacBook 的 USB-C。我已经探索过这个想法,因为我有很多 Pis,但我根本无法让它发挥作用。我开始写一个居高临下的回复,说明我是如何尝试过这个的,以及它是如何永远无法工作的,他可能只有一个恰好支持这一点的显示器,不会适用于其他用户。但是……我意识到我在做什么并开始按退格退格退格退格……同时我一直记得 Lunar 的最佳功能实际上是用户发送的想法,我不应该认为我知道得更好。我可能问了正确的问题,因为回复正是我需要立即完成的。 下载具有完整桌面环境的最新 Raspberry Pi OS 30 分钟后,刷入它,更新到测试版固件版本,并设置正确的/boot/config.txt 中的值,Pi 能够在显示器渲染 MacBook 桌面时使用 ddcutil 发送 DDC 请求。我不能让它溜走,所以我开始为下一个版本的 Lunar 实现一个基于网络的 DDC 控制:服务器将使用 mDNS 在网络上做广告,这样 Lunar 就不必每隔 x 秒扫描整个 LAN Lunar 将使用简单的 HTTP 请求将亮度、对比度、音量和输入值发送到服务器
我从一开始就确定本地网络延迟和 HTTP 开销与 DDC 延迟相比可以忽略不计,因此我不必研究更复杂的解决方案,如 USB 串行、websockets 或 MQTT。尽管副项目在软件开发界是一件值得称赞的事情,但我不建议做这样的事情。在一家美国公司全职工作 9 多个小时后,我在短短的时间内完成上述所有工作非常困难(这也经历了 2 个不同的转变:被一家企业集团收购,与另一家初创公司合并)。我欠我的经理很多,如果没有他鼓励的建议,我不会有力量去做接下来的事情,并且总是表现出真诚的微笑。有一天,他告诉我,他终于开始着手修复我们 gRPC 网关中一个长期存在的问题。他承认这是两个月来他第一次在所有会议和视频通话之间抽出时间编写一些代码(他真正喜欢的事情)。 10 分钟后,另一个非美国团队需要他的帮助,他的编码时间再次被预定的会议填满。这就是技术经理的生活。现在 Lunar 正在开发 M1,而 Buy me a Coffee 捐赠表明人们在这个应用程序中发现了价值,我认为是时候停止做我不喜欢的事情(为我从未使用过的产品的公司工作)并开始做我一直喜欢的事情(创建我也喜欢使用的软件,并与他人分享)。所以在 4 月 1 日,我完成了在美国公司的合同,并开始在 Lunar 实施许可制度。
听起来很简单吧?那么远非如此。为了销售而准备产品,花了我整整两个月的时间。而且比我在 M1 上用 Gamma 和 DDC 进行 4 个月的实验所投入的精力还要多(是的,那是有趣的部分)。这部分旅程是最艰难的,一点也不有趣。我的看法是:如果您刚开始销售您的作品,请选择一种需要最少工作量的付款或许可解决方案,无论最初看起来多么昂贵。他们有一个 macOS SDK,这意味着不需要实现结帐视图、许可证激活对话框、保护对应用程序的访问等。用户可以直接从应用程序购买许可证,无需将他们重定向到网站 反破解技术是已经比我在几个月内完成的更好的实施即使如此,我还是错误地选择了 Paddle 本身不支持的许可系统,这让我陷入了 2 个月的许可服务器兔子洞.我想要 Sketch 拥有的系统:一次性支付无限许可,还包括 1 年的免费更新。
在 6 月份成功推出后,大多数用户对 Gamma 解决方案感到满意,有些用户甚至尝试了 Raspberry Pi 方法: Lunar.app - M1 Mac 控制 3rd Party Monitor 亮度和对比度的一种方式 - 硬件 - MPU Talk 虽然一个用户仍然坚持寻找 I²C 支持。他两次试图让我注意到在 M1 上使用 I²C 的方法,第二次他终于成功了。他在 GitHub 上对 Lunar 的 M1 问题的评论在用户中点燃了新的希望,一些技术性更强的用户开始尝试 IOAVServiceReadI2C 和 IOAVServiceWriteI2C 函数。由于我当时对 DDC 规范的理解很浅,所以在最初的几次尝试中我无法获得有效的概念证明。我从 ESP32 和 Arduino 板的实验中了解到,I²C 实际上是一种串行总线,这意味着您可以通过链接辅助设备,从主设备的相同 2 个引脚与多个设备进行通信。这种可能性带来了对芯片地址的要求,主设备应通过线路发送该地址以到达该链中的特定设备。在 DDC 标准中,辅助设备是监视器,芯片地址为 0x37。
EDID 芯片位于地址 0x50,这是我们在@zhuowei 的 EDID 读取示例中的地址经过反复试验,用户@tao-j 发现了上述细节,并设法最终改变了他的 M1 MacBook 的亮度。不幸的是,这只是开始,因为 Mac Mini 支持多个显示器,并且不清楚在调用 IOAVServiceCreate() 时我们正在控制哪个显示器。我找到了一种通过迭代 I/O Kit 注册表树并查找 AppleCLCD2 类来获取每个监视器特定 AVService 的方法。要知道哪个 AppleCLCD2 属于哪个显示器,我必须将 CoreDisplay_DisplayCreateInfoDictionary 返回的标识数据与注册表节点的属性进行交叉引用。通过这种复杂的逻辑,我设法让 DDC 也能在 Mac Mini 上运行,但只能在 Thunderbolt 3 端口上运行。 HDMI 端口仍然无法用于 DDC,没有人知道为什么。最后,M1 上的 DDC 终于以与在 Intel Mac 上相同的方式工作了!
#define BRIGHTNESS 0x10 IOAVServiceRef avService = IOAVServiceCreate ( kCFAllocatorDefault ); IO返回错误; UInt8 数据 [256]; memset ( data , 0 , sizeof ( data )); UInt8 亮度 = 70 ;数据[0]=0x84;数据 [1] = 0x03;数据 [2] = 亮度;数据 [ 3 ] = 亮度 >> 8 ;数据 [4] = 亮度 & 255;数据[5] = 0x6E ^ 0x51 ^ 数据[ 0 ] ^ 数据[ 1 ] ^ 数据[ 2 ] ^ 数据[ 3 ] ^ 数据[ 4 ]; IOAVServiceWriteI2C ( avService , 0x37 , 0x51 , data , 6 );当 M1 GPU 发送任何 I²C 数据时,某些显示器会丢失信号或在连接时出现闪烁 Mac Mini 的 HDMI 端口不通过 I²C 发送任何类型的数据目前这些似乎是硬件问题,我只是无论我多么明显地表明这些无法解决,我都必须继续回复清晨的支持电子邮件。我在最后留下了这些,因为细节可能会让大多数人感到厌烦,但它们可能仍然对极少数读者有用。所有显示器内部都有一个强大的微处理器,其目的是通过多种类型的连接接收视频数据,并通过面板上令人难以置信的微小晶体从这些数据中创建图像。同一个微处理器根据您可以使用其物理按钮在监视器设置中更改的亮度值,使该晶体面板后面的 LED 面板变暗或变亮。
由于连接到显示器的设备需要了解有关其功能的信息(例如分辨率、颜色配置文件等),因此需要一种计算机和显示器都知道的语言,以便它们可以进行通信。这种语言称为通信协议。大多数显示器的处理器内部实现的协议称为显示数据通道或简称 DDC。为了允许从主机设备读取或更改不同的监视器属性,VESA 创建了在 DDC 上工作的监视器控制命令集(或简称 MCCS)。 MCCS 允许 Lunar 和其他应用程序更改显示器亮度、对比度、音量、输入等。 I²C 是一种 Wire 协议,它基本上指定了如何将通过两条电线发送的电脉冲转换为信息位。 DDC 指定哪些位序列是有效的,而 I²C 指定像监视器微处理器这样的设备如何通过 HDMI、DisplayPort、USB-C 等电缆内的电线获取这些位。 # 为什么 macOS 阻止我更改显示器上的音量,而 Windows 允许这样做?
macOS 不会阻止音量,它只是没有实现任何方式来改变显示器的音量。 Windows 实际上只改变软件音量,所以如果您的显示器实际音量为 50%,Windows 只能在软件中降低音量,因此您将听到 0% 到 50% 之间的任何声音。如果您检查显示器 OSD,您会看到显示器的音量值始终保持在 50%。现在 macOS 可能也能做到这一点,所以至少我们有办法降低音量。但事实并非如此。所以如果你想在 Mac 上改变显示器的实际音量,Lunar 可以做到。