Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099fe5a714 | ||
|
|
b63f2aee34 | ||
|
|
9d88a9ba38 | ||
|
|
bae0ef26fe | ||
|
|
f0438d7c88 | ||
|
|
f46649780c | ||
|
|
73edbaedb7 | ||
|
|
3b150fc540 | ||
|
|
7c96fbf427 | ||
|
|
fc31721ae0 | ||
|
|
fb841cce14 | ||
|
|
d5caffe9fd | ||
|
|
2442d4a681 | ||
|
|
ab0c1284fb | ||
|
|
55e528d95b | ||
|
|
ffcc9517f2 | ||
|
|
3ea4921861 | ||
|
|
1e91b7aa27 | ||
|
|
eb4b101f45 | ||
|
|
290e524901 | ||
|
|
4b010925ed | ||
|
|
910001f0e1 | ||
|
|
61628b1864 | ||
|
|
1a79ffd855 | ||
|
|
5423239e62 | ||
|
|
44a4fa9d2d | ||
|
|
26494e6844 | ||
|
|
4f8a9c307c | ||
|
|
a3860ebd69 | ||
|
|
b3a787d03d | ||
|
|
41e4c2d434 | ||
|
|
77cb962e6f | ||
|
|
c85055a222 | ||
|
|
81b4c30aa2 | ||
|
|
e6d8c537f2 | ||
|
|
0c51c1c1ff | ||
|
|
45ea309f3b | ||
|
|
e195a56739 | ||
|
|
d86a5385a8 | ||
|
|
3ce1265c8a | ||
|
|
c2bf551834 | ||
|
|
785a97721a | ||
|
|
05c9d98b61 | ||
|
|
9c26d9d8bb | ||
|
|
ad7d3fd3ff | ||
|
|
f666ed57ab | ||
|
|
c0b1a6e9ce | ||
|
|
19e51e349c | ||
|
|
5d012bd637 | ||
|
|
01c7411ddc | ||
|
|
3e4d96536f | ||
|
|
4bcb2c4e89 | ||
|
|
b2b87888e4 | ||
|
|
164ef2838c | ||
|
|
ae2633800d | ||
|
|
552f0e19a4 | ||
|
|
890c84d69d | ||
|
|
ac69dee387 | ||
|
|
ac62dc17ce | ||
|
|
f2f80e7d52 | ||
|
|
10e7ff078a | ||
|
|
e677fff160 | ||
|
|
63db26611e | ||
|
|
5ec6577b5a | ||
|
|
0359742e70 | ||
|
|
b70f4074e5 | ||
|
|
a4c300407b | ||
|
|
758f64ced4 | ||
|
|
7ab803d4ca | ||
|
|
092697850d | ||
|
|
422e92dd89 | ||
|
|
a15c0fc91d | ||
|
|
358fa3ca9c | ||
|
|
10722fa217 | ||
|
|
7267fafb97 | ||
|
|
b5e95936dc | ||
|
|
047b38a41b | ||
|
|
a93fbb2ad3 | ||
|
|
0b927a3ef6 | ||
|
|
d35cf27de9 | ||
|
|
19348b4b60 | ||
|
|
ce1eed391d | ||
|
|
fe6fe92656 | ||
|
|
3aea938cfb | ||
|
|
c39428fa42 | ||
|
|
7caa5bfc8a | ||
|
|
5f69961a9f | ||
|
|
f44d998016 | ||
|
|
3ea65a4af8 | ||
|
|
2ad49c3167 | ||
|
|
ef2093a25e | ||
|
|
2737eabd8a | ||
|
|
b8f809c50e | ||
|
|
c3e625a8f6 | ||
|
|
7c15707e3b | ||
|
|
dbd3756793 | ||
|
|
85fe5a910a | ||
|
|
d78088e6af | ||
|
|
bf77d9b2a0 | ||
|
|
f4ca7bc412 | ||
|
|
d0ea377ead | ||
|
|
9a829f72e3 | ||
|
|
482a56c9b9 | ||
|
|
32409983b2 | ||
|
|
fe0667919e | ||
|
|
0037a892cd | ||
|
|
ad22dd3215 | ||
|
|
619179c3c8 | ||
|
|
7e5e669b09 | ||
|
|
437c4ab1c5 | ||
|
|
ea01876ea7 | ||
|
|
9de170ce6c | ||
|
|
6b3e89d522 | ||
|
|
cbfd0538e7 | ||
|
|
d3af6d397e | ||
|
|
dd81da204e | ||
|
|
00aef6402e | ||
|
|
a36c624601 | ||
|
|
adbc214f4d | ||
|
|
e64f092f6c | ||
|
|
cab2052477 | ||
|
|
0d66d104fb | ||
|
|
1afe335379 | ||
|
|
568be07043 | ||
|
|
9d70b09358 | ||
|
|
6570d5c00e | ||
|
|
af7d0c58ce |
@@ -0,0 +1,116 @@
|
|||||||
|
name: Report a bug in the application
|
||||||
|
description: I encountered a bug that was not reported before.
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for reporting unknown bugs only!
|
||||||
|
- [x] I promise to have read the [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section in the hardware documentation first! 🤞
|
||||||
|
- [x] I promise to have read the [ISSUES](https://github.com/leukipp/touchkio?tab=readme-ov-file#issues) section and know how to debug the application! 🤞
|
||||||
|
- [x] I also swear to have searched for similar open and closed issues on [GITHUB](https://github.com/leukipp/touchkio/issues?q=is%3Aissue) first! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: problem_description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Problem description
|
||||||
|
placeholder: problem description
|
||||||
|
description: |
|
||||||
|
Describe the issue you are experiencing and how to reproduce it.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Diagnostics
|
||||||
|
- type: textarea
|
||||||
|
id: arguments_json
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Used arguments
|
||||||
|
placeholder: arguments json
|
||||||
|
description: |
|
||||||
|
You can use `cat ~/.config/touchkio/Arguments.json` to get the file content.
|
||||||
|
render: json
|
||||||
|
- type: textarea
|
||||||
|
id: log_file
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Log file
|
||||||
|
placeholder: drag-and-drop the file or copy-and-paste the full content here
|
||||||
|
description: |
|
||||||
|
From the host machine use `scp USER@IP:~/.config/touchkio/logs/main.log main.log` to download the log file.
|
||||||
|
Alternatively, you can use `cat ~/.config/touchkio/logs/main.log` on a ssh connection to get the file content.
|
||||||
|
- type: textarea
|
||||||
|
id: additional_information
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
placeholder: additional information
|
||||||
|
description: |
|
||||||
|
If you have any additional information that might be useful, use this field.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Request a new feature for the application
|
||||||
|
description: I want to request a new feature that is not yet available.
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for requesting new features only!
|
||||||
|
- [x] I promise to have read the [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section in the hardware documentation first! 🤞
|
||||||
|
- [x] I also swear to have searched for similar open and closed feature requests on [GITHUB](https://github.com/leukipp/touchkio/issues?q=is%3Aissue) first! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: feature_description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
placeholder: feature description
|
||||||
|
description: |
|
||||||
|
Describe the feature you want to request and why it would be useful for others.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: General inquiry or suggestion for the project
|
||||||
|
description: I want to ask a question or improve the documentation.
|
||||||
|
title: "[General]: "
|
||||||
|
labels: ["question"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for general inquiries only!
|
||||||
|
- [x] I promise that the `Report a bug in the application` issue form isn't suitable for my inquiry! 🤞
|
||||||
|
- [x] I promise that the `Request a new feature for the application` issue form isn't suitable for my inquiry! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: general_comment
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: General comment
|
||||||
|
placeholder: general comment
|
||||||
|
description: |
|
||||||
|
Ask a question, request some help, leave a general information or suggest an improvement for the documentation.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details (optional)
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
github: leukipp
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
docs/
|
docs/
|
||||||
out/
|
out/
|
||||||
|
build.json
|
||||||
|
|||||||
+244
-21
@@ -17,33 +17,256 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
|
|||||||
| ⬛ | Not operational | The house is on fire. |
|
| ⬛ | Not operational | The house is on fire. |
|
||||||
|
|
||||||
### DSI
|
### DSI
|
||||||
| Device | System | Display | Status |
|
| Device | System | Display | Status |
|
||||||
| ---------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------ |
|
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ⬜ |
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | ⬜ |
|
||||||
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | ⬜ |
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2) | ⬜ |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | 🟩 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2) | 🟩 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | 🟩 |
|
||||||
| 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/) | 🟩 |
|
| 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) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 10.1" DSI Touch (800x1280)](https://www.waveshare.com/10.1-dsi-touch-a.htm) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 12.3" DSI Touch (720x1920)](https://www.waveshare.com/12.3-dsi-touch-a.htm) | 🟩 |
|
||||||
|
|
||||||
### HDMI
|
### HDMI
|
||||||
| Device | System | Display | Status |
|
| 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 | [LAFVIN Touch Display 5" (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 7EP-CAPLCD 7" (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
||||||
| 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 | [Waveshare CAPLCD 8" (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
||||||
| 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 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [EVICIV 10.1" Touch (1920x1200)](https://de.aliexpress.com/item/1005001870058398.html?spm=a2g0o.order_list.order_list_main.10.21ef5c5fCTxEDm&gatewayAdapt=glo2deu) | 🟨 |
|
||||||
| 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 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
||||||
| 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 CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
|
||||||
| 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 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 | Generic Non-Touch | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi Capacitive Touch 10.1" (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||||
| Generic PC (x64) | Ubuntu (64-bit), X11 | Generic Non-Touch | 🟧 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
|
||||||
| Generic PC (x64) | Ubuntu (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare Capacitive Touch 14" (2160x1440)](https://www.waveshare.com/14inch-2160x1440-lcd.htm) | 🟨 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch FHD 18.5" (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
|
||||||
|
| Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
|
||||||
|
| Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||||
|
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||||
|
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
**Minimal features** are designed to run on any system without issues:
|
||||||
|
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
||||||
|
|
||||||
|
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
||||||
|
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
||||||
|
|
||||||
|
Hardware support is verified during application startup and can be checked in the terminal or in the log file under the `Supported` section.
|
||||||
|
The necessary requirements for MQTT sensors to work are listed here:
|
||||||
|
| Name | Requirements | References |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| App (Update) | Requires `sudo` rights, `.deb` install and `touchkio.service` running . | [#70](https://github.com/leukipp/touchkio/issues/70), [#77](https://github.com/leukipp/touchkio/issues/77) |
|
||||||
|
| Display (Status) | Working `wlopm`, `kscreen-doctor` or `xset` command. | [#38](https://github.com/leukipp/touchkio/issues/38), [#57](https://github.com/leukipp/touchkio/issues/57) |
|
||||||
|
| Display (Brightness) | Requires `sudo` rights, device under `/sys/class/backlight/*/brightness` exists or working `ddcutil` command. | [#30](https://github.com/leukipp/touchkio/issues/30), [#101](https://github.com/leukipp/touchkio/issues/101) |
|
||||||
|
| Keyboard | Raspberry Pi OS (Wayland) with `squeekboard` running. | [#7](https://github.com/leukipp/touchkio/issues/7), [#85](https://github.com/leukipp/touchkio/issues/85) |
|
||||||
|
| Battery | Device under `/sys/class/power_supply/*/capacity` exists. | [#33](https://github.com/leukipp/touchkio/issues/33) |
|
||||||
|
| Volume | Device (non-dummy) `pactl get-default-sink` exists. | [#82](https://github.com/leukipp/touchkio/issues/82) |
|
||||||
|
| Reboot | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
||||||
|
| Shutdown | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Operating System
|
||||||
|
|
||||||
|
<details><summary>I have installed Ubuntu GNOME.</summary>
|
||||||
|
|
||||||
|
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control is only available when using X11 (`xset`).
|
||||||
|
- GNOME is the least supported window manager.
|
||||||
|
- It's recommended to switch to KDE wayland, if you want better support for display status control (`kscreen-doctor`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I have installed Raspberry Pi OS on a RPI3.</summary>
|
||||||
|
|
||||||
|
- Raspberry Pi 3 devices have produced a bunch of issues in the past.
|
||||||
|
- [RPI 3B thermostat card issue](https://github.com/leukipp/touchkio/issues/17).
|
||||||
|
- [RPI 3B wayland rendering issue](https://github.com/leukipp/touchkio/issues/104).
|
||||||
|
- [Webpage cards don't work (RPI 3B)](https://github.com/leukipp/touchkio/issues/24).
|
||||||
|
- It looks like the GPU is not properly supported by Electron.
|
||||||
|
- Running `touchkio --disable-gpu` may improve the situation, but upgrading the hardware is the best solution.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I want to update APT packages automatically.</summary>
|
||||||
|
|
||||||
|
- The **Package Upgrades** MQTT sensor will show if `apt` package upgrades are available, but updates are [intentionally](https://github.com/leukipp/touchkio/issues/7) not triggered by TouchKio.
|
||||||
|
- It's recommended to keep your system up to date for security and compatibility, as TouchKio primarily supports the latest OS versions.
|
||||||
|
- Consider configuring `sudo apt install unattended-upgrades` for automatic updates.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Display
|
||||||
|
|
||||||
|
<details><summary>Display status can't be controlled through MQTT.</summary>
|
||||||
|
|
||||||
|
- The following commands are currently implemented to modify the display status. Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
|
||||||
|
- `wlopm --[on,off] \*` (Raspberry Pi OS, Wayland)
|
||||||
|
- `kscreen-doctor --dpms [on,off]` (Debian KDE, Wayland)
|
||||||
|
- `xset dpms force [on,off]` (Generic, X11)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Display brightness can't be controlled through MQTT.</summary>
|
||||||
|
|
||||||
|
- Have a look at the [features](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features) section to check if all requirements are fulfilled.
|
||||||
|
- The **Display** MQTT control is a light entity with brightness support. So make sure to to [click on the entity](https://github.com/leukipp/touchkio/issues/75) to check for brightness support.
|
||||||
|
- HDMI screens typically do not offer brightness control out of the box, so additional setup steps are required.
|
||||||
|
- Brightness support for [ddcutil](https://github.com/leukipp/touchkio/issues/101#issuecomment-3521247263) is build-in and checked on application startup.
|
||||||
|
- Alternatively installing [ddcci-driver-linux](https://github.com/leukipp/touchkio/issues/132#issue-3659009749) or [ddcci-dkms](https://github.com/leukipp/touchkio/issues/101#issuecomment-3571523927) may also work.
|
||||||
|
- Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
|
||||||
|
- `sudo cat /sys/class/backlight/*/brightness` returns some numeric value.
|
||||||
|
- `sudo ddcutil setvcp 10 42` changes the brightness to 42% without asking for a password.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Turning HDMI screen off via MQTT causes it to turn back on.</summary>
|
||||||
|
|
||||||
|
- There are certain HDMI screens (e.g. Viewsonic) where the MQTT display status command doesn't work as expected.
|
||||||
|
- The screen [immediately turns on again](https://github.com/leukipp/touchkio/issues/38), which may be caused by incompatible HDMI cables being used.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Touch
|
||||||
|
|
||||||
|
<details><summary>Automated screen blanking on inactivity.</summary>
|
||||||
|
|
||||||
|
- You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||||
|
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant [automation](https://www.home-assistant.io/docs/automation/basics) (e.g. presence detection or **Last Active** MQTT sensor) to manage the screen status.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Touch in multitouch mode not waking the screen.</summary>
|
||||||
|
|
||||||
|
- It's recommended to use **Mouse Emulation** for touch screens, otherwise touch based [screen wake-up](https://github.com/leukipp/touchkio/issues/127) from display off state will fail, especially if build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) is disabled.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Touch events may propagate through a display that is turned off.</summary>
|
||||||
|
|
||||||
|
- It's recommended to use **Mouse Emulation** for touch screens, otherwise Home Assistant actions could be triggered [unintentionally](https://github.com/leukipp/touchkio/issues/61) on touch.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
<details><summary>Connecting via https/mqtts with a self-signed certificate.</summary>
|
||||||
|
|
||||||
|
- When connecting to a service with a [custom certificate](https://github.com/leukipp/touchkio/issues/42#issuecomment-3041870215), ensure the specified FQDN matches.
|
||||||
|
- Using the `--ignore-certificate-errors` flag is [not recommended](https://github.com/leukipp/touchkio/issues/76) for browsing external sites.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Local DNS entries in /etc/hosts are ignored.</summary>
|
||||||
|
|
||||||
|
- Electron bypasses the system resolver stack, ignoring `/etc/hosts` and local DNS servers.
|
||||||
|
- To use the system resolver, modify `~/.config/systemd/user/touchkio.service` to disable these features:
|
||||||
|
```bash
|
||||||
|
ExecStart=/usr/bin/touchkio --disable-features=UseDNSHttps,AsyncDns
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>During VNC access the display turns gray.</summary>
|
||||||
|
|
||||||
|
- On Raspberry Pi OS the display command may fail with `ERROR: Setting power mode for output '[DSI-*,HDMI-*]' failed`.
|
||||||
|
- This can happen if you have `wayvnc` running for remote access and is caused by a [known bug](https://github.com/leukipp/touchkio/issues/78).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Media
|
||||||
|
|
||||||
|
<details><summary>I need Music and Voice Assistant.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/88#issuecomment-3366659265).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I need a camera stream from the device.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/130#issuecomment-3591770594).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I need additional MQTT entities to control hardware.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/138#issuecomment-3591784725).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details><summary>Some of the MQTT controls are missing.</summary>
|
||||||
|
|
||||||
|
- Certain [features](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features) from the MQTT integration may require elevated privileges to work correctly.
|
||||||
|
- Test if your local user has the [necessary permissions](https://github.com/leukipp/touchkio/issues/39) to run `sudo -n true` without being [prompted for a password](https://github.com/leukipp/touchkio/issues/116#issuecomment-3471566411).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>The ddcutil command doesn't seem to be used.</summary>
|
||||||
|
|
||||||
|
- Currently `ddcutil` is only used for brightness control of HDMI screens.
|
||||||
|
- Make sure that your screen is supporting [continuous](https://github.com/leukipp/touchkio/issues/101#issuecomment-3521247263) adjustments of brightness values.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>The on-screen keyboard doesn't automatically pop-out.</summary>
|
||||||
|
|
||||||
|
- Most of the available on-screen keyboards will not render above any [fullscreen window](https://forums.raspberrypi.com/viewtopic.php?p=2327148).
|
||||||
|
- There is a [workaround](https://github.com/leukipp/touchkio/issues/85) for `squeekboard`, which will only work on Raspberry Pi OS.
|
||||||
|
- If the on-screen keyboard still doesn't [automatically pop-out](https://github.com/leukipp/touchkio/issues/4) when entering a text field inside the webview you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the visibility.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
<details><summary>Terminal is full with "ERROR:gbm_wrapper.cc" messages.</summary>
|
||||||
|
|
||||||
|
- On the terminal you may see some _ERROR:gbm_wrapper.cc_ messages.
|
||||||
|
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>CPU and RAM usage increases without reason.</summary>
|
||||||
|
|
||||||
|
- This was [especially observed](https://github.com/leukipp/touchkio/issues/123) when the screen is permanently on and renders a dashboard with interactive elements (e.g. custom lovelace cards).
|
||||||
|
- Switching to another **Page Url** or turning the **Display** off during idle times improves resource usage.
|
||||||
|
- Alternatively a timed **Refresh** of the webview via MQTT can also help.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Error message "GPU Process abnormal-exit".</summary>
|
||||||
|
|
||||||
|
- _GPU Process abnormal-exit (code 512)_ means that the Electron GPU process crashed, probably caused by some issues inside the webview.
|
||||||
|
- Disabling the GPU via `touchkio --disable-gpu` may help.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Error message "Render Process killed".</summary>
|
||||||
|
|
||||||
|
- _Render Process killed (code 9)_ will lead to a temporary [white screen](https://github.com/leukipp/touchkio/issues/115).
|
||||||
|
- The problem likely stems from Electron’s calculated RAM limit (~2GB) and the dashboard using too much memory.
|
||||||
|
- Although there are flags to [increase the limit](https://github.com/leukipp/touchkio/issues/36#issuecomment-3406441947), a standard webview shouldn’t require that much RAM.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
In case your hardware is not listed above don't worry, give it a try.
|
In case your hardware is not listed above don't worry, give it a try.
|
||||||
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
|
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
|
||||||
The only problems that may arise are when controlling the display or keyboard via the Home Assistant integration.
|
The only problems that may arise are when controlling the display via the Home Assistant integration.
|
||||||
|
|
||||||
|
There is a possibility to test `pre-releases` if you would like to try upcoming versions before the official release is published.
|
||||||
|
These early builds are available to a smaller group of users who have chosen to take part in testing.
|
||||||
|
|
||||||
|
If you are interested in long-term testing, check out the related [help request](https://github.com/leukipp/touchkio/issues/96).
|
||||||
|
When a [pre-release](https://github.com/leukipp/touchkio/releases) is available and you want to test it on-demand, run:
|
||||||
|
```bash
|
||||||
|
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh) update early
|
||||||
|
```
|
||||||
|
|
||||||
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
|
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
|
||||||
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# TouchKio
|
# TouchKio
|
||||||
|
[](https://github.com/leukipp/touchkio/actions)
|
||||||
|
[](https://github.com/leukipp/touchkio/releases)
|
||||||
|
[](https://github.com/leukipp/touchkio/releases)
|
||||||
|
[](https://github.com/leukipp/touchkio/releases)
|
||||||
|
[](https://github.com/sponsors/leukipp)
|
||||||
|
|
||||||
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
|
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
|
||||||
This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display.
|
This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display.
|
||||||
Additional releases for other Linux systems are available as **.zip** file.
|
Additional releases for other Linux systems are available as **.zip** file.
|
||||||
@@ -11,17 +17,23 @@ Moreover, the device running the **kiosk application** also offers several **Hom
|
|||||||
## Features
|
## Features
|
||||||
- [x] Fast and easy setup.
|
- [x] Fast and easy setup.
|
||||||
- [x] Remember login credentials.
|
- [x] Remember login credentials.
|
||||||
|
- [x] Touch optimized web browsing.
|
||||||
|
- [x] Dynamic window status bar layout.
|
||||||
- [x] Side panel widget for kiosk control.
|
- [x] Side panel widget for kiosk control.
|
||||||
- [x] Touch friendly web browser interface.
|
- [x] Navigation bar to switch between pages.
|
||||||
- [x] Configurable zoom and theme support.
|
- [x] Adjustable browser zoom and theme support.
|
||||||
- [x] Single-tap screen wake-up functionality.
|
- [x] Extended touch screen wake-up functionality.
|
||||||
- [x] Remote controllable via MQTT.
|
- [x] Remote controllable via MQTT.
|
||||||
- [x] Manage kiosk window status.
|
- [x] Toggle the on-screen keyboard.
|
||||||
- [x] Manage touch keyboard status.
|
|
||||||
- [x] Touch display power and brightness.
|
- [x] Touch display power and brightness.
|
||||||
- [x] Show available system package upgrades.
|
- [x] Manage kiosk window status and zoom.
|
||||||
|
- [x] Screenshot image of the kiosk webview.
|
||||||
|
- [x] Show network interfaces and addresses.
|
||||||
|
- [x] List all available system package upgrades.
|
||||||
|
- [x] Multi-webpage switching and url navigation.
|
||||||
|
- [x] Volume control for connected audio outputs.
|
||||||
- [x] Execute system reboot and shutdown commands.
|
- [x] Execute system reboot and shutdown commands.
|
||||||
- [x] Monitor temperature, processor and memory usage.
|
- [x] Monitor battery, temperature, processor and memory usage.
|
||||||
|
|
||||||
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
|
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
|
||||||
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
|
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
|
||||||
@@ -40,11 +52,12 @@ This setup allows seamless communication between your kiosk device and Home Assi
|
|||||||
For a comprehensive guide on setting up MQTT with Home Assistant, please refer to the official documentation available here: https://www.home-assistant.io/integrations/mqtt.
|
For a comprehensive guide on setting up MQTT with Home Assistant, please refer to the official documentation available here: https://www.home-assistant.io/integrations/mqtt.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
|
On the first run of the application, you will encounter a **setup procedure (CLI)** and the Home Assistant **login screen (UI)**.
|
||||||
It's recommended to create a **dedicated user** (local access only) for your kiosk device.
|
It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
|
||||||
|
|
||||||
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
||||||
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
|
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
|
||||||
|
After the first login the Home Assistant credentials are stored inside the `~/.config/touchkio/` folder.
|
||||||
|
|
||||||
#### Option 1 - The easy way
|
#### Option 1 - The easy way
|
||||||
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
||||||
@@ -54,7 +67,6 @@ bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install
|
|||||||
```
|
```
|
||||||
Make sure that you run this with your **standard user** and not with root (sudo).
|
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.
|
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.
|
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:
|
If you need manual control use:
|
||||||
@@ -68,9 +80,8 @@ systemctl --user start|stop|status|restart touchkio.service
|
|||||||
When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section.
|
When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section.
|
||||||
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
|
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
|
||||||
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
|
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
|
||||||
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g:
|
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g: `sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup`
|
||||||
`sudo dpkg -i touchkio_1.x.x_arm64.deb && touchkio --setup`
|
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
|
||||||
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
|
|
||||||
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
|
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
|
||||||
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
|
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
|
||||||
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
|
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
|
||||||
@@ -87,18 +98,27 @@ yarn build
|
|||||||
|
|
||||||
</div></details>
|
</div></details>
|
||||||
|
|
||||||
|
#### Update
|
||||||
|
If you have already installed TouchKio and want to upgrade to the latest version, simply install the newer version over the existing one.
|
||||||
|
|
||||||
|
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script can also be run to update an existing installation to the **latest version**.
|
||||||
|
The setup procedure can be skipped to use the existing default arguments from the configuration file:
|
||||||
|
```bash
|
||||||
|
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh) update
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
|
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
|
||||||
These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified.
|
These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified.
|
||||||
|
|
||||||
### WEB
|
### WEB
|
||||||
The available arguments to control the kiosk application via terminal are as follows:
|
The available arguments to control the kiosk application via terminal are as follows:
|
||||||
| Name | Default | Description |
|
| Name | Default | Description |
|
||||||
| ------------------------- | ------- | ------------------------------------------------------ |
|
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
| `--web-url` (Required) | - | Url of the Home Assistant instance (HTTP(S)://IP:PORT) |
|
| `--web-url` (Required) | - | Url of the Home Assistant instance<a id="ref1"></a><sup><a href="#foot1">[1]</a></sup> (HTTP(S)://IP:PORT) |
|
||||||
| `--web-theme` (Optional) | `dark` | Theme settings of the web browser (`light` or `dark`) |
|
| `--web-theme` (Optional) | `dark` | Theme settings of the web browser (`light` or `dark`) |
|
||||||
| `--web-zoom` (Optional) | `1.25` | Zoom settings of the web browser (`1.0` is `100%`) |
|
| `--web-zoom` (Optional) | `1.25` | Zoom settings of the web browser (`1.0` is `100%`) |
|
||||||
| `--web-widget` (Optional) | `true` | Enables the sidebar widget (`true` or `false`) |
|
| `--web-widget` (Optional) | `true` | Enables the sidebar widget (`true` or `false`) |
|
||||||
|
|
||||||
These arguments allow you to customize the appearance of the web browser view.
|
These arguments allow you to customize the appearance of the web browser view.
|
||||||
|
|
||||||
@@ -107,6 +127,20 @@ For example:
|
|||||||
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
|
||||||
|
> You can configure multiple pages by separating them with a comma:
|
||||||
|
> `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
|
||||||
|
|
||||||
|
In the `~/.config/touchkio/Arguments.json` file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"web_url": [
|
||||||
|
"https://demo.home-assistant.io",
|
||||||
|
"https://demo.immichkiosk.app"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### MQTT
|
### MQTT
|
||||||
To broadcast your local sensor data to Home Assistant, you can use the following arguments, which require a running MQTT broker:
|
To broadcast your local sensor data to Home Assistant, you can use the following arguments, which require a running MQTT broker:
|
||||||
| Name | Default | Description |
|
| Name | Default | Description |
|
||||||
@@ -124,14 +158,37 @@ For example:
|
|||||||
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
|
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
TouchKio provides a simple webview window designed specifically for Touch Displays. Electron apps are known to be resource intensive due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
||||||
|
|
||||||
|
### Touch Controls
|
||||||
|
Additional controls can be found along the right edge of the kiosk application. For more details, you may want to have a closer look here:
|
||||||
|
| Name | Description |
|
||||||
|
| ------------ | -------------------------------------------------------- |
|
||||||
|
| `Widget` | [See #16](https://github.com/leukipp/touchkio/issues/16) |
|
||||||
|
| `Navigation` | [See #45](https://github.com/leukipp/touchkio/issues/45) |
|
||||||
|
| `Pager` | [See #64](https://github.com/leukipp/touchkio/issues/64) |
|
||||||
|
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
The application also supports basic shortcuts to enhance navigation and usability for users who prefer or require non-touch input methods:
|
||||||
|
| Name | Description |
|
||||||
|
| --------------------------- | ---------------------------- |
|
||||||
|
| `Control+Left` | Navigate to previous page |
|
||||||
|
| `Control+Right` | Navigate to next page |
|
||||||
|
| `Control+Num_Subtract` | Decrease the page zoom |
|
||||||
|
| `Control+Num_Add` | Increase the page zoom |
|
||||||
|
| `Alt+Left`/`Mouse+Back` | Navigate backward in history |
|
||||||
|
| `Alt+Right`/`Mouse+Forward` | Navigate forward in history |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
|
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
|
||||||
|
|
||||||
Clone this repository and run `yarn install` to install the dependencies.
|
Clone this repository and run `yarn install` to install the dependencies.
|
||||||
Then use `yarn start` to execute the start script located in the [package.json](https://github.com/leukipp/touchkio/blob/main/package.json) file.
|
Then use `yarn start` to execute the start script located in the [package.json](https://github.com/leukipp/touchkio/blob/main/package.json) file.
|
||||||
There you can adjust the `--web-url` and other arguments for development runs.
|
There you may want to adjust the `--web-url` and other arguments for development runs.
|
||||||
|
|
||||||
If you connect to your device via SSH, you may have to export display variables so that the kiosk application can be loaded inside the desktop environment:
|
If you access your device over SSH, make sure to export the display variables so the kiosk application can launch in the desktop environment.
|
||||||
```bash
|
```bash
|
||||||
export DISPLAY=":0"
|
export DISPLAY=":0"
|
||||||
export WAYLAND_DISPLAY="wayland-0"
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
@@ -177,24 +234,26 @@ While using external sensors that directly integrate with Home Assistant and by
|
|||||||
|
|
||||||
<details><summary>Don't waste your time reading this.</summary><div></br>
|
<details><summary>Don't waste your time reading this.</summary><div></br>
|
||||||
|
|
||||||
The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some *Rust* got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed.
|
The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some _Rust_ got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed.
|
||||||
The kiosk application interacts with squeekboard via this `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
||||||
|
|
||||||
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
||||||
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
The `wlopm --off \*` command changes the `/sys/class/backlight/*/bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
||||||
However, `swayidle` still seems to consider the screen to be off and as a result it will not turn off again unless there is some interaction in the meantime.
|
However, `swayidle` still seems to consider the screen to be off and as a result it will not turn off again unless there is some interaction in the meantime.
|
||||||
|
|
||||||
When using the MQTT integration, the kiosk application must be able to **detect changes** made on the **device** itself.
|
When using the MQTT integration, the kiosk application must be able to **detect changes** made on the **device** itself.
|
||||||
I managed to achieve this for the `brightness` file by implementing a simple `fs.watch(..)` file listener.
|
I managed to achieve this for the `/sys/class/backlight/*/brightness` file by implementing a simple `fs.watch(..)` file listener.
|
||||||
However, I found that it **never triggered** for the `bl_power` file.
|
However, I found that it **never triggered** for the `/sys/class/backlight/*/bl_power` or the `/sys/class/drm/*/dpms` file.
|
||||||
Although the file content changes, none of the filesystem listeners where fired.
|
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.
|
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 second** for any changes.
|
|
||||||
|
As a result, I went for a **polling solution**, checking the state of the `/sys/class/backlight/*/brightness` and `/sys/class/drm/*/dpms` file **every second** for any changes.
|
||||||
|
In case `ddcutil` is installed the file `~/.config/touchkio/Cache/Brightness.vcp` will be written/monitored to support brightness support for HDMI screens.
|
||||||
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
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.
|
The display power status and brightness can be adjusted via the MQTT integration.
|
||||||
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
||||||
Support for changing the brightness of connected display is implemented by using `/sys/class/backlight/*/brightness`.
|
Support for changing the brightness of a connected display is implemented by using `sudo tee /sys/class/backlight/*/brightness` or `sudo ddcutil setvcp 10`.
|
||||||
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
||||||
|
|
||||||
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
||||||
@@ -203,40 +262,55 @@ Implementing stronger **security measures** would complicate the setup process a
|
|||||||
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
||||||
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
||||||
|
|
||||||
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
|
To address the problem where the first **touch** on a **turned-off screen** may trigger a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
|
||||||
When the screen **turns off** the **focus** is removed from the kiosk window.
|
When the screen **turns on/off**, the timestamps of these events are recorded.
|
||||||
This way, the first click only turns the screen on and focuses the window, allowing subsequent clicks to work as expected.
|
If a touch event is detected and the **timestamp** of the **last screen-off** is more recent than the **last screen-on** timestamp, the touch event is discarded and the screen is turned on instead.
|
||||||
|
From that point onward, subsequent touch interactions will function normally as expected.
|
||||||
|
|
||||||
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
||||||
This adjustment provides a user experience similar to that of a proper mobile device.
|
This adjustment provides a user experience similar to that of a proper mobile device.
|
||||||
|
|
||||||
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
|
||||||
|
|
||||||
</div></details>
|
</div></details>
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
> [!NOTE]
|
||||||
|
> ### Please read the hardware [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section first if you encounter any issues.
|
||||||
|
|
||||||
**Minimal features** are designed to run on any system without issues:
|
For basic debugging **(TouchKio)**, stop the service and launch `touchkio` directly on the terminal to monitor the log output in real-time.
|
||||||
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
This output is also written into `~/.config/touchkio/logs/main.log` for review.
|
||||||
|
|
||||||
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
For extended logging **(Electron)** you can run `touchkio --enable-logging`, which will write an additional log file into `~/.config/touchkio/logs/electron.log`.
|
||||||
- Sensor features that may work out-of-the-box include those that do not require any interaction with the display or keyboard.
|
Refer to the [--log-level=[0-3]](https://www.electronjs.org/docs/latest/api/command-line-switches#--log-leveln) and [--v=[0-3]](https://www.electronjs.org/docs/latest/api/command-line-switches#--vlog_level) options to adjust the logging verbosity.
|
||||||
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
|
||||||
- On some Debian based systems (e.g. Ubuntu), the display status control may only be available when using X11.
|
|
||||||
|
|
||||||
**Known Issues** that are by-design or for which there isn't a solution so far:
|
If you need to debug the webview **(Chrome DevTools)** use `touchkio --app-debug`.
|
||||||
- You can use Raspberry Pi's build-in screen blanking functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
|
||||||
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g. presence detection) to manage the screen status.
|
In case [undocumented problems](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) arise, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version`, additional information about your system (such as operating system, hardware, etc.), and any relevant log files.
|
||||||
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out when entering a text field.
|
|
||||||
- This [issue](https://github.com/leukipp/touchkio/issues/4) also occurs in the default browser but as a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard.
|
|
||||||
- Hyperlinks that redirect the browser away from the main window are intentionally disabled.
|
|
||||||
- This decision was made to maintain a clean design, as navigation buttons to move back and forth between different sites were not included.
|
|
||||||
- On the terminal you may see some *ERROR:gbm_wrapper.cc* messages.
|
|
||||||
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
Inspired by the one and only Raspberry Pi Master Lord, [Jeff Geerling](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2).
|
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)).
|
||||||
|
|
||||||
|
Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions.
|
||||||
|
|
||||||
|
### Tutorials
|
||||||
|
If you are looking for hardware or a well-designed mounting solution for the Raspberry Pi Display, check out the [blog post](https://www.thestockpot.net/videos/home-assistant-wall-display) from Dillan Stock ([@TheStockPot-AU](https://www.youtube.com/@TheStockPot-AU)):
|
||||||
|
|
||||||
|
<a href="https://www.youtube.com/watch?v=uTxURzmrVtA">
|
||||||
|
<img src="https://img.youtube.com/vi/uTxURzmrVtA/maxresdefault.jpg" alt="What My Smart Home Was Missing" title="@TheStockPot-AU" style="width:100%">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Also, have a look at this curated collection of helpful videos contributed by the community:
|
||||||
|
|
||||||
|
<p align="center" style="width:100%">
|
||||||
|
<a href="https://www.youtube.com/watch?v=_adl1fiXlgk">
|
||||||
|
<img src="https://img.youtube.com/vi/_adl1fiXlgk/mqdefault.jpg" alt="Building a Home Assistant Kiosk - Why Did I Wait So Long?" title="@Jims-Garage" style="width:32%">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=uXcjx-zL_UU">
|
||||||
|
<img src="https://img.youtube.com/vi/uXcjx-zL_UU/mqdefault.jpg" alt="Touch Display for Home Assistant with TouchKIO" title="@haus_automation" style="width:32%">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=t2YQm7GPmpY">
|
||||||
|
<img src="https://img.youtube.com/vi/t2YQm7GPmpY/mqdefault.jpg" alt="DIY Home Assistant Kiosk Build: Raspberry Pi & 3D Printing Livestream" title="@homeautomatorza" style="width:32%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
||||||
|
|||||||
+35
-2
@@ -1,4 +1,22 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const jszip = require("jszip");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const generateBuildFile = (platform, arch, maker) => {
|
||||||
|
const package = fs.readFileSync(path.join(__dirname, "package.json"), "utf8");
|
||||||
|
const hash = crypto.createHash("sha256").update(package);
|
||||||
|
const data = {
|
||||||
|
id: hash.digest("hex").slice(-6),
|
||||||
|
platform: platform,
|
||||||
|
arch: arch,
|
||||||
|
maker: maker,
|
||||||
|
date: date,
|
||||||
|
};
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
@@ -33,10 +51,25 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
|
postPackage: async (config, results) => {
|
||||||
|
for (const outputPath of results.outputPaths) {
|
||||||
|
const [name, platform, arch] = path.basename(outputPath).split("-");
|
||||||
|
const buildFile = path.join(outputPath, "resources", "app", "build.json");
|
||||||
|
fs.writeFileSync(buildFile, generateBuildFile(platform, arch, "deb"), { encoding: "utf8" });
|
||||||
|
}
|
||||||
|
},
|
||||||
postMake: async (config, results) => {
|
postMake: async (config, results) => {
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const artifacts = [];
|
const artifacts = [];
|
||||||
result.artifacts.forEach((artifact) => {
|
for (const artifact of result.artifacts) {
|
||||||
|
if (artifact.includes(".zip")) {
|
||||||
|
const [name, platform, arch] = path.basename(artifact).split("-");
|
||||||
|
const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json");
|
||||||
|
const options = { type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 9 } };
|
||||||
|
const zip = await jszip.loadAsync(fs.readFileSync(artifact));
|
||||||
|
zip.file(buildFile, generateBuildFile(platform, arch, "zip"));
|
||||||
|
fs.writeFileSync(artifact, await zip.generateAsync(options));
|
||||||
|
}
|
||||||
if (artifact.includes("amd64")) {
|
if (artifact.includes("amd64")) {
|
||||||
const renamed = artifact.replace("amd64", "x64");
|
const renamed = artifact.replace("amd64", "x64");
|
||||||
fs.renameSync(artifact, renamed);
|
fs.renameSync(artifact, renamed);
|
||||||
@@ -44,7 +77,7 @@ module.exports = {
|
|||||||
} else {
|
} else {
|
||||||
artifacts.push(artifact);
|
artifacts.push(artifact);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
result.artifacts = artifacts;
|
result.artifacts = artifacts;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
--border-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--background-color: rgba(28, 28, 28, 1);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--primary-color: rgba(33, 33, 33, 1);
|
||||||
|
--border-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0 10px;
|
||||||
|
column-gap: 10px;
|
||||||
|
list-style-type: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li.dynamic {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
|
li.separator {
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
li.separator:has(+ li.hidden) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
li.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
li.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
cursor: text;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="home">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>home-circle</title>
|
||||||
|
<path
|
||||||
|
d="M19.07,4.93C17.22,3 14.66,1.96 12,2C9.34,1.96 6.79,3 4.94,4.93C3,6.78 1.96,9.34 2,12C1.96,14.66 3,17.21 4.93,19.06C6.78,21 9.34,22.04 12,22C14.66,22.04 17.21,21 19.06,19.07C21,17.22 22.04,14.66 22,12C22.04,9.34 21,6.78 19.07,4.93M17,12V18H13.5V13H10.5V18H7V12H5L12,5L19.5,12H17Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="refresh">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>refresh-circle</title>
|
||||||
|
<path
|
||||||
|
d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2M18 11H13L14.81 9.19A3.94 3.94 0 0 0 12 8A4 4 0 1 0 15.86 13H17.91A6 6 0 1 1 12 6A5.91 5.91 0 0 1 16.22 7.78L18 6Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="previous">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>page-previous</title>
|
||||||
|
<path
|
||||||
|
d="M4,21H19A2,2 0 0,0 21,19V13H17V15L13,12L17,9V11H21V5A2,2 0 0,0 19,3H4A2,2 0 0,0 2,5V19A2,2 0 0,0 4,21M4,15H8V17H4V15M4,11H11V13H4V11M4,7H11V9H4V7M21,11H24V13H21V11Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="next">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>page-next</title>
|
||||||
|
<path
|
||||||
|
d="M20,3H5A2,2 0 0,0 3,5V11H7V9L11,12L7,15V13H3V19A2,2 0 0,0 5,21H20A2,2 0 0,0 22,19V5A2,2 0 0,0 20,3M17,17H13V15H17V17M20,13H13V11H20V13M20,9H13V7H20V9M3,13H0V11H3V13Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li class="dynamic">
|
||||||
|
<input id="url" type="text" spellcheck="false" readonly="true" />
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="backward">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>history</title>
|
||||||
|
<path
|
||||||
|
d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="forward">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>update</title>
|
||||||
|
<path
|
||||||
|
d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="minus">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>magnify-minus</title>
|
||||||
|
<path
|
||||||
|
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M5,8V10H13V8H5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="plus">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>magnify-plus</title>
|
||||||
|
<path
|
||||||
|
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M8,5V8H5V10H8V13H10V10H13V8H10V5H8Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
<script>
|
||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const registerSendEvents = () => {
|
||||||
|
document.querySelectorAll("button").forEach((button) => {
|
||||||
|
button.addEventListener("click", (e) => {
|
||||||
|
ipcRenderer.send("button-click", { id: e.target.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll("input").forEach((input) => {
|
||||||
|
input.addEventListener("blur", (e) => {
|
||||||
|
ipcRenderer.send("input-blur", { id: e.target.id });
|
||||||
|
});
|
||||||
|
input.addEventListener("focus", (e) => {
|
||||||
|
ipcRenderer.send("input-focus", { id: e.target.id });
|
||||||
|
});
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
ipcRenderer.send("input-enter", { id: e.target.id, text: e.target.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerReceiveEvents = () => {
|
||||||
|
ipcRenderer.on("button-disabled", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("button-hidden", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("input-readonly", (e, input) => {
|
||||||
|
document.getElementById(input.id).readOnly = input.readonly;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("input-text", (e, input) => {
|
||||||
|
document.getElementById(input.id).value = input.text;
|
||||||
|
document.getElementById(input.id).placeholder = input.placeholder;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("input-select", (e, input) => {
|
||||||
|
window.getSelection()?.removeAllRanges();
|
||||||
|
if (input.select) {
|
||||||
|
document.getElementById(input.id).select();
|
||||||
|
} else {
|
||||||
|
document.getElementById(input.id).blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSendEvents();
|
||||||
|
registerReceiveEvents();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--background-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--previous-cursor: w-resize;
|
||||||
|
--next-cursor: e-resize;
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--background-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--previous-cursor: w-resize;
|
||||||
|
--next-cursor: e-resize;
|
||||||
|
}
|
||||||
|
[data-theme="hidden"] {
|
||||||
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
--previous-cursor: none;
|
||||||
|
--next-cursor: none;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li:not(.disabled) #previous {
|
||||||
|
cursor: var(--previous-cursor);
|
||||||
|
}
|
||||||
|
li:not(.disabled) #next {
|
||||||
|
cursor: var(--next-cursor);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100vw;
|
||||||
|
height: 50vh;
|
||||||
|
border: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
button:not(:active) {
|
||||||
|
transition: background-color 500ms step-end;
|
||||||
|
}
|
||||||
|
menu:has(button:active) button {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition-duration: 0ms;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="previous"></button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="next"></button>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
<script>
|
||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const registerSendEvents = () => {
|
||||||
|
document.querySelectorAll("button").forEach((button) => {
|
||||||
|
button.addEventListener("pointerdown", (e) => {
|
||||||
|
ipcRenderer.send("button-click", { id: e.target.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerReceiveEvents = () => {
|
||||||
|
ipcRenderer.on("button-disabled", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSendEvents();
|
||||||
|
registerReceiveEvents();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: rgba(229, 229, 229, 1);
|
||||||
|
--background-color: rgba(66, 67, 68, 1);
|
||||||
|
--minimize-color: rgba(52, 199, 89, 1);
|
||||||
|
--fullscreen-color: rgba(255, 149, 0, 1);
|
||||||
|
--terminate-color: rgba(255, 59, 48, 1);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--primary-color: rgba(26, 26, 26, 1);
|
||||||
|
--background-color: rgba(237, 236, 235, 1);
|
||||||
|
--minimize-color: rgba(52, 199, 89, 1);
|
||||||
|
--fullscreen-color: rgba(255, 149, 0, 1);
|
||||||
|
--terminate-color: rgba(255, 59, 48, 1);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
height: 100vh;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 116px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
column-gap: 10px;
|
||||||
|
list-style-type: none;
|
||||||
|
background: var(--background-color);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#minimize > svg {
|
||||||
|
fill: var(--minimize-color);
|
||||||
|
}
|
||||||
|
#fullscreen > svg {
|
||||||
|
fill: var(--fullscreen-color);
|
||||||
|
}
|
||||||
|
#terminate > svg {
|
||||||
|
fill: var(--terminate-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1 id="title">TouchKio</h1>
|
||||||
|
</header>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<button id="minimize">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>minus-circle</title>
|
||||||
|
<path d="M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="fullscreen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>chevron-up-circle</title>
|
||||||
|
<path
|
||||||
|
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7.4,15.4L12,10.8L16.6,15.4L18,14L12,8L6,14L7.4,15.4Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="terminate">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>close-circle</title>
|
||||||
|
<path
|
||||||
|
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
<script>
|
||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const registerSendEvents = () => {
|
||||||
|
document.querySelectorAll("button").forEach((button) => {
|
||||||
|
button.addEventListener("click", (e) => {
|
||||||
|
ipcRenderer.send("button-click", { id: e.target.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerReceiveEvents = () => {
|
||||||
|
ipcRenderer.on("button-disabled", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
|
||||||
|
});
|
||||||
|
ipcRenderer.on("text-content", (e, text) => {
|
||||||
|
document.getElementById(text.id).textContent = text.content;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSendEvents();
|
||||||
|
registerReceiveEvents();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
<style>
|
<style>
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--primary-color: rgba(225, 225, 225, 1);
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
@@ -46,6 +47,14 @@
|
|||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
li.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -60,16 +69,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<menu>
|
<menu>
|
||||||
<li>
|
|
||||||
<button id="refresh">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<title>web-refresh</title>
|
|
||||||
<path
|
|
||||||
d="M13.82 14H9.66C9.56 13.34 9.5 12.68 9.5 12S9.56 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.5 14.46 13 14.4 13.46C15 12.96 15.72 12.57 16.5 12.32C16.5 12.21 16.5 12.11 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.06 20 12.12 20 12.18C20.7 12.35 21.35 12.63 21.95 13C22 12.68 22 12.34 22 12C22 6.5 17.5 2 12 2C6.47 2 2 6.5 2 12C2 17.5 6.5 22 12 22C12.34 22 12.68 22 13 21.95C12.59 21.29 12.29 20.55 12.13 19.75C12.09 19.82 12.05 19.89 12 19.96C11.17 18.76 10.5 17.43 10.09 16H12.5C12.81 15.25 13.26 14.58 13.82 14M18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.34 18.92 8M12 4.03C12.83 5.23 13.5 6.57 13.91 8H10.09C10.5 6.57 11.17 5.23 12 4.03M4.26 14C4.1 13.36 4 12.69 4 12S4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12S7.56 13.34 7.64 14H4.26M5.08 16H8C8.35 17.25 8.8 18.45 9.4 19.56C7.57 18.93 6.03 17.65 5.08 16M8 8H5.08C6.03 6.34 7.57 5.06 9.4 4.44C8.8 5.55 8.35 6.75 8 8M20.83 15.67L22 14.5V18.5H18L19.77 16.73C19.32 16.28 18.69 16 18 16C16.62 16 15.5 17.12 15.5 18.5S16.62 21 18 21C18.82 21 19.54 20.61 20 20H21.71C21.12 21.47 19.68 22.5 18 22.5C15.79 22.5 14 20.71 14 18.5S15.79 14.5 18 14.5C19.11 14.5 20.11 14.95 20.83 15.67Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<button id="fullscreen">
|
<button id="fullscreen">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
@@ -81,11 +80,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="minimize">
|
<button id="theme">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<title>image-size-select-small</title>
|
<title>compare</title>
|
||||||
<path
|
<path
|
||||||
d="M23,15H21V17H23V15M23,11H21V13H23V11M23,19H21V21C22,21 23,20 23,19M15,3H13V5H15V3M23,7H21V9H23V7M21,3V5H23C23,4 22,3 21,3M3,21H11V15H1V19A2,2 0 0,0 3,21M3,7H1V9H3V7M15,19H13V21H15V19M19,3H17V5H19V3M19,19H17V21H19V19M3,3C2,3 1,4 1,5H3V3M3,11H1V13H3V11M11,3H9V5H11V3M7,3H5V5H7V3Z"
|
d="M19,3H14V5H19V18L14,12V21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,18H5L10,12M10,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H10V23H12V1H10V3Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -100,26 +99,39 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="navigation">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>web</title>
|
||||||
|
<path
|
||||||
|
d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
<script>
|
<script>
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
// Register button click send message
|
const registerSendEvents = () => {
|
||||||
document.querySelectorAll("button").forEach((button) => {
|
document.querySelectorAll("button").forEach((button) => {
|
||||||
button.addEventListener("click", (e) => {
|
button.addEventListener("click", (e) => {
|
||||||
ipcRenderer.send("button-click", { id: e.target.id });
|
ipcRenderer.send("button-click", { id: e.target.id });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
// Register button hide receive message
|
const registerReceiveEvents = () => {
|
||||||
ipcRenderer.on("button-hide", (e, button) => {
|
ipcRenderer.on("button-hidden", (e, button) => {
|
||||||
document.getElementById(button.id).parentNode.hidden = true;
|
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
|
||||||
});
|
});
|
||||||
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Register data theme receive message
|
registerSendEvents();
|
||||||
ipcRenderer.on("data-theme", (e, data) => {
|
registerReceiveEvents();
|
||||||
document.documentElement.setAttribute("data-theme", data.theme);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 90 KiB |
@@ -2,61 +2,282 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const readline = require("readline/promises");
|
const readline = require("readline/promises");
|
||||||
const integration = require("./src/integration");
|
const integration = require("./js/integration");
|
||||||
const hardware = require("./src/hardware");
|
const hardware = require("./js/hardware");
|
||||||
const webview = require("./src/webview");
|
const webview = require("./js/webview");
|
||||||
const { app } = require("electron");
|
const log = require("electron-log");
|
||||||
|
const { app, powerMonitor } = require("electron");
|
||||||
|
const Events = require("events");
|
||||||
|
|
||||||
|
global.APP = global.APP || {};
|
||||||
global.ARGS = global.ARGS || {};
|
global.ARGS = global.ARGS || {};
|
||||||
|
global.EVENTS = global.EVENTS || new Events();
|
||||||
|
|
||||||
|
// Check display environment variable
|
||||||
if (!process.env.DISPLAY) {
|
if (!process.env.DISPLAY) {
|
||||||
console.error(`$DISPLAY variable not set to run the GUI application, are you connected via SSH?`);
|
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
|
||||||
console.error(`If you have installed the service use:`);
|
console.error(`If you have installed the service use:`);
|
||||||
console.error(` systemctl --user start touchkio.service`);
|
console.error(` systemctl --user start touchkio.service`);
|
||||||
console.error(`Alternatively export the variables first:`);
|
console.error(`Alternatively export the variables first:`);
|
||||||
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
|
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move electron log file
|
||||||
|
const elog = path.join(app.getPath("logs"), "electron.log");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(elog)) {
|
||||||
|
fs.renameSync(elog, elog.replace(".log", ".old.log"));
|
||||||
|
}
|
||||||
|
console.debug = () => {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to move electron log file:", error.message);
|
||||||
|
}
|
||||||
|
app.commandLine.appendSwitch("log-file", elog);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This promise resolves when the app has finished initializing,
|
* This promise resolves when the app has finished initializing,
|
||||||
* allowing to safely create browser windows and perform other
|
* allowing to safely create browser windows and perform other
|
||||||
* initialization tasks.
|
* initialization tasks.
|
||||||
*/
|
*/
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
const lock = app.requestSingleInstanceLock();
|
if (!(await initApp()) || !(await initArgs()) || !(await initLog())) {
|
||||||
if (!lock) {
|
return;
|
||||||
console.error("TouchKio is already running");
|
|
||||||
return app.quit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show used arguments
|
||||||
|
const args = Object.assign({}, ARGS);
|
||||||
|
if ("mqtt_password" in args) {
|
||||||
|
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
|
||||||
|
}
|
||||||
|
console.info(`Arguments: ${JSON.stringify(args, null, 2)}`);
|
||||||
|
process.stdout.write("\n");
|
||||||
|
|
||||||
|
// Chained init functions
|
||||||
|
const chained = [
|
||||||
|
["webview.js", webview.init],
|
||||||
|
["hardware.js", hardware.init],
|
||||||
|
["integration.js", integration.init],
|
||||||
|
];
|
||||||
|
for (const [name, init] of chained) {
|
||||||
|
console.debug(`${name}: init()`);
|
||||||
|
if (!(await init())) {
|
||||||
|
console.debug(`${name}: init() --> aborted`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the global app object.
|
||||||
|
*
|
||||||
|
* @returns {bool} Returns true if the initialization was successful.
|
||||||
|
*/
|
||||||
|
const initApp = async () => {
|
||||||
|
const packageJsonPath = path.join(app.getAppPath(), "package.json");
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||||
|
|
||||||
|
const buildJsonPath = path.join(app.getAppPath(), "build.json");
|
||||||
|
const buildFileExists = fs.existsSync(buildJsonPath);
|
||||||
|
|
||||||
|
// Set required app infos
|
||||||
|
APP.name = app.getName();
|
||||||
|
APP.title = packageJson.title;
|
||||||
|
APP.version = app.getVersion();
|
||||||
|
APP.path = app.getAppPath();
|
||||||
|
APP.icon = path.join(app.getAppPath(), "img", "icon.png");
|
||||||
|
APP.log = path.join(app.getPath("logs"), "main.log");
|
||||||
|
APP.cache = path.join(app.getPath("userData"), "Cache");
|
||||||
|
APP.config = app.getPath("userData");
|
||||||
|
|
||||||
|
// Set additional update infos
|
||||||
|
APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`;
|
||||||
|
APP.releases = {
|
||||||
|
url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`,
|
||||||
|
latest: null,
|
||||||
|
};
|
||||||
|
APP.issues = `https://github.com/${packageJson.author}/${packageJson.name}/issues`;
|
||||||
|
APP.scripts = {
|
||||||
|
install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set additional build infos
|
||||||
|
APP.build = {};
|
||||||
|
if (buildFileExists) {
|
||||||
|
APP.build = JSON.parse(fs.readFileSync(buildJsonPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request single app instance lock
|
||||||
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
console.error(`${APP.title} is already running`);
|
||||||
|
return app.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register app quit events
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
APP.exiting = true;
|
||||||
|
});
|
||||||
|
app.on("will-quit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
process.exitCode = process.exitCode !== 0 ? 1 : 0;
|
||||||
|
const level = process.exitCode === 0 ? "warn" : "error";
|
||||||
|
console[level](`${APP.title} Terminated (${process.exitCode})`);
|
||||||
|
app.exit(process.exitCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register process exit events
|
||||||
|
["SIGINT", "SIGTERM", "SIGQUIT", "exit"].forEach((signal) => {
|
||||||
|
process.on(signal, () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exitCode = 0;
|
||||||
|
APP.exiting = true;
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
powerMonitor.on("shutdown", () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exitCode = 0;
|
||||||
|
APP.exiting = true;
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the global args object.
|
||||||
|
*
|
||||||
|
* @returns {bool} Returns true if the initialization was successful.
|
||||||
|
*/
|
||||||
|
const initArgs = async () => {
|
||||||
let args = parseArgs(process);
|
let args = parseArgs(process);
|
||||||
let argsProvided = !!Object.keys(args).length;
|
let argsProvided = !!Object.keys(args).length;
|
||||||
|
|
||||||
let argsFilePath = path.join(app.getPath("userData"), "Arguments.json");
|
let argsFilePath = path.join(APP.config, "Arguments.json");
|
||||||
let argsFileExists = fs.existsSync(argsFilePath);
|
let argsFileExists = fs.existsSync(argsFilePath);
|
||||||
|
|
||||||
|
let argsFileHashPath = path.join(APP.cache, "Arguments.hash");
|
||||||
|
let argsFileHashExists = fs.existsSync(argsFileHashPath);
|
||||||
|
|
||||||
|
// Show version and release info
|
||||||
|
if ("help" in args || "version" in args) {
|
||||||
|
let build = "";
|
||||||
|
if (APP.build.id) {
|
||||||
|
build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`;
|
||||||
|
}
|
||||||
|
console.info(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`);
|
||||||
|
return app.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Setup arguments from file path
|
// Setup arguments from file path
|
||||||
if ("setup" in args || (!argsProvided && !argsFileExists)) {
|
if ((!argsProvided && !argsFileExists) || "setup" in args) {
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
do {
|
do {
|
||||||
args = await promptArgs(process);
|
args = await promptArgs(process);
|
||||||
} while (!Object.keys(args).length);
|
} while (!Object.keys(args).length);
|
||||||
writeArgs(argsFilePath, args);
|
writeArgs(argsFilePath, args);
|
||||||
} else if (!argsProvided && argsFileExists) {
|
} else if (argsFileExists) {
|
||||||
args = readArgs(argsFilePath);
|
args = { ...readArgs(argsFilePath), ...args };
|
||||||
}
|
}
|
||||||
global.ARGS = args;
|
|
||||||
|
|
||||||
// Show used arguments
|
// Check arguments object
|
||||||
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`);
|
if (!Object.keys(args).length) {
|
||||||
|
console.error(`No arguments provided`);
|
||||||
|
return app.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Chained init functions
|
// Split url arguments
|
||||||
const chained = [webview.init, hardware.init, integration.init];
|
args.web_url = args.web_url || [];
|
||||||
for (const init of chained) {
|
if (!Array.isArray(args.web_url)) {
|
||||||
if (!(await init())) {
|
args.web_url = args.web_url.split(",").map((url) => url.trim());
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
// Calculate arguments hash
|
||||||
|
const argsFileHash = crypto.createHash("sha256").update(JSON.stringify(args)).digest("hex");
|
||||||
|
const argsUpdated = argsFileHashExists && argsFileHash !== fs.readFileSync(argsFileHashPath, "utf8");
|
||||||
|
if (argsUpdated && !("app_reset" in args)) {
|
||||||
|
args.app_reset = "arguments";
|
||||||
|
}
|
||||||
|
if (fs.existsSync(APP.cache)) {
|
||||||
|
fs.writeFileSync(argsFileHashPath, argsFileHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global arguments
|
||||||
|
ARGS = args;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the global log object.
|
||||||
|
*
|
||||||
|
* @returns {bool} Returns true if the initialization was successful.
|
||||||
|
*/
|
||||||
|
const initLog = async () => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(APP.log)) {
|
||||||
|
fs.renameSync(APP.log, APP.log.replace(".log", ".old.log"));
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to move main log file:", error.message);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Set log level and path
|
||||||
|
const level = "enable_logging" in ARGS ? "silly" : "app_debug" in ARGS ? "debug" : "verbose";
|
||||||
|
log.transports.file.level = level;
|
||||||
|
log.transports.console.level = level;
|
||||||
|
log.transports.file.resolvePathFn = () => {
|
||||||
|
return APP.log;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catch unhandled errors
|
||||||
|
log.errorHandler.startCatching({
|
||||||
|
showDialog: false,
|
||||||
|
onError({ error, versions }) {
|
||||||
|
if (!error?.message?.includes("Object has been destroyed")) {
|
||||||
|
const build = { ...APP.build, ...versions };
|
||||||
|
const whoopsie = "💥 Whoopsie!";
|
||||||
|
const section2 = `# Description\n- Hardware information?\n- How to reproduce?\n- Additional logs?\n`;
|
||||||
|
const section3 = `# Error\n\`\`\`bash\n${new Date().toISOString()}: ${error.stack}\n\`\`\`\n`;
|
||||||
|
const section4 = `# Application\n\`\`\`json\n${JSON.stringify(build, null, 2)}\n\`\`\`\n`;
|
||||||
|
const title = encodeURIComponent(`${whoopsie} - ${error}`);
|
||||||
|
const body = encodeURIComponent(`${section2}\n${section3}\n${section4}`);
|
||||||
|
console.error(`${whoopsie} -`, error, build);
|
||||||
|
console.info(`🪲 Report issue --> ${APP.issues}/new?title=${title}&body=${body}`);
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit console log
|
||||||
|
APP.logs = [];
|
||||||
|
log.hooks.push((message, transport, type) => {
|
||||||
|
const data = message.data.map((d) => (typeof d === "object" ? String(d?.message) || JSON.stringify(d) : String(d)));
|
||||||
|
const text = data.filter((s) => s && s.trim()).join(" ");
|
||||||
|
if (!text.startsWith("(node:") && type === "console") {
|
||||||
|
APP.logs.unshift({
|
||||||
|
time: message.date,
|
||||||
|
level: message.level,
|
||||||
|
text: text,
|
||||||
|
});
|
||||||
|
if (APP.logs.length > 10) {
|
||||||
|
APP.logs.splice(10);
|
||||||
|
}
|
||||||
|
EVENTS.emit("consoleLog", APP.logs[0]);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overwrite console log
|
||||||
|
Object.assign(console, log.functions);
|
||||||
|
console.silly("Welcome To The Jungle!");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses command-line arguments from the given process object.
|
* Parses command-line arguments from the given process object.
|
||||||
@@ -65,14 +286,13 @@ app.whenReady().then(async () => {
|
|||||||
* @returns {Object} An object mapping argument names to their corresponding values.
|
* @returns {Object} An object mapping argument names to their corresponding values.
|
||||||
*/
|
*/
|
||||||
const parseArgs = (proc) => {
|
const parseArgs = (proc) => {
|
||||||
let args = {};
|
const args = proc.argv.slice(1).filter((arg) => arg !== ".");
|
||||||
proc.argv.slice(1).forEach((arg) => {
|
return Object.fromEntries(
|
||||||
if (arg !== ".") {
|
args.flatMap((arg) => {
|
||||||
const [key, value] = arg.split("=");
|
const match = arg.match(/^--?([^=]+)(?:=(.*))?$/);
|
||||||
args[key.replace("--", "").replace("-", "_")] = value;
|
return match ? [[match[1].replace(/-/g, "_"), match[2] ?? null]] : [];
|
||||||
}
|
}),
|
||||||
});
|
);
|
||||||
return args;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,30 +364,41 @@ const promptArgs = async (proc) => {
|
|||||||
// Prompt questions and wait for the answers
|
// Prompt questions and wait for the answers
|
||||||
let args = {};
|
let args = {};
|
||||||
let ignore = [];
|
let ignore = [];
|
||||||
for (const { key, question, fallback } of prompts) {
|
try {
|
||||||
if (key === "mqtt") {
|
for (const { key, question, fallback } of prompts) {
|
||||||
const prompt = `${question} (${fallback}): `;
|
if (key === "mqtt") {
|
||||||
const answer = await read.question(prompt);
|
const prompt = `${question} (${fallback}): `;
|
||||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
const answer = await read.question(prompt);
|
||||||
if (!["y", "yes"].includes(value)) {
|
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||||
ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]);
|
if (!["y", "yes"].includes(value)) {
|
||||||
|
ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]);
|
||||||
|
}
|
||||||
|
} else if (key === "check") {
|
||||||
|
const json = JSON.stringify(args, null, 2);
|
||||||
|
const prompt = `${question}\n${json}\n(${fallback}): `;
|
||||||
|
const answer = await read.question(prompt);
|
||||||
|
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||||
|
if (!["y", "yes"].includes(value)) {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
} else if (!ignore.includes(key)) {
|
||||||
|
const prompt = `${question} (${fallback}): `;
|
||||||
|
const answer = await read.question(prompt);
|
||||||
|
const value = answer.trim() || fallback;
|
||||||
|
if (key === "web_url") {
|
||||||
|
args[key] = value.split(",").map((v) => v.trim());
|
||||||
|
} else {
|
||||||
|
args[key] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (key === "check") {
|
|
||||||
const json = JSON.stringify(args, null, 2);
|
|
||||||
const prompt = `${question}\n${json}\n(${fallback}): `;
|
|
||||||
const answer = await read.question(prompt);
|
|
||||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
|
||||||
if (!["y", "yes"].includes(value)) {
|
|
||||||
args = {};
|
|
||||||
}
|
|
||||||
} else if (!ignore.includes(key)) {
|
|
||||||
const prompt = `${question} (${fallback}): `;
|
|
||||||
const answer = await read.question(prompt);
|
|
||||||
const value = answer.trim() || fallback;
|
|
||||||
args[key] = value;
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`\n${error.message}`);
|
||||||
|
args = {};
|
||||||
|
app.exit(1);
|
||||||
|
} finally {
|
||||||
|
read.close();
|
||||||
}
|
}
|
||||||
read.close();
|
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
};
|
};
|
||||||
@@ -179,11 +410,15 @@ const promptArgs = async (proc) => {
|
|||||||
* @param {Object} args - The arguments object.
|
* @param {Object} args - The arguments object.
|
||||||
*/
|
*/
|
||||||
const writeArgs = (path, args) => {
|
const writeArgs = (path, args) => {
|
||||||
const argc = Object.assign({}, args);
|
try {
|
||||||
if ("mqtt_password" in argc) {
|
const argc = Object.assign({}, args);
|
||||||
argc.mqtt_password = encrypt(argc.mqtt_password);
|
if ("mqtt_password" in argc) {
|
||||||
|
argc.mqtt_password = encrypt(argc.mqtt_password);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to write ${path}:`, error.message);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,11 +428,16 @@ const writeArgs = (path, args) => {
|
|||||||
* @returns {Object} The arguments object.
|
* @returns {Object} The arguments object.
|
||||||
*/
|
*/
|
||||||
const readArgs = (path) => {
|
const readArgs = (path) => {
|
||||||
const args = JSON.parse(fs.readFileSync(path));
|
try {
|
||||||
if ("mqtt_password" in args) {
|
const args = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||||
args.mqtt_password = decrypt(args.mqtt_password);
|
if ("mqtt_password" in args) {
|
||||||
|
args.mqtt_password = decrypt(args.mqtt_password);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse ${path}:`, error.message);
|
||||||
}
|
}
|
||||||
return args;
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,7 +448,7 @@ const readArgs = (path) => {
|
|||||||
*/
|
*/
|
||||||
const encrypt = (value) => {
|
const encrypt = (value) => {
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32);
|
const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
|
||||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||||
let encrypted = cipher.update(value, "utf8", "hex");
|
let encrypted = cipher.update(value, "utf8", "hex");
|
||||||
encrypted += cipher.final("hex");
|
encrypted += cipher.final("hex");
|
||||||
@@ -224,7 +464,7 @@ const encrypt = (value) => {
|
|||||||
const decrypt = (value) => {
|
const decrypt = (value) => {
|
||||||
const p = Buffer.from(value, "base64").toString("utf8").split(":");
|
const p = Buffer.from(value, "base64").toString("utf8").split(":");
|
||||||
const iv = Buffer.from(p.shift(), "hex");
|
const iv = Buffer.from(p.shift(), "hex");
|
||||||
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32);
|
const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
|
||||||
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||||
const buffer = Buffer.from(p.join(":"), "hex");
|
const buffer = Buffer.from(p.join(":"), "hex");
|
||||||
let decrypted = decipher.update(buffer, "binary", "utf8");
|
let decrypted = decipher.update(buffer, "binary", "utf8");
|
||||||
|
|||||||
+67
-52
@@ -1,72 +1,64 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Read arguments
|
||||||
|
ARG_EARLY=false
|
||||||
|
ARG_UPDATE=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
early) ARG_EARLY=true ;;
|
||||||
|
update) ARG_UPDATE=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Determine system architecture
|
# Determine system architecture
|
||||||
echo -e "Determining system architecture..."
|
echo -e "Determining system architecture..."
|
||||||
|
|
||||||
BITS=$(getconf LONG_BIT)
|
BITS=$(getconf LONG_BIT)
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in
|
||||||
aarch64)
|
x86_64) ARCH="x64" ;;
|
||||||
ARCH="arm64"
|
aarch64) ARCH="arm64" ;;
|
||||||
;;
|
*) { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; } ;;
|
||||||
x86_64)
|
|
||||||
ARCH="x64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$BITS" -ne 64 ]; then
|
[ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; }
|
||||||
echo "Architecture $ARCH running $BITS-bit operating system is not supported."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Architecture $ARCH running $BITS-bit operating system is supported."
|
echo "Architecture $ARCH running $BITS-bit operating system is supported."
|
||||||
|
|
||||||
# Download the latest .deb package
|
# Download the latest .deb package
|
||||||
echo -e "\nDownloading the latest release..."
|
echo -e "\nDownloading the latest release..."
|
||||||
|
|
||||||
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
|
|
||||||
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
|
|
||||||
sed 's/"browser_download_url": "//;s/"//g')
|
|
||||||
|
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
|
chmod 755 "$TMP_DIR"
|
||||||
|
|
||||||
|
JSON=$(wget -qO- "https://api.github.com/repos/leukipp/touchkio/releases" | tr -d '\r\n')
|
||||||
|
if $ARG_EARLY; then
|
||||||
|
DEB_REG='"prerelease":\s*(true|false).*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
|
||||||
|
else
|
||||||
|
DEB_REG='"prerelease":\s*false.*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEB_URL=$(echo "$JSON" | grep -oP "$DEB_REG" | head -n 1)
|
||||||
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
||||||
|
|
||||||
if [ -z "$DEB_URL" ]; then
|
[ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
|
||||||
echo "Download url for .deb file not found."
|
wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then
|
|
||||||
echo "Failed to download the .deb file."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install the latest .deb package
|
# Install the latest .deb package
|
||||||
echo -e "\nInstalling the latest release..."
|
echo -e "\nInstalling the latest release..."
|
||||||
|
|
||||||
if ! command -v dpkg &> /dev/null; then
|
command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; }
|
||||||
echo "Package manager dpkg was not found."
|
sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; }
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! sudo dpkg -i "$DEB_PATH"; then
|
|
||||||
echo "Installation of .deb file failed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the systemd user service
|
# Create the systemd user service
|
||||||
echo -e "\nCreating systemd user service..."
|
echo -e "\nCreating systemd user service..."
|
||||||
|
|
||||||
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service"
|
SERVICE_NAME="touchkio.service"
|
||||||
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; }
|
SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME"
|
||||||
|
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE."; exit 1; }
|
||||||
|
|
||||||
bash -c "cat << EOF > \"$SERVICE_FILE\"
|
SERVICE_CONTENT="[Unit]
|
||||||
[Unit]
|
|
||||||
Description=TouchKio
|
Description=TouchKio
|
||||||
After=graphical.target
|
After=graphical.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/touchkio
|
ExecStart=/usr/bin/touchkio
|
||||||
@@ -74,35 +66,58 @@ Restart=on-failure
|
|||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target"
|
||||||
EOF"
|
|
||||||
|
|
||||||
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
if $ARG_UPDATE; then
|
||||||
echo "Service $SERVICE_FILE enabled"
|
if systemctl --user --quiet is-active "${SERVICE_NAME}"; then
|
||||||
|
systemctl --user restart "${SERVICE_NAME}"
|
||||||
|
echo "Existing $SERVICE_NAME restarted."
|
||||||
|
else
|
||||||
|
echo "Existing $SERVICE_NAME not running, start touchkio manually."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVICE_CREATE=true
|
||||||
|
if [ -f "$SERVICE_FILE" ]; then
|
||||||
|
read -p "Service $SERVICE_FILE exists, overwrite? (y/N) " overwrite
|
||||||
|
[[ ${overwrite:-n} == [Yy]* ]] || SERVICE_CREATE=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $SERVICE_CREATE; then
|
||||||
|
echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { echo "Failed to write to $SERVICE_FILE."; exit 1; }
|
||||||
|
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE."; exit 1; }
|
||||||
|
echo "Service $SERVICE_FILE enabled."
|
||||||
|
else
|
||||||
|
echo "Service $SERVICE_FILE not created."
|
||||||
|
fi
|
||||||
|
|
||||||
# Export display variables
|
# Export display variables
|
||||||
echo -e "\nExporting display variables..."
|
echo -e "\nExporting display variables..."
|
||||||
|
|
||||||
if [ -z "$DISPLAY" ]; then
|
if [ -z "$DISPLAY" ]; then
|
||||||
export DISPLAY=":0"
|
export DISPLAY=":0"
|
||||||
echo "DISPLAY was not set, defaulting to \"$DISPLAY\""
|
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
|
||||||
else
|
else
|
||||||
echo "DISPLAY is set to \"$DISPLAY\""
|
echo "DISPLAY is set to \"$DISPLAY\"."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$WAYLAND_DISPLAY" ]; then
|
if [ -z "$WAYLAND_DISPLAY" ]; then
|
||||||
export WAYLAND_DISPLAY="wayland-0"
|
export WAYLAND_DISPLAY="wayland-0"
|
||||||
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\""
|
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"."
|
||||||
else
|
else
|
||||||
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\""
|
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start the setup mode
|
# Start the setup mode
|
||||||
echo ""
|
read -p $'\nStart touchkio setup? (Y/n) ' setup
|
||||||
read -p "Start touchkio setup? (Y/n) " answer
|
|
||||||
if [[ ${answer:-y} == [Yy]* ]]; then
|
if [[ ${setup:-y} == [Yy]* ]]; then
|
||||||
echo "/usr/bin/touchkio --setup"
|
echo "/usr/bin/touchkio --setup"
|
||||||
/usr/bin/touchkio --setup
|
/usr/bin/touchkio --setup
|
||||||
else
|
else
|
||||||
echo "/usr/bin/touchkio"
|
echo "/usr/bin/touchkio"
|
||||||
/usr/bin/touchkio
|
/usr/bin/touchkio
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|||||||
+1221
File diff suppressed because it is too large
Load Diff
+1192
File diff suppressed because it is too large
Load Diff
+1503
File diff suppressed because it is too large
Load Diff
+13
-9
@@ -1,27 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "touchkio",
|
"name": "touchkio",
|
||||||
|
"title": "TouchKio",
|
||||||
"description": "Home Assistant Touch Kiosk",
|
"description": "Home Assistant Touch Kiosk",
|
||||||
"homepage": "https://github.com/leukipp/touchkio",
|
"homepage": "https://github.com/leukipp/touchkio",
|
||||||
"author": "leukipp",
|
"author": "leukipp",
|
||||||
"version": "1.1.2",
|
"version": "1.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron . --web-url=https://demo.home-assistant.io",
|
"start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --trace-warnings",
|
||||||
"build": "electron-forge make"
|
"build": "electron-forge make"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.6.0",
|
"@electron-forge/cli": "^7.10.2",
|
||||||
"@electron-forge/maker-deb": "^7.6.0",
|
"@electron-forge/maker-deb": "^7.10.2",
|
||||||
"@electron-forge/maker-zip": "^7.6.0",
|
"@electron-forge/maker-zip": "^7.10.2",
|
||||||
"@electron-forge/publisher-github": "^7.6.0",
|
"@electron-forge/publisher-github": "^7.10.2",
|
||||||
"electron": "^33.3.1",
|
"electron": "^39.2.4",
|
||||||
"prettier": "^3.4.2"
|
"jszip": "^3.10.1",
|
||||||
|
"prettier": "^3.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mqtt": "^5.10.3"
|
"axios": "^1.13.2",
|
||||||
|
"electron-log": "^5.4.3",
|
||||||
|
"mqtt": "^5.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-729
@@ -1,729 +0,0 @@
|
|||||||
const os = require("os");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const fsp = require("fs/promises");
|
|
||||||
const cpr = require("child_process");
|
|
||||||
|
|
||||||
global.HARDWARE = global.HARDWARE || {
|
|
||||||
initialized: false,
|
|
||||||
status: null,
|
|
||||||
session: {},
|
|
||||||
support: {},
|
|
||||||
display: {
|
|
||||||
status: {
|
|
||||||
path: null,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
brightness: {
|
|
||||||
path: null,
|
|
||||||
value: null,
|
|
||||||
max: null,
|
|
||||||
},
|
|
||||||
notifiers: [],
|
|
||||||
},
|
|
||||||
keyboard: {
|
|
||||||
visible: null,
|
|
||||||
notifiers: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the hardware with the provided arguments.
|
|
||||||
*
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async () => {
|
|
||||||
if (!compatibleDevice()) {
|
|
||||||
console.warn("Device not supported");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init globals
|
|
||||||
HARDWARE.session.user = sessionUser();
|
|
||||||
HARDWARE.session.type = sessionType();
|
|
||||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
|
||||||
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
|
||||||
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
|
||||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
|
||||||
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
|
||||||
HARDWARE.initialized = true;
|
|
||||||
HARDWARE.status = "Supported";
|
|
||||||
|
|
||||||
// Show supported features
|
|
||||||
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
|
||||||
|
|
||||||
// Show session infos
|
|
||||||
console.log("\nUser:", HARDWARE.session.user);
|
|
||||||
console.log("Session:", HARDWARE.session.type);
|
|
||||||
|
|
||||||
// Show device infos
|
|
||||||
console.log("\nModel:", getModel());
|
|
||||||
console.log("Vendor:", getVendor());
|
|
||||||
console.log("Serial Number:", getSerialNumber());
|
|
||||||
console.log("Host Name:", getHostName());
|
|
||||||
|
|
||||||
// Show system infos
|
|
||||||
console.log("\nUp Time:", getUpTime());
|
|
||||||
console.log("Memory Size:", getMemorySize());
|
|
||||||
console.log("Memory Usage:", getMemoryUsage());
|
|
||||||
console.log("Processor Usage:", getProcessorUsage());
|
|
||||||
console.log("Processor Temperature:", getProcessorTemperature());
|
|
||||||
|
|
||||||
// Show display infos
|
|
||||||
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
|
||||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
|
||||||
|
|
||||||
// Check for keyboard visibility
|
|
||||||
setKeyboardVisibility("OFF", (reply, error) => {
|
|
||||||
if (!reply || error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dbusMonitor("/sm/puri/OSK0", (property, error) => {
|
|
||||||
if (!property || error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
|
||||||
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for display changes
|
|
||||||
interval(update, 1000);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared hardware properties.
|
|
||||||
*/
|
|
||||||
const update = async () => {
|
|
||||||
if (!HARDWARE.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display status has changed
|
|
||||||
if (HARDWARE.support.displayStatus) {
|
|
||||||
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;
|
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display brightness has changed
|
|
||||||
if (HARDWARE.support.displayBrightness) {
|
|
||||||
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;
|
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies device compatibility by checking the presence of necessary sys paths.
|
|
||||||
*
|
|
||||||
* @returns {bool} Returns true if all paths exists.
|
|
||||||
*/
|
|
||||||
const compatibleDevice = () => {
|
|
||||||
if (os.platform() !== "linux") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
|
||||||
return paths.every((path) => fs.existsSync(path));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the session user name using `os.userInfo()`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} Returns session user name or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const sessionUser = () => {
|
|
||||||
try {
|
|
||||||
return os.userInfo().username;
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the session type for the user using `loginctl`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} Returns session type 'x11'/'wayland' or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const sessionType = () => {
|
|
||||||
if (!commandExists("loginctl")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return execSyncCommand("loginctl", [
|
|
||||||
"show-session",
|
|
||||||
"$(loginctl show-user $(whoami) -p Display --value)",
|
|
||||||
"-p Type --value",
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the session type for the logged in user.
|
|
||||||
*
|
|
||||||
* @returns {bool} Returns true if the session matches the type.
|
|
||||||
*/
|
|
||||||
const session = (type) => {
|
|
||||||
return HARDWARE.session.type == type;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the model name using `/sys/firmware/devicetree/base/model` or `/sys/class/dmi/id/product_name`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The model name of the device or 'Generic' if not found.
|
|
||||||
*/
|
|
||||||
const getModel = () => {
|
|
||||||
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
return execSyncCommand("sudo", ["cat", path]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "Generic";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the vendor name using `/sys/class/dmi/id/board_vendor`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The vendor name of the device or 'Generic' if not found.
|
|
||||||
*/
|
|
||||||
const getVendor = () => {
|
|
||||||
const paths = ["/sys/class/dmi/id/board_vendor"];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
return execSyncCommand("sudo", ["cat", path]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const model = getModel();
|
|
||||||
if (model.includes("Raspberry Pi")) {
|
|
||||||
return "Raspberry Pi Ltd";
|
|
||||||
}
|
|
||||||
return "Generic";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The serial number of the device or '123456' if not found.
|
|
||||||
*/
|
|
||||||
const getSerialNumber = () => {
|
|
||||||
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
return execSyncCommand("sudo", ["cat", path]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "123456";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the host machine id using `/etc/machine-id`.
|
|
||||||
*
|
|
||||||
* @returns {string} The machine id of the system or '123456' if not found.
|
|
||||||
*/
|
|
||||||
const getMachineId = () => {
|
|
||||||
return execSyncCommand("cat", ["/etc/machine-id"]) || "123456";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the host name of the current system using `os.hostname()`.
|
|
||||||
*
|
|
||||||
* @returns {string} The host name of the system.
|
|
||||||
*/
|
|
||||||
const getHostName = () => {
|
|
||||||
return os.hostname();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the up time of the system in minutes using `os.uptime()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The up time of the system in minutes.
|
|
||||||
*/
|
|
||||||
const getUpTime = () => {
|
|
||||||
return os.uptime() / 60;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total available memory in gibibytes using `os.totalmem()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The total available memory in GiB.
|
|
||||||
*/
|
|
||||||
const getMemorySize = () => {
|
|
||||||
return os.totalmem() / 1024 ** 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current memory usage as a percentage using `os.totalmem()` and `os.freemem()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The percentage of used memory.
|
|
||||||
*/
|
|
||||||
const getMemoryUsage = () => {
|
|
||||||
return ((os.totalmem() - os.freemem()) / os.totalmem()) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the CPU load average over the last 5 minutes as a percentage using `os.loadavg()` and `os.cpus()`.
|
|
||||||
*
|
|
||||||
* @returns {number} The CPU load average percentage over the last 5 minutes.
|
|
||||||
*/
|
|
||||||
const getProcessorUsage = () => {
|
|
||||||
return (os.loadavg()[1] / os.cpus().length) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current CPU temperature using `/sys/class/thermal`.
|
|
||||||
*
|
|
||||||
* @returns {number|null} The CPU temperature in degrees celsius or null if nothing was found.
|
|
||||||
*/
|
|
||||||
const getProcessorTemperature = () => {
|
|
||||||
const thermal = "/sys/class/thermal";
|
|
||||||
for (const zone of fs.readdirSync(thermal)) {
|
|
||||||
const typeFile = path.join(thermal, zone, "type");
|
|
||||||
const tempFile = path.join(thermal, zone, "temp");
|
|
||||||
if (!fs.existsSync(typeFile) || !fs.existsSync(tempFile)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const type = fs.readFileSync(typeFile, "utf8").trim();
|
|
||||||
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz"].includes(type)) {
|
|
||||||
return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current display status path using `/sys/class/drm`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The display status path or null if nothing was found.
|
|
||||||
*/
|
|
||||||
const getDisplayStatusPath = () => {
|
|
||||||
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 (content === "connected") {
|
|
||||||
return path.join(drm, card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current display power status using `wlopm` or `xset`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getDisplayStatus = () => {
|
|
||||||
if (!HARDWARE.support.displayStatus) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (session("wayland") && commandExists("wlopm")) {
|
|
||||||
const status = execSyncCommand("wlopm", []);
|
|
||||||
if (status !== null) {
|
|
||||||
const out = status.split("\n")[0].split(" ");
|
|
||||||
return out.pop().toUpperCase();
|
|
||||||
}
|
|
||||||
} else if (session("x11") && commandExists("xset")) {
|
|
||||||
const status = execSyncCommand("xset", ["-q"]);
|
|
||||||
if (status !== null) {
|
|
||||||
const on = /Monitor is On/.test(status);
|
|
||||||
return on ? "ON" : "OFF";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the display power status using `wlopm` or `xset`.
|
|
||||||
*
|
|
||||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
|
||||||
* the appropriate command to set the display status.
|
|
||||||
*
|
|
||||||
* @param {string} status - The desired status ('ON' or 'OFF').
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const setDisplayStatus = (status, callback = null) => {
|
|
||||||
if (!HARDWARE.support.displayStatus) {
|
|
||||||
if (typeof callback === "function") callback(null, "Not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!["ON", "OFF"].includes(status)) {
|
|
||||||
console.error("Status must be 'ON' or 'OFF'");
|
|
||||||
if (typeof callback === "function") callback(null, "Invalid status");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (session("wayland") && commandExists("wlopm")) {
|
|
||||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
|
||||||
} else if (session("x11") && commandExists("xset")) {
|
|
||||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current display brightness path using `/sys/class/backlight`.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The display brightness path or null if nothing was found.
|
|
||||||
*/
|
|
||||||
const getDisplayBrightnessPath = () => {
|
|
||||||
const backlight = "/sys/class/backlight";
|
|
||||||
for (const address of fs.readdirSync(backlight)) {
|
|
||||||
const brightnessFile = path.join(backlight, address, "brightness");
|
|
||||||
if (!fs.existsSync(brightnessFile)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return path.join(backlight, address);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
|
|
||||||
*
|
|
||||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getDisplayBrightnessMax = () => {
|
|
||||||
if (!HARDWARE.support.displayBrightness) {
|
|
||||||
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.support.displayBrightness) {
|
|
||||||
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 {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const setDisplayBrightness = (brightness, callback = null) => {
|
|
||||||
if (!HARDWARE.support.displayBrightness) {
|
|
||||||
if (typeof callback === "function") callback(null, "Not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
|
||||||
console.error("Brightness must be a number between 1 and 100");
|
|
||||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const max = HARDWARE.display.brightness.max;
|
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
|
||||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
|
||||||
proc.stdin.write(value.toString());
|
|
||||||
proc.stdin.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the keyboard visibility using global properties.
|
|
||||||
*
|
|
||||||
* @returns {string|null} The keyboard visibility as 'ON'/'OFF' or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const getKeyboardVisibility = () => {
|
|
||||||
if (!HARDWARE.support.keyboardVisibility) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return HARDWARE.keyboard.visibility ? "ON" : "OFF";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the keyboard visibility using `dbus-send`.
|
|
||||||
*
|
|
||||||
* This function takes a desired visibility ('ON' or 'OFF') and executes
|
|
||||||
* the appropriate command to show or hide the keyboard.
|
|
||||||
*
|
|
||||||
* @param {boolean} visibility - The desired visibility ('ON' or 'OFF').
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const setKeyboardVisibility = (visibility, callback = null) => {
|
|
||||||
if (!HARDWARE.support.keyboardVisibility) {
|
|
||||||
if (typeof callback === "function") callback(null, "Not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!["ON", "OFF"].includes(visibility)) {
|
|
||||||
console.error("Visibility must be 'ON' or 'OFF'");
|
|
||||||
if (typeof callback === "function") callback(null, "Invalid visibility");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const visible = visibility === "ON";
|
|
||||||
HARDWARE.keyboard.visibility = visible;
|
|
||||||
dbusCall("/sm/puri/OSK0", "SetVisible", [`boolean:${visible}`], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if system upgrades are available using `apt`.
|
|
||||||
*
|
|
||||||
* @returns {Array<string>} A list of package names that are available for upgrade.
|
|
||||||
*/
|
|
||||||
const checkPackageUpgrades = () => {
|
|
||||||
if (!commandExists("apt")) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const output = execSyncCommand("apt", ["list", "--upgradable", "2>/dev/null"]);
|
|
||||||
const packages = (output || "").trim().split("\n");
|
|
||||||
packages.shift();
|
|
||||||
return packages;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuts down the system using `sudo shutdown -h now`.
|
|
||||||
*
|
|
||||||
* This function executes the command asynchronously.
|
|
||||||
* The output of the command will be provided through the callback function.
|
|
||||||
*
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const shutdownSystem = (callback = null) => {
|
|
||||||
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reboots the system using `sudo reboot`.
|
|
||||||
*
|
|
||||||
* This function executes the command asynchronously.
|
|
||||||
* The output of the command will be provided through the callback function.
|
|
||||||
*
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const rebootSystem = (callback = null) => {
|
|
||||||
execAsyncCommand("sudo", ["reboot"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a process is running using `pidof`.
|
|
||||||
*
|
|
||||||
* @param {string} name - The process name to check.
|
|
||||||
* @returns {boolean} Returns true if the process runs.
|
|
||||||
*/
|
|
||||||
const processRuns = (name) => {
|
|
||||||
try {
|
|
||||||
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
|
|
||||||
} catch {}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a command is available using `which`.
|
|
||||||
*
|
|
||||||
* @param {string} name - The command name to check.
|
|
||||||
* @returns {boolean} Returns true if the command is available.
|
|
||||||
*/
|
|
||||||
const commandExists = (name) => {
|
|
||||||
try {
|
|
||||||
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
|
|
||||||
} catch {}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a command synchronously and returns the output.
|
|
||||||
*
|
|
||||||
* @param {string} cmd - The command to execute.
|
|
||||||
* @param {Array<string>} args - The arguments for the command.
|
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
|
||||||
*/
|
|
||||||
const execSyncCommand = (cmd, args = []) => {
|
|
||||||
try {
|
|
||||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
|
||||||
return output.trim().replace(/\0/g, "");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Execute Sync:", error.message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a command asynchronously.
|
|
||||||
*
|
|
||||||
* @param {string} cmd - The command to execute.
|
|
||||||
* @param {Array<string>} args - The arguments for the command.
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
* @returns {object} The spawned process object.
|
|
||||||
*/
|
|
||||||
const execAsyncCommand = (cmd, args = [], callback = null) => {
|
|
||||||
let errorOutput = "";
|
|
||||||
let successOutput = "";
|
|
||||||
let proc = cpr.spawn(cmd, args);
|
|
||||||
proc.stderr.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
errorOutput += data.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proc.stdout.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
successOutput += data.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proc.on("close", (code) => {
|
|
||||||
try {
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
if (code === 0) {
|
|
||||||
callback(successOutput.trim().replace(/\0/g, ""), null);
|
|
||||||
} else {
|
|
||||||
callback(null, errorOutput.trim().replace(/\0/g, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Execute Async:", error.message);
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(null, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return proc;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a D-Bus method call synchronously using `dbus-send`.
|
|
||||||
*
|
|
||||||
* @param {string} path - The D-Bus object path.
|
|
||||||
* @param {string} method - The D-Bus method name.
|
|
||||||
* @param {Array<string>} values - The argument values for the D-Bus method.
|
|
||||||
* @param {function} callback - A callback function that receives the output or error.
|
|
||||||
*/
|
|
||||||
const dbusCall = (path, method, values, callback = null) => {
|
|
||||||
const cmd = "dbus-send";
|
|
||||||
const iface = path.slice(1).replace(/\//g, ".");
|
|
||||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
|
||||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
|
||||||
try {
|
|
||||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(output.trim().replace(/\0/g, ""), null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Call D-Bus:", error.message);
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(null, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
|
|
||||||
*
|
|
||||||
* @param {string} path - The D-Bus object path.
|
|
||||||
* @param {function} callback - A callback function that receives the changed property.
|
|
||||||
* @returns {object} The spawned process object.
|
|
||||||
*/
|
|
||||||
const dbusMonitor = (path, callback) => {
|
|
||||||
const cmd = "dbus-monitor";
|
|
||||||
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
|
||||||
const proc = cpr.spawn(cmd, args);
|
|
||||||
proc.stdout.on("data", (data) => {
|
|
||||||
try {
|
|
||||||
const signal = data.toString();
|
|
||||||
if (signal.includes("member=PropertiesChanged") && signal.includes(`path=${path}`)) {
|
|
||||||
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
|
|
||||||
if (dicts.length) {
|
|
||||||
dicts.forEach((dict) => {
|
|
||||||
const key = dict.match(/string "(.*?)"/);
|
|
||||||
const value = dict.match(/(?<=variant\s+)(.*)/);
|
|
||||||
if (key && value && typeof callback === "function") {
|
|
||||||
callback({ [key[1].trim()]: value[1].trim().split(" ").pop() }, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback({ Visible: `${HARDWARE.keyboard.visibility}` }, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Monitor D-Bus:", error.message);
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(null, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proc.stderr.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
console.error("Monitor D-Bus:", data.toString());
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(null, data.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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,
|
|
||||||
getModel,
|
|
||||||
getVendor,
|
|
||||||
getSerialNumber,
|
|
||||||
getMachineId,
|
|
||||||
getHostName,
|
|
||||||
getUpTime,
|
|
||||||
getMemorySize,
|
|
||||||
getMemoryUsage,
|
|
||||||
getProcessorUsage,
|
|
||||||
getProcessorTemperature,
|
|
||||||
getDisplayStatusPath,
|
|
||||||
getDisplayStatus,
|
|
||||||
setDisplayStatus,
|
|
||||||
getDisplayBrightnessPath,
|
|
||||||
getDisplayBrightnessMax,
|
|
||||||
getDisplayBrightness,
|
|
||||||
setDisplayBrightness,
|
|
||||||
getKeyboardVisibility,
|
|
||||||
setKeyboardVisibility,
|
|
||||||
checkPackageUpgrades,
|
|
||||||
shutdownSystem,
|
|
||||||
rebootSystem,
|
|
||||||
};
|
|
||||||
@@ -1,763 +0,0 @@
|
|||||||
const mqtt = require("mqtt");
|
|
||||||
const hardware = require("./hardware");
|
|
||||||
const { app } = require("electron");
|
|
||||||
|
|
||||||
global.INTEGRATION = global.INTEGRATION || {
|
|
||||||
initialized: false,
|
|
||||||
status: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the integration with the provided arguments.
|
|
||||||
*
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async () => {
|
|
||||||
if (!ARGS.mqtt_url) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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 model = hardware.getModel();
|
|
||||||
const vendor = hardware.getVendor();
|
|
||||||
const hostName = hardware.getHostName();
|
|
||||||
const serialNumber = hardware.getSerialNumber();
|
|
||||||
const serialNumberSuffix = serialNumber.slice(-6);
|
|
||||||
|
|
||||||
const deviceId = serialNumberSuffix.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
||||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
|
||||||
|
|
||||||
// Init globals
|
|
||||||
INTEGRATION.node = `rpi_${deviceId}`;
|
|
||||||
INTEGRATION.discovery = discovery;
|
|
||||||
INTEGRATION.device = {
|
|
||||||
name: `TouchKio ${deviceName}`,
|
|
||||||
model: model,
|
|
||||||
manufacturer: vendor,
|
|
||||||
serial_number: serialNumber,
|
|
||||||
identifiers: [INTEGRATION.node],
|
|
||||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connection settings
|
|
||||||
const options = user === null || password == null ? null : { username: user, password: password };
|
|
||||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
|
||||||
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
|
||||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
|
||||||
|
|
||||||
// Client connected
|
|
||||||
INTEGRATION.client
|
|
||||||
.on("connect", () => {
|
|
||||||
console.log(`MQTT Connected: ${url.toString()}\n`);
|
|
||||||
|
|
||||||
// Init client controls
|
|
||||||
initShutdown(INTEGRATION.client);
|
|
||||||
initReboot(INTEGRATION.client);
|
|
||||||
initRefresh(INTEGRATION.client);
|
|
||||||
initKiosk(INTEGRATION.client);
|
|
||||||
initDisplay(INTEGRATION.client);
|
|
||||||
initKeyboard(INTEGRATION.client);
|
|
||||||
|
|
||||||
// Init client sensors
|
|
||||||
initModel(INTEGRATION.client);
|
|
||||||
initSerialNumber(INTEGRATION.client);
|
|
||||||
initHostName(INTEGRATION.client);
|
|
||||||
initUpTime(INTEGRATION.client);
|
|
||||||
initMemorySize(INTEGRATION.client);
|
|
||||||
initMemoryUsage(INTEGRATION.client);
|
|
||||||
initProcessorUsage(INTEGRATION.client);
|
|
||||||
initProcessorTemperature(INTEGRATION.client);
|
|
||||||
initPackageUpgrades(INTEGRATION.client);
|
|
||||||
initHeartbeat(INTEGRATION.client);
|
|
||||||
initLastActive(INTEGRATION.client);
|
|
||||||
|
|
||||||
// Integration initialized
|
|
||||||
INTEGRATION.initialized = true;
|
|
||||||
INTEGRATION.status = "Online";
|
|
||||||
})
|
|
||||||
.on("error", (error) => {
|
|
||||||
console.error("MQTT:", error);
|
|
||||||
INTEGRATION.status = "Offline";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update sensor states from notifiers
|
|
||||||
HARDWARE.display.notifiers.push(() => {
|
|
||||||
updateDisplay(INTEGRATION.client);
|
|
||||||
});
|
|
||||||
HARDWARE.keyboard.notifiers.push(() => {
|
|
||||||
updateKeyboard(INTEGRATION.client);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update time sensors periodically (30s)
|
|
||||||
setInterval(() => {
|
|
||||||
updateHeartbeat(INTEGRATION.client);
|
|
||||||
updateLastActive(INTEGRATION.client);
|
|
||||||
}, 30 * 1000);
|
|
||||||
|
|
||||||
// Update system sensors periodically (1min)
|
|
||||||
setInterval(() => {
|
|
||||||
update();
|
|
||||||
}, 60 * 1000);
|
|
||||||
|
|
||||||
// Update package sensors periodically (60min)
|
|
||||||
setInterval(() => {
|
|
||||||
updatePackageUpgrades(INTEGRATION.client);
|
|
||||||
}, 3600 * 1000);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared integration properties.
|
|
||||||
*/
|
|
||||||
const update = async () => {
|
|
||||||
if (!INTEGRATION.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update client sensors
|
|
||||||
updateKiosk(INTEGRATION.client);
|
|
||||||
updateUpTime(INTEGRATION.client);
|
|
||||||
updateLastActive(INTEGRATION.client);
|
|
||||||
updateMemoryUsage(INTEGRATION.client);
|
|
||||||
updateProcessorUsage(INTEGRATION.client);
|
|
||||||
updateProcessorTemperature(INTEGRATION.client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the shutdown button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initShutdown = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/shutdown`;
|
|
||||||
const config = {
|
|
||||||
name: "Shutdown",
|
|
||||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:power",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Shutdown system...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
hardware.shutdownSystem();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the reboot button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initReboot = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/reboot`;
|
|
||||||
const config = {
|
|
||||||
name: "Reboot",
|
|
||||||
unique_id: `${INTEGRATION.node}_reboot`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:restart",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Rebooting system...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
hardware.rebootSystem();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the refresh button and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initRefresh = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/refresh`;
|
|
||||||
const config = {
|
|
||||||
name: "Refresh",
|
|
||||||
unique_id: `${INTEGRATION.node}_refresh`,
|
|
||||||
command_topic: `${root}/execute`,
|
|
||||||
icon: "mdi:web-refresh",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/execute`) {
|
|
||||||
console.log("Refreshing webview...");
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
WEBVIEW.view.webContents.reloadIgnoringCache();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/execute`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the kiosk select status and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initKiosk = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
|
||||||
const config = {
|
|
||||||
name: `Kiosk`,
|
|
||||||
unique_id: `${INTEGRATION.node}_kiosk`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
options: ["Framed", "Fullscreen", "Maximized", "Minimized", "Terminated"],
|
|
||||||
icon: "mdi:overscan",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/set`) {
|
|
||||||
const status = message.toString();
|
|
||||||
console.log("Set Kiosk Status:", status);
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
switch (status) {
|
|
||||||
case "Framed":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
break;
|
|
||||||
case "Fullscreen":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(true);
|
|
||||||
break;
|
|
||||||
case "Maximized":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
WEBVIEW.window.maximize();
|
|
||||||
break;
|
|
||||||
case "Minimized":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
WEBVIEW.window.minimize();
|
|
||||||
break;
|
|
||||||
case "Terminated":
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/set`);
|
|
||||||
updateKiosk(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the kiosk status via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateKiosk = (client) => {
|
|
||||||
const kiosk = WEBVIEW.status;
|
|
||||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
|
||||||
if (kiosk !== null) {
|
|
||||||
client.publish(`${root}/status`, `${kiosk}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the display status, brightness and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initDisplay = (client) => {
|
|
||||||
if (!HARDWARE.support.displayStatus) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
|
||||||
const config = {
|
|
||||||
name: `Display`,
|
|
||||||
unique_id: `${INTEGRATION.node}_display`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
icon: "mdi:monitor-shimmer",
|
|
||||||
platform: "light",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
if (HARDWARE.support.displayBrightness) {
|
|
||||||
config.brightness_scale = 100;
|
|
||||||
config.brightness_command_topic = `${root}/brightness/set`;
|
|
||||||
config.brightness_state_topic = `${root}/brightness/status`;
|
|
||||||
}
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/set`) {
|
|
||||||
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`)
|
|
||||||
.subscribe(`${root}/brightness/set`);
|
|
||||||
updateDisplay(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the display status, brightness via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateDisplay = (client) => {
|
|
||||||
const status = hardware.getDisplayStatus();
|
|
||||||
const brightness = hardware.getDisplayBrightness();
|
|
||||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
|
||||||
if (status !== null) {
|
|
||||||
client.publish(`${root}/status`, `${status}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
if (brightness !== null) {
|
|
||||||
client.publish(`${root}/brightness/status`, `${brightness}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the keyboard visibility and handles the execute logic.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initKeyboard = (client) => {
|
|
||||||
if (!HARDWARE.support.keyboardVisibility) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
|
||||||
const config = {
|
|
||||||
name: `Keyboard`,
|
|
||||||
unique_id: `${INTEGRATION.node}_keyboard`,
|
|
||||||
command_topic: `${root}/set`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
icon: "mdi:keyboard-close-outline",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
|
||||||
.on("message", (topic, message) => {
|
|
||||||
if (topic === `${root}/set`) {
|
|
||||||
const status = message.toString();
|
|
||||||
console.log("Set Keyboard Visibility:", status);
|
|
||||||
hardware.setKeyboardVisibility(status);
|
|
||||||
switch (status) {
|
|
||||||
case "OFF":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(true);
|
|
||||||
break;
|
|
||||||
case "ON":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
WEBVIEW.window.maximize();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(`${root}/set`);
|
|
||||||
updateKeyboard(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the keyboard visibility via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateKeyboard = (client) => {
|
|
||||||
const visibility = hardware.getKeyboardVisibility();
|
|
||||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
|
||||||
if (visibility !== null) {
|
|
||||||
client.publish(`${root}/status`, `${visibility}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the model sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initModel = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
|
||||||
const config = {
|
|
||||||
name: `Model`,
|
|
||||||
unique_id: `${INTEGRATION.node}_model`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:raspberry-pi",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateModel(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the model sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateModel = (client) => {
|
|
||||||
const model = hardware.getModel();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
|
||||||
if (model !== null) {
|
|
||||||
client.publish(`${root}/status`, `${model}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the serial number sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initSerialNumber = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
|
||||||
const config = {
|
|
||||||
name: `Serial Number`,
|
|
||||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:hexadecimal",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateSerialNumber(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the serial number sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateSerialNumber = (client) => {
|
|
||||||
const serialNumber = hardware.getSerialNumber();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
|
||||||
if (serialNumber !== null) {
|
|
||||||
client.publish(`${root}/status`, `${serialNumber}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the host name sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initHostName = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
|
||||||
const config = {
|
|
||||||
name: `Host Name`,
|
|
||||||
unique_id: `${INTEGRATION.node}_host_name`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:console-network",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateHostName(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the host name sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateHostName = (client) => {
|
|
||||||
const hostName = hardware.getHostName();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
|
||||||
if (hostName !== null) {
|
|
||||||
client.publish(`${root}/status`, `${hostName}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the up time sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initUpTime = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
|
||||||
const config = {
|
|
||||||
name: `Up Time`,
|
|
||||||
unique_id: `${INTEGRATION.node}_up_time`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "min",
|
|
||||||
icon: "mdi:timeline-clock",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateUpTime(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the up time sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateUpTime = (client) => {
|
|
||||||
const upTime = hardware.getUpTime();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
|
||||||
if (upTime !== null) {
|
|
||||||
client.publish(`${root}/status`, `${upTime}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the memory size sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initMemorySize = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
|
||||||
const config = {
|
|
||||||
name: `Memory Size`,
|
|
||||||
unique_id: `${INTEGRATION.node}_memory_size`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(2) }}",
|
|
||||||
unit_of_measurement: "GiB",
|
|
||||||
icon: "mdi:memory",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateMemorySize(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the memory size sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateMemorySize = (client) => {
|
|
||||||
const memorySize = hardware.getMemorySize();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
|
||||||
if (memorySize !== null) {
|
|
||||||
client.publish(`${root}/status`, `${memorySize}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the memory usage sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initMemoryUsage = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
|
||||||
const config = {
|
|
||||||
name: `Memory Usage`,
|
|
||||||
unique_id: `${INTEGRATION.node}_memory_usage`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "%",
|
|
||||||
icon: "mdi:memory-arrow-down",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateMemoryUsage(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the memory usage sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateMemoryUsage = (client) => {
|
|
||||||
const memoryUsage = hardware.getMemoryUsage();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
|
||||||
if (memoryUsage !== null) {
|
|
||||||
client.publish(`${root}/status`, `${memoryUsage}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the processor usage sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initProcessorUsage = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
|
||||||
const config = {
|
|
||||||
name: `Processor Usage`,
|
|
||||||
unique_id: `${INTEGRATION.node}_processor_usage`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "%",
|
|
||||||
icon: "mdi:cpu-64-bit",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateProcessorUsage(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the processor usage sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateProcessorUsage = (client) => {
|
|
||||||
const processorUsage = hardware.getProcessorUsage();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
|
||||||
if (processorUsage !== null) {
|
|
||||||
client.publish(`${root}/status`, `${processorUsage}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the processor temperature sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initProcessorTemperature = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
|
||||||
const config = {
|
|
||||||
name: `Processor Temperature`,
|
|
||||||
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "°C",
|
|
||||||
icon: "mdi:radiator",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateProcessorTemperature(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the processor temperature sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateProcessorTemperature = (client) => {
|
|
||||||
const processorTemperature = hardware.getProcessorTemperature();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
|
||||||
if (processorTemperature !== null) {
|
|
||||||
client.publish(`${root}/status`, `${processorTemperature}`, { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the package upgrades sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initPackageUpgrades = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
|
||||||
const config = {
|
|
||||||
name: `Package Upgrades`,
|
|
||||||
unique_id: `${INTEGRATION.node}_package_upgrades`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
json_attributes_topic: `${root}/attributes`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:package-down",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updatePackageUpgrades(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the package upgrades sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updatePackageUpgrades = (client) => {
|
|
||||||
const packages = hardware.checkPackageUpgrades();
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
|
||||||
if (packages !== null) {
|
|
||||||
const attributes = {
|
|
||||||
total: packages.length,
|
|
||||||
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
|
||||||
};
|
|
||||||
client.publish(`${root}/status`, `${attributes.total}`, { qos: 1, retain: true });
|
|
||||||
client.publish(`${root}/attributes`, JSON.stringify(attributes), { qos: 1, retain: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the heartbeat sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initHeartbeat = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
|
||||||
const config = {
|
|
||||||
name: `Heartbeat`,
|
|
||||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ value }}",
|
|
||||||
icon: "mdi:heart-flash",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateHeartbeat(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the heartbeat sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateHeartbeat = (client) => {
|
|
||||||
const now = new Date();
|
|
||||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
|
||||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
|
||||||
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the last active sensor.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const initLastActive = (client) => {
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
|
||||||
const config = {
|
|
||||||
name: `Last Active`,
|
|
||||||
unique_id: `${INTEGRATION.node}_last_active`,
|
|
||||||
state_topic: `${root}/status`,
|
|
||||||
value_template: "{{ (value | float) | round(0) }}",
|
|
||||||
unit_of_measurement: "min",
|
|
||||||
icon: "mdi:gesture-tap-hold",
|
|
||||||
device: INTEGRATION.device,
|
|
||||||
};
|
|
||||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
|
||||||
updateLastActive(client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the last active sensor via the mqtt connection.
|
|
||||||
*
|
|
||||||
* @param {Object} client - Instance of the mqtt client.
|
|
||||||
*/
|
|
||||||
const updateLastActive = (client) => {
|
|
||||||
const now = new Date();
|
|
||||||
const then = WEBVIEW.pointer.time;
|
|
||||||
const lastActive = Math.abs(now - then) / (1000 * 60);
|
|
||||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
|
||||||
client.publish(`${root}/status`, `${lastActive}`, { qos: 1, retain: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
-325
@@ -1,325 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const hardware = require("./hardware");
|
|
||||||
const integration = require("./integration");
|
|
||||||
const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron");
|
|
||||||
|
|
||||||
global.WEBVIEW = global.WEBVIEW || {
|
|
||||||
initialized: false,
|
|
||||||
status: null,
|
|
||||||
locked: false,
|
|
||||||
pointer: {
|
|
||||||
position: {},
|
|
||||||
time: new Date(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the webview with the provided arguments.
|
|
||||||
*
|
|
||||||
* @returns {boolean} Returns true if the initialization was successful.
|
|
||||||
*/
|
|
||||||
const init = async () => {
|
|
||||||
if (!ARGS.web_url) {
|
|
||||||
console.error("Please provide the '--web-url' parameter");
|
|
||||||
return app.quit();
|
|
||||||
}
|
|
||||||
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 fullscreen = ARGS.app_debug !== "true";
|
|
||||||
|
|
||||||
// Init global layout
|
|
||||||
WEBVIEW.viewZoom = zoom;
|
|
||||||
WEBVIEW.viewTheme = theme;
|
|
||||||
WEBVIEW.widgetTheme = theme;
|
|
||||||
WEBVIEW.widgetEnabled = widget;
|
|
||||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
|
||||||
|
|
||||||
// Init global root window
|
|
||||||
WEBVIEW.window = new BaseWindow({
|
|
||||||
title: `TouchKio - ${url.host}`,
|
|
||||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
|
||||||
});
|
|
||||||
WEBVIEW.window.setMenuBarVisibility(false);
|
|
||||||
WEBVIEW.window.setFullScreen(fullscreen);
|
|
||||||
if (!fullscreen) {
|
|
||||||
WEBVIEW.window.maximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init global webview
|
|
||||||
WEBVIEW.view = new WebContentsView({
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.view);
|
|
||||||
WEBVIEW.view.webContents.loadURL(url.toString());
|
|
||||||
WEBVIEW.view.setBackgroundColor("#FFFFFFFF");
|
|
||||||
|
|
||||||
// Init global widget
|
|
||||||
WEBVIEW.widget = new WebContentsView({
|
|
||||||
transparent: true,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
|
|
||||||
WEBVIEW.widget.webContents.loadFile(path.join(__dirname, "widget.html"));
|
|
||||||
WEBVIEW.widget.setBackgroundColor("#00000000");
|
|
||||||
|
|
||||||
// Register events
|
|
||||||
windowEvents();
|
|
||||||
widgetEvents();
|
|
||||||
domEvents();
|
|
||||||
appEvents();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shared webview properties.
|
|
||||||
*/
|
|
||||||
const update = async () => {
|
|
||||||
if (!WEBVIEW.initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update window status
|
|
||||||
if (WEBVIEW.window.isFullScreen()) {
|
|
||||||
WEBVIEW.status = "Fullscreen";
|
|
||||||
} else if (WEBVIEW.window.isMinimized()) {
|
|
||||||
WEBVIEW.status = "Minimized";
|
|
||||||
} else if (WEBVIEW.window.isMaximized()) {
|
|
||||||
WEBVIEW.status = "Maximized";
|
|
||||||
} else {
|
|
||||||
WEBVIEW.status = "Framed";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update widget status
|
|
||||||
if (!HARDWARE.support.keyboardVisibility) {
|
|
||||||
WEBVIEW.widget.webContents.send("button-hide", { id: "keyboard" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update integration sensor
|
|
||||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
|
||||||
integration.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register window events and handler.
|
|
||||||
*/
|
|
||||||
const windowEvents = () => {
|
|
||||||
// Handle window resize events
|
|
||||||
const resize = () => {
|
|
||||||
const window = WEBVIEW.window.getBounds();
|
|
||||||
const widget = { width: 60, height: 200 };
|
|
||||||
|
|
||||||
// Update view size
|
|
||||||
WEBVIEW.view.setBounds({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: window.width,
|
|
||||||
height: window.height,
|
|
||||||
});
|
|
||||||
WEBVIEW.view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
||||||
WEBVIEW.view.webContents.focus();
|
|
||||||
|
|
||||||
// Update widget size
|
|
||||||
if (WEBVIEW.widgetEnabled) {
|
|
||||||
WEBVIEW.widget.setBounds({
|
|
||||||
x: window.width - 15,
|
|
||||||
y: parseInt(window.height / 2 - widget.height / 2),
|
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
});
|
|
||||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
WEBVIEW.window.on("ready-to-show", resize);
|
|
||||||
WEBVIEW.window.on("resize", resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
// Handle window focus events
|
|
||||||
WEBVIEW.window.on("focus", () => {
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
hardware.setDisplayStatus("ON");
|
|
||||||
WEBVIEW.window.blur();
|
|
||||||
}
|
|
||||||
WEBVIEW.locked = false;
|
|
||||||
});
|
|
||||||
HARDWARE.display.notifiers.push(() => {
|
|
||||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
|
||||||
if (WEBVIEW.locked) {
|
|
||||||
WEBVIEW.window.blur();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle window status updates
|
|
||||||
WEBVIEW.window.on("minimize", update);
|
|
||||||
WEBVIEW.window.on("restore", update);
|
|
||||||
WEBVIEW.window.on("maximize", update);
|
|
||||||
WEBVIEW.window.on("unmaximize", update);
|
|
||||||
WEBVIEW.window.on("enter-full-screen", update);
|
|
||||||
WEBVIEW.window.on("leave-full-screen", update);
|
|
||||||
|
|
||||||
// Handle window touch events for activity tracking
|
|
||||||
setInterval(() => {
|
|
||||||
const posOld = WEBVIEW.pointer.position;
|
|
||||||
const posNew = screen.getCursorScreenPoint();
|
|
||||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
|
||||||
const now = new Date();
|
|
||||||
const then = WEBVIEW.pointer.time;
|
|
||||||
|
|
||||||
// Update integration sensor
|
|
||||||
WEBVIEW.pointer.time = now;
|
|
||||||
if (Math.abs(now - then) / 1000 > 30) {
|
|
||||||
console.log("Update Last Active");
|
|
||||||
integration.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WEBVIEW.pointer.position = posNew;
|
|
||||||
}, 1 * 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register widget events and handler.
|
|
||||||
*/
|
|
||||||
const widgetEvents = () => {
|
|
||||||
if (!WEBVIEW.widgetEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle widget focus events
|
|
||||||
WEBVIEW.widget.webContents.on("focus", () => {
|
|
||||||
const window = WEBVIEW.window.getBounds();
|
|
||||||
const widget = WEBVIEW.widget.getBounds();
|
|
||||||
|
|
||||||
// Show widget
|
|
||||||
WEBVIEW.widget.setBounds({
|
|
||||||
x: window.width - 60,
|
|
||||||
y: widget.y,
|
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
});
|
|
||||||
WEBVIEW.widget.webContents.send("data-theme", { theme: WEBVIEW.widgetTheme });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle widget blur events
|
|
||||||
WEBVIEW.widget.webContents.on("blur", () => {
|
|
||||||
const window = WEBVIEW.window.getBounds();
|
|
||||||
const widget = WEBVIEW.widget.getBounds();
|
|
||||||
|
|
||||||
// Hide widget
|
|
||||||
WEBVIEW.widget.setBounds({
|
|
||||||
x: window.width - 15,
|
|
||||||
y: widget.y,
|
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
});
|
|
||||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle widget button click events
|
|
||||||
ipcMain.on("button-click", (e, button) => {
|
|
||||||
switch (button.id) {
|
|
||||||
case "refresh":
|
|
||||||
WEBVIEW.view.webContents.reloadIgnoringCache();
|
|
||||||
break;
|
|
||||||
case "fullscreen":
|
|
||||||
if (WEBVIEW.window.isFullScreen()) {
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
} else {
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "minimize":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
WEBVIEW.window.minimize();
|
|
||||||
break;
|
|
||||||
case "keyboard":
|
|
||||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
|
||||||
hardware.setKeyboardVisibility(toggle);
|
|
||||||
switch (toggle) {
|
|
||||||
case "OFF":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.unmaximize();
|
|
||||||
WEBVIEW.window.setFullScreen(true);
|
|
||||||
break;
|
|
||||||
case "ON":
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
WEBVIEW.window.setFullScreen(false);
|
|
||||||
WEBVIEW.window.maximize();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register dom events and handler.
|
|
||||||
*/
|
|
||||||
const domEvents = () => {
|
|
||||||
// Enable webview touch emulation
|
|
||||||
WEBVIEW.view.webContents.debugger.attach("1.1");
|
|
||||||
WEBVIEW.view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
|
||||||
configuration: "mobile",
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable webview hyperlinks
|
|
||||||
WEBVIEW.view.webContents.setWindowOpenHandler(() => {
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove webview scrollbar
|
|
||||||
WEBVIEW.view.webContents.on("dom-ready", () => {
|
|
||||||
WEBVIEW.view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Webview fully loaded
|
|
||||||
WEBVIEW.view.webContents.on("did-finish-load", () => {
|
|
||||||
if (ARGS.app_debug === "true") {
|
|
||||||
WEBVIEW.view.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
WEBVIEW.initialized = true;
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register app events and handler.
|
|
||||||
*/
|
|
||||||
const appEvents = () => {
|
|
||||||
// Handle signal and exit events
|
|
||||||
process.on("SIGINT", app.quit);
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
WEBVIEW.status = "Terminated";
|
|
||||||
integration.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle multiple instances
|
|
||||||
app.on("second-instance", () => {
|
|
||||||
if (WEBVIEW.window.isMinimized()) {
|
|
||||||
WEBVIEW.window.restore();
|
|
||||||
}
|
|
||||||
WEBVIEW.window.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user