15 Commits
Author SHA1 Message Date
leukipp aed79ab519 build: update version 2025-02-23 22:32:18 +01:00
leukipp e2397bad65 ref: update status values 2025-02-23 22:31:31 +01:00
leukipp e7622bef17 ref: use null status 2025-02-23 22:18:25 +01:00
leukipp 2553f52983 build: downgrade dependencies 2025-02-23 22:15:36 +01:00
leukipp 30b5ec2d8b docs: add hardware 2025-02-23 21:13:50 +01:00
leukipp 94c7e34706 docs: update readme 2025-02-23 20:54:02 +01:00
leukipp 09dda65333 ref: start touchkio 2025-02-23 20:29:23 +01:00
leukipp 99f465a044 build: update dependencies 2025-02-23 20:18:26 +01:00
leukipp 02d1309345 Merge branch 'main' into develop 2025-02-23 16:48:19 +01:00
Sebastian 3142606d12 Update README.md (#20)
Adding info on how to control the automatically created service
2025-02-23 16:46:30 +01:00
leukipp 51189e8595 fix: reduce file system read cycle #19 2025-02-23 16:45:40 +01:00
leukipp 71a9d4c1de ref: code cleanup 2025-02-23 16:38:23 +01:00
leukipp f022db9478 ref: use async update 2025-02-23 15:15:06 +01:00
leukipp fcc0e5813e feat: check for DISPLAY variable #12 2025-02-23 13:36:46 +01:00
leukipp 6d23efd713 ref: add debug arguments 2025-02-23 13:01:14 +01:00
8 changed files with 113 additions and 58 deletions
+16 -11
View File
@@ -1,20 +1,20 @@
# Supported Hardware
This document contains an incomplete list of device and display hardware combinations that are either intended to be tested or have already been tested.
This document contains an incomplete list of device and display hardware combinations that have already been tested.
The [release](https://github.com/leukipp/touchkio/releases) page has builds exclusively for **arm64** and **x64**, but custom builds for other architectures can be made (see [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development)), allowing the application to operate on any hardware.
At least the **minimal** features, such as displaying a **kiosk window** (which doesn't necessarily need to be Home Assistant) will work.
## Hardware
If you are running Linux with a graphical user interface (Wayland or X11), you should be well equipped to use the application. Additionally, any single board computer (SBC) clones of the Raspberry Pi that operate on Raspberry Pi OS **(64-bit)** are likely to function properly as well.
If you are running Linux with a graphical user interface (Wayland or X11), you should be well equipped to use the application. Additionally, any single board computer (SBC) clones of the Raspberry Pi that operate on Raspberry Pi OS **(64-bit)** are likely to function as well.
| | Status | Notes |
| --- | --------------------- | ------------------------------------------------------------------------- |
| ⬜ | Unknown | Who knows. |
| 🟩 | Fully operational | Working display power, brightness and keyboard control via MQTT. |
| 🟨 | Partially operational | Display brightness control is not available via MQTT. |
| 🟧 | Partially operational | Display brightness and keyboard control is not available via MQTT. |
| 🟥 | Partially operational | Display power, brightness and keyboard control is not available via MQTT. |
| ⬛ | Crashed | The house is on fire. |
| ⬜ | Somehow operational | Issues can occur and the overall performance is very slow. |
| ⬛ | Not operational | The house is on fire. |
### DSI
| Device | System | Display | Status |
@@ -27,13 +27,18 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
### HDMI
| Device | System | Display | Status |
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------------------- | ------ |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
| Generic PC (x64) | Ubuntu (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu (64-bit), Wayland | Generic Non-Touch | 🟥 |
| Device | System | Display | Status |
| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------ |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
| Generic PC (x64) | Ubuntu (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu (64-bit), Wayland | Generic Non-Touch | 🟥 |
## Contributions
In case your hardware is not listed above don't worry, give it a try.
+12 -4
View File
@@ -48,11 +48,19 @@ If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HA
#### Option 1 - The easy way
Run this command to download and install the latest **.deb** (arm64 or x64) release.
It will also create a systemd file for auto-startup and will guide you through the setup process:
It will also create a systemd user file for auto-startup and will guide you through the setup process:
```bash
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh)
```
Make sure that you run this with your **standard user** and not with root (sudo).
If you are paranoid, or smart, or both, have a look into the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script before executing external code on your machine.
The script can also be re-run to **update** an existing installation to the latest version.
The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot.
If you need manual control use:
```bash
systemctl --user start|stop|status|restart touchkio.service
```
<details><summary>Alternatives</summary><div>
@@ -72,7 +80,7 @@ The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) scrip
Pre-built release files are available for arm64 and x64 Linux systems.
If you are using a different architecture, you can still utilize this repository to build your own application.
For more information, please refer to the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section, however this will do the job:
For more information please refer to the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section, however this will do the job:
```bash
yarn build
```
@@ -128,7 +136,7 @@ If you connect to your device via SSH, you may have to export display variables
export DISPLAY=":0"
export WAYLAND_DISPLAY="wayland-0"
```
To make this permanent, consider adding the export variables to the `~/.bashrc` file.
To make this permanent, consider adding the export variables into the `~/.bashrc` file.
### Extensions
@@ -181,7 +189,7 @@ I managed to achieve this for the `brightness` file by implementing a simple `fs
However, I found that it **never triggered** for the `bl_power` file.
Although the file content changes, none of the filesystem listeners where fired.
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
As a result, I went for a **polling solution**, checking the state of the file every **500 milliseconds** for any changes.
As a result, I went for a **polling solution**, checking the state of the file **every second** for any changes.
While I understand this is not ideal, it's necessary to ensure proper functionality.
The display power status and brightness can be adjusted via the MQTT integration.
+16 -3
View File
@@ -7,6 +7,16 @@ const hardware = require("./src/hardware");
const webview = require("./src/webview");
const { app } = require("electron");
global.ARGS = global.ARGS || {};
if (!process.env.DISPLAY) {
console.error(`$DISPLAY variable not set to run the GUI application, are you connected via SSH?`);
console.error(`If you have installed the service use:`);
console.error(` systemctl --user start touchkio.service`);
console.error(`Alternatively export the variables first:`);
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
}
/**
* This promise resolves when the app has finished initializing,
* allowing to safely create browser windows and perform other
@@ -34,14 +44,15 @@ app.whenReady().then(async () => {
} else if (!argsProvided && argsFileExists) {
args = readArgs(argsFilePath);
}
global.ARGS = args;
// Show used arguments
console.log(`Arguments: ${JSON.stringify(args, null, 2)}`);
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`);
// Chained init functions
const chained = [webview.init, hardware.init, integration.init];
for (const init of chained) {
if (!(await init(args))) {
if (!(await init())) {
break;
}
}
@@ -227,4 +238,6 @@ const decrypt = (value) => {
* @param {number} ms - Sleep time in milliseconds.
* @returns {Promise} A promise resolving after the timeout.
*/
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const sleep = (ms) => {
return new Promise((r) => setTimeout(r, ms));
};
+3
View File
@@ -102,4 +102,7 @@ read -p "Start touchkio setup? (Y/n) " answer
if [[ ${answer:-y} == [Yy]* ]]; then
echo "/usr/bin/touchkio --setup"
/usr/bin/touchkio --setup
else
echo "/usr/bin/touchkio"
/usr/bin/touchkio
fi
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Home Assistant Touch Kiosk",
"homepage": "https://github.com/leukipp/touchkio",
"author": "leukipp",
"version": "1.1.1",
"version": "1.1.2",
"license": "MIT",
"main": "index.js",
"scripts": {
+35 -15
View File
@@ -1,11 +1,12 @@
const os = require("os");
const fs = require("fs");
const path = require("path");
const process = require("child_process");
const fsp = require("fs/promises");
const cpr = require("child_process");
global.HARDWARE = global.HARDWARE || {
initialized: false,
status: "invalid",
status: null,
session: {},
support: {},
display: {
@@ -29,10 +30,9 @@ global.HARDWARE = global.HARDWARE || {
/**
* Initializes the hardware with the provided arguments.
*
* @param {Object} args - The command-line arguments to customize the initialization process.
* @returns {boolean} Returns true if the initialization was successful.
*/
const init = async (args) => {
const init = async () => {
if (!compatibleDevice()) {
console.warn("Device not supported");
return false;
@@ -48,7 +48,7 @@ const init = async (args) => {
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
HARDWARE.initialized = true;
HARDWARE.status = "valid";
HARDWARE.status = "Supported";
// Show supported features
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
@@ -91,7 +91,7 @@ const init = async (args) => {
});
// Check for display changes
setInterval(update, 500);
interval(update, 1000);
return true;
};
@@ -99,14 +99,14 @@ const init = async (args) => {
/**
* Updates the shared hardware properties.
*/
const update = () => {
const update = async () => {
if (!HARDWARE.initialized) {
return;
}
// Display status has changed
if (HARDWARE.support.displayStatus) {
const status = fs.readFileSync(`${HARDWARE.display.status.path}/dpms`, "utf8").trim();
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
if (status !== HARDWARE.display.status.value) {
console.log("Update Display Status:", getDisplayStatus());
HARDWARE.display.status.value = status;
@@ -118,7 +118,7 @@ const update = () => {
// Display brightness has changed
if (HARDWARE.support.displayBrightness) {
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
if (brightness !== HARDWARE.display.brightness.value) {
console.log("Update Display Brightness:", getDisplayBrightness());
HARDWARE.display.brightness.value = brightness;
@@ -533,7 +533,7 @@ const rebootSystem = (callback = null) => {
*/
const processRuns = (name) => {
try {
return !!process.execSync(`pidof ${name}`, { encoding: "utf8" });
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
} catch {}
return false;
};
@@ -546,7 +546,7 @@ const processRuns = (name) => {
*/
const commandExists = (name) => {
try {
return !!process.execSync(`which ${name}`, { encoding: "utf8" });
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
} catch {}
return false;
};
@@ -560,7 +560,7 @@ const commandExists = (name) => {
*/
const execSyncCommand = (cmd, args = []) => {
try {
const output = process.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
return output.trim().replace(/\0/g, "");
} catch (error) {
console.error("Execute Sync:", error.message);
@@ -579,7 +579,7 @@ const execSyncCommand = (cmd, args = []) => {
const execAsyncCommand = (cmd, args = [], callback = null) => {
let errorOutput = "";
let successOutput = "";
let proc = process.spawn(cmd, args);
let proc = cpr.spawn(cmd, args);
proc.stderr.on("data", (data) => {
if (data) {
errorOutput += data.toString();
@@ -623,7 +623,7 @@ const dbusCall = (path, method, values, callback = null) => {
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
try {
const output = process.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
if (typeof callback === "function") {
callback(output.trim().replace(/\0/g, ""), null);
}
@@ -645,7 +645,7 @@ const dbusCall = (path, method, values, callback = null) => {
const dbusMonitor = (path, callback) => {
const cmd = "dbus-monitor";
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
const proc = process.spawn(cmd, args);
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
try {
const signal = data.toString();
@@ -681,6 +681,26 @@ const dbusMonitor = (path, callback) => {
return proc;
};
/**
* Helper function for asynchronous interval calls.
*
* @param {function} callback - An async callback function.
* @param {number} ms - Sleep time in milliseconds.
*/
const interval = (callback, ms) => {
const run = () => {
setTimeout(async () => {
try {
await callback();
} catch (error) {
console.error("Interval Callback:", error.message);
}
run();
}, ms);
};
run();
};
module.exports = {
init,
update,
+13 -12
View File
@@ -4,27 +4,26 @@ const { app } = require("electron");
global.INTEGRATION = global.INTEGRATION || {
initialized: false,
status: "offline",
status: null,
};
/**
* Initializes the integration with the provided arguments.
*
* @param {Object} args - The command-line arguments to customize the initialization process.
* @returns {boolean} Returns true if the initialization was successful.
*/
const init = async (args) => {
if (!args.mqtt_url) {
const init = async () => {
if (!ARGS.mqtt_url) {
return false;
}
if (!/^mqtts?:\/\//.test(args.mqtt_url)) {
if (!/^mqtts?:\/\//.test(ARGS.mqtt_url)) {
console.error("Please provide the '--mqtt-url' parameter with mqtt(s)");
return app.quit();
}
const url = new URL(args.mqtt_url);
const user = args.mqtt_user ? args.mqtt_user : null;
const password = args.mqtt_password ? args.mqtt_password : null;
const discovery = args.mqtt_discovery ? args.mqtt_discovery : "homeassistant";
const url = new URL(ARGS.mqtt_url);
const user = ARGS.mqtt_user ? ARGS.mqtt_user : null;
const password = ARGS.mqtt_password ? ARGS.mqtt_password : null;
const discovery = ARGS.mqtt_discovery ? ARGS.mqtt_discovery : "homeassistant";
const model = hardware.getModel();
const vendor = hardware.getVendor();
@@ -81,11 +80,11 @@ const init = async (args) => {
// Integration initialized
INTEGRATION.initialized = true;
INTEGRATION.status = "online";
INTEGRATION.status = "Online";
})
.on("error", (error) => {
console.error("MQTT:", error);
INTEGRATION.status = "offline";
INTEGRATION.status = "Offline";
});
// Update sensor states from notifiers
@@ -118,7 +117,7 @@ const init = async (args) => {
/**
* Updates the shared integration properties.
*/
const update = () => {
const update = async () => {
if (!INTEGRATION.initialized) {
return;
}
@@ -308,10 +307,12 @@ const initDisplay = (client) => {
const status = message.toString();
console.log("Set Display Status:", status);
hardware.setDisplayStatus(status);
hardware.update();
} else if (topic === `${root}/brightness/set`) {
const brightness = parseInt(message, 10);
console.log("Set Display Brightness:", brightness);
hardware.setDisplayBrightness(brightness);
hardware.update();
}
})
.subscribe(`${root}/set`)
+17 -12
View File
@@ -5,7 +5,7 @@ const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = requi
global.WEBVIEW = global.WEBVIEW || {
initialized: false,
status: "offline",
status: null,
locked: false,
pointer: {
position: {},
@@ -16,22 +16,22 @@ global.WEBVIEW = global.WEBVIEW || {
/**
* Initializes the webview with the provided arguments.
*
* @param {Object} args - The command-line arguments to customize the initialization process.
* @returns {boolean} Returns true if the initialization was successful.
*/
const init = async (args) => {
if (!args.web_url) {
const init = async () => {
if (!ARGS.web_url) {
console.error("Please provide the '--web-url' parameter");
return app.quit();
}
if (!/^https?:\/\//.test(args.web_url)) {
if (!/^https?:\/\//.test(ARGS.web_url)) {
console.error("Please provide the '--web-url' parameter with http(s)");
return app.quit();
}
const url = new URL(args.web_url);
const theme = ["light", "dark"].includes(args.web_theme) ? args.web_theme : "dark";
const zoom = !isNaN(parseFloat(args.web_zoom)) ? parseFloat(args.web_zoom) : 1.25;
const widget = args.web_widget ? args.web_widget === "true" : true;
const url = new URL(ARGS.web_url);
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25;
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
const fullscreen = ARGS.app_debug !== "true";
// Init global layout
WEBVIEW.viewZoom = zoom;
@@ -46,7 +46,10 @@ const init = async (args) => {
icon: path.join(__dirname, "..", "img", "icon.png"),
});
WEBVIEW.window.setMenuBarVisibility(false);
WEBVIEW.window.setFullScreen(true);
WEBVIEW.window.setFullScreen(fullscreen);
if (!fullscreen) {
WEBVIEW.window.maximize();
}
// Init global webview
WEBVIEW.view = new WebContentsView({
@@ -83,7 +86,7 @@ const init = async (args) => {
/**
* Updates the shared webview properties.
*/
const update = () => {
const update = async () => {
if (!WEBVIEW.initialized) {
return;
}
@@ -288,7 +291,9 @@ const domEvents = () => {
// Webview fully loaded
WEBVIEW.view.webContents.on("did-finish-load", () => {
// view.webContents.openDevTools();
if (ARGS.app_debug === "true") {
WEBVIEW.view.webContents.openDevTools();
}
WEBVIEW.initialized = true;
update();
});