204 lines
6.8 KiB
Python
Executable File
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()
|