// 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; } }