Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion python/common/src/piscsi/sys_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import subprocess
import logging
import sys
import time
from subprocess import run, CalledProcessError
from shutil import disk_usage
from re import findall, match
Expand Down Expand Up @@ -39,7 +40,7 @@ def running_env():
if Path(PROC_MODEL_PATH).is_file():
try:
with open(PROC_MODEL_PATH, "r") as open_file:
hardware = open_file.read().rstrip()
hardware = open_file.read().rstrip("\x00")
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
# As a fallback, look for PC vendor information
Expand Down Expand Up @@ -308,3 +309,52 @@ def get_throttled(enabled_modes, test_modes):
return matched_states
else:
return []

@staticmethod
def get_cpu_mem_usage():
"""
Returns a dict with:
- cpu_percent: (int) CPU usage percentage
- mem_total: (int) Total memory in kilobytes
- mem_available: (int) Available memory in kilobytes
"""

# Get CPU usage percentage by reading /proc/stat twice
def get_cpu_times():
with open("/proc/stat", "r") as f:
line = f.readline()
# Format: cpu user nice system idle iowait irq softirq steal guest guest_nice
fields = line.strip().split()
# Sum all time values except idle and iowait
idle = int(fields[4])
iowait = int(fields[5]) if len(fields) > 5 else 0
total = sum(int(x) for x in fields[1:])
return total, idle + iowait

total1, idle1 = get_cpu_times()
time.sleep(0.1) # Small delay for measurement
total2, idle2 = get_cpu_times()

total_diff = total2 - total1
idle_diff = idle2 - idle1

if total_diff > 0:
cpu_percent = int(100.0 * (total_diff - idle_diff) / total_diff)
else:
cpu_percent = 0

# Get memory usage
meminfo = {}
with open("/proc/meminfo", "r") as f:
for line in f:
key, value = line.split(":")
meminfo[key.strip()] = int(value.strip().split()[0])

mem_total = meminfo.get("MemTotal", 0)
mem_available = meminfo.get("MemAvailable", 0)

return {
"cpu_percent": cpu_percent,
"mem_total": mem_total,
"mem_available": mem_available,
}
2 changes: 1 addition & 1 deletion python/ctrlboard/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ pyusb==1.3.1
RPi.GPIO==0.7.1
smbus==1.1.post2
smbus2==0.5.0
Unidecode==1.3.2
Unidecode==1.3.6
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from observer import Observer
from piscsi.file_cmds import FileCmds
from piscsi.piscsi_cmds import PiscsiCmds
from piscsi.sys_cmds import SysCmds
from piscsi.socket_cmds import SocketCmds
from piscsi_menu_controller import PiscsiMenuController

Expand All @@ -24,12 +25,14 @@ def __init__(
self,
menu_controller: PiscsiMenuController,
sock_cmd: SocketCmds,
sys_cmd: SysCmds,
piscsi_cmd: PiscsiCmds,
):
self.message = None
self._menu_controller = menu_controller
self._menu_renderer_config = self._menu_controller.get_menu_renderer().get_config()
self.sock_cmd = sock_cmd
self.sys_cmd = sys_cmd
self.piscsi_cmd = piscsi_cmd
self.context_stack = []
self.piscsi_profile_cycler: Optional[PiscsiProfileCycler] = None
Expand Down Expand Up @@ -88,7 +91,7 @@ def handle_button2(self):
"""Method for handling the second cycle button (cycle shutdown)"""
if self.piscsi_shutdown_cycler is None:
self.piscsi_shutdown_cycler = PiscsiShutdownCycler(
self._menu_controller, self.sock_cmd, self.piscsi_cmd
self._menu_controller, self.sock_cmd, self.sys_cmd, self.piscsi_cmd
)
else:
self.piscsi_shutdown_cycler.cycle()
Expand Down Expand Up @@ -170,6 +173,28 @@ def handle_action_menu_slot_info(self, info_object):
context_object=context_object,
)

# noinspection PyUnusedLocal
def handle_action_menu_system_info(self, info_object):
"""Method handles the rotary button press on 'System Info' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object)
self._menu_controller.segue(
CtrlBoardMenuBuilder.SYSTEMINFO_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_left,
context_object=context_object,
)

# noinspection PyUnusedLocal
def handle_action_menu_system_commands(self, info_object):
"""Method handles the rotary button press on 'System Commands' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object)
self._menu_controller.segue(
CtrlBoardMenuBuilder.SYSTEMCMDS_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_left,
context_object=context_object,
)

# noinspection PyUnusedLocal
def handle_device_info_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the info menu."""
Expand All @@ -181,6 +206,27 @@ def handle_device_info_menu_return(self, info_object):
context_object=context_object,
)

# noinspection PyUnusedLocal
def handle_system_info_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the system info menu."""
self.context_stack.pop()
context_object = self._menu_controller.get_active_menu().context_object
self._menu_controller.segue(
CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_right,
context_object=context_object,
)

def handle_system_commands_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the system commands menu."""
self.context_stack.pop()
context_object = self._menu_controller.get_active_menu().context_object
self._menu_controller.segue(
CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_right,
context_object=context_object,
)

# noinspection PyUnusedLocal
def handle_action_menu_loadprofile(self, info_object):
"""Method handles the rotary button press on 'Load Profile' in the action menu."""
Expand Down Expand Up @@ -209,11 +255,20 @@ def handle_profiles_menu_loadprofile(self, info_object):
transition_attributes=self._menu_renderer_config.transition_attributes_right,
)

def handle_system_commands_menu_reboot(self, info_object):
"""Method handles the rotary button press on 'Reboot' in the system commands menu."""
self._menu_controller.show_message("Rebooting!", 2)
self.sys_cmd.reboot_system()
self._menu_controller.segue(
CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_right,
)

# noinspection PyUnusedLocal
def handle_action_menu_shutdown(self, info_object):
"""Method handles the rotary button press on 'Shutdown' in the action menu."""
def handle_system_commands_menu_shutdown(self, info_object):
"""Method handles the rotary button press on 'Shutdown' in the system commands menu."""
self._menu_controller.show_message("Shutting down!", 2)
self.piscsi_cmd.shutdown("system")
self._menu_controller.show_message("Shutting down!", 150)
self._menu_controller.segue(
CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config.transition_attributes_right,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,32 @@
class PiscsiShutdownCycler(Cycler):
"""Class implementing the shutdown cycler for the PiSCSI Control Board UI"""

def __init__(self, menu_controller, sock_cmd, piscsi_cmd):
def __init__(self, menu_controller, sock_cmd, sys_cmd, piscsi_cmd):
super().__init__(
menu_controller,
sock_cmd,
piscsi_cmd,
return_entry=True,
empty_messages=False,
)
self.sys_cmd = sys_cmd
self.executed_once = False

def populate_cycle_entries(self):
cycle_entries = ["Shutdown"]
cycle_entries = ["Shutdown", "Reboot"]

return cycle_entries

def perform_selected_entry_action(self, selected_entry):
if self.executed_once is False:
self.executed_once = True
self._menu_controller.show_timed_message("Shutting down...")
self.piscsi_cmd.shutdown("system")
return "shutdown"
if selected_entry == "Shutdown":
self._menu_controller.show_timed_message("Shutting down...")
self.piscsi_cmd.shutdown("system")
elif selected_entry == "Reboot":
self._menu_controller.show_timed_message("Rebooting...")
self.sys_cmd.reboot_system()
return selected_entry.lower()

return None

Expand Down
90 changes: 87 additions & 3 deletions python/ctrlboard/src/ctrlboard_menu_builder.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""Module for building the control board UI specific menus"""

import logging
import threading
import time
from unidecode import unidecode

from menu.menu import Menu
from menu.menu_builder import MenuBuilder
from piscsi.file_cmds import FileCmds
from piscsi.piscsi_cmds import PiscsiCmds
from piscsi.sys_cmds import SysCmds


class CtrlBoardMenuBuilder(MenuBuilder):
Expand All @@ -16,20 +20,45 @@ class CtrlBoardMenuBuilder(MenuBuilder):
IMAGES_MENU = "images_menu"
PROFILES_MENU = "profiles_menu"
DEVICEINFO_MENU = "device_info_menu"
SYSTEMINFO_MENU = "system_info_menu"
SYSTEMCMDS_MENU = "system_commands_menu"

ACTION_OPENACTIONMENU = "openactionmenu"
ACTION_RETURN = "return"
ACTION_SLOT_ATTACHINSERT = "slot_attachinsert"
ACTION_SLOT_DETACHEJECT = "slot_detacheject"
ACTION_SLOT_INFO = "slot_info"
ACTION_REBOOT = "reboot"
ACTION_SHUTDOWN = "shutdown"
ACTION_LOADPROFILE = "loadprofile"
ACTION_IMAGE_ATTACHINSERT = "image_attachinsert"
ACTION_SYSTEMINFO = "system_info"
ACTION_SYSTEMCMDS = "system_commands"

def __init__(self, piscsi_cmd: PiscsiCmds):
super().__init__()
self._piscsi_client = piscsi_cmd
self.file_cmd = FileCmds(piscsi=piscsi_cmd)
self.sys_cmd = SysCmds()
self._server_info_cache = None
self._server_info_lock = threading.Lock()
self._server_info_refresh_interval = 10 # seconds
self._start_server_info_updater()

def _start_server_info_updater(self):
def updater():
while True:
info = self._piscsi_client.get_server_info()
with self._server_info_lock:
self._server_info_cache = info
time.sleep(self._server_info_refresh_interval)

thread = threading.Thread(target=updater, daemon=True)
thread.start()

def get_server_info(self):
with self._server_info_lock:
return self._server_info_cache or {}

def build(self, name: str, context_object=None) -> Menu:
if name == CtrlBoardMenuBuilder.SCSI_ID_MENU:
Expand All @@ -42,6 +71,10 @@ def build(self, name: str, context_object=None) -> Menu:
return self.create_profiles_menu(context_object)
if name == CtrlBoardMenuBuilder.DEVICEINFO_MENU:
return self.create_device_info_menu(context_object)
if name == CtrlBoardMenuBuilder.SYSTEMINFO_MENU:
return self.create_system_info_menu(context_object)
if name == CtrlBoardMenuBuilder.SYSTEMCMDS_MENU:
return self.create_system_commands_menu(context_object)

log = logging.getLogger(__name__)
log.debug("Provided menu name [%s] cannot be built!", name)
Expand Down Expand Up @@ -128,8 +161,12 @@ def create_action_menu(self, context_object=None):
{"context": self.ACTION_MENU, "action": self.ACTION_LOADPROFILE},
)
menu.add_entry(
"Shutdown",
{"context": self.ACTION_MENU, "action": self.ACTION_SHUTDOWN},
"System Info",
{"context": self.ACTION_MENU, "action": self.ACTION_SYSTEMINFO},
)
menu.add_entry(
"System Commands",
{"context": self.ACTION_MENU, "action": self.ACTION_SYSTEMCMDS},
)
return menu

Expand All @@ -141,7 +178,9 @@ def create_images_menu(self, context_object=None):
images = images_info["files"]
sorted_images = sorted(images, key=lambda d: d["name"])
for image in sorted_images:
image_str = image["name"] + " [" + image["detected_type"] + "]"
image_str = (
unidecode(image["name"], errors="replace") + " [" + image["detected_type"] + "]"
)
image_context = {
"context": self.IMAGES_MENU,
"name": str(image["name"]),
Expand Down Expand Up @@ -204,6 +243,51 @@ def create_device_info_menu(self, context_object=None):

return menu

def create_system_info_menu(self, context_object=None):
"""Create a menu displaying system information"""
menu = Menu(CtrlBoardMenuBuilder.SYSTEMINFO_MENU)
menu.add_entry("Return", {"context": self.SYSTEMINFO_MENU, "action": self.ACTION_RETURN})

ip_addr, _ = self.sys_cmd.get_ip_and_host()
system_stats = self.sys_cmd.get_cpu_mem_usage()
server_info = self.get_server_info()

menu.add_entry("[" + self.sys_cmd.get_pretty_host() + "]")
menu.add_entry("IP: " + ip_addr if ip_addr else "No network")
menu.add_entry(
"Disk: "
+ str(int(self.sys_cmd.disk_space(server_info["image_dir"])["free"] / 1024 / 1024))
+ " MB free"
)
menu.add_entry("CPU: " + str(system_stats["cpu_percent"]) + "%")
menu.add_entry(
"Mem: "
+ str(int(system_stats["mem_available"] / 1024))
+ "/"
+ str(int(system_stats["mem_total"] / 1024))
+ " MB free"
)
menu.add_entry("PiSCSI v" + server_info["version"])
menu.add_entry(self.sys_cmd.running_env()["env"])

return menu

def create_system_commands_menu(self, context_object=None):
"""Create a menu displaying system commands"""
menu = Menu(CtrlBoardMenuBuilder.SYSTEMCMDS_MENU)
menu.add_entry("Return", {"context": self.SYSTEMCMDS_MENU, "action": self.ACTION_RETURN})

menu.add_entry(
"Reboot",
{"context": self.SYSTEMCMDS_MENU, "action": self.ACTION_REBOOT},
)
menu.add_entry(
"Shutdown",
{"context": self.SYSTEMCMDS_MENU, "action": self.ACTION_SHUTDOWN},
)

return menu

def get_piscsi_client(self):
"""Returns an instance of the piscsi client"""
return self._piscsi_client
Loading