#!/usr/bin/env python3 # Copyright 2009-2020 Free Software Foundation, Inc. # This file is part of GNU Radio # # SPDX-License-Identifier: GPL-2.0-or-later # import argparse import gettext import locale import logging import logging.handlers import os import platform import sys from pathlib import Path VERSION_AND_DISCLAIMER_TEMPLATE = """\ GNU Radio Companion (%s) - This program is part of GNU Radio. GRC comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it. """ LOG_LEVELS = { 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL, } # Load GNU Radio # Do this globally so it is available for both run_gtk() and run_qt() try: from gnuradio import gr except ImportError as ex: # Throw a new exception with more information print("Cannot find GNU Radio! (Have you sourced the environment file?)", file=sys.stderr) # If this is a background session (not launched through a script), show a Tkinter error dialog. # Tkinter should already be installed default with Python, so this shouldn't add new dependencies if not sys.stdin.isatty(): import tkinter from tkinter import messagebox # Hide the main window root = tkinter.Tk() root.withdraw() # Show the error dialog # TODO: Have a more helpful dialog here. Maybe a link to the wiki pages? messagebox.showerror("Cannot find GNU Radio", "Cannot find GNU Radio!") # Throw the new exception raise Exception("Cannot find GNU Radio!") from None # Enable Logging # Do this globally so it is available for both run_gtk() and run_qt() # TODO: Advanced logging - https://docs.python.org/3/howto/logging-cookbook.html#formatting-styles # Note: All other modules need to use the 'grc.' convention log = logging.getLogger('grc') # Set the root log name # Since other files are in the 'grc' module, they automatically get a child logger when using: # log = logging.getLogger(__name__) # This log level should be set to DEBUG so the logger itself catches everything. # The StreamHandler level can be set independently to choose what messages are sent to the console. # The default console logging should be WARNING log.setLevel(logging.DEBUG) def run_gtk(args, log): ''' Runs the GTK version of GNU Radio Companion ''' import gi gi.require_version('Gtk', '3.0') gi.require_version('PangoCairo', '1.0') from gi.repository import Gtk # Delay importing until the logging is setup from .gui.Platform import Platform from .gui.Application import Application # The platform is loaded differently between QT and GTK, so this is required both places log.debug("Loading platform") platform = Platform( version=gr.version(), version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()), prefs=gr.prefs(), install_prefix=gr.prefix() ) platform.build_library() log.debug("Loading application") app = Application(args.flow_graphs, platform) log.debug("Running") sys.exit(app.run()) def run_qt(args, log): ''' Runs the Qt version of GNU Radio Companion ''' import platform import locale import gettext from .gui_qt import grc from .gui_qt import helpers from .gui_qt import properties # Delay importing until the logging is setup from .gui_qt.Platform import Platform ''' Global Settings/Constants ''' # Initialize a class with all of the default settings and properties # TODO: Move argv to separate argument parsing class that overrides default properties? # TODO: Split settings/constants into separate classes rather than a single properites class? settings = properties.Properties(sys.argv) ''' Translation Support ''' # Try to get the current locale. Always add English lc, encoding = locale.getdefaultlocale() if lc: languages = [lc] languages += settings.DEFAULT_LANGUAGE log.debug("Using locale - %s" % str(languages)) # Still run even if the english translation isn't found language = gettext.translation(settings.APP_NAME, settings.path.LANGUAGE, languages=languages, fallback=True) if type(language) == gettext.NullTranslations: log.error("Unable to find any translation") log.error("Default English translation missing") else: log.info("Using translation - %s" % language.info()["language"]) # Still need to install null translation to let the system handle calls to _() language.install() ''' OS Platform ''' # Figure out system specific properties and setup defaults. # Some properties can be overridden by preferences # Get the current OS if platform.system() == "Linux": log.debug("Detected Linux") settings.system.OS = "Linux" # Determine if Unity is running.... try: # current_desktop = os.environ['DESKTOP_SESSION'] current_desktop = os.environ['XDG_CURRENT_DESKTOP'] log.debug("Desktop Session - %s" % current_desktop) if current_desktop == "Unity": log.debug("Detected GRC is running under unity") # Use the native menubar rather than leaving it in the window settings.window.NATIVE_MENUBAR = True except: log.warning("Unable to determine the Linux desktop system") elif platform.system() == "Darwin": log.debug("Detected Mac OS X") settings.system.OS = "OS X" # Setup Mac specific QT elements settings.window.NATIVE_MENUBAR = True elif platform.system() == "Windows": log.warning("Detected Windows") settings.system.OS = "Windows" else: log.warning("Unknown operating system") ''' Preferences ''' # TODO: Move earlier? Need to load user preferences and override the default properties/settings # The platform is loaded differently between QT and GTK, so this is required both places log.debug("Loading platform") # TODO: Might be beneficial to rename Platform to avoid confusion with the builtin Python module # Possible names: internal, model? model = Platform( version=gr.version(), version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()), prefs=gr.prefs(), install_prefix=gr.prefix() ) model.build_library() # Launch GRC app = grc.Application(settings, model, args.flow_graphs) sys.exit(app.run()) def get_config_file_path(config_file: str = "grc.conf") -> str: oldpath = os.path.join(os.path.expanduser("~/.gnuradio"), config_file) try: from gnuradio.gr import paths newpath = os.path.join(paths.userconf(), config_file) if os.path.exists(newpath): return newpath if os.path.exists(oldpath): log.warn(f"Found specification for config path '{newpath}', but file does not exist. " + f"Old default config file path '{oldpath}' exists; using that. " + "Please consider moving configuration to new location.") return oldpath # Default to the correct path if both are configured. # neither old, nor new path exist: create new path, return that path_parts = Path(newpath).parts[:-1] pathdir = os.path.join(*path_parts) os.makedirs(pathdir, exist_ok=True) return newpath except ImportError: log.warn("Could not retrieve GNU Radio configuration directory from GNU Radio. Trying defaults.") xdgconf = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) xdgcand = os.path.join(xdgconf, config_file) if os.path.exists(xdgcand): return xdgcand if os.path.exists(oldpath): log.warn(f"Using legacy config path '{oldpath}'. Please consider moving configuration " + f"files to '{newpath}'.") return oldpath # neither old, nor new path exist: create new path, return that path_parts = Path(xdgcand).parts[:-1] pathdir = os.path.join(*path_parts) os.makedirs(pathdir, exist_ok=True) return xdgcand def get_state_directory() -> str: oldpath = os.path.expanduser("~/.gnuradio") try: from gnuradio.gr import paths newpath = paths.persistent() if os.path.exists(newpath): return newpath if os.path.exists(oldpath): log.warn(f"Found specification for persistent state path '{newpath}', but file does not exist. " + f"Old default persistent state path '{oldpath}' exists; using that. " + "Please consider moving state to new location.") return oldpath # Default to the correct path if both are configured. # neither old, nor new path exist: create new path, return that os.makedirs(newpath, exist_ok=True) return newpath except (ImportError, NameError): log.warn("Could not retrieve GNU Radio persistent state directory from GNU Radio. Trying defaults.") xdgstate = os.getenv("XDG_STATE_HOME", os.path.expanduser("~/.local/state")) xdgcand = os.path.join(xdgstate, "gnuradio") if os.path.exists(xdgcand): return xdgcand if os.path.exists(oldpath): log.warn(f"Using legacy state path '{oldpath}'. Please consider moving state " + f"files to '{newpath}'.") return oldpath # neither old, nor new path exist: create new path, return that os.makedirs(xdgcand, exist_ok=True) return xdgcand def main(): grc_version_from_config = "" grc_qt_config_file = get_config_file_path('grc_qt.conf') if os.path.isfile(grc_qt_config_file): try: from qtpy.QtCore import QSettings qsettings = QSettings(grc_qt_config_file, QSettings.IniFormat) grc_version_from_config = qsettings.value('grc/default_grc', "", type=str) except Exception as e: log.warning("main.py could not read grc_qt.conf") log.warning(e) # Argument parsing parser = argparse.ArgumentParser( description=VERSION_AND_DISCLAIMER_TEMPLATE % gr.version()) parser.add_argument('flow_graphs', nargs='*') # Custom Configurations # TODO: parser.add_argument('--config') # Logging support parser.add_argument('--log', choices=['debug', 'info', 'warning', 'error', 'critical'], default='info') # TODO: parser.add_argument('--log-output') # Graphics framework (QT or GTK) gui_group = parser.add_argument_group('Framework') gui_group_exclusive = gui_group.add_mutually_exclusive_group() gui_group_exclusive.add_argument("--qt", dest='framework', action='store_const', const='qt', help="GNU Radio Companion (QT)") gui_group_exclusive.add_argument("--gtk", dest='framework', action='store_const', const='gtk', help="GNU Radio Companion (GTK)") # Default options if not already set with add_argument() args = parser.parse_args() # Print the startup message py_version = sys.version.split()[0] log.info("Starting GNU Radio Companion {} (Python {})".format(gr.version(), py_version)) # File logging log_file = os.path.join(get_state_directory(), "grc.log") try: fileHandler = logging.FileHandler(log_file) file_msg_format = '%(asctime)s [%(levelname)s] %(message)s' if args.log == 'debug': file_msg_format += ' (%(name)s:%(lineno)s)' fileHandler.setLevel(logging.DEBUG) log.info(f'Logging to {log_file} (DEBUG and higher)') else: fileHandler.setLevel(logging.INFO) log.info(f'Logging to {log_file} (INFO and higher)') file_formatter = logging.Formatter(file_msg_format) fileHandler.setFormatter(file_formatter) log.addHandler(fileHandler) except (PermissionError, FileNotFoundError) as e: log.error(f'Cannot write to {log_file} - {e}') # GUI Framework if args.framework == 'qt': run_qt(args, log) elif args.framework == 'gtk': run_gtk(args, log) else: # args.framework == None if grc_version_from_config == 'grc_qt': run_qt(args, log) else: run_gtk(args, log)