commit bc9d6f8639b1cc540455f222edca138c8528dd10 Author: Muyue Date: Mon Oct 13 11:06:35 2025 +0200 Initial release v1.0.0 Add GPIO-based volume button driver for Lenovo IdeaPad Flex 5i Chromebook Gen 8 Features: - Volume button detection via GPIO polling - Key repeat with configurable timings - Systemd service integration - Professional documentation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96cc406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# System +.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..daddead --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-01-13 + +### Added +- Initial release +- GPIO-based volume button detection for Lenovo IdeaPad Flex 5i Chromebook Gen 8 +- Support for Volume Up and Volume Down buttons via GPIO pins 3 and 4 +- Key repeat functionality (1 second hold delay, 200ms repeat interval) +- Systemd service for automatic startup +- Installation script for easy deployment +- Comprehensive README documentation +- MIT License + +### Technical Details +- Python 3 implementation using gpiod and evdev libraries +- 10ms GPIO polling interval for responsive button detection +- Open Drain Low (ODL) logic handling +- Proper state machine for button press/hold/release detection +- Graceful shutdown handling with SIGINT/SIGTERM +- Virtual input device creation via uinput + +### Compatibility +- Tested on Lenovo IdeaPad Flex 5i Chromebook Gen 8 (Taeko) +- Intel Core i3-1215U processor +- MrChromebox custom BIOS (version 2408.1) +- Arch Linux kernel 6.17.1 +- Python 3.13.7 +- libgpiod 2.2.2 +- python-evdev 1.9.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6483120 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ +# Contributing to Chromebook Volume Buttons Driver + +Thank you for considering contributing to this project. This document provides guidelines for contributions. + +## Code of Conduct + +- Be respectful and constructive +- Focus on technical merit +- Help maintain a welcoming environment + +## How to Contribute + +### Reporting Bugs + +When reporting bugs, please include: + +1. **Hardware Information:** + - Chromebook model and codename + - Processor model + - BIOS version + - Linux kernel version + +2. **Software Environment:** + - Python version + - libgpiod version + - python-evdev version + +3. **Issue Description:** + - Expected behavior + - Actual behavior + - Steps to reproduce + - Relevant log output + +4. **GPIO Information:** +```bash +sudo gpioinfo | grep -E "VOLDN|VOLUP" +``` + +### Suggesting Enhancements + +Enhancement suggestions should include: +- Clear description of the feature +- Use case and motivation +- Implementation considerations +- Potential impact on existing functionality + +### Pull Requests + +1. **Before Starting:** + - Check existing issues and pull requests + - Discuss major changes in an issue first + - Ensure you have test hardware available + +2. **Code Standards:** + - Follow PEP 8 style guide + - Use meaningful variable and function names + - Add docstrings for functions and classes + - Keep lines under 100 characters where practical + +3. **Commit Guidelines:** + - Write clear, descriptive commit messages + - Use present tense ("Add feature" not "Added feature") + - Reference issues when applicable + - Keep commits focused and atomic + +4. **Testing:** + - Test on actual hardware + - Verify service starts/stops correctly + - Check for resource leaks during extended operation + - Ensure CPU usage remains low + +5. **Documentation:** + - Update README.md for feature changes + - Update CHANGELOG.md following Keep a Changelog format + - Add inline comments for complex logic + - Update hardware compatibility list if applicable + +### Development Setup + +```bash +# Clone repository +git clone https://gitea.legion-muyue.fr/Muyue/chromebook-volume-buttons.git +cd chromebook-volume-buttons + +# Install dependencies +sudo pacman -S python-evdev libgpiod + +# Test script directly +sudo python3 volume_buttons.py +``` + +### GPIO Mapping for New Hardware + +If porting to different Chromebook models: + +1. Identify GPIO chip and pins: +```bash +sudo gpioinfo +``` + +2. Look for volume button GPIO lines + +3. Update constants in `volume_buttons.py`: +```python +CHIP_PATH = "/dev/gpiochipX" # Update chip number +VOLUME_DOWN_PIN = X # Update pin number +VOLUME_UP_PIN = Y # Update pin number +``` + +4. Test and document hardware in README.md + +### Code Review Process + +1. Maintainer reviews code for: + - Correctness + - Style compliance + - Documentation completeness + - Hardware compatibility impact + +2. Changes may be requested before merge + +3. Once approved, changes are merged to main branch + +### Release Process + +1. Version numbers follow Semantic Versioning +2. CHANGELOG.md updated with all changes +3. Git tag created for release +4. Release notes generated from CHANGELOG.md + +## Questions + +For questions about contributing, open an issue with the "question" label. + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7eb4a86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Muyue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4cc652 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# Chromebook Volume Buttons Driver + +Hardware volume button driver for Chromebook devices with non-functional side-mounted volume controls. + +## Hardware Compatibility + +**Primary Target:** +- Lenovo IdeaPad Flex 5i Chromebook Gen 8 +- Codename: Taeko (Google Brya family) +- Processor: Intel Core i3-1215U +- BIOS: MrChromebox custom firmware + +**GPIO Configuration:** +- Volume Down: GPIO 3 (EC:EC_VOLDN_BTN_ODL) +- Volume Up: GPIO 4 (EC:EC_VOLUP_BTN_ODL) +- Chip: /dev/gpiochip1 (cros-ec-gpio) + +## Features + +- Real-time volume button detection via GPIO polling +- Automatic key repeat after 1 second hold +- Repeat interval: 200ms +- Systemd service for automatic startup +- Low CPU overhead (10ms polling interval) + +## Prerequisites + +**Required packages:** +```bash +sudo pacman -S python-evdev libgpiod +``` + +**Python dependencies:** +- python-gpiod (>= 2.2.0) +- python-evdev (>= 1.9.0) + +## Installation + +### Quick Installation + +```bash +git clone https://gitea.legion-muyue.fr/Muyue/chromebook-volume-buttons.git +cd chromebook-volume-buttons +sudo bash install.sh +``` + +### Manual Installation + +1. Copy the script to system location: +```bash +sudo cp volume_buttons.py /usr/local/bin/chromebook-volume-buttons +sudo chmod +x /usr/local/bin/chromebook-volume-buttons +``` + +2. Install systemd service: +```bash +sudo cp chromebook-volume-buttons.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable chromebook-volume-buttons.service +sudo systemctl start chromebook-volume-buttons.service +``` + +## Usage + +The service starts automatically on boot. Manual control: + +```bash +# Check status +sudo systemctl status chromebook-volume-buttons.service + +# View live logs +sudo journalctl -u chromebook-volume-buttons.service -f + +# Restart service +sudo systemctl restart chromebook-volume-buttons.service + +# Stop service +sudo systemctl stop chromebook-volume-buttons.service + +# Disable auto-start +sudo systemctl disable chromebook-volume-buttons.service +``` + +## Technical Details + +### Architecture + +The driver operates in userspace by: +1. Reading GPIO states via libgpiod (cros-ec-gpio chip) +2. Detecting button state transitions (pressed/released) +3. Emulating keyboard events via uinput (/dev/input/eventX) +4. Implementing key repeat logic for held buttons + +### GPIO Signaling + +The buttons use Open Drain Low (ODL) logic: +- `Value.INACTIVE` = Button pressed (LOW) +- `Value.ACTIVE` = Button released (HIGH) + +### Timing Specifications + +| Parameter | Value | Description | +|-----------|-------|-------------| +| Poll Interval | 10ms | GPIO sampling rate | +| Hold Delay | 1000ms | Time before repeat starts | +| Repeat Interval | 200ms | Time between repeated events | + +## Troubleshooting + +### Buttons Not Detected + +1. Verify GPIO accessibility: +```bash +sudo gpioinfo | grep -E "VOLDN|VOLUP" +``` + +Expected output: +``` +line 3: "EC:EC_VOLDN_BTN_ODL" input +line 4: "EC:EC_VOLUP_BTN_ODL" input +``` + +2. Check permissions: +```bash +ls -l /dev/gpiochip1 +``` + +3. Verify service status: +```bash +sudo systemctl status chromebook-volume-buttons.service +``` + +### High CPU Usage + +Check polling interval in source code (default: 10ms = 0.01s). Adjust `POLL_INTERVAL` if needed. + +### Volume Not Changing + +Ensure no conflicting volume control services are running. Check with: +```bash +ps aux | grep -i volume +``` + +## Development + +### Testing Without Service + +Run the script directly for debugging: +```bash +sudo python3 volume_buttons.py +``` + +Press Ctrl+C to stop. + +### Modifying Timing + +Edit the following constants in `volume_buttons.py`: + +```python +self.HOLD_DELAY = 1.0 # Seconds before repeat +self.REPEAT_INTERVAL = 0.2 # Seconds between repeats +POLL_INTERVAL = 0.01 # GPIO polling interval +``` + +## License + +MIT License - See LICENSE file for details. + +## Contributing + +Contributions are welcome. Please: +1. Test on compatible hardware +2. Follow existing code style +3. Update documentation for changes +4. Submit pull requests with clear descriptions + +## Hardware Variations + +If your Chromebook model differs, check GPIO mappings: + +```bash +sudo gpioinfo +``` + +Look for volume button GPIO lines and update constants in the script: +```python +VOLUME_DOWN_PIN = 3 # Update if different +VOLUME_UP_PIN = 4 # Update if different +``` + +## References + +- ChromiumOS EC GPIO Documentation +- Linux GPIO Userspace API (libgpiod) +- Python evdev Documentation +- MrChromebox Firmware + +## Author + +Muyue + +## Acknowledgments + +- MrChromebox for custom BIOS support +- ChromiumOS embedded controller team +- Linux GPIO subsystem maintainers diff --git a/chromebook-volume-buttons.service b/chromebook-volume-buttons.service new file mode 100644 index 0000000..70c5e31 --- /dev/null +++ b/chromebook-volume-buttons.service @@ -0,0 +1,19 @@ +[Unit] +Description=Chromebook Volume Buttons Handler +Documentation=https://github.com/chromebook-volume-buttons +After=multi-user.target +Wants=multi-user.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /usr/local/bin/chromebook-volume-buttons +Restart=always +RestartSec=5 +User=root + +# Security settings +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a06fa93 --- /dev/null +++ b/install.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Installation script for Chromebook Volume Buttons Driver + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Chromebook Volume Buttons Driver Installation ===" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Error: This script must be run with sudo privileges" + echo "Usage: sudo bash install.sh" + exit 1 +fi + +# Copy Python script to system location +echo "[1/5] Installing Python script to /usr/local/bin/..." +cp "${SCRIPT_DIR}/volume_buttons.py" /usr/local/bin/chromebook-volume-buttons +chmod +x /usr/local/bin/chromebook-volume-buttons +echo " Script installed successfully" + +# Install systemd service +echo "[2/5] Installing systemd service..." +cp "${SCRIPT_DIR}/chromebook-volume-buttons.service" /etc/systemd/system/ +echo " Service file installed" + +# Reload systemd daemon +echo "[3/5] Reloading systemd daemon..." +systemctl daemon-reload +echo " Daemon reloaded" + +# Enable service for auto-start +echo "[4/5] Enabling service for automatic startup..." +systemctl enable chromebook-volume-buttons.service +echo " Service enabled (will start on boot)" + +# Start service immediately +echo "[5/5] Starting service..." +systemctl start chromebook-volume-buttons.service +echo " Service started" + +echo "" +echo "=== Installation completed successfully ===" +echo "" +echo "Service status:" +systemctl status chromebook-volume-buttons.service --no-pager -l || true + +echo "" +echo "Useful commands:" +echo " View live logs: sudo journalctl -u chromebook-volume-buttons.service -f" +echo " Stop service: sudo systemctl stop chromebook-volume-buttons.service" +echo " Restart service: sudo systemctl restart chromebook-volume-buttons.service" +echo " Disable service: sudo systemctl disable chromebook-volume-buttons.service" +echo " Check status: sudo systemctl status chromebook-volume-buttons.service" diff --git a/volume_buttons.py b/volume_buttons.py new file mode 100755 index 0000000..dfeebbd --- /dev/null +++ b/volume_buttons.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +Script pour activer les boutons volume du Chromebook Taeko +Les boutons sont connectés aux GPIO 3 (Volume Down) et 4 (Volume Up) +sur le chip cros-ec-gpio (gpiochip1) +""" + +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 + +# Les GPIO sont actifs à LOW (ODL = Open Drain Low) +# Note: gpiod retourne des Value.ACTIVE/INACTIVE, pas des entiers +POLL_INTERVAL = 0.01 # 10ms + +class VolumeButtonMapper: + def __init__(self): + self.ui = None + self.request = None + self.running = True + + # États des boutons (1 = relâché, 0 = appuyé) + self.voldown_state = 1 + self.volup_state = 1 + + # Timestamps pour la répétition automatique + self.voldown_press_time = None + self.voldown_last_repeat = None + self.volup_press_time = None + self.volup_last_repeat = None + + # Paramètres de répétition + self.HOLD_DELAY = 1.0 # Délai avant répétition (1 seconde) + self.REPEAT_INTERVAL = 0.2 # Intervalle de répétition (0.2 secondes) + + def setup(self): + """Initialise le périphérique virtuel et les GPIO""" + print(f"Initialisation du mapper de boutons volume...") + + # Créer un périphérique d'entrée virtuel + cap = { + e.EV_KEY: [e.KEY_VOLUMEDOWN, e.KEY_VOLUMEUP] + } + self.ui = UInput(cap, name='chromebook-volume-buttons', version=0x1) + print(f"Périphérique virtuel créé: {self.ui.device.path}") + + # Configurer les GPIO en lecture simple (sans 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 configurés:") + print(f" - Volume Down: GPIO {VOLUME_DOWN_PIN}") + print(f" - Volume Up: GPIO {VOLUME_UP_PIN}") + print(f"Appuyez sur les boutons volume pour tester...") + + def send_volume_event(self, key_code, button_name): + """Envoie un événement de volume (appui complet)""" + self.ui.write(e.EV_KEY, key_code, 1) # Appuyer + self.ui.syn() + self.ui.write(e.EV_KEY, key_code, 0) # Relâcher + self.ui.syn() + + def check_buttons(self): + """Vérifie l'état des boutons et gère la répétition automatique""" + try: + # Lire les deux GPIO + values = self.request.get_values([VOLUME_DOWN_PIN, VOLUME_UP_PIN]) + voldown_gpio = values[0] + volup_gpio = values[1] + + current_time = time.time() + + # === GESTION VOLUME DOWN === + # ODL (Open Drain Low): INACTIVE = bouton appuyé, ACTIVE = bouton relâché + voldown_pressed = (voldown_gpio == gpiod.line.Value.INACTIVE) + + # Détection du changement d'état + if voldown_pressed and self.voldown_state == 1: + # Transition relâché -> appuyé + print("Volume Down appuyé") + self.voldown_state = 0 + self.voldown_press_time = current_time + self.voldown_last_repeat = None + + # Action immédiate au premier appui + self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") + print(" -> Son baissé") + + elif not voldown_pressed and self.voldown_state == 0: + # Transition appuyé -> relâché + print("Volume Down relâché") + self.voldown_state = 1 + self.voldown_press_time = None + self.voldown_last_repeat = None + + elif voldown_pressed and self.voldown_state == 0: + # Bouton toujours appuyé - vérifier si répétition nécessaire + time_held = current_time - self.voldown_press_time + + if time_held >= self.HOLD_DELAY: + # Bouton maintenu assez longtemps + if self.voldown_last_repeat is None: + # Première répétition + self.voldown_last_repeat = current_time + self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") + print(" -> Son baissé (répétition)") + elif (current_time - self.voldown_last_repeat) >= self.REPEAT_INTERVAL: + # Répétitions suivantes + self.voldown_last_repeat = current_time + self.send_volume_event(e.KEY_VOLUMEDOWN, "Volume Down") + print(" -> Son baissé (répétition)") + + # === GESTION VOLUME UP === + # ODL (Open Drain Low): INACTIVE = bouton appuyé, ACTIVE = bouton relâché + volup_pressed = (volup_gpio == gpiod.line.Value.INACTIVE) + + # Détection du changement d'état + if volup_pressed and self.volup_state == 1: + # Transition relâché -> appuyé + print("Volume Up appuyé") + self.volup_state = 0 + self.volup_press_time = current_time + self.volup_last_repeat = None + + # Action immédiate au premier appui + self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") + print(" -> Son augmenté") + + elif not volup_pressed and self.volup_state == 0: + # Transition appuyé -> relâché + print("Volume Up relâché") + self.volup_state = 1 + self.volup_press_time = None + self.volup_last_repeat = None + + elif volup_pressed and self.volup_state == 0: + # Bouton toujours appuyé - vérifier si répétition nécessaire + time_held = current_time - self.volup_press_time + + if time_held >= self.HOLD_DELAY: + # Bouton maintenu assez longtemps + if self.volup_last_repeat is None: + # Première répétition + self.volup_last_repeat = current_time + self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") + print(" -> Son augmenté (répétition)") + elif (current_time - self.volup_last_repeat) >= self.REPEAT_INTERVAL: + # Répétitions suivantes + self.volup_last_repeat = current_time + self.send_volume_event(e.KEY_VOLUMEUP, "Volume Up") + print(" -> Son augmenté (répétition)") + + except Exception as ex: + print(f"Erreur lors de la lecture des GPIO: {ex}") + + def run(self): + """Boucle principale de monitoring des GPIO""" + try: + self.setup() + + # Boucle de polling + while self.running: + # Vérifier les deux boutons + self.check_buttons() + + # Petit délai pour éviter de surcharger le CPU + time.sleep(POLL_INTERVAL) + + except PermissionError: + print("\nErreur: Permission refusée.") + print("Ce script doit être exécuté avec les privilèges root:") + print(f" sudo python3 {sys.argv[0]}") + sys.exit(1) + except KeyboardInterrupt: + print("\nArrêt demandé par l'utilisateur...") + except Exception as ex: + print(f"\nErreur: {ex}") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + self.cleanup() + + def cleanup(self): + """Nettoie les ressources""" + print("\nNettoyage...") + if self.request: + self.request.release() + if self.ui: + self.ui.close() + print("Arrêté proprement.") + + def signal_handler(self, signum, frame): + """Gère les signaux pour un arrêt propre""" + self.running = False + +def main(): + mapper = VolumeButtonMapper() + + # Configurer les gestionnaires de signaux + signal.signal(signal.SIGINT, mapper.signal_handler) + signal.signal(signal.SIGTERM, mapper.signal_handler) + + mapper.run() + +if __name__ == "__main__": + main()