#!/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("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("GPIO configured:") print(f" - Volume Down: GPIO {VOLUME_DOWN_PIN}") print(f" - Volume Up: GPIO {VOLUME_UP_PIN}") print("Press volume buttons to test...") def send_volume_event(self, key_code): """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 _handle_button_state(self, gpio_value, button_state, press_time, last_repeat, current_time, key_code, button_name, action_verb): """Handle button state transitions and repetition for a single button Returns: (new_state, new_press_time, new_last_repeat) """ # ODL (Open Drain Low): INACTIVE = button pressed, ACTIVE = button released button_pressed = (gpio_value == gpiod.line.Value.INACTIVE) # Transition: released -> pressed if button_pressed and button_state == 1: print(f"{button_name} pressed") self.send_volume_event(key_code) print(f" -> Volume {action_verb}") return (0, current_time, None) # Transition: pressed -> released elif not button_pressed and button_state == 0: print(f"{button_name} released") return (1, None, None) # Button still pressed - check for repetition elif button_pressed and button_state == 0: time_held = current_time - press_time if time_held >= self.HOLD_DELAY: if last_repeat is None or (current_time - last_repeat) >= self.REPEAT_INTERVAL: self.send_volume_event(key_code) print(f" -> Volume {action_verb} (repeat)") return (button_state, press_time, current_time) # No state change return (button_state, press_time, last_repeat) 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]) current_time = time.time() # Handle volume down button self.voldown_state, self.voldown_press_time, self.voldown_last_repeat = self._handle_button_state( values[0], self.voldown_state, self.voldown_press_time, self.voldown_last_repeat, current_time, e.KEY_VOLUMEDOWN, "Volume Down", "decreased" ) # Handle volume up button self.volup_state, self.volup_press_time, self.volup_last_repeat = self._handle_button_state( values[1], self.volup_state, self.volup_press_time, self.volup_last_repeat, current_time, e.KEY_VOLUMEUP, "Volume Up", "increased" ) 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()