chromebook-volume-buttons/volume_buttons.py

204 lines
6.8 KiB
Python
Executable File

#!/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()