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
227 lines
8.5 KiB
Python
Executable File
227 lines
8.5 KiB
Python
Executable File
#!/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()
|