# -*- coding: UTF-8 -*-
# This file is part of the jetson_stats package (https://github.com/rbonghi/jetson_stats or http://rnext.it).
# Copyright (c) 2019-2023 Raffaello Bonghi.
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import re
import abc
import curses
# Logging
import logging
# Timer
from datetime import datetime, timedelta
# Get variables
from ..core.common import get_var
# Graphics elements
from .lib.colors import NColors
from .lib.common import check_curses, set_xterm_title
# Create logger
logger = logging.getLogger(__name__)
# Initialization abstract class
# In according with: https://gist.github.com/alanjcastonguay/25e4db0edd3534ab732d6ff615ca9fc1
ABC = abc.ABCMeta('ABC', (object,), {})
# Gui refresh rate
GUI_REFRESH = 1000 // 20
# Copyright small
COPYRIGHT_SMALL_RE = re.compile(r""".*__cr__ = ["'](.*?)['"]""", re.S)
class Page(ABC):
def __init__(self, name, stdscr, jetson):
self.name = name
self.stdscr = stdscr
self.jetson = jetson
self.dialog_window = None
def setcontroller(self, controller):
self.controller = controller
def size_page(self):
height, width = self.stdscr.getmaxyx()
first = 0
# Remove a line for sudo header
if self.controller.message:
height -= 1
first = 1
return height, width, first
def register_dialog_window(self, dialog_window_object):
self.dialog_window = dialog_window_object
@abc.abstractmethod
@check_curses
def draw(self, key, mouse):
pass
def keyboard(self, key):
pass
class JTOPGUI:
"""
The easiest way to use curses is to use a wrapper around a main function
Essentially, what goes in the main function is the body of your program,
The `stdscr' parameter passed to it is the curses screen generated by our
wrapper.
"""
def __init__(self, stdscr, jetson, pages, init_page=0, start=True, loop=False, seconds=5, color_filter=False):
# Initialize colors
NColors(color_filter)
# Set curses reference, refresh and jetson controller
self.stdscr = stdscr
self.jetson = jetson
self.message = False
# Initialize all Object pages
self.pages = []
for obj in pages:
page = obj(stdscr, jetson)
page.setcontroller(self)
self.pages += [page]
# Set default page
self.n_page = 0
self.set(init_page)
# Initialize keyboard status
self.key = -1
self.old_key = -1
# Initialize mouse
self.mouse = ()
# Run the GUI
if start:
self.run(loop, seconds)
def run(self, loop, seconds):
# In this program, we don't want keystrokes echoed to the console,
# so we run this to disable that
curses.noecho()
# Additionally, we want to make it so that the user does not have to press
# enter to send keys to our program, so here is how we get keys instantly
curses.cbreak()
# Try to hide the cursor
if hasattr(curses, 'curs_set'):
try:
curses.curs_set(0)
except Exception:
pass
# Lastly, keys such as the arrow keys are sent as funny escape sequences to
# our program. We can make curses give us nicer values (such as curses.KEY_LEFT)
# so it is easier on us.
self.stdscr.keypad(True)
# Enable mouse mask
_, _ = curses.mousemask(curses.BUTTON1_CLICKED)
# Refreshing page curses loop
# https://stackoverflow.com/questions/54409978/python-curses-refreshing-text-with-a-loop
self.stdscr.nodelay(1)
# Using current time
old = datetime.now()
# Here is the loop of our program, we keep clearing and redrawing in this loop
while not self.events() and self.jetson.ok(spin=True):
# Get page selected
page = self.pages[self.n_page]
# Check if dialog window is open and disable mouse event on main pages
record_mouse = self.mouse
if page.dialog_window and page.dialog_window.enable_dialog_window:
self.mouse = ()
# Draw pages
self.draw(page)
self.mouse = record_mouse
# Draw dialog window if it exists
if page.dialog_window:
page.dialog_window.show(self.stdscr, self.key, self.mouse)
# Increase page automatically if loop enabled
if loop and datetime.now() - old >= timedelta(seconds=seconds):
self.increase(loop=True)
old = datetime.now()
def draw(self, page):
# First, clear the screen
self.stdscr.erase()
# Write head of the jtop
self.header()
# Draw the page
page.draw(self.key, self.mouse)
# Draw menu
self.menu()
# Draw the screen
self.stdscr.refresh()
# Set a timeout and read keystroke
self.stdscr.timeout(GUI_REFRESH)
def increase(self, loop=False):
# check reset
if loop and self.n_page >= len(self.pages) - 1:
idx = 0
else:
idx = self.n_page + 1
# Fix set new page
self.set(idx + 1)
def decrease(self, loop=False):
# check reset
if loop and self.n_page <= 0:
idx = len(self.pages) + 1
else:
idx = self.n_page + 1
self.set(idx - 1)
def set(self, idx):
if idx <= len(self.pages) and idx > 0:
self.n_page = idx - 1
def title_terminal(self):
status = []
# Title script
# Reference: https://stackoverflow.com/questions/25872409/set-gnome-terminal-window-title-in-python
# Read NVP model
if self.jetson.nvpmodel is not None:
status += [self.jetson.nvpmodel.name.replace('MODE_', '').replace('_', ' ')]
# Load CPU status
idle = 100 - self.jetson.cpu['total']['idle']
status += ["CPU {idle:.1f}%".format(idle=idle)]
# Read GPU status
if self.jetson.gpu:
gpu = list(self.jetson.gpu.values())[0]
load = gpu['status']['load']
status += ["GPU {idle:.1f}%".format(idle=load)]
str_xterm = '|'.join(status)
# Print jtop basic info
set_xterm_title("jtop {name}".format(name=str_xterm))
@check_curses
def header(self):
self.title_terminal()
# Detect if jtop is running on jetson or on other platforms
if self.jetson.board['platform']['Machine'] == 'x86_64':
self.header_x86()
elif 'L4T' in self.jetson.board['hardware']:
self.header_jetson()
else:
self.stdscr.addstr(0, 0, "Unrecognized hardware", curses.A_BOLD)
def header_x86(self):
platform = self.jetson.board['platform']
release = platform['Release'].split("-")[0]
message = "{system} {machine} machine - {distribution} [{release}]".format(system=platform['System'].upper(),
machine=platform['Machine'],
distribution=platform['Distribution'],
release=release)
self.stdscr.addstr(0, 0, message, curses.A_BOLD)
def header_jetson(self):
model = self.jetson.board['hardware']["Model"]
jetpack = self.jetson.board['hardware']["Jetpack"]
L4T = self.jetson.board['hardware']["L4T"]
# Write title
idx = 0
if self.jetson.interval != self.jetson.interval_user:
self.message = True
_, width = self.stdscr.getmaxyx()
self.stdscr.addstr(0, 0, ("{0:<" + str(width) + "}").format(" "), NColors.iyellow())
user = int(self.jetson.interval_user * 1000)
interval = int(self.jetson.interval * 1000)
string_sudo = "I CANNOT SET SPEED AT {user}ms - SERVER AT {interval}ms".format(user=user, interval=interval)
self.stdscr.addstr(0, (width - len(string_sudo)) // 2, string_sudo, NColors.iyellow())
idx = 1
# Write first line
head_string = "Model: {model} - ".format(model=model) if model else ""
if jetpack:
head_string += "Jetpack {jetpack} [L4T {L4T}]".format(jetpack=jetpack, L4T=L4T)
self.stdscr.addstr(idx, 0, head_string, curses.A_BOLD)
else:
head_string += "[L4T {L4T}]".format(L4T=L4T)
# Print only the model
self.stdscr.addstr(idx, 0, head_string, curses.A_BOLD)
# Print error message
self.stdscr.addstr(idx, len(head_string) + 1, "Jetpack NOT DETECTED", NColors.red() | curses.A_BOLD)
@check_curses
def menu(self):
height, width = self.stdscr.getmaxyx()
# Set background for all menu line
self.stdscr.addstr(height - 1, 0, ("{0:<" + str(width - 1) + "}").format(" "), curses.A_REVERSE)
position = 1
for idx, page in enumerate(self.pages):
color = curses.A_NORMAL if self.n_page == idx else curses.A_REVERSE
self.stdscr.addstr(height - 1, position, str(idx + 1), color | curses.A_BOLD)
self.stdscr.addstr(height - 1, position + 1, page.name + " ", color)
position += len(page.name) + 3
# Quit button
self.stdscr.addstr(height - 1, position, "Q", curses.A_REVERSE | curses.A_BOLD)
self.stdscr.addstr(height - 1, position + 1, "uit ", curses.A_REVERSE)
# Author name
name_author = get_var(COPYRIGHT_SMALL_RE) + " "
self.stdscr.addstr(height - 1, width - len(name_author), name_author, curses.A_REVERSE)
def event_menu(self, mx, my):
height, _ = self.stdscr.getmaxyx()
# Check if is an event menu
if my == height - 1:
# Check which page
position = 1
for idx, page in enumerate(self.pages):
size = len(page.name) + 3
# Check if mouse is inside menu name
if mx >= position and mx < position + size:
# Set new page
self.set(idx + 1)
return False
# Increase counter
position += size
# Quit button
if mx >= position and mx < position + 4:
return True
return False
def events(self):
event = self.stdscr.getch()
# Run keyboard check
status_mouse = False
status_keyboard = self.keyboard(event)
# Clear event mouse
self.mouse = ()
# Check event mouse
if event == curses.KEY_MOUSE:
try:
_, mx, my, _, _ = curses.getmouse()
# Run event menu controller
status_mouse = self.event_menu(mx, my)
self.mouse = (mx, my)
except curses.error:
pass
return status_keyboard or status_mouse
def keyboard(self, event):
self.key = event
if self.old_key != self.key:
# keyboard check list
if self.key == curses.KEY_LEFT:
self.decrease(loop=True)
elif self.key == curses.KEY_RIGHT or self.key == ord('\t'):
self.increase(loop=True)
elif self.key in [ord(str(n)) for n in range(10)]:
num = int(chr(self.key))
self.set(num)
elif self.key == ord('q') or self.key == ord('Q') or self.ESC_BUTTON(self.key):
# keyboard check quit button
return True
else:
page = self.pages[self.n_page]
# Run key controller
page.keyboard(self.key)
# Store old value key
self.old_key = self.key
return False
def ESC_BUTTON(self, key):
"""
Check there is another character prevent combination ALT +
https://stackoverflow.com/questions/5977395/ncurses-and-esc-alt-keys
"""
if key == 27:
n = self.stdscr.getch()
if n == -1:
return True
return False
# EOF