## 🔧 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>
283 lines
8.1 KiB
JavaScript
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;
|
|
}
|
|
}
|