feat: allow other displays and devices #2

This commit is contained in:
leukipp
2025-01-07 16:06:35 +01:00
parent 286e8c1758
commit 8d2f3da93e
+153 -81
View File
@@ -1,13 +1,22 @@
const os = require("os"); const os = require("os");
const fs = require("fs"); const fs = require("fs");
const path = require("path");
const process = require("child_process"); const process = require("child_process");
global.HARDWARE = global.HARDWARE || { global.HARDWARE = global.HARDWARE || {
initialized: false, initialized: false,
status: "invalid", status: "invalid",
display: { display: {
status: null, name: null,
brightness: null, status: {
path: null,
value: null,
},
brightness: {
path: null,
value: null,
max: null,
},
notifiers: [], notifiers: [],
}, },
}; };
@@ -23,20 +32,29 @@ const init = async (args) => {
return false; return false;
} }
// Check supported hardware // Check device support
if (!hardwareExists()) { if (!compatibleDevice()) {
console.warn("Hardware not supported"); console.warn("Device not supported");
return false; return false;
} }
// Check supported model // Check model support
const model = getModel(); const model = getModel();
if (!model || !model.includes("Raspberry Pi 5")) { if (!model || !model.includes("Raspberry Pi")) {
console.warn(`Device ${model} not supported`); console.warn(`${model} not supported`);
return false; return false;
} }
console.log("\nModel:", getModel()); // Init globals
HARDWARE.display.name = getDisplayName();
HARDWARE.display.status.path = getDisplayStatusPath(HARDWARE.display.name);
HARDWARE.display.brightness.path = getDisplayBrightnessPath(HARDWARE.display.name);
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
HARDWARE.initialized = true;
HARDWARE.status = "valid";
// Show device infos
console.log("\nModel:", model);
console.log("Serial Number:", getSerialNumber()); console.log("Serial Number:", getSerialNumber());
console.log("Host Name:", getHostName()); console.log("Host Name:", getHostName());
console.log("Up Time:", getUpTime()); console.log("Up Time:", getUpTime());
@@ -44,12 +62,11 @@ const init = async (args) => {
console.log("Memory Usage:", getMemoryUsage()); console.log("Memory Usage:", getMemoryUsage());
console.log("Processor Usage:", getProcessorUsage()); console.log("Processor Usage:", getProcessorUsage());
console.log("Processor Temperature:", getProcessorTemperature()); console.log("Processor Temperature:", getProcessorTemperature());
console.log("Display Status:", getDisplayStatus());
console.log("Display Brightness:", getDisplayBrightness(), "\n");
// Init globals // Show display infos
HARDWARE.initialized = true; console.log("\nDisplay Name:", HARDWARE.display.name);
HARDWARE.status = "valid"; console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
// Check for display changes // Check for display changes
setInterval(update, 500); setInterval(update, 500);
@@ -64,40 +81,43 @@ const update = () => {
if (!HARDWARE.initialized) { if (!HARDWARE.initialized) {
return; return;
} }
const path = "/sys/class/backlight/10-0045";
// Display status has changed // Display status has changed
const status = fs.readFileSync(`${path}/bl_power`, "utf8").trim(); if (HARDWARE.display.status.path !== null) {
if (status !== HARDWARE.display.status) { const status = fs.readFileSync(`${HARDWARE.display.status.path}/enabled`, "utf8").trim();
console.log("Update Display Status:", getDisplayStatus()); if (status !== HARDWARE.display.status.value) {
HARDWARE.display.status = status; console.log("Update Display Status:", getDisplayStatus());
HARDWARE.display.notifiers.forEach((notifier) => { HARDWARE.display.status.value = status;
notifier(); HARDWARE.display.notifiers.forEach((notifier) => {
}); notifier();
});
}
} }
// Display brightness has changed // Display brightness has changed
const brightness = fs.readFileSync(`${path}/brightness`, "utf8").trim(); if (HARDWARE.display.brightness.path !== null) {
if (brightness !== HARDWARE.display.brightness) { const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
console.log("Update Display Brightness:", getDisplayBrightness()); if (brightness !== HARDWARE.display.brightness.value) {
HARDWARE.display.brightness = brightness; console.log("Update Display Brightness:", getDisplayBrightness());
HARDWARE.display.notifiers.forEach((notifier) => { HARDWARE.display.brightness.value = brightness;
notifier(); HARDWARE.display.notifiers.forEach((notifier) => {
}); notifier();
});
}
} }
}; };
/** /**
* Verifies hardware compatibility by checking the presence of necessary sys files. * Verifies device compatibility by checking the presence of necessary sys files.
* *
* @returns {bool} Returns true if all files exists. * @returns {bool} Returns true if all files exists.
*/ */
const hardwareExists = () => { const compatibleDevice = () => {
const files = [ const files = [
"/sys/class/drm",
"/sys/class/backlight",
"/sys/firmware/devicetree/base/model", "/sys/firmware/devicetree/base/model",
"/sys/firmware/devicetree/base/serial-number", "/sys/firmware/devicetree/base/serial-number",
"/sys/class/backlight/10-0045/brightness",
"/sys/class/backlight/10-0045/bl_power",
]; ];
return files.every((file) => fs.existsSync(file)); return files.every((file) => fs.existsSync(file));
}; };
@@ -117,9 +137,7 @@ const getModel = () => {
* @returns {string|null} The serial number of the Raspberry Pi or null if an error occurs. * @returns {string|null} The serial number of the Raspberry Pi or null if an error occurs.
*/ */
const getSerialNumber = () => { const getSerialNumber = () => {
return execSyncCommand("cat", [ return execSyncCommand("cat", ["/sys/firmware/devicetree/base/serial-number"]);
"/sys/firmware/devicetree/base/serial-number",
]);
}; };
/** /**
@@ -192,6 +210,41 @@ const getProcessorTemperature = () => {
return null; return null;
}; };
/**
* Gets the primary display name using `wlopm`.
*
* @returns {string|null} The output of the command or null if an error occurs.
*/
const getDisplayName = () => {
const status = execSyncCommand("wlopm", []);
if (status !== null) {
const out = status.split("\n")[0].split(" ");
return out.shift();
}
return null;
};
/**
* Gets the current display status path using `/sys/class/drm`.
*
* @param {string} name - Primary display name.
* @returns {string|null} The display status path or null if nothing was found.
*/
const getDisplayStatusPath = (name) => {
const drm = "/sys/class/drm";
for (const card of fs.readdirSync(drm)) {
const statusFile = path.join(drm, card, "status");
if (!fs.existsSync(statusFile)) {
continue;
}
const content = fs.readFileSync(statusFile, "utf8").trim();
if (card.includes(name) && content === "connected") {
return path.join(drm, card);
}
}
return null;
};
/** /**
* Gets the current display power status using `wlopm`. * Gets the current display power status using `wlopm`.
* *
@@ -200,18 +253,10 @@ const getProcessorTemperature = () => {
const getDisplayStatus = () => { const getDisplayStatus = () => {
const status = execSyncCommand("wlopm", []); const status = execSyncCommand("wlopm", []);
if (status !== null) { if (status !== null) {
return status.slice(status.lastIndexOf(" ") + 1).toUpperCase(); const out = status.split("\n")[0].split(" ");
return out.pop().toUpperCase();
} }
return null; return null;
/* This also works, but it's better to use `wlopm` for getter and setter methods.
const status = execSyncCommand("cat", [
"/sys/class/backlight/10-0045/bl_power",
]);
if (status !== null) {
return status === "0" ? "ON" : "OFF";
}
*/
}; };
/** /**
@@ -229,60 +274,85 @@ const setDisplayStatus = (status, callback = null) => {
if (typeof callback === "function") callback(null, "Invalid status"); if (typeof callback === "function") callback(null, "Invalid status");
return; return;
} }
const args = status === "ON" ? ["--on", "*"] : ["--off", "*"]; const args = [`--${status.toLowerCase()}`, HARDWARE.display.name];
execAsyncCommand("wlopm", args, callback); execAsyncCommand("wlopm", args, callback);
/* This also works, but interferes with the built-in screen blanking function.
const value = status === "ON" ? "0" : "4";
const proc = execAsyncCommand(
"tee",
["/sys/class/backlight/10-0045/bl_power"],
callback
);
proc.stdin.write(value.toString());
proc.stdin.end();
*/
}; };
/** /**
* Gets the current display brightness level using `/sys/class/backlight/10-0045/brightness`. * Gets the current display brightness path using `/sys/class/backlight`.
* *
* This function retrieves the brightness level, which is in the range of 0-31, * @param {string} name - Primary display name.
* and maps it to a percentage in the range of 0-100 percent. * @returns {string|null} The display brightness path or null if nothing was found.
*
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
*/ */
const getDisplayBrightness = () => { const getDisplayBrightnessPath = (name) => {
const brightness = execSyncCommand("cat", [ const backlight = "/sys/class/backlight";
"/sys/class/backlight/10-0045/brightness", for (const address of fs.readdirSync(backlight)) {
]); const nameFile = path.join(backlight, address, "display_name");
if (brightness !== null) { if (!fs.existsSync(nameFile)) {
return Math.round((parseInt(brightness, 10) / 31) * 100); continue;
}
const content = fs.readFileSync(nameFile, "utf8").trim();
if (content.includes(name)) {
return path.join(backlight, address);
}
} }
return null; return null;
}; };
/** /**
* Sets the display brightness level using `/sys/class/backlight/10-0045/brightness`. * Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
* *
* This function takes a brightness value in the range of 1-100 percent, * @returns {number|null} The brightness maximum value or null if an error occurs.
* maps it to the range of 0-31, and writes it to the system. */
const getDisplayBrightnessMax = () => {
if (!HARDWARE.display.brightness.path) {
return null;
}
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
if (max !== null) {
return parseInt(max, 10);
}
return null;
};
/**
* Gets the current display brightness level using `/sys/class/backlight/.../brightness`.
*
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
*/
const getDisplayBrightness = () => {
if (!HARDWARE.display.brightness.path) {
return null;
}
const brightness = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/brightness`]);
if (brightness !== null) {
const max = HARDWARE.display.brightness.max;
return Math.round((parseInt(brightness, 10) / max) * 100);
}
return null;
};
/**
* Sets the display brightness level using `/sys/class/backlight/.../brightness`.
*
* This function takes a brightness value between 1 to 100 percent,
* maps it to the proper range and writes it to the system.
* *
* @param {number} brightness - The desired brightness level (1-100). * @param {number} brightness - The desired brightness level (1-100).
* @param {function} callback - A callback function that receives the output or error. * @param {function} callback - A callback function that receives the output or error.
*/ */
const setDisplayBrightness = (brightness, callback = null) => { const setDisplayBrightness = (brightness, callback = null) => {
if (!HARDWARE.display.brightness.path) {
return;
}
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) { if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
console.error("Brightness must be a number between 1 and 100"); console.error("Brightness must be a number between 1 and 100");
if (typeof callback === "function") callback(null, "Invalid brightness"); if (typeof callback === "function") callback(null, "Invalid brightness");
return; return;
} }
const value = Math.max(1, Math.min(Math.round((brightness / 100) * 31), 31)); const max = HARDWARE.display.brightness.max;
const proc = execAsyncCommand( const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
"tee", const proc = execAsyncCommand("tee", [`${HARDWARE.display.brightness.path}/brightness`], callback);
["/sys/class/backlight/10-0045/brightness"],
callback
);
proc.stdin.write(value.toString()); proc.stdin.write(value.toString());
proc.stdin.end(); proc.stdin.end();
}; };
@@ -320,9 +390,7 @@ const rebootSystem = (callback = null) => {
*/ */
const execSyncCommand = (cmd, args = []) => { const execSyncCommand = (cmd, args = []) => {
try { try {
const output = process.execSync([cmd, ...args].join(" "), { const output = process.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
encoding: "utf8",
});
return output.trim().replace(/\0/g, ""); return output.trim().replace(/\0/g, "");
} catch (error) { } catch (error) {
console.error("Execute Sync:", error.message); console.error("Execute Sync:", error.message);
@@ -380,8 +448,12 @@ module.exports = {
getMemoryUsage, getMemoryUsage,
getProcessorUsage, getProcessorUsage,
getProcessorTemperature, getProcessorTemperature,
getDisplayName,
getDisplayStatusPath,
getDisplayStatus, getDisplayStatus,
setDisplayStatus, setDisplayStatus,
getDisplayBrightnessPath,
getDisplayBrightnessMax,
getDisplayBrightness, getDisplayBrightness,
setDisplayBrightness, setDisplayBrightness,
shutdownSystem, shutdownSystem,