Files
gnome-asus-kbd-rgb/extension/backend.js
Gilles Soulier f94ba663f8 feat: Forçage état RGB au boot + correction couleurs GNOME + presets ronds
## 🔧 Correctifs critiques

### Forçage de l'état RGB au démarrage (résout clavier éteint au boot)
- **Problème résolu**: Clavier parfois éteint au redémarrage, impossible à rallumer
- Suppression vérification `if (brightness == 0)` dans writeRGB()
- _applyCurrentState() force TOUJOURS brightness + RGB au boot
- Logs explicites pour diagnostic
- Fichiers: backend.js, ui.js
- Documentation: docs/ANALYSE_PERSISTANCE.md

### Correction couleurs GNOME officielles
- 7 des 9 presets utilisaient de mauvaises valeurs RGB
- Correction basée sur les valeurs hex officielles GNOME:
  * Turquoise #2190a4: (33,144,164) 
  * Vert #3a944a: (58,148,74) 
  * Jaune #c88800: (200,136,0) 
  * Orange #ed5b00: (237,91,0) 
  * Rouge #e62d42: (230,45,66) 
  * Rose #d56199: (213,97,153) 
  * Ardoise #6f8396: (111,131,150) 
- Fichiers: schemas/gschema.xml, ui.js (_rgbToGnomeAccent)

##  Améliorations UI

### Presets en cercles avec surbrillance
- Presets affichés en cercles parfaits (border-radius: 50%)
- Cercle blanc épais (3px) + box-shadow sur preset actif
- Fonction _updatePresetSelection() avec tolérance RGB ±10
- Mise à jour automatique à chaque changement de couleur

### Synchronisation thème universelle
- Correction: sync thème GNOME fonctionne maintenant depuis:
  *  Roue chromatique
  *  Sliders RGB
  *  Presets (corrigé!)
  *  Slider Master
- Refactorisation _onPresetClicked() pour utiliser _onRGBChanged()

## 📚 Documentation et outils

- docs/ANALYSE_PERSISTANCE.md: Analyse technique complète du problème de persistance
- docs/RESULTAT_TEST_PERSISTANCE.md: Résultats des tests de validation
- tools/test-persistance.sh: Script de test automatisé pour diagnostic

## 🧪 Tests effectués

 Initialisation au démarrage GNOME Shell
 Forçage RGB même avec brightness=0
 Couleurs GNOME corrigées dans les logs
 Presets ronds avec surbrillance fonctionnelle
 Synchronisation thème depuis tous les modes

Test au redémarrage PC: À valider par l'utilisateur

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 06:05:00 +01:00

283 lines
8.1 KiB
JavaScript

// backend.js - Interface sysfs et logique métier pour le rétroéclairage ASUS RGB
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
// Chemins sysfs pour le rétroéclairage ASUS
const SYSFS_BASE = '/sys/class/leds/asus::kbd_backlight';
const BRIGHTNESS_PATH = `${SYSFS_BASE}/brightness`;
const MAX_BRIGHTNESS_PATH = `${SYSFS_BASE}/max_brightness`;
const RGB_MODE_PATH = `${SYSFS_BASE}/kbd_rgb_mode`;
// Niveaux d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
const BRIGHTNESS_LEVELS = [0, null, null, null]; // Sera rempli dynamiquement avec max_brightness
// Timer pour le debouncing
let debounceTimer = null;
const DEBOUNCE_DELAY = 75; // ms
/**
* Vérifie si le matériel ASUS RGB est présent sur le système
* @returns {boolean} true si le matériel est supporté
*/
export function checkHardwareSupport() {
try {
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
const rgbFile = Gio.File.new_for_path(RGB_MODE_PATH);
return brightnessFile.query_exists(null) && rgbFile.query_exists(null);
} catch (e) {
console.error('Erreur lors de la vérification du matériel:', e);
return false;
}
}
/**
* Vérifie si l'utilisateur a les permissions d'écriture
* @returns {boolean} true si les permissions sont OK
*/
export function checkPermissions() {
try {
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
const info = brightnessFile.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
// Vérifier si on peut écrire
return info.get_attribute_boolean('access::can-write');
} catch (e) {
console.error('Erreur lors de la vérification des permissions:', e);
return false;
}
}
/**
* Lit la valeur maximale de brightness
* @returns {number} Valeur max, ou 3 par défaut
*/
export function getMaxBrightness() {
try {
const file = Gio.File.new_for_path(MAX_BRIGHTNESS_PATH);
const [success, contents] = file.load_contents(null);
if (success) {
const maxValue = parseInt(new TextDecoder().decode(contents).trim());
// Initialiser les niveaux de brightness
BRIGHTNESS_LEVELS[1] = Math.floor(maxValue / 3);
BRIGHTNESS_LEVELS[2] = Math.floor(2 * maxValue / 3);
BRIGHTNESS_LEVELS[3] = maxValue;
return maxValue;
}
} catch (e) {
console.error('Erreur lors de la lecture de max_brightness:', e);
}
// Valeurs par défaut si échec
BRIGHTNESS_LEVELS[1] = 1;
BRIGHTNESS_LEVELS[2] = 2;
BRIGHTNESS_LEVELS[3] = 3;
return 3;
}
/**
* Lit la brightness actuelle
* @returns {number} Valeur actuelle, ou -1 en cas d'erreur
*/
export function readBrightness() {
try {
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
const [success, contents] = file.load_contents(null);
if (success) {
return parseInt(new TextDecoder().decode(contents).trim());
}
} catch (e) {
console.error('Erreur lors de la lecture de brightness:', e);
}
return -1;
}
/**
* Écrit la brightness (niveau 0-3)
* @param {number} level - Niveau d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
* @returns {boolean} true si succès
*/
export function writeBrightness(level) {
try {
// Valider et clamper le niveau
level = Math.max(0, Math.min(3, Math.floor(level)));
const value = BRIGHTNESS_LEVELS[level];
if (value === null || value === undefined) {
console.error('Niveau de brightness invalide:', level);
return false;
}
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
const contents = `${value}\n`;
const [success] = file.replace_contents(
new TextEncoder().encode(contents),
null,
false,
Gio.FileCreateFlags.NONE,
null
);
if (success) {
console.log(`Brightness mise à ${level} (${value})`);
}
return success;
} catch (e) {
console.error('Erreur lors de l\'écriture de brightness:', e);
return false;
}
}
/**
* Clamp une valeur RGB entre 0 et 255
* @param {number} value - Valeur à clamper
* @returns {number} Valeur clampée
*/
function clampRGB(value) {
return Math.max(0, Math.min(255, Math.floor(value)));
}
/**
* Applique le master gain aux valeurs RGB
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @param {number} masterGain - Gain master (0-100)
* @returns {Object} {r, g, b} avec gain appliqué
*/
function applyMasterGain(r, g, b, masterGain) {
const gain = masterGain / 100.0;
return {
r: clampRGB(r * gain),
g: clampRGB(g * gain),
b: clampRGB(b * gain)
};
}
/**
* Écrit les valeurs RGB dans kbd_rgb_mode
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @param {number} masterGain - Gain master (0-100), défaut 100
* @returns {boolean} true si succès
*/
export function writeRGB(r, g, b, masterGain = 100) {
try {
// Clamper les valeurs
r = clampRGB(r);
g = clampRGB(g);
b = clampRGB(b);
// Appliquer le master gain
const adjusted = applyMasterGain(r, g, b, masterGain);
// TOUJOURS écrire RGB, même si brightness = 0
// Cela garantit que le contrôleur RGB est dans un état connu
// Particulièrement important au démarrage pour contrer les réinitialisations firmware
// Format: "1 0 R G B 0\n"
const file = Gio.File.new_for_path(RGB_MODE_PATH);
const contents = `1 0 ${adjusted.r} ${adjusted.g} ${adjusted.b} 0\n`;
const [success] = file.replace_contents(
new TextEncoder().encode(contents),
null,
false,
Gio.FileCreateFlags.NONE,
null
);
if (success) {
console.log(`RGB mis à (${adjusted.r}, ${adjusted.g}, ${adjusted.b}) [master: ${masterGain}%]`);
}
return success;
} catch (e) {
console.error('Erreur lors de l\'écriture RGB:', e);
return false;
}
}
/**
* Applique RGB avec debouncing pour éviter de spammer sysfs
* @param {number} r - Rouge
* @param {number} g - Vert
* @param {number} b - Bleu
* @param {number} masterGain - Gain master
* @param {Function} callback - Callback optionnel appelé après l'écriture
*/
export function writeRGBDebounced(r, g, b, masterGain, callback = null) {
// Annuler le timer précédent
if (debounceTimer !== null) {
GLib.source_remove(debounceTimer);
debounceTimer = null;
}
// Créer un nouveau timer
debounceTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DEBOUNCE_DELAY, () => {
writeRGB(r, g, b, masterGain);
if (callback) {
callback();
}
debounceTimer = null;
return GLib.SOURCE_REMOVE;
});
}
/**
* Parse une chaîne preset "R,G,B" en objet
* @param {string} presetString - Format "R,G,B"
* @returns {Object} {r, g, b} ou null si invalide
*/
export function parsePreset(presetString) {
try {
const parts = presetString.split(',').map(s => parseInt(s.trim()));
if (parts.length === 3 && parts.every(n => !isNaN(n))) {
return {
r: clampRGB(parts[0]),
g: clampRGB(parts[1]),
b: clampRGB(parts[2])
};
}
} catch (e) {
console.error('Erreur lors du parsing du preset:', e);
}
return null;
}
/**
* Convertit RGB en hex
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @returns {string} Format "#RRGGBB"
*/
export function rgbToHex(r, g, b) {
const toHex = (n) => {
const hex = clampRGB(n).toString(16).toUpperCase();
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Nettoie les ressources (annule les timers en cours)
*/
export function cleanup() {
if (debounceTimer !== null) {
GLib.source_remove(debounceTimer);
debounceTimer = null;
}
}