ogclient: add kiosk
Add Kiosk project as a subtree of ogClient. Kiosk is an interactive GUI featuring basic functionality to monitor and operate ogClient withing the client through a graphical interface written in PyQt6. Right after ogClient launches in live mode it performs a fork() call to launch Kiosk as an external process to prevent Kiosk backtraces to affect ogClient. A pair of sockets are created through socket.socketpair() and each process is assigned one of them to leverage the inter process communication. API Kiosk -> ogClient: - Poweroff: request client poweroff {"command": "poweroff"} - Reboot: request client reboot {"command": "reboot"} - Restore: restore image into a partition {"command": "restore", "image": "windows.img", "disk": 1, "partition": 2} - Boot: request an OS boot {"command": "boot", "disk": 1, "partition": 2} API ogClient -> Kiosk: - Busy: inform about ogClient thread status {"command": "busy", "status": True} - Refresh: reload the theme. {"command": "refresh"} - close: request Kiosk termination. {"command": "close"} Add internationalization documentation in README Add "CACHE" mode in image restore to only restore images available in the cache partition. Use set_state() function in OgRest to define the idle or busy status and notify Kiosk about the status change.ogkiosk
|
@ -130,3 +130,6 @@ dmypy.json
|
|||
|
||||
# ignore swp
|
||||
*.swp
|
||||
|
||||
# Qt
|
||||
*.qm
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# Kiosk
|
||||
Interactive kiosk software for ogClient.
|
||||
|
||||
## Translation
|
||||
|
||||
Qt uses .ts files to store the translations for each language. These files are
|
||||
generated from strings surounded by the tr() function in the code.
|
||||
The program needs a binary file with the translation information, this file
|
||||
has a qm extension and it is generated by the tool lrelease-qt6.
|
||||
|
||||
Dependencies:
|
||||
Generating the .ts files requires pylupdate6 provided by
|
||||
pip install pyqt6
|
||||
|
||||
For the .qm file generation install the package qt6-tools package from your
|
||||
distribution's repository.
|
||||
|
||||
Update .ts files:
|
||||
run update_translations.sh in the misc folder.
|
||||
|
||||
Generate .qm files:
|
||||
run compile_translations.sh in the misc folder.
|
||||
|
||||
Edit translations:
|
||||
The .ts files can be manually edited as they are XML files.
|
||||
Optinally the software linguist-qt6 can be used to edit the files through a
|
||||
graphical interface. This program is also provided by the package qt6-tools.
|
||||
|
||||
Steps to translate with Linguist:
|
||||
- Launch linguist-qt6
|
||||
- Open the .ts file from the translations directory.
|
||||
- Translate the strings as needed.
|
||||
- Save the translations.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
translations_dir="$script_dir/../src/kiosk/translations"
|
||||
|
||||
# Compile each .ts file in the directory
|
||||
for ts_file in "$translations_dir"/*.ts; do
|
||||
lrelease-qt6 "$ts_file"
|
||||
done
|
||||
|
||||
echo "All translations have been compiled"
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M160 80c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 352c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-352zM0 272c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 160c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48L0 272zM368 96l32 0c26.5 0 48 21.5 48 48l0 288c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48z"/></svg>
|
After Width: | Height: | Size: 638 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm256 32a32 32 0 1 1 0-64 32 32 0 1 1 0 64zm-96-32a96 96 0 1 0 192 0 96 96 0 1 0 -192 0zM96 240c0-35 17.5-71.1 45.2-98.8S205 96 240 96c8.8 0 16-7.2 16-16s-7.2-16-16-16c-45.4 0-89.2 22.3-121.5 54.5S64 194.6 64 240c0 8.8 7.2 16 16 16s16-7.2 16-16z"/></svg>
|
After Width: | Height: | Size: 582 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64L0 352c0 35.3 28.7 64 64 64l176 0-10.7 32L160 448c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-69.3 0L336 416l176 0c35.3 0 64-28.7 64-64l0-288c0-35.3-28.7-64-64-64L64 0zM512 64l0 288L64 352 64 64l448 0z"/></svg>
|
After Width: | Height: | Size: 551 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"/></svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 224c0 17.7 14.3 32 32 32s32-14.3 32-32l0-224zM143.5 120.6c13.6-11.3 15.4-31.5 4.1-45.1s-31.5-15.4-45.1-4.1C49.7 115.4 16 181.8 16 256c0 132.5 107.5 240 240 240s240-107.5 240-240c0-74.2-33.8-140.6-86.6-184.6c-13.6-11.3-33.8-9.4-45.1 4.1s-9.4 33.8 4.1 45.1c38.9 32.3 63.5 81 63.5 135.4c0 97.2-78.8 176-176 176s-176-78.8-176-176c0-54.4 24.7-103.1 63.5-135.4z"/></svg>
|
After Width: | Height: | Size: 692 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M463.5 224l8.5 0c13.3 0 24-10.7 24-24l0-128c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8l119.5 0z"/></svg>
|
After Width: | Height: | Size: 656 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
|
After Width: | Height: | Size: 426 B |
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import socket
|
||||
import select
|
||||
import logging
|
||||
from src.kiosk.kiosk import launch_kiosk
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='(%(asctime)s): [%(levelname)s] - %(message)s', # Optional log format
|
||||
stream=sys.stdout # Direct the output to stdout
|
||||
)
|
||||
|
||||
|
||||
def send_event(socket, data):
|
||||
try:
|
||||
json_data = json.dumps(data)
|
||||
logging.info(f'sending to kiosk: {json_data}')
|
||||
socket.send(json_data.encode('utf-8'))
|
||||
except TypeError as e:
|
||||
logging.error(f'Failed to encode data to JSON: {e}')
|
||||
except Exception as e:
|
||||
logging.error(f'Unexpected error in send_kiosk_event: {e}')
|
||||
|
||||
def test_requests_on_interrupt():
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
kiosk_sock_pair = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
client_socket = kiosk_sock_pair[0]
|
||||
kiosk_socket = kiosk_sock_pair[1]
|
||||
try:
|
||||
kiosk_pid = os.fork()
|
||||
if kiosk_pid == 0:
|
||||
logging.info('Launching Kiosk...')
|
||||
try:
|
||||
ret = launch_kiosk(socket=kiosk_socket, debug_mode=True)
|
||||
sys.exit(ret)
|
||||
except Exception as e:
|
||||
logging.exception(f"Error during Kiosk execution: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logging.error(f'Could not launch Kiosk: {e}')
|
||||
raise
|
||||
|
||||
try:
|
||||
logging.info('Parent process: Monitoring Kiosk communication...')
|
||||
while True:
|
||||
readable, _, _ = select.select([client_socket], [], [])
|
||||
if client_socket in readable:
|
||||
data = client_socket.recv(4096)
|
||||
if data:
|
||||
print(f'Received from Kiosk: {data.decode('utf-8')}')
|
||||
else:
|
||||
print('Kiosk disconnected')
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
logging.info('Interrupted by user. Executing test requests')
|
||||
test_requests_on_interrupt()
|
||||
except Exception as e:
|
||||
logging.error(f'Error during communication: {e}')
|
||||
finally:
|
||||
send_event(client_socket, {'command': 'close'})
|
||||
client_socket.close()
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
translations_dir="$script_dir/../src/kiosk/translations"
|
||||
kiosk_dir="$script_dir/../src/kiosk"
|
||||
|
||||
# Update all .ts files in the translations directory
|
||||
for ts_file in "$kiosk_dir"/translations/*.ts; do
|
||||
if [[ -f $ts_file ]]; then
|
||||
echo "Updating translations in: $ts_file"
|
||||
pylupdate6 "$kiosk_dir"/*.py --verbose --no-obsolete -ts "$ts_file"
|
||||
else
|
||||
echo "No .ts files found in the translations directory."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All translations have been updated"
|
27
ogclient
|
@ -8,6 +8,8 @@
|
|||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
|
@ -80,9 +82,32 @@ def main():
|
|||
try:
|
||||
client = ogClient(config=CONFIG)
|
||||
client.connect()
|
||||
client.run()
|
||||
except OgError as e:
|
||||
logging.critical(e)
|
||||
|
||||
if MODE == 'live' and not shutil.which('browser'):
|
||||
try:
|
||||
from src.kiosk.kiosk import launch_kiosk
|
||||
kiosk_pid = os.fork()
|
||||
if kiosk_pid == 0:
|
||||
client.shutdown()
|
||||
logging.info('Launching Kiosk...')
|
||||
try:
|
||||
ret = launch_kiosk(socket=client.get_kiosk_socket_1(), debug_mode=False)
|
||||
sys.exit(ret)
|
||||
except Exception as e:
|
||||
logging.exception(f"Error during Kiosk execution: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logging.error(f"Could not launch Kiosk: {e}")
|
||||
|
||||
try:
|
||||
client.run()
|
||||
except OgError as e:
|
||||
logging.critical(e)
|
||||
finally:
|
||||
if MODE == 'live' and not shutil.which('browser'):
|
||||
client.send_kiosk_event({'command': 'close'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QToolButton
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt6.QtGui import QPixmap, QIcon
|
||||
from src.kiosk.config import *
|
||||
from src.kiosk.theme import *
|
||||
|
||||
|
||||
class BootView(QWidget):
|
||||
|
||||
command_requested = pyqtSignal(dict)
|
||||
|
||||
def request_command(self, payload):
|
||||
self.command_requested.emit(payload)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.checked_os = None
|
||||
probe_partitions()
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.setSpacing(10)
|
||||
self.layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
image_label = QLabel()
|
||||
pixmap = get_image('header.png')
|
||||
image_label.setPixmap(pixmap)
|
||||
image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addWidget(image_label)
|
||||
|
||||
boot_os_list = get_boot_os_list()
|
||||
|
||||
self.restore_button = QPushButton(self.tr(' Restore'))
|
||||
self.boot_button = QPushButton(self.tr(' Boot'))
|
||||
|
||||
self.os_layout = QVBoxLayout()
|
||||
self.os_layout.setSpacing(2)
|
||||
self.os_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addLayout(self.os_layout)
|
||||
os_row_layout = None
|
||||
|
||||
win_icon = get_icon('windows.png')
|
||||
linux_icon = get_icon('linux.png')
|
||||
|
||||
for part_idx, part_data in enumerate(boot_os_list):
|
||||
if not os_row_layout or os_row_layout.count() == get_os_columns():
|
||||
os_row_layout = QHBoxLayout()
|
||||
os_row_layout.setSpacing(2)
|
||||
os_row_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.os_layout.addLayout(os_row_layout)
|
||||
|
||||
button = QToolButton()
|
||||
button.setText(' {os_name} ({os_disk},{os_part})'.format(
|
||||
os_name=part_data['name'],
|
||||
os_disk=part_data['disk'],
|
||||
os_part=part_data['partition']
|
||||
))
|
||||
button.setStyleSheet(f'height: {BUTTON_HEIGHT}px; width: {BUTTON_WIDTH}px;')
|
||||
button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
|
||||
button.setCheckable(True)
|
||||
if part_data['os'] == OSFamily.WINDOWS:
|
||||
button.setIcon(win_icon)
|
||||
elif part_data['os'] == OSFamily.LINUX:
|
||||
button.setIcon(linux_icon)
|
||||
button.setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE))
|
||||
button.toggled.connect(lambda checked, b=button: self.on_part_button_checked(checked, b))
|
||||
button.setProperty('part_idx', part_idx)
|
||||
os_row_layout.addWidget(button)
|
||||
|
||||
if not self.checked_os:
|
||||
button.setChecked(True)
|
||||
|
||||
power_layout = QHBoxLayout()
|
||||
power_layout.setSpacing(10)
|
||||
power_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addLayout(power_layout)
|
||||
|
||||
# Reboot
|
||||
reboot_button = QPushButton()
|
||||
reboot_button.setStyleSheet(f'height: {BUTTON_HEIGHT}px; width: {BUTTON_HEIGHT}px;')
|
||||
icon = get_icon('rotate-right.png')
|
||||
reboot_button.setIcon(icon)
|
||||
reboot_button.setIconSize(QSize(POWER_ICON_SIZE, POWER_ICON_SIZE))
|
||||
power_layout.addWidget(reboot_button)
|
||||
reboot_button.clicked.connect(self.on_reboot_pressed)
|
||||
|
||||
boot_qss = f'height: {BUTTON_HEIGHT - 6}px; width: {BUTTON_HEIGHT * 2}px; font-size: 22px;'
|
||||
|
||||
# Restore
|
||||
icon = get_icon('compact-disc.png')
|
||||
self.restore_button.setIcon(icon)
|
||||
self.restore_button.setStyleSheet(boot_qss)
|
||||
power_layout.addWidget(self.restore_button)
|
||||
self.restore_button.clicked.connect(self.on_restore_pressed)
|
||||
self.restore_button.setVisible(len(boot_os_list) > 0)
|
||||
|
||||
# Boot
|
||||
icon = get_icon('display.png')
|
||||
self.boot_button.setIcon(icon)
|
||||
self.boot_button.setStyleSheet(boot_qss)
|
||||
power_layout.addWidget(self.boot_button)
|
||||
self.boot_button.clicked.connect(self.on_boot_os_pressed)
|
||||
self.boot_button.setVisible(len(boot_os_list) > 0)
|
||||
|
||||
# Poweroff
|
||||
poweroff_button = QPushButton()
|
||||
icon = get_icon('power-off.png')
|
||||
poweroff_button.setIcon(icon)
|
||||
poweroff_button.setIconSize(QSize(POWER_ICON_SIZE, POWER_ICON_SIZE))
|
||||
poweroff_button.setStyleSheet(f'height: {BUTTON_HEIGHT}px; width: {BUTTON_HEIGHT}px;')
|
||||
power_layout.addWidget(poweroff_button)
|
||||
poweroff_button.clicked.connect(self.on_poweroff_pressed)
|
||||
|
||||
# Footer
|
||||
image_label = QLabel()
|
||||
pixmap = get_image('footer.png')
|
||||
image_label.setPixmap(pixmap)
|
||||
image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addWidget(image_label)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
|
||||
def on_part_button_checked(self, checked, checked_button):
|
||||
partition_has_image = False
|
||||
if checked:
|
||||
if self.checked_os and self.checked_os != checked_button:
|
||||
self.checked_os.setChecked(False)
|
||||
self.checked_os = checked_button
|
||||
part_idx = checked_button.property('part_idx')
|
||||
if part_idx is not None:
|
||||
part_data = get_boot_os_list()[part_idx]
|
||||
partition_has_image = bool(part_data.get('image', ''))
|
||||
else:
|
||||
self.checked_os = None
|
||||
|
||||
self.boot_button.setEnabled(checked)
|
||||
self.restore_button.setEnabled(checked and partition_has_image)
|
||||
|
||||
def on_reboot_pressed(self):
|
||||
payload = {'command': 'reboot'}
|
||||
self.request_command(payload)
|
||||
|
||||
def on_poweroff_pressed(self):
|
||||
payload = {'command': 'poweroff'}
|
||||
self.request_command(payload)
|
||||
|
||||
def on_boot_os_pressed(self):
|
||||
part_idx = self.checked_os.property('part_idx')
|
||||
part_data = get_boot_os_list()[part_idx]
|
||||
payload = {
|
||||
'command': 'boot',
|
||||
'disk': part_data['disk'],
|
||||
'partition': part_data['partition']
|
||||
}
|
||||
self.request_command(payload)
|
||||
|
||||
def on_restore_pressed(self):
|
||||
part_idx = self.checked_os.property('part_idx')
|
||||
part_data = get_boot_os_list()[part_idx]
|
||||
payload = {
|
||||
'command': 'restore',
|
||||
'image': part_data['image'],
|
||||
'disk': part_data['disk'],
|
||||
'partition': part_data['partition']
|
||||
}
|
||||
self.request_command(payload)
|
|
@ -0,0 +1,75 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from src.utils.probe import os_probe, get_os_family, OSFamily
|
||||
from src.utils.fs import mount_mkdir, umount
|
||||
from PyQt6.QtGui import QColor
|
||||
from src.utils.disk import *
|
||||
from src.utils.net import *
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
config_data = {}
|
||||
|
||||
|
||||
def get_boot_os_list():
|
||||
return config_data.get('os_list', [])
|
||||
|
||||
def probe_partitions():
|
||||
os_list = []
|
||||
for disknum, disk in enumerate(get_disks(), start=1):
|
||||
try:
|
||||
partitions = get_partition_data(device=f'/dev/{disk}')
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
for pa in partitions:
|
||||
partnum = pa.partno + 1
|
||||
mountpoint = pa.padev.replace('dev', 'mnt')
|
||||
if mount_mkdir(pa.padev, mountpoint):
|
||||
try:
|
||||
os_family = get_os_family(mountpoint)
|
||||
if os_family == OSFamily.UNKNOWN:
|
||||
continue
|
||||
os_name = os_probe(mountpoint)
|
||||
os_data = {
|
||||
'name': os_name,
|
||||
'partition': partnum,
|
||||
'disk': disknum,
|
||||
'os': os_family,
|
||||
'image': ''
|
||||
}
|
||||
os_list.append(os_data)
|
||||
finally:
|
||||
umount(mountpoint)
|
||||
config_data['os_list'] = os_list
|
||||
|
||||
def get_main_color_theme():
|
||||
color_values = config_data.get('color_theme', [49, 69, 106])
|
||||
return QColor(*color_values)
|
||||
|
||||
def side_panel_enabled():
|
||||
return config_data.get('side_panel', True) and not is_busy_enabled()
|
||||
|
||||
def get_status():
|
||||
return config_data.get('status', 'idle')
|
||||
|
||||
def set_status(value):
|
||||
config_data['status'] = value
|
||||
|
||||
def is_busy_enabled():
|
||||
return config_data.get('status', 'idle') == 'busy'
|
||||
|
||||
def get_ip():
|
||||
return getifaddr(get_ethernet_interface())
|
||||
|
||||
def get_log_path():
|
||||
return f'/opt/opengnsys/log/{get_ip()}.log'
|
||||
|
||||
def get_os_columns():
|
||||
return config_data.get('os_columns', 2)
|
|
@ -0,0 +1,16 @@
|
|||
The following files belong to the Font-Awesome project:
|
||||
compact-disc.svg
|
||||
display.svg
|
||||
rotate-right.svg
|
||||
chart-simple.svg
|
||||
power-off.svg
|
||||
linux.svg
|
||||
windows.svg
|
||||
|
||||
The png versions are derived from the original svg files with the same name.
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,217 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QPropertyAnimation, QRect, QFile, QTextStream, QTranslator,
|
||||
QLocale, QSocketNotifier
|
||||
)
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout
|
||||
)
|
||||
import sys
|
||||
|
||||
from src.kiosk.sidepanel import SidePanel, ViewType
|
||||
import src.kiosk.thirdparty.breeze.breeze_pyqt6
|
||||
from src.kiosk.spinner import *
|
||||
from src.kiosk.config import *
|
||||
|
||||
|
||||
SIDE_PANEL_WIDTH = 200
|
||||
|
||||
|
||||
class Kiosk(QMainWindow):
|
||||
def __init__(self, socket):
|
||||
super().__init__()
|
||||
self.side_panel = None
|
||||
self.spinner = None
|
||||
|
||||
# socket to read/send events from/to ogClient
|
||||
self.socket = socket
|
||||
if socket:
|
||||
self.socket_notifier = QSocketNotifier(
|
||||
self.socket.fileno(), QSocketNotifier.Type.Read
|
||||
)
|
||||
self.socket_notifier.activated.connect(self.read_from_socket)
|
||||
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
|
||||
self.setWindowTitle('Kiosk')
|
||||
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QHBoxLayout()
|
||||
central_widget.setLayout(layout)
|
||||
|
||||
self.main_content = QWidget()
|
||||
self.main_content.setLayout(QVBoxLayout())
|
||||
layout.addWidget(self.main_content)
|
||||
|
||||
self.reload_theme()
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.socket_notifier:
|
||||
self.socket_notifier.setEnabled(False)
|
||||
self.socket_notifier.disconnect()
|
||||
self.socket_notifier.deleteLater()
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
super().closeEvent(event)
|
||||
|
||||
def send_to_ogclient(self, data):
|
||||
try:
|
||||
json_data = json.dumps(data)
|
||||
self.socket.send(json_data.encode('utf-8'))
|
||||
except TypeError as e:
|
||||
print(f'Failed to encode data to JSON: {e}')
|
||||
except Exception as e:
|
||||
print(f'Unexpected error in send_kiosk_event: {e}')
|
||||
|
||||
def read_from_socket(self):
|
||||
try:
|
||||
data = self.socket.recv(1024).decode('utf-8')
|
||||
if not data:
|
||||
return # Socket closed
|
||||
payload = json.loads(data)
|
||||
|
||||
if payload.get('command') == 'state':
|
||||
status = payload.get('status', 'idle')
|
||||
self.apply_busy_mode_configuration(status)
|
||||
elif payload.get('command') == 'refresh':
|
||||
self.reload_theme()
|
||||
elif payload.get('command') == 'close':
|
||||
self.close()
|
||||
except OSError as e:
|
||||
print(f'Failed to read from socket: {e}')
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'Failed to decode JSON: {e}')
|
||||
|
||||
def reload_theme(self):
|
||||
# Floating side panel
|
||||
if self.side_panel:
|
||||
self.side_panel.setParent(None)
|
||||
self.side_panel.deleteLater()
|
||||
self.side_panel = SidePanel()
|
||||
self.side_panel.setParent(self)
|
||||
self.side_panel.setFixedWidth(SIDE_PANEL_WIDTH)
|
||||
self.side_panel.panel_hidden = True
|
||||
self.side_panel.setGeometry(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height())
|
||||
self.side_panel.widget_change_requested.connect(self.update_view)
|
||||
self.side_panel.emit_default_widget()
|
||||
self.side_panel.setVisible(True)
|
||||
self.side_panel.raise_()
|
||||
|
||||
self.animation = QPropertyAnimation(self.side_panel, b'geometry')
|
||||
self.animation.setDuration(200)
|
||||
|
||||
def apply_busy_mode_configuration(self, status):
|
||||
if get_status() == status:
|
||||
return
|
||||
|
||||
set_status(status)
|
||||
if is_busy_enabled():
|
||||
self.toggle_panel()
|
||||
self.side_panel.request_widget_change(ViewType.SYSTEM_MONITOR)
|
||||
|
||||
self.spinner = LoadSpinner(self)
|
||||
self.spinner.setHeight(30)
|
||||
self.spinner.setColor(get_main_color_theme())
|
||||
self.spinner.start()
|
||||
self.spinner.show()
|
||||
self.spinner.setToolTip(self.tr('The client is busy, please wait...'))
|
||||
return
|
||||
|
||||
if self.spinner:
|
||||
self.spinner.setParent(None)
|
||||
self.spinner.deleteLater()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if side_panel_enabled() and event.key() == Qt.Key.Key_F1:
|
||||
self.side_panel.request_widget_change(ViewType.SYSTEM_MONITOR)
|
||||
elif side_panel_enabled() and event.key() == Qt.Key.Key_F2:
|
||||
self.side_panel.request_widget_change(ViewType.BOOT_OS)
|
||||
elif event.key() == Qt.Key.Key_P:
|
||||
self.toggle_panel()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def toggle_panel(self):
|
||||
if self.side_panel.panel_hidden and side_panel_enabled():
|
||||
# Show the panel
|
||||
self.animation.setStartValue(self.side_panel.geometry())
|
||||
self.animation.setEndValue(QRect(0, 0, SIDE_PANEL_WIDTH, self.height()))
|
||||
self.side_panel.panel_hidden = False
|
||||
else:
|
||||
# Hide the panel
|
||||
self.animation.setStartValue(self.side_panel.geometry())
|
||||
self.animation.setEndValue(QRect(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height()))
|
||||
self.side_panel.panel_hidden = True
|
||||
|
||||
self.animation.start()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
|
||||
# Adjust panel geometry based on visibility
|
||||
if self.side_panel.panel_hidden:
|
||||
self.side_panel.setGeometry(-SIDE_PANEL_WIDTH, 0, SIDE_PANEL_WIDTH, self.height())
|
||||
else:
|
||||
self.side_panel.setGeometry(0, 0, SIDE_PANEL_WIDTH, self.height())
|
||||
|
||||
def update_view(self, widget):
|
||||
layout = self.main_content.layout()
|
||||
|
||||
# Clear existing widgets
|
||||
for i in reversed(range(layout.count())):
|
||||
widget_item = layout.itemAt(i).widget()
|
||||
if widget_item:
|
||||
widget_item.setParent(None)
|
||||
widget_item.deleteLater()
|
||||
|
||||
layout.addWidget(widget)
|
||||
widget.command_requested.connect(self.send_to_ogclient)
|
||||
|
||||
def closeEvent(self, event):
|
||||
logging.info("Closing Kiosk application...")
|
||||
if self.socket_notifier:
|
||||
self.socket_notifier.setEnabled(False)
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
event.accept()
|
||||
|
||||
def launch_kiosk(socket, debug_mode):
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
translator = QTranslator()
|
||||
locale = QLocale.system().name()
|
||||
translation_file = f'kiosk_{locale}.qm'
|
||||
translations_dir = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'translations')
|
||||
|
||||
if translator.load(translation_file, translations_dir):
|
||||
app.installTranslator(translator)
|
||||
|
||||
file = QFile(':/light/stylesheet.qss')
|
||||
file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text)
|
||||
stream = QTextStream(file)
|
||||
app.setStyleSheet(stream.readAll())
|
||||
|
||||
gui = Kiosk(socket)
|
||||
if debug_mode:
|
||||
gui.showMaximized()
|
||||
else:
|
||||
gui.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||
gui.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
|
||||
gui.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
gui.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
|
||||
|
||||
screen = QApplication.primaryScreen()
|
||||
geometry = screen.availableGeometry()
|
||||
gui.setGeometry(geometry)
|
||||
gui.show()
|
||||
|
||||
return app.exec()
|
|
@ -0,0 +1,478 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QSizePolicy, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QProgressBar,
|
||||
QFrame, QTextEdit
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
QTimer, Qt, QFileSystemWatcher, QRegularExpression, pyqtSignal
|
||||
)
|
||||
from PyQt6.QtGui import (
|
||||
QTextCharFormat, QColor, QFont, QTextCursor, QSyntaxHighlighter
|
||||
)
|
||||
from collections import defaultdict, deque
|
||||
from src.kiosk.config import *
|
||||
from src.utils.net import *
|
||||
import psutil
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
POLLING_INTERVAL = 500
|
||||
MAX_ITEM_WIDTH = 300
|
||||
|
||||
|
||||
class CPUMonitor(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
|
||||
frame_layout = QVBoxLayout()
|
||||
self.setLayout(frame_layout)
|
||||
self.cpu_label = QLabel()
|
||||
self.cpu_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.cpu_bar = QProgressBar()
|
||||
self.cpu_bar.setFixedWidth(MAX_ITEM_WIDTH)
|
||||
|
||||
title_label = QLabel(self.tr('CPU'))
|
||||
title_label.setStyleSheet('font-weight: 600;')
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
frame_layout.addWidget(title_label)
|
||||
frame_layout.addWidget(self.cpu_label)
|
||||
frame_layout.addStretch()
|
||||
frame_layout.addWidget(self.cpu_bar)
|
||||
frame_layout.setAlignment(self.cpu_bar, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def update_metrics(self):
|
||||
cpu_percent = psutil.cpu_percent(interval=0)
|
||||
self.cpu_bar.setValue(int(cpu_percent))
|
||||
self.cpu_label.setText(self.tr('CPU Usage: {cpu_percent}%')
|
||||
.format(cpu_percent=cpu_percent))
|
||||
|
||||
|
||||
class MemoryMonitor(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
|
||||
frame_layout = QVBoxLayout()
|
||||
self.setLayout(frame_layout)
|
||||
self.memory_label = QLabel()
|
||||
self.memory_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.memory_bar = QProgressBar()
|
||||
self.memory_bar.setFixedWidth(MAX_ITEM_WIDTH)
|
||||
|
||||
title_label = QLabel(self.tr('MEMORY'))
|
||||
title_label.setStyleSheet('font-weight: 600;')
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
frame_layout.addWidget(title_label)
|
||||
frame_layout.addWidget(self.memory_label)
|
||||
frame_layout.addStretch()
|
||||
frame_layout.addWidget(self.memory_bar)
|
||||
frame_layout.setAlignment(self.memory_bar, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def update_metrics(self):
|
||||
memory = psutil.virtual_memory()
|
||||
memory_percent = memory.percent
|
||||
self.memory_bar.setValue(int(memory_percent))
|
||||
|
||||
total_mib = memory.total // (1024**2)
|
||||
available_mib = memory.available // (1024**2)
|
||||
buffers_mib = memory.buffers // (1024**2)
|
||||
cached_mib = memory.cached // (1024**2)
|
||||
buffcache_mib = buffers_mib + cached_mib
|
||||
used_mib = total_mib - available_mib
|
||||
|
||||
self.memory_label.setText(self.tr(
|
||||
'Total: {total_mib} MiB\n'
|
||||
'Used: {used_mib} MiB\n'
|
||||
'Buff/Cache: {buffcache_mib} MiB'
|
||||
|
||||
).format(
|
||||
total_mib=total_mib,
|
||||
used_mib=used_mib,
|
||||
buffcache_mib=buffcache_mib
|
||||
))
|
||||
|
||||
|
||||
class BaseMonitorMultiWidget(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.title_label = QLabel(self.tr('MONITOR'))
|
||||
self.title_label.setStyleSheet('font-weight: 600;')
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addWidget(self.title_label)
|
||||
|
||||
self.data_layout = QHBoxLayout()
|
||||
self.layout.addLayout(self.data_layout)
|
||||
|
||||
# Dictionary to track widgets by name
|
||||
self.widget_map = {}
|
||||
|
||||
def update_widgets(self, data):
|
||||
# Remove widgets that are no longer in the data
|
||||
existing_names = set(self.widget_map.keys())
|
||||
current_names = set(data.keys())
|
||||
|
||||
for name in existing_names - current_names:
|
||||
self.remove_widget(name)
|
||||
|
||||
# Update or create widgets for current data
|
||||
for name, info in data.items():
|
||||
if name in self.widget_map:
|
||||
self.update_widget(self.widget_map[name], name, info)
|
||||
else:
|
||||
self.add_widget(name, info)
|
||||
|
||||
def add_widget(self, name, info):
|
||||
widget = self.create_widget(name, info)
|
||||
self.widget_map[name] = widget
|
||||
self.data_layout.addWidget(widget)
|
||||
|
||||
def remove_widget(self, name):
|
||||
widget = self.widget_map.pop(name, None)
|
||||
if widget:
|
||||
self.data_layout.removeWidget(widget)
|
||||
widget.setParent(None)
|
||||
widget.deleteLater()
|
||||
|
||||
def create_widget(self, name, info):
|
||||
raise NotImplementedError("Subclasses must implement create_widget.")
|
||||
|
||||
def update_widget(self, widget, name, info):
|
||||
raise NotImplementedError("Subclasses must implement update_widget.")
|
||||
|
||||
|
||||
class DiskMonitor(BaseMonitorMultiWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title_label.setText(self.tr('DISK USAGE'))
|
||||
self.prev_disk_io = psutil.disk_io_counters(perdisk=True)
|
||||
|
||||
def collect_data(self):
|
||||
data = {}
|
||||
current_disk_io = psutil.disk_io_counters(perdisk=True)
|
||||
|
||||
# Collect disk usage information
|
||||
disk_usage = defaultdict(lambda: {'total': 0, 'used': 0})
|
||||
partitions = psutil.disk_partitions(all=False)
|
||||
for partition in partitions:
|
||||
try:
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
device = os.path.basename(partition.device).rstrip('1234567890')
|
||||
if not (device.startswith('sd') or device.startswith('nvme') or device.startswith('vd')):
|
||||
continue
|
||||
disk_usage[device]['total'] += usage.total
|
||||
disk_usage[device]['used'] += usage.used
|
||||
except PermissionError:
|
||||
continue
|
||||
|
||||
# Combine usage and I/O statistics
|
||||
for disk, usage in disk_usage.items():
|
||||
if disk in self.prev_disk_io and disk in current_disk_io:
|
||||
read_bytes = current_disk_io[disk].read_bytes - self.prev_disk_io[disk].read_bytes
|
||||
write_bytes = current_disk_io[disk].write_bytes - self.prev_disk_io[disk].write_bytes
|
||||
|
||||
data[disk] = {
|
||||
'total_bytes': usage['total'],
|
||||
'used_bytes': usage['used'],
|
||||
'read_bytes_per_sec': read_bytes / 1024,
|
||||
'write_bytes_per_sec': write_bytes / 1024,
|
||||
}
|
||||
|
||||
# Update previous disk I/O stats for the next cycle
|
||||
self.prev_disk_io = current_disk_io
|
||||
return data
|
||||
|
||||
def create_widget(self, name, info) -> QWidget:
|
||||
widget = QWidget()
|
||||
widget.setMaximumWidth(MAX_ITEM_WIDTH + 12)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
label = QLabel()
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(label)
|
||||
|
||||
progress_bar = QProgressBar()
|
||||
progress_bar.setMaximumWidth(MAX_ITEM_WIDTH)
|
||||
progress_bar.setMaximum(100)
|
||||
layout.addWidget(progress_bar)
|
||||
|
||||
# Attach data for updates
|
||||
widget.label = label
|
||||
widget.progress_bar = progress_bar
|
||||
return widget
|
||||
|
||||
def update_widget(self, widget, name, info):
|
||||
total_gib = info['total_bytes'] / (1024 ** 3)
|
||||
used_gib = info['used_bytes'] / (1024 ** 3)
|
||||
read_kib = info['read_bytes_per_sec']
|
||||
write_kib = info['write_bytes_per_sec']
|
||||
|
||||
widget.label.setText(self.tr(
|
||||
'Device: /dev/{name}\n'
|
||||
'Total: {total_gib:.1f} GiB\n'
|
||||
'Used: {used_gib:.1f} GiB\n'
|
||||
'Read: {read_kib:6.1f} KiB/s\n'
|
||||
'Write: {write_kib:6.1f} KiB/s'
|
||||
).format(
|
||||
name=name,
|
||||
total_gib=total_gib,
|
||||
used_gib=used_gib,
|
||||
read_kib=read_kib,
|
||||
write_kib=write_kib
|
||||
))
|
||||
|
||||
usage_percent = (info['used_bytes'] / info['total_bytes']) * 100
|
||||
widget.progress_bar.setValue(int(usage_percent))
|
||||
|
||||
def update_metrics(self):
|
||||
data = self.collect_data()
|
||||
self.update_widgets(data)
|
||||
|
||||
|
||||
class NetworkMonitor(BaseMonitorMultiWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title_label.setText(self.tr('NETWORK'))
|
||||
self.prev_net_io = psutil.net_io_counters(pernic=True)
|
||||
|
||||
def collect_data(self):
|
||||
current_net_io = psutil.net_io_counters(pernic=True)
|
||||
interfaces = psutil.net_if_addrs()
|
||||
stats = psutil.net_if_stats()
|
||||
|
||||
data = {}
|
||||
for iface, addrs in interfaces.items():
|
||||
if not stats[iface].isup or iface == 'lo':
|
||||
continue
|
||||
|
||||
if iface in self.prev_net_io and iface in current_net_io:
|
||||
prev_io = self.prev_net_io[iface]
|
||||
curr_io = current_net_io[iface]
|
||||
bytes_recv_rate = (curr_io.bytes_recv - prev_io.bytes_recv) / 1024
|
||||
bytes_sent_rate = (curr_io.bytes_sent - prev_io.bytes_sent) / 1024
|
||||
else:
|
||||
bytes_recv_rate = bytes_sent_rate = 0
|
||||
|
||||
ipv4 = ipv6 = 'N/A'
|
||||
for addr in addrs:
|
||||
if addr.family == socket.AF_INET:
|
||||
ipv4 = addr.address
|
||||
elif addr.family == socket.AF_INET6 and not is_link_local_ipv6(addr.address):
|
||||
ipv6 = addr.address.split('%')[0]
|
||||
|
||||
data[iface] = {
|
||||
'ipv4': ipv4,
|
||||
'ipv6': ipv6,
|
||||
'recv_rate': bytes_recv_rate,
|
||||
'send_rate': bytes_sent_rate,
|
||||
}
|
||||
|
||||
self.prev_net_io = current_net_io
|
||||
return data
|
||||
|
||||
def create_widget(self, name, info) -> QWidget:
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
label = QLabel()
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(label)
|
||||
|
||||
# Attach data for updates
|
||||
widget.label = label
|
||||
widget.progress_bar = None
|
||||
return widget
|
||||
|
||||
def update_widget(self, widget, name, info):
|
||||
widget.label.setText(self.tr(
|
||||
'Interface: {name}\n'
|
||||
'IPv4: {ipv4}\n'
|
||||
'↓ {recv_rate:6.1f} KiB/s\n'
|
||||
'↑ {send_rate:6.1f} KiB/s'
|
||||
).format(
|
||||
name=name,
|
||||
ipv4=info['ipv4'],
|
||||
recv_rate=info['recv_rate'],
|
||||
send_rate=info['send_rate']
|
||||
))
|
||||
|
||||
def update_metrics(self):
|
||||
data = self.collect_data()
|
||||
self.update_widgets(data)
|
||||
|
||||
|
||||
class LogHighlighter(QSyntaxHighlighter):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.highlight_rules = []
|
||||
|
||||
patterns = [
|
||||
(r'\(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\)', QColor('darkblue'), QFont.Weight.Normal), # Timestamp
|
||||
(r'\S+[\\/]\S+', QColor('black'), 420),
|
||||
(r'ogClient', QColor('blue'), QFont.Weight.Normal),
|
||||
(r'\[INFO\]', QColor('darkgreen'), QFont.Weight.DemiBold),
|
||||
(r'\[DEBUG\]', QColor('navy'), QFont.Weight.DemiBold),
|
||||
(r'\[WARNING\]', QColor('orangered'), QFont.Weight.DemiBold),
|
||||
(r'\[ERROR\]', QColor('red'), QFont.Weight.DemiBold),
|
||||
]
|
||||
|
||||
for pattern, color, weight in patterns:
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setForeground(color)
|
||||
fmt.setFontWeight(weight)
|
||||
self.highlight_rules.append((QRegularExpression(pattern), fmt))
|
||||
|
||||
def highlightBlock(self, text):
|
||||
for pattern, fmt in self.highlight_rules:
|
||||
match_iter = pattern.globalMatch(text)
|
||||
while match_iter.hasNext():
|
||||
match = match_iter.next()
|
||||
self.setFormat(match.capturedStart(), match.capturedLength(), fmt)
|
||||
|
||||
class LogViewWidget(QFrame):
|
||||
|
||||
MAX_LOG_LINES = 300
|
||||
AUTOSCROLL_THRESHOLD = 50
|
||||
|
||||
def __init__(self, log_file_path):
|
||||
super().__init__()
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
|
||||
|
||||
self.log_file_path = log_file_path
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
title_label = QLabel(self.tr('LOGS'))
|
||||
title_label.setStyleSheet('font-weight: 600;')
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
self.log_display = QTextEdit(self)
|
||||
self.log_display.setReadOnly(True)
|
||||
self.highlighter = LogHighlighter(self.log_display.document())
|
||||
self.layout.addWidget(self.log_display)
|
||||
|
||||
self.file_watcher = QFileSystemWatcher(self)
|
||||
self.file_watcher.fileChanged.connect(self.on_file_changed)
|
||||
self.file_watcher.addPath(self.log_file_path)
|
||||
|
||||
self.user_scrolling = False
|
||||
self.log_display.verticalScrollBar().valueChanged.connect(self.on_scroll)
|
||||
|
||||
self.read_log_file()
|
||||
self.log_display.moveCursor(self.log_display.textCursor().MoveOperation.End)
|
||||
|
||||
def read_last_lines(file_path, num_lines):
|
||||
with open(file_path, 'r') as log_file:
|
||||
last_lines = deque(log_file, maxlen=num_lines)
|
||||
return ''.join(last_lines)
|
||||
|
||||
def read_log_file(self):
|
||||
try:
|
||||
new_content = LogViewWidget.read_last_lines(
|
||||
self.log_file_path, self.MAX_LOG_LINES)
|
||||
except Exception as e:
|
||||
self.log_display.append(self.tr('Error reading file: {error}').format(error=e))
|
||||
return
|
||||
|
||||
scroll_bar = self.log_display.verticalScrollBar()
|
||||
previous_value = scroll_bar.value()
|
||||
|
||||
scroll_bar.valueChanged.disconnect(self.on_scroll)
|
||||
|
||||
# Update the log display
|
||||
current_content = self.log_display.toPlainText()
|
||||
if new_content != current_content:
|
||||
self.log_display.setPlainText(new_content)
|
||||
|
||||
# Restore or update scrolling based on user activity
|
||||
if self.user_scrolling:
|
||||
scroll_bar.setValue(previous_value)
|
||||
else:
|
||||
self.log_display.moveCursor(self.log_display.textCursor().MoveOperation.End)
|
||||
|
||||
scroll_bar.valueChanged.connect(self.on_scroll)
|
||||
|
||||
def on_scroll(self, value):
|
||||
scroll_bar = self.log_display.verticalScrollBar()
|
||||
if value < (scroll_bar.maximum() - self.AUTOSCROLL_THRESHOLD):
|
||||
self.user_scrolling = True
|
||||
else:
|
||||
self.user_scrolling = False
|
||||
|
||||
def on_file_changed(self, path):
|
||||
if path == self.log_file_path:
|
||||
self.read_log_file()
|
||||
|
||||
|
||||
class MonitorView(QWidget):
|
||||
|
||||
command_requested = pyqtSignal(dict)
|
||||
|
||||
def request_command(self, payload):
|
||||
self.data_emitted.emit(payload)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.monitor_layout = QVBoxLayout()
|
||||
self.layout0 = QHBoxLayout()
|
||||
self.monitor_layout.addLayout(self.layout0)
|
||||
|
||||
# Memory Usage
|
||||
self.mem_monitor = MemoryMonitor()
|
||||
self.layout0.addWidget(self.mem_monitor)
|
||||
|
||||
# CPU Usage
|
||||
self.cpu_monitor = CPUMonitor()
|
||||
self.layout0.addWidget(self.cpu_monitor)
|
||||
|
||||
# Disk Usage
|
||||
self.disk_monitor = DiskMonitor()
|
||||
self.monitor_layout.addWidget(self.disk_monitor)
|
||||
|
||||
# Network Usage
|
||||
self.net_monitor = NetworkMonitor()
|
||||
self.monitor_layout.addWidget(self.net_monitor)
|
||||
|
||||
self.monitor_layout.addStretch(1)
|
||||
self.layout.addLayout(self.monitor_layout)
|
||||
|
||||
# Timer to refresh the data
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_metrics)
|
||||
self.timer.start(POLLING_INTERVAL)
|
||||
|
||||
self.update_metrics()
|
||||
|
||||
log_widget = LogViewWidget(get_log_path())
|
||||
self.layout.addWidget(log_widget)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def update_metrics(self):
|
||||
self.cpu_monitor.update_metrics()
|
||||
|
||||
# Memory usage
|
||||
self.mem_monitor.update_metrics()
|
||||
|
||||
# Disk usage
|
||||
self.disk_monitor.update_metrics()
|
||||
|
||||
# Network usage
|
||||
self.net_monitor.update_metrics()
|
|
@ -0,0 +1,81 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QCoreApplication
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout, QFrame
|
||||
)
|
||||
from PyQt6.QtGui import QPixmap, QIcon
|
||||
from src.kiosk.monitor import MonitorView
|
||||
from src.kiosk.boot import BootView
|
||||
from src.kiosk.config import *
|
||||
from src.kiosk.theme import *
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ViewType(Enum):
|
||||
SYSTEM_MONITOR = 1
|
||||
BOOT_OS = 2
|
||||
|
||||
def get_view_title(self):
|
||||
if self == ViewType.SYSTEM_MONITOR:
|
||||
return QCoreApplication.translate('ViewType', 'System monitor')
|
||||
elif self == ViewType.BOOT_OS:
|
||||
return QCoreApplication.translate('ViewType', 'Boot OS')
|
||||
else:
|
||||
return ''
|
||||
|
||||
def get_view_icon(self):
|
||||
if self == ViewType.SYSTEM_MONITOR:
|
||||
icon_name = 'chart-simple.png'
|
||||
elif self == ViewType.BOOT_OS:
|
||||
icon_name = 'display.png'
|
||||
else:
|
||||
return QIcon()
|
||||
|
||||
return get_icon(icon_name)
|
||||
|
||||
def get_widget_instance(self):
|
||||
if self == ViewType.SYSTEM_MONITOR:
|
||||
return MonitorView()
|
||||
elif self == ViewType.BOOT_OS:
|
||||
return BootView()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class SidePanel(QFrame):
|
||||
|
||||
widget_change_requested = pyqtSignal(QWidget)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.panel_hidden = False
|
||||
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
for view_type in ViewType:
|
||||
button = QPushButton()
|
||||
button.setText(' ' + view_type.get_view_title())
|
||||
button.setStyleSheet('height: 40px;')
|
||||
button.setIcon(view_type.get_view_icon())
|
||||
layout.addWidget(button)
|
||||
button.clicked.connect(lambda _, view=view_type: self.request_widget_change(view))
|
||||
|
||||
def request_widget_change(self, view_type):
|
||||
self.widget_change_requested.emit(view_type.get_widget_instance())
|
||||
|
||||
def emit_default_widget(self):
|
||||
self.request_widget_change(ViewType.BOOT_OS)
|
||||
|
||||
def emit_monitor_widget(self):
|
||||
self.request_widget_change(ViewType.SYSTEM_MONITOR)
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from PyQt6.QtCore import Qt, QTimer, QRect
|
||||
from PyQt6.QtGui import QPainter, QPen
|
||||
from PyQt6.QtWidgets import QApplication, QWidget
|
||||
|
||||
|
||||
class LoadSpinner(QWidget):
|
||||
OFFSET = 5
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.span = 0
|
||||
self.growing = True
|
||||
self.color = Qt.GlobalColor.blue
|
||||
self.start_angle = 0
|
||||
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
size = 50
|
||||
self.setFixedSize(size, size)
|
||||
self.updateFrame()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.rotate)
|
||||
self.timer.setInterval(30)
|
||||
|
||||
def setColor(self, color):
|
||||
self.color = color
|
||||
|
||||
def setWidth(self, w):
|
||||
self.setFixedSize(w, w)
|
||||
self.updateFrame()
|
||||
|
||||
def setHeight(self, h):
|
||||
self.setFixedSize(h, h)
|
||||
self.updateFrame()
|
||||
|
||||
def start(self):
|
||||
self.timer.start()
|
||||
|
||||
def stop(self):
|
||||
self.timer.stop()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
|
||||
pen = QPen(self.color)
|
||||
pen.setWidth(self.height() // 10)
|
||||
painter.setPen(pen)
|
||||
|
||||
painter.setOpacity(0.2)
|
||||
painter.drawArc(self.frame, 0, 5760)
|
||||
|
||||
painter.setOpacity(1.0)
|
||||
painter.drawArc(self.frame, self.start_angle * 16, self.span * 16)
|
||||
|
||||
def rotate(self):
|
||||
advance = 3
|
||||
grow = 8
|
||||
|
||||
if self.growing:
|
||||
self.start_angle = (self.start_angle + advance) % 360
|
||||
self.span += grow
|
||||
if self.span > 260:
|
||||
self.growing = False
|
||||
else:
|
||||
self.start_angle = (self.start_angle + grow) % 360
|
||||
self.span = self.span + advance - grow
|
||||
if self.span < 10:
|
||||
self.growing = True
|
||||
|
||||
self.update()
|
||||
|
||||
def updateFrame(self):
|
||||
self.frame = QRect(
|
||||
self.OFFSET,
|
||||
self.OFFSET,
|
||||
self.width() - self.OFFSET * 2,
|
||||
self.height() - self.OFFSET * 2,
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2020-2025 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
from src.kiosk.config import *
|
||||
from PyQt6.QtGui import QPixmap, QPainter, QColor, QIcon
|
||||
from PyQt6.QtCore import Qt
|
||||
import os
|
||||
|
||||
BUTTON_HEIGHT = 55
|
||||
BUTTON_WIDTH = 390
|
||||
BUTTON_ICON_SIZE = 25
|
||||
POWER_ICON_SIZE = 50
|
||||
|
||||
def colorize_pixmap(pixmap, color):
|
||||
# Create a new pixmap with the same size
|
||||
colorized_pixmap = QPixmap(pixmap.size())
|
||||
colorized_pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
# Paint the original pixmap and apply the color overlay
|
||||
painter = QPainter(colorized_pixmap)
|
||||
painter.drawPixmap(0, 0, pixmap)
|
||||
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
||||
painter.fillRect(colorized_pixmap.rect(), color)
|
||||
painter.end()
|
||||
|
||||
return colorized_pixmap
|
||||
|
||||
def get_icon(icon_name):
|
||||
icon_pixmap = get_image(icon_name)
|
||||
return QIcon(colorize_pixmap(icon_pixmap, get_main_color_theme()))
|
||||
|
||||
def get_image(image_name):
|
||||
base_dir = base_dir = os.path.abspath(
|
||||
os.path.dirname(__file__)
|
||||
)
|
||||
img_path = os.path.join(base_dir, 'img', image_name)
|
||||
pixmap = QPixmap(img_path)
|
||||
if pixmap.isNull():
|
||||
print(f'Could not load {img_path}')
|
||||
return pixmap
|
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="ca_ES" sourcelanguage="en_US">
|
||||
<context>
|
||||
<name>BaseMonitorMultiWidget</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="96"/>
|
||||
<source>MONITOR</source>
|
||||
<translation>MONITOR</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BootView</name>
|
||||
<message>
|
||||
<location filename="../boot.py" line="42"/>
|
||||
<source> Restore</source>
|
||||
<translation> Restaurar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../boot.py" line="43"/>
|
||||
<source> Boot</source>
|
||||
<translation> Iniciar</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CPUMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="40"/>
|
||||
<source>CPU</source>
|
||||
<translation>CPU</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="52"/>
|
||||
<source>CPU Usage: {cpu_percent}%</source>
|
||||
<translation>Ús de la CPU: {cpu_percent}%</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DiskMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="144"/>
|
||||
<source>DISK USAGE</source>
|
||||
<translation>ÚS DEL DISC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="208"/>
|
||||
<source>Device: /dev/{name}
|
||||
Total: {total_gib:.1f} GiB
|
||||
Used: {used_gib:.1f} GiB
|
||||
Read: {read_kib:.1f} KiB/s
|
||||
Write: {write_kib:.1f} KiB/s</source>
|
||||
<translation>Dispositiu: /dev/{name}
|
||||
Total: {total_gib:.1f} GiB
|
||||
Usat: {used_gib:.1f} GiB
|
||||
Lectura: {read_kib:.1f} KiB/s
|
||||
Escriptura: {write_kib:.1f} KiB/s</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Kiosk</name>
|
||||
<message>
|
||||
<location filename="../kiosk.py" line="124"/>
|
||||
<source>The client is busy, please wait...</source>
|
||||
<translation>El client està ocupat, si us plau, espereu...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>LogViewWidget</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="316"/>
|
||||
<source>LOGS</source>
|
||||
<translation>LOGS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="351"/>
|
||||
<source>Error reading file: {error}</source>
|
||||
<translation>Error en llegir el fitxer: {error}</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MemoryMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="67"/>
|
||||
<source>MEMORY</source>
|
||||
<translation>MEMÒRIA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="80"/>
|
||||
<source>Total: {total_mib} MiB
|
||||
Available: {available_mib} MiB</source>
|
||||
<translation>Total: {total_mib} MiB
|
||||
Disponible: {available_mib} MiB</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NetworkMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="233"/>
|
||||
<source>NETWORK</source>
|
||||
<translation>XARXA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="286"/>
|
||||
<source>Interface: {name}
|
||||
IPv4: {ipv4}
|
||||
↓ {recv_rate:.1f} KiB/s ↑ {send_rate:.1f} KiB/s</source>
|
||||
<translation>Interface: {name}
|
||||
IPv4: {ipv4}
|
||||
↓ {recv_rate:.1f} KiB/s ↑ {send_rate:.1f} KiB/s</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ViewType</name>
|
||||
<message>
|
||||
<location filename="../sidepanel.py" line="26"/>
|
||||
<source>System monitor</source>
|
||||
<translation>Monitor de sistema</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sidepanel.py" line="28"/>
|
||||
<source>Boot OS</source>
|
||||
<translation>Iniciar SO</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="es_ES" sourcelanguage="en_US">
|
||||
<context>
|
||||
<name>BaseMonitorMultiWidget</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="96"/>
|
||||
<source>MONITOR</source>
|
||||
<translation>MONITOR</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BootView</name>
|
||||
<message>
|
||||
<location filename="../boot.py" line="42"/>
|
||||
<source> Restore</source>
|
||||
<translation> Restaurar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../boot.py" line="43"/>
|
||||
<source> Boot</source>
|
||||
<translation> Arrancar</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CPUMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="40"/>
|
||||
<source>CPU</source>
|
||||
<translation>CPU</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="52"/>
|
||||
<source>CPU Usage: {cpu_percent}%</source>
|
||||
<translation>Uso de CPU: {cpu_percent}%</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DiskMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="144"/>
|
||||
<source>DISK USAGE</source>
|
||||
<translation>USO DE DISCO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="208"/>
|
||||
<source>Device: /dev/{name}
|
||||
Total: {total_gib:.1f} GiB
|
||||
Used: {used_gib:.1f} GiB
|
||||
Read: {read_kib:.1f} KiB/s
|
||||
Write: {write_kib:.1f} KiB/s</source>
|
||||
<translation>Dispositivo: /dev/{name}
|
||||
Total: {total_gib:.1f} GiB
|
||||
Usado: {used_gib:.1f} GiB
|
||||
Lectura: {read_kib:.1f} KiB/s
|
||||
Escritura: {write_kib:.1f} KiB/s</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Kiosk</name>
|
||||
<message>
|
||||
<location filename="../kiosk.py" line="124"/>
|
||||
<source>The client is busy, please wait...</source>
|
||||
<translation>El cliente está ocupado, por favor espere...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>LogViewWidget</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="316"/>
|
||||
<source>LOGS</source>
|
||||
<translation>LOGS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="351"/>
|
||||
<source>Error reading file: {error}</source>
|
||||
<translation>Error leyendo archivo: {error}</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MemoryMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="67"/>
|
||||
<source>MEMORY</source>
|
||||
<translation>MEMORIA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="80"/>
|
||||
<source>Total: {total_mib} MiB
|
||||
Available: {available_mib} MiB</source>
|
||||
<translation>Total: {total_mib} MiB
|
||||
Disponible: {available_mib} MiB</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NetworkMonitor</name>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="233"/>
|
||||
<source>NETWORK</source>
|
||||
<translation>RED</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../monitor.py" line="286"/>
|
||||
<source>Interface: {name}
|
||||
IPv4: {ipv4}
|
||||
↓ {recv_rate:.1f} KiB/s ↑ {send_rate:.1f} KiB/s</source>
|
||||
<translation>Interface: {name}
|
||||
IPv4: {ipv4}
|
||||
↓ {recv_rate:.1f} KiB/s ↑ {send_rate:.1f} KiB/s</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ViewType</name>
|
||||
<message>
|
||||
<location filename="../sidepanel.py" line="26"/>
|
||||
<source>System monitor</source>
|
||||
<translation>Monitor de sistema</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sidepanel.py" line="28"/>
|
||||
<source>Boot OS</source>
|
||||
<translation>Arranque SO</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
|
@ -298,6 +298,17 @@ class OgLiveOperations:
|
|||
image_path = self._fetch_image_tiptorrent(repo, name)
|
||||
self._restore_image(image_path, devpath)
|
||||
|
||||
def _restore_image_cache(self, name, devpath):
|
||||
if not get_cache_dev_path():
|
||||
raise OgError('No cache partition is mounted')
|
||||
|
||||
image_path = f'{OG_CACHE_IMAGE_PATH}{name}.img'
|
||||
|
||||
if (not os.path.exists(image_path)):
|
||||
raise OgError(f'could not find {image_path} in cache')
|
||||
|
||||
self._restore_image(image_path, devpath)
|
||||
|
||||
def _restore_image(self, image_path, devpath):
|
||||
logging.info(f'Restoring image at {image_path} into {devpath}')
|
||||
logging.info('*DO NOT REBOOT OR POWEROFF* the client during this time')
|
||||
|
@ -572,18 +583,23 @@ class OgLiveOperations:
|
|||
disk = int(request.getDisk())
|
||||
partition = int(request.getPartition())
|
||||
name = request.getName().removesuffix('.img')
|
||||
repo = request.getRepo()
|
||||
ctype = request.getType()
|
||||
profile = request.getProfile()
|
||||
cid = request.getId()
|
||||
partdev = get_partition_device(disk, partition)
|
||||
is_cache_only = (ctype == 'CACHE')
|
||||
if not is_cache_only:
|
||||
repo = request.getRepo()
|
||||
profile = request.getProfile()
|
||||
cid = request.getId()
|
||||
|
||||
partdev = get_partition_device(disk, partition)
|
||||
mount_cache()
|
||||
|
||||
self._ogbrowser_clear_logs()
|
||||
self._restartBrowser(self._url_log)
|
||||
|
||||
logging.info(f'Request to restore image {name}.img via {ctype} from {repo}')
|
||||
if is_cache_only:
|
||||
logging.info(f'Request to restore image {name}.img via {ctype}')
|
||||
else:
|
||||
logging.info(f'Request to restore image {name}.img via {ctype} from {repo}')
|
||||
|
||||
if shutil.which('restoreImageCustom'):
|
||||
logging.warning(f'Ignoring restoreImageCustom, use postconfiguration scripts instead.')
|
||||
|
@ -593,6 +609,8 @@ class OgLiveOperations:
|
|||
self._restore_image_unicast(repo, name, partdev, cache)
|
||||
elif ctype == 'TIPTORRENT':
|
||||
self._restore_image_tiptorrent(repo, name, partdev)
|
||||
elif is_cache_only:
|
||||
self._restore_image_cache(name, partdev)
|
||||
|
||||
extend_filesystem(disk, partition)
|
||||
|
||||
|
|
|
@ -40,8 +40,13 @@ class ogClient:
|
|||
self.event_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.event_sock.setblocking(0)
|
||||
self.event_sock.bind(('127.0.0.1', ogClient.EVENT_SOCKET_PORT))
|
||||
self.kiosk_sock = None
|
||||
elif self.mode in {'live'}:
|
||||
self.event_sock = None
|
||||
self.kiosk_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
else:
|
||||
self.event_sock = None
|
||||
self.kiosk_sock = None
|
||||
|
||||
if self.CONFIG['samba']['activate']:
|
||||
assert('user' in self.CONFIG['samba'])
|
||||
|
@ -78,6 +83,16 @@ class ogClient:
|
|||
def get_event_socket(self):
|
||||
return self.event_sock
|
||||
|
||||
def get_kiosk_socket_0(self):
|
||||
if self.kiosk_sock:
|
||||
return self.kiosk_sock[0]
|
||||
return None
|
||||
|
||||
def get_kiosk_socket_1(self):
|
||||
if self.kiosk_sock:
|
||||
return self.kiosk_sock[1]
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
return self.state
|
||||
|
||||
|
@ -99,6 +114,51 @@ class ogClient:
|
|||
self.send(response.get())
|
||||
logging.debug('Sending event OK')
|
||||
|
||||
def handle_kiosk_event(self, data):
|
||||
payload = json.loads(data)
|
||||
command = payload.get('command', 'unknown')
|
||||
logging.info(f'Received {command} command from kiosk')
|
||||
|
||||
if self.ogrest.is_busy() and not (command == 'poweroff' or command == 'reboot'):
|
||||
logging.error('Request has been received while ogClient is busy')
|
||||
return
|
||||
|
||||
report_kiosk_busy = (command != 'poweroff' and command != 'reboot')
|
||||
self.ogrest.set_state(ThreadState.BUSY, self, update_kiosk=report_kiosk_busy)
|
||||
|
||||
if command == 'poweroff':
|
||||
threading.Thread(target=ogThread.poweroff, args=(self.ogrest,)).start()
|
||||
elif command == 'reboot':
|
||||
threading.Thread(target=ogThread.reboot, args=(self.ogrest,)).start()
|
||||
elif command == 'boot':
|
||||
request = restRequest()
|
||||
request.partition = payload.get('partition')
|
||||
request.disk = payload.get('disk')
|
||||
threading.Thread(target=ogThread.boot_os_local, args=(self, request, self.ogrest,)).start()
|
||||
elif command == 'restore':
|
||||
request = restRequest()
|
||||
request.name = payload.get('image')
|
||||
request.partition = payload.get('partition')
|
||||
request.disk = payload.get('disk')
|
||||
request.type = 'CACHE'
|
||||
threading.Thread(target=ogThread.image_restore_local, args=(self, request, self.ogrest,)).start()
|
||||
else:
|
||||
logging.info(f'Received unknown command ·{command}" from kiosk')
|
||||
self.ogrest.set_state(ThreadState.IDLE, self)
|
||||
|
||||
def send_kiosk_event(self, data):
|
||||
kiosk_sock = self.get_kiosk_socket_0()
|
||||
if not kiosk_sock:
|
||||
return
|
||||
logging.info(f'Sending Kiosk event: {data}')
|
||||
try:
|
||||
json_data = json.dumps(data)
|
||||
kiosk_sock.send(json_data.encode("utf-8"))
|
||||
except TypeError as e:
|
||||
logging.error(f"Failed to encode data to JSON: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error in send_kiosk_event: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
self.data = ""
|
||||
self.content_len = 0
|
||||
|
@ -152,7 +212,7 @@ class ogClient:
|
|||
|
||||
if len(data) == 0:
|
||||
self.sock.close()
|
||||
self.ogrest.kill_process()
|
||||
self.ogrest.kill_process(self)
|
||||
self.connect()
|
||||
return
|
||||
|
||||
|
@ -185,10 +245,18 @@ class ogClient:
|
|||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
self.sock.close()
|
||||
|
||||
def shutdown(self):
|
||||
self.sock.close()
|
||||
if self.event_sock:
|
||||
self.even_sock.close()
|
||||
if self.kiosk_sock:
|
||||
self.kiosk_sock[0].close()
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
sock = self.get_socket()
|
||||
event_sock = self.get_event_socket()
|
||||
kiosk_sock = self.get_kiosk_socket_0()
|
||||
state = self.get_state()
|
||||
|
||||
if state == State.CONNECTING:
|
||||
|
@ -198,7 +266,11 @@ class ogClient:
|
|||
elif state == State.FORCE_DISCONNECTED:
|
||||
return 0
|
||||
else:
|
||||
readset = [ sock, event_sock ] if event_sock else [ sock ]
|
||||
readset = [ sock ]
|
||||
if event_sock:
|
||||
readset.append(event_sock)
|
||||
if kiosk_sock:
|
||||
readset.append(kiosk_sock)
|
||||
writeset = [ ]
|
||||
exceptset = [ ]
|
||||
|
||||
|
@ -212,5 +284,8 @@ class ogClient:
|
|||
elif state == State.RECEIVING and event_sock in readable:
|
||||
message = event_sock.recv(4096).decode('utf-8').rstrip()
|
||||
self.handle_session_event(message)
|
||||
elif state == State.RECEIVING and kiosk_sock in readable:
|
||||
message = kiosk_sock.recv(4096).decode('utf-8').rstrip()
|
||||
self.handle_kiosk_event(message)
|
||||
else:
|
||||
raise OgError(f'Invalid ogClient run state: {str(state)}.')
|
||||
|
|
|
@ -88,7 +88,7 @@ class ogThread():
|
|||
if not request.getrun():
|
||||
response = restResponse(ogResponses.BAD_REQUEST, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -104,7 +104,7 @@ class ogThread():
|
|||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def poweroff(ogRest):
|
||||
time.sleep(2)
|
||||
|
@ -113,6 +113,18 @@ class ogThread():
|
|||
def reboot(ogRest):
|
||||
ogRest.operations.reboot()
|
||||
|
||||
def boot_os_local(client, request, ogRest):
|
||||
try:
|
||||
ogRest.operations.session(request, ogRest)
|
||||
except OgError as e:
|
||||
logging.error(e)
|
||||
except Exception as e:
|
||||
logging.exception(exc)
|
||||
return
|
||||
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
client.disconnect()
|
||||
|
||||
def session(client, request, ogRest):
|
||||
try:
|
||||
ogRest.operations.session(request, ogRest)
|
||||
|
@ -137,7 +149,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def hardware(client, ogRest):
|
||||
try:
|
||||
|
@ -151,7 +163,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def setup(client, request, ogRest):
|
||||
try:
|
||||
|
@ -164,7 +176,18 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def image_restore_local(client, request, ogRest):
|
||||
try:
|
||||
ogRest.operations.image_restore(request, ogRest)
|
||||
except OgError as e:
|
||||
logging.error(e)
|
||||
except Exception as e:
|
||||
logging.exception(exc)
|
||||
return
|
||||
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def image_restore(client, request, ogRest):
|
||||
try:
|
||||
|
@ -177,7 +200,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def image_create(client, request, ogRest):
|
||||
try:
|
||||
|
@ -209,7 +232,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def cache_delete(client, request, ogRest):
|
||||
try:
|
||||
|
@ -222,7 +245,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def cache_fetch(client, request, ogRest):
|
||||
try:
|
||||
|
@ -235,7 +258,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def refresh(client, ogRest):
|
||||
try:
|
||||
|
@ -248,7 +271,7 @@ class ogThread():
|
|||
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
ogRest.state = ThreadState.IDLE
|
||||
ogRest.set_state(ThreadState.IDLE, client, update_kiosk=False)
|
||||
|
||||
class ogResponses(Enum):
|
||||
BAD_REQUEST=0
|
||||
|
@ -288,6 +311,17 @@ class ogRest():
|
|||
else:
|
||||
raise OgError(f'Ogrest mode \'{self.mode}\'not supported')
|
||||
|
||||
def set_state(self, state, client, update_kiosk=True):
|
||||
self.state = state
|
||||
if update_kiosk:
|
||||
kiosk_status = 'idle'
|
||||
if self.is_busy():
|
||||
kiosk_status = 'busy'
|
||||
client.send_kiosk_event({'command': 'state', 'status': kiosk_status})
|
||||
|
||||
def is_busy(self):
|
||||
return self.state == ThreadState.BUSY
|
||||
|
||||
def send_internal_server_error(self, client, exc=None):
|
||||
if isinstance(exc, OgError):
|
||||
logging.error(exc)
|
||||
|
@ -295,7 +329,7 @@ class ogRest():
|
|||
logging.exception(exc)
|
||||
response = restResponse(ogResponses.INTERNAL_ERR, seq=client.seq)
|
||||
client.send(response.get())
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def process_request(self, request, client):
|
||||
method = request.get_method()
|
||||
|
@ -306,13 +340,14 @@ class ogRest():
|
|||
if (not "stop" in URI and
|
||||
not "reboot" in URI and
|
||||
not "poweroff" in URI):
|
||||
if self.state == ThreadState.BUSY:
|
||||
if self.is_busy():
|
||||
logging.error('Request has been received while ogClient is busy')
|
||||
response = restResponse(ogResponses.SERVICE_UNAVAILABLE, seq=client.seq)
|
||||
client.send(response.get())
|
||||
return
|
||||
else:
|
||||
self.state = ThreadState.BUSY
|
||||
report_kiosk_busy = ( not "refresh" in URI )
|
||||
self.set_state(ThreadState.BUSY, client, update_kiosk=report_kiosk_busy)
|
||||
|
||||
if ("GET" in method):
|
||||
if "hardware" in URI:
|
||||
|
@ -327,7 +362,7 @@ class ogRest():
|
|||
logging.error('Unsupported request: %s', {URI[:ogRest.LOG_LENGTH]})
|
||||
response = restResponse(ogResponses.BAD_REQUEST, seq=client.seq)
|
||||
client.send(response.get())
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
elif ("POST" in method):
|
||||
if ("poweroff" in URI):
|
||||
self.process_poweroff(client)
|
||||
|
@ -353,15 +388,15 @@ class ogRest():
|
|||
logging.error('Unsupported request: %s', URI[:ogRest.LOG_LENGTH])
|
||||
response = restResponse(ogResponses.BAD_REQUEST, seq=client.seq)
|
||||
client.send(response.get())
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
else:
|
||||
response = restResponse(ogResponses.BAD_REQUEST, seq=client.seq)
|
||||
client.send(response.get())
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
|
||||
return 0
|
||||
|
||||
def kill_process(self):
|
||||
def kill_process(self, client):
|
||||
try:
|
||||
os.kill(self.proc.pid, signal.SIGTERM)
|
||||
except:
|
||||
|
@ -373,7 +408,7 @@ class ogRest():
|
|||
except:
|
||||
pass
|
||||
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def process_reboot(self, client):
|
||||
response = restResponse(ogResponses.IN_PROGRESS, seq=client.seq)
|
||||
|
@ -381,8 +416,8 @@ class ogRest():
|
|||
|
||||
if self.mode != 'virtual':
|
||||
client.disconnect()
|
||||
if self.state == ThreadState.BUSY:
|
||||
self.kill_process()
|
||||
if self.is_busy():
|
||||
self.kill_process(client)
|
||||
|
||||
threading.Thread(target=ogThread.reboot, args=(self,)).start()
|
||||
|
||||
|
@ -392,8 +427,8 @@ class ogRest():
|
|||
|
||||
if self.mode != 'virtual':
|
||||
client.disconnect()
|
||||
if self.state == ThreadState.BUSY:
|
||||
self.kill_process()
|
||||
if self.is_busy():
|
||||
self.kill_process(client)
|
||||
|
||||
threading.Thread(target=ogThread.poweroff, args=(self,)).start()
|
||||
|
||||
|
@ -412,7 +447,7 @@ class ogRest():
|
|||
def process_schedule(self, client):
|
||||
response = restResponse(ogResponses.OK, seq=client.seq)
|
||||
client.send(response.get())
|
||||
self.state = ThreadState.IDLE
|
||||
self.set_state(ThreadState.IDLE, client)
|
||||
|
||||
def process_setup(self, client, request):
|
||||
threading.Thread(target=ogThread.setup, args=(client, request, self,)).start()
|
||||
|
@ -422,8 +457,8 @@ class ogRest():
|
|||
|
||||
def process_stop(self, client):
|
||||
client.disconnect()
|
||||
if self.state == ThreadState.BUSY:
|
||||
self.kill_process()
|
||||
if self.is_busy():
|
||||
self.kill_process(client)
|
||||
self.terminated = True
|
||||
|
||||
sys.exit(0)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import ipaddress
|
||||
import array
|
||||
import sys
|
||||
import socket
|
||||
|
@ -121,3 +122,10 @@ def getifhwaddr(device):
|
|||
struct.pack('256s', bytes(device[:15], 'utf-8'))
|
||||
)[18:24]
|
||||
return "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", hwaddr)
|
||||
|
||||
def is_link_local_ipv6(address):
|
||||
try:
|
||||
ip = ipaddress.IPv6Address(address)
|
||||
return ip.is_link_local
|
||||
except ValueError:
|
||||
return False
|
||||
|
|