This guide explains how to control an iMac display's brightness in Ubuntu using an ESP32 microcontroller. This solution is particularly useful for iMacs running Ubuntu where the standard brightness controls don't work. Derived from https://medium.com/@fixingthings/imac-2009-2010-2011-gpu-upgrade-fixing-led-lcd-pwm-brightness-with-an-esp32-bc32da61a0e7 and adapted for Ubuntu.
- ESP32 development board
- PCI-E 6-pin extension cable or splitter (to intercept PWM signal)
- USB cable for ESP32
- Basic soldering equipment
- Open the iMac and locate the LCD driver board
- Identify the PCI-E 6-pin cable connected to the LCD
- Cut the PWM wire on your extension cable (typically bottom right pin)
- Connect the PWM wire to GPIO23 on the ESP32
- Connect ESP32 to a USB port
- Install mbpfan (
sudo apt install mbpfan
) to manage fan control
Using PlatformIO in Visual Studio Code:
-
Create new project with:
- Board: esp32dev
- Framework: arduino
-
Add this code to
src/main.cpp
:
#include <Arduino.h>
// Define the PWM properties
const int pwmPin = 23; // GPIO23/D23 for PWM output
const int pwmFreq = 10000; // 10kHz PWM frequency
const int pwmChannel = 0; // PWM channel (0-15)
const int pwmResolution = 8; // 8-bit resolution (0-255)
const int ledPin = 2; // Built-in LED for status
void setup() {
// Configure Serial communication
Serial.begin(115200);
// Configure status LED
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, HIGH); // Turn on LED to show we're running
// Configure PWM
ledcSetup(pwmChannel, pwmFreq, pwmResolution);
ledcAttachPin(pwmPin, pwmChannel);
// Set initial brightness to 70%
int initialBrightness = (70 * 255) / 100;
ledcWrite(pwmChannel, initialBrightness);
Serial.println("ESP32 LCD Brightness Control Ready");
Serial.println("Send values 0-100 for brightness percentage");
}
void loop() {
if (Serial.available() > 0) {
// Read the incoming brightness value
String input = Serial.readStringUntil('\n');
int percentBrightness = input.toInt();
// Convert percentage (0-100) to PWM value (0-255)
// Enforce minimum brightness of 0%
percentBrightness = constrain(percentBrightness, 0, 100);
int pwmValue = map(percentBrightness, 0, 100, 0, 255);
// Additional safety check
if (pwmValue < 13) {
//pwmValue = 13; // Minimum safe brightness
Serial.println("Warning: a minimum safe brightness is 5%");
}
// Apply the brightness
ledcWrite(pwmChannel, pwmValue);
// Blink status LED to show we received command
digitalWrite(ledPin, LOW);
delay(50);
digitalWrite(ledPin, HIGH);
// Send confirmation
Serial.print("Brightness set to: ");
Serial.print(percentBrightness);
Serial.println("%");
}
}
- Install required packages:
sudo apt install python3-pip python3-serial
- Create the script at
/usr/local/bin/imacdisplay.py
:
#!/usr/bin/env python3
import serial
import argparse
import sys
import time
from pathlib import Path
import json
def get_config_file():
return Path.home() / '.config' / 'imacdisplay.conf'
def load_config():
config_file = get_config_file()
try:
if config_file.exists():
with open(config_file) as f:
return json.load(f)
except Exception as e:
print(f"Error loading config: {e}")
return {'port': '/dev/ttyUSB0', 'last_brightness': 70}
def save_config(brightness):
config_file = get_config_file()
config = load_config()
config['last_brightness'] = brightness
try:
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, 'w') as f:
json.dump(config, f)
except Exception as e:
print(f"Error saving config: {e}")
def setup_serial(port='/dev/ttyUSB0'):
try:
ser = serial.Serial(port, 115200, timeout=2)
time.sleep(1) # Wait for connection to stabilize
return ser
except serial.SerialException as e:
print(f"Error: Could not open serial port {port}")
print(f"Details: {e}")
return None
def set_brightness(ser, value, allow_zero=False):
"""Set brightness with safety limits
Args:
ser: Serial connection
value: Brightness value to set
allow_zero: If True, allows 0%, otherwise enforces 5% minimum
"""
try:
if allow_zero:
value = max(0, min(100, value))
else:
value = max(5, min(100, value))
ser.write(f"{value}\n".encode())
time.sleep(0.1) # Wait for response
response = ser.readline().decode().strip()
print(f"Set brightness to {value}%")
save_config(value) # Save the current brightness
return value
except Exception as e:
print(f"Error setting brightness: {e}")
return None
def get_brightness():
"""Get last saved brightness value"""
config = load_config()
return config.get('last_brightness', 70)
def main():
parser = argparse.ArgumentParser(description='iMac Display Brightness Control')
group = parser.add_mutually_exclusive_group()
group.add_argument('-s', '--set', type=int, help='Set brightness (5-100, or 0 with -z)')
group.add_argument('-g', '--get', action='store_true', help='Get current brightness')
group.add_argument('-i', '--increment', type=int, help='Increase brightness by amount')
group.add_argument('-d', '--decrement', type=int, help='Decrease brightness by amount')
parser.add_argument('-p', '--port', default='/dev/ttyUSB0', help='Serial port')
parser.add_argument('-z', '--allow-zero', action='store_true', help='Allow setting brightness to 0')
args = parser.parse_args()
# Debug prints to see what's happening
print(f"Command arguments: {args}")
if args.get:
print(get_brightness())
return
# Setup serial connection for all operations that need it
if args.set is not None or args.increment is not None or args.decrement is not None:
ser = setup_serial(args.port)
if not ser:
print("Failed to setup serial connection")
return
try:
current = get_brightness()
print(f"Current brightness: {current}")
if args.set is not None:
# Explicit check for zero setting
if args.set == 0 and not args.allow_zero:
print("Error: Cannot set brightness to 0 without --allow-zero (-z) flag")
return
print(f"Setting brightness to {args.set} with allow_zero={args.allow_zero}")
set_brightness(ser, args.set, allow_zero=args.allow_zero)
elif args.increment is not None:
new_value = min(current + args.increment, 100)
print(f"Incrementing brightness to {new_value}")
set_brightness(ser, new_value, allow_zero=False)
elif args.decrement is not None:
new_value = max(current - args.decrement, 5)
print(f"Decrementing brightness to {new_value}")
set_brightness(ser, new_value, allow_zero=False)
finally:
ser.close()
print("Serial connection closed")
else:
parser.print_help()
if __name__ == '__main__':
main()
- Make the script executable:
sudo chmod +x /usr/local/bin/imacdisplay.py
Basic commands:
# Get current brightness
imacdisplay.py -g
# Set brightness to 70%
imacdisplay.py -s 70
# Increase brightness by 10%
imacdisplay.py -i 10
# Decrease brightness by 10%
imacdisplay.py -d 10
- Open Settings → Keyboard → Keyboard Shortcuts → Custom
- Add two new shortcuts:
- Increase Brightness:
- Command:
/usr/local/bin/imacdisplay.py -i 10
- Shortcut: Ctrl + Shift + Up
- Command:
- Decrease Brightness:
- Command:
/usr/local/bin/imacdisplay.py -d 10
- Shortcut: Ctrl + Shift + Down
- Command:
- Increase Brightness:
- Minimum brightness is limited to 5% to prevent complete blackout
- PWM values are properly mapped to ensure safe operation
- Configuration is saved to restore last brightness setting
- The ESP32 must be connected via USB
- Fan control might need mbpfan package
- Initial brightness is set to 70%
- The script maintains a configuration file at
~/.config/imacdisplay.conf
- Optional: install imac_idle.py to turn off the backlight when idle and turn it back on by keystroke.
-
If serial connection fails:
- Check if ESP32 is properly connected
- Verify correct port in
/dev/ttyUSB0
- Ensure user has proper permissions
-
If brightness doesn't change:
- Verify PWM wire connection to GPIO23
- Check if ESP32 LED blinks when receiving commands
-
If script fails:
- Ensure python3-serial is installed
- Check script permissions
- Verify user has access to serial port