chromebook-volume-buttons/volume_buttons.py

241 lines
8.5 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(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()