#!/usr/bin/env python3 """ Chromebook Volume Buttons Driver Hardware volume button driver for Chromebook devices with non-functional side-mounted volume controls. This driver reads GPIO states from the ChromeOS Embedded Controller and emulates keyboard volume events via uinput, enabling physical volume buttons on Chromebooks running Linux with custom firmware. Target Hardware: - Lenovo IdeaPad Flex 5i Chromebook Gen 8 (Taeko) - GPIO 3 (EC:EC_VOLDN_BTN_ODL) - Volume Down - GPIO 4 (EC:EC_VOLUP_BTN_ODL) - Volume Up - Chip: /dev/gpiochip1 (cros-ec-gpio) Author: Muyue Repository: https://gitea.legion-muyue.fr/Muyue/chromebook-volume-buttons """ import sys import gpiod from evdev import UInput, ecodes as e import time import signal # Configuration CHIP_PATH = "/dev/gpiochip1" # cros-ec-gpio VOLUME_DOWN_PIN = 3 # EC:EC_VOLDN_BTN_ODL VOLUME_UP_PIN = 4 # EC:EC_VOLUP_BTN_ODL POLL_INTERVAL = 0.08 class VolumeButtonMapper: def __init__(self): self.ui = None self.request = None self.running = True # Button states (1 = released, 0 = pressed) self.voldown_state = 1 self.volup_state = 1 # Timestamps for automatic repetition self.voldown_press_time = None self.voldown_last_repeat = None self.volup_press_time = None self.volup_last_repeat = None # Repetition parameters self.HOLD_DELAY = 0.8 # Delay before repetition (0.8 seconds) self.REPEAT_INTERVAL = 0.1 # Repetition interval (0.1 seconds) def setup(self): """Initialize the virtual device and GPIO""" print(f"Initializing volume button mapper...") # Create a virtual input device cap = { e.EV_KEY: [e.KEY_VOLUMEDOWN, e.KEY_VOLUMEUP] } self.ui = UInput(cap, name='chromebook-volume-buttons', version=0x1) print(f"Virtual device created: {self.ui.device.path}") # Configure GPIO for simple reading (without edge detection) self.request = gpiod.request_lines( CHIP_PATH, consumer="volume-buttons", config={ VOLUME_DOWN_PIN: gpiod.LineSettings( direction=gpiod.line.Direction.INPUT, bias=gpiod.line.Bias.PULL_UP ), VOLUME_UP_PIN: gpiod.LineSettings( direction=gpiod.line.Direction.INPUT, bias=gpiod.line.Bias.PULL_UP ) } ) print(f"GPIO configured:") print(f" - Volume Down: GPIO {VOLUME_DOWN_PIN}") print(f" - Volume Up: GPIO {VOLUME_UP_PIN}") print(f"Press volume buttons to test...") def send_volume_event(self, key_code, button_name): """Send a volume event (complete press)""" self.ui.write(e.EV_KEY, key_code, 1) # Pressed self.ui.syn() self.ui.write(e.EV_KEY, key_code, 0) # Released self.ui.syn() def check_buttons(self): """Check button states and handle automatic repetition""" try: # Read both GPIOs values = self.request.get_values([VOLUME_DOWN_PIN, VOLUME_UP_PIN]) voldown_gpio = values[0] volup_gpio = values[1] current_time = time.time() # === VOLUME DOWN MANAGEMENT === # ODL (Open Drain Low): INACTIVE = button pressed, ACTIVE = button released voldown_pressed = (voldown_gpio == gpiod.line.Value.INACTIVE) # State change detection if voldown_pressed and self.voldown_state == 1: # Transition released -> pressed print("Volume Down pressed") self.voldown_state = 0 self.voldown_press_time = current_time self.voldown_last_repeat = None # Immediate action on first press self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") print(" -> Volume decreased") elif not voldown_pressed and self.voldown_state == 0: # Transition pressed -> released print("Volume Down released") self.voldown_state = 1 self.voldown_press_time = None self.voldown_last_repeat = None elif voldown_pressed and self.voldown_state == 0: # Button still pressed - check if repeat needed time_held = current_time - self.voldown_press_time if time_held >= self.HOLD_DELAY: # Button held long enough if self.voldown_last_repeat is None: # First repetition self.voldown_last_repeat = current_time self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") print(" -> Volume decreased (repeat)") elif (current_time - self.voldown_last_repeat) >= self.REPEAT_INTERVAL: # Next repetitions self.voldown_last_repeat = current_time self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") print(" -> Volume decreased (repeat)") # === VOLUME UP MANAGEMENT === # ODL (Open Drain Low): INACTIVE = button pressed, ACTIVE = button released volup_pressed = (volup_gpio == gpiod.line.Value.INACTIVE) # State change detection if volup_pressed and self.volup_state == 1: # Transition released -> pressed print("Volume Up pressed") self.volup_state = 0 self.volup_press_time = current_time self.volup_last_repeat = None # Immediate action on first press self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") print(" -> Volume increased") elif not volup_pressed and self.volup_state == 0: # Transition pressed -> released print("Volume Up released") self.volup_state = 1 self.volup_press_time = None self.volup_last_repeat = None elif volup_pressed and self.volup_state == 0: # Button still pressed - check if repeat needed time_held = current_time - self.volup_press_time if time_held >= self.HOLD_DELAY: # Button held long enough if self.volup_last_repeat is None: # First repetition self.volup_last_repeat = current_time self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") print(" -> Volume increased (repeat)") elif (current_time - self.volup_last_repeat) >= self.REPEAT_INTERVAL: # Next repetitions self.volup_last_repeat = current_time self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") print(" -> Volume increased (repeat)") except Exception as ex: print(f"Error reading GPIO: {ex}") def run(self): """Main GPIO monitoring loop""" try: self.setup() # Polling loop while self.running: # Check both buttons self.check_buttons() # Small delay to avoid overloading the CPU time.sleep(POLL_INTERVAL) except PermissionError: print("\nError: Permission denied.") print("This script must be run with root privileges:") print(f" sudo python3 {sys.argv[0]}") sys.exit(1) except KeyboardInterrupt: print("\nStop requested by user...") except Exception as ex: print(f"\nError: {ex}") import traceback traceback.print_exc() sys.exit(1) finally: self.cleanup() def cleanup(self): """Clean up resources""" print("\nCleaning up...") if self.request: self.request.release() if self.ui: self.ui.close() print("Stopped cleanly.") def signal_handler(self, signum, frame): """Handle signals for clean shutdown""" self.running = False def main(): mapper = VolumeButtonMapper() # Configure signal handlers signal.signal(signal.SIGINT, mapper.signal_handler) signal.signal(signal.SIGTERM, mapper.signal_handler) mapper.run() if __name__ == "__main__": main()