From 91ac7b070e6d4b5ad4b79803d24360e24651e443 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Tue, 4 Nov 2025 22:21:53 +0000 Subject: [PATCH 1/7] [nasa/nos3#319] Update igniter --- cfg/gui/add_dialog.py | 66 ++ cfg/gui/cfg_gui.ui | 1198 ---------------------- cfg/gui/cfg_gui.ui.nIFTUJ | 1112 -------------------- cfg/gui/cfg_gui_main.py | 393 -------- cfg/gui/cfg_gui_ui.py | 431 -------- cfg/gui/datetime_dialog.py | 223 ++++ cfg/gui/igniter_entrypoint.py | 6 + cfg/gui/nos3_gui.py | 1794 +++++++++++++++++++++++++++++++++ cfg/gui/resources/orange.json | 155 +++ scripts/cfg/igniter_launch.sh | 2 +- scripts/cfg/prepare.sh | 2 +- 11 files changed, 2246 insertions(+), 3136 deletions(-) create mode 100644 cfg/gui/add_dialog.py delete mode 100644 cfg/gui/cfg_gui.ui delete mode 100644 cfg/gui/cfg_gui.ui.nIFTUJ delete mode 100644 cfg/gui/cfg_gui_main.py delete mode 100644 cfg/gui/cfg_gui_ui.py create mode 100644 cfg/gui/datetime_dialog.py create mode 100644 cfg/gui/igniter_entrypoint.py create mode 100644 cfg/gui/nos3_gui.py create mode 100644 cfg/gui/resources/orange.json diff --git a/cfg/gui/add_dialog.py b/cfg/gui/add_dialog.py new file mode 100644 index 00000000..6f5772ed --- /dev/null +++ b/cfg/gui/add_dialog.py @@ -0,0 +1,66 @@ +import customtkinter as ctk +from tkinter import messagebox + + +class SimpleNameDialog: + def __init__(self, parent, title, prompt): + self.result = None + self.parent = parent + + # Create dialog window + self.dialog = ctk.CTkToplevel(parent) + self.dialog.title(title) + self.dialog.geometry("400x150") + self.dialog.transient(parent) + + # Center the dialog + self.dialog.update_idletasks() + x = (self.dialog.winfo_screenwidth() // 2) - (400 // 2) + y = (self.dialog.winfo_screenheight() // 2) - (150 // 2) + self.dialog.geometry(f"400x150+{x}+{y}") + + # Prompt + ctk.CTkLabel(self.dialog, text=prompt).pack(pady=(20, 10)) + + # Entry + self.entry = ctk.CTkEntry(self.dialog, width=300) + self.entry.pack(pady=10) + + # Buttons + button_frame = ctk.CTkFrame(self.dialog) + button_frame.pack(fill="x", pady=10) + + cancel_btn = ctk.CTkButton(button_frame, text="Cancel", command=self.cancel, width=100) + cancel_btn.pack(side="right", padx=(10, 20)) + + ok_btn = ctk.CTkButton(button_frame, text="OK", command=self.ok, width=100) + ok_btn.pack(side="right") + + # Bind Enter and Escape keys + self.dialog.bind('', lambda e: self.ok()) + self.dialog.bind('', lambda e: self.cancel()) + + # Schedule grab_set and focus_set to happen after dialog is fully created + self.dialog.after(100, self._set_grab_and_focus) + + # Wait for dialog to close + parent.wait_window(self.dialog) + + def _set_grab_and_focus(self): + """Set grab and focus after the dialog is fully visible""" + try: + self.dialog.grab_set() + self.entry.focus_set() + except Exception as e: + print(f"Warning: Could not set grab: {e}") + + def ok(self): + value = self.entry.get().strip() + if value: + self.result = value + self.dialog.destroy() + else: + messagebox.showerror("Error", "Please enter a name.", parent=self.dialog) + + def cancel(self): + self.dialog.destroy() \ No newline at end of file diff --git a/cfg/gui/cfg_gui.ui b/cfg/gui/cfg_gui.ui deleted file mode 100644 index 02234ade..00000000 --- a/cfg/gui/cfg_gui.ui +++ /dev/null @@ -1,1198 +0,0 @@ - - - Form - - - - 0 - 0 - 658 - 655 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - - Form - - - - - 0 - 0 - 661 - 661 - - - - QTabWidget::TabShape::Rounded - - - 0 - - - - Config - - - - - 130 - 20 - 421 - 26 - - - - Qt::AlignmentFlag::AlignCenter - - - true - - - None - - - - - - 550 - 20 - 94 - 26 - - - - Browse... - - - - - - 10 - 20 - 121 - 21 - - - - Current Config: - - - - - - 10 - 260 - 631 - 321 - - - - Spacecraft Config - - - - - 0 - 20 - 631 - 301 - - - - - - - - 0 - 0 - - - - true - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOn - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - true - - - - - 0 - 0 - 613 - 68 - - - - - 0 - 0 - - - - - QLayout::SizeConstraint::SetDefaultConstraint - - - - - - - - - - - 150 - -1 - 48 - 21 - - - - 1 - - - - - - - 10 - 60 - 631 - 181 - - - - Master Config - - - - - 0 - 20 - 631 - 161 - - - - - - - false - - - - - - - - - - 220 - 590 - 94 - 26 - - - - Save - - - - - - 330 - 590 - 94 - 26 - - - - Save As... - - - - - - 50 - 585 - 111 - 41 - - - - - - - true - - - - - - 480 - 585 - 131 - 41 - - - - - - - true - - - Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter - - - - - - Build - - - - - 10 - 120 - 631 - 491 - - - - Console Output - - - Qt::AlignmentFlag::AlignCenter - - - false - - - false - - - - - 0 - 20 - 631 - 471 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - true - - - - - - - 10 - 20 - 641 - 81 - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - 79 - 0 - 561 - 80 - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - 0 - 0 - 561 - 80 - - - - - - - All - - - - - - - All - - - - - - - FSW - - - - - - - CFG - - - - - - - SIM - - - - - - - SIM - - - - - - - GSW - - - - - - - FSW - - - - - - - GSW - - - - - - - - - - 0 - 0 - 81 - 41 - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - 0 - 0 - 81 - 41 - - - - - - - Build - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - 0 - 40 - 81 - 41 - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - 0 - 0 - 81 - 41 - - - - - - - Clean - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - - Launch - - - - - 10 - 10 - 631 - 611 - - - - - - - - - 10 - 570 - 611 - 31 - - - - - 45 - - - - - Play - - - - ../../../../../../../../usr/share/icons/Humanity/actions/24/gtk-media-play-ltr.svg../../../../../../../../usr/share/icons/Humanity/actions/24/gtk-media-play-ltr.svg - - - - - - - Pause - - - - ../../../../../../../../usr/share/icons/Humanity/actions/24/media-playback-pause.svg../../../../../../../../usr/share/icons/Humanity/actions/24/media-playback-pause.svg - - - - - - - - - 10 - 10 - 611 - 41 - - - - - - - Launch - - - - - - - Stop - - - - - - - - - 10 - 60 - 611 - 451 - - - - NOS3 Time Driver - - - Qt::AlignmentFlag::AlignCenter - - - false - - - false - - - - - 0 - 20 - 611 - 431 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - true - - - - - - - 190 - 520 - 261 - 41 - - - - - - - - - - - - - Run For - - - - - Run Until - - - - - - - - - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - - - diff --git a/cfg/gui/cfg_gui.ui.nIFTUJ b/cfg/gui/cfg_gui.ui.nIFTUJ deleted file mode 100644 index 6af78023..00000000 --- a/cfg/gui/cfg_gui.ui.nIFTUJ +++ /dev/null @@ -1,1112 +0,0 @@ - - - Form - - - - 0 - 0 - 655 - 655 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - - Form - - - - - 0 - 0 - 661 - 661 - - - - QTabWidget::Rounded - - - 0 - - - - Config - - - - - 130 - 20 - 421 - 26 - - - - Qt::AlignCenter - - - true - - - None - - - - - - 550 - 20 - 94 - 26 - - - - Browse... - - - - - - 10 - 20 - 121 - 21 - - - - Current Config: - - - - - - 10 - 300 - 631 - 281 - - - - Spacecraft Config - - - - - 0 - 20 - 631 - 261 - - - - - - - QTextEdit::NoWrap - - - false - - - - - - - - - 150 - -1 - 48 - 21 - - - - 1 - - - - - - - 10 - 60 - 631 - 231 - - - - Master Config - - - - - 0 - 20 - 631 - 211 - - - - - - - false - - - - - - - - - - 220 - 590 - 94 - 26 - - - - Save - - - - - - 330 - 590 - 94 - 26 - - - - Save As... - - - - - - Build - - - - - 10 - 120 - 631 - 491 - - - - Console Output - - - false - - - false - - - - - 0 - 20 - 631 - 471 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - true - - - - - - - 110 - 20 - 421 - 81 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 79 - 0 - 341 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 340 - 80 - - - - - - - FSW - - - - - - - FSW - - - - - - - GSW - - - - - - - SIM - - - - - - - SIM - - - - - - - GSW - - - - - - - All - - - - - - - All - - - - - - - - - - 0 - 0 - 81 - 41 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 81 - 41 - - - - - - - Build - - - Qt::AlignCenter - - - - - - - - - - 0 - 40 - 81 - 41 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 81 - 41 - - - - - - - Clean - - - Qt::AlignCenter - - - - - - - - - - - Launch - - - - - 10 - 10 - 631 - 611 - - - - - - - - - 10 - 570 - 611 - 31 - - - - - 45 - - - - - Play - - - - ../../../../../../../../usr/share/icons/Humanity/actions/24/gtk-media-play-ltr.svg../../../../../../../../usr/share/icons/Humanity/actions/24/gtk-media-play-ltr.svg - - - - - - - Pause - - - - ../../../../../../../../usr/share/icons/Humanity/actions/24/media-playback-pause.svg../../../../../../../../usr/share/icons/Humanity/actions/24/media-playback-pause.svg - - - - - - - - - 10 - 10 - 611 - 41 - - - - - - - Launch - - - - - - - Stop - - - - - - - - - 10 - 60 - 611 - 451 - - - - Console Output - - - false - - - false - - - - - 0 - 20 - 611 - 431 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - - true - - - - - - - 200 - 530 - 93 - 26 - - - - - Run For - - - - - Run Until - - - - - - - 290 - 530 - 181 - 26 - - - - - - - Qt::AlignCenter - - - - - - - - - diff --git a/cfg/gui/cfg_gui_main.py b/cfg/gui/cfg_gui_main.py deleted file mode 100644 index 0e49b8f3..00000000 --- a/cfg/gui/cfg_gui_main.py +++ /dev/null @@ -1,393 +0,0 @@ -from pathlib import Path -from PySide6.QtWidgets import QWidget, QApplication, QFileDialog, QTextEdit, QPushButton, QDateTimeEdit, QLabel, QCheckBox, QVBoxLayout, QSizePolicy, QDoubleSpinBox, QLayout, QMessageBox -from PySide6.QtCore import QProcess, QDateTime -from PySide6.QtGui import QTextCharFormat, QPixmap -from cfg_gui_ui import Ui_Form -import sys, re, xmltodict, datetime, threading -import xml.etree.ElementTree as ET -import os - -# TODO: Update master xml sc-x-cfg filename if modified in sc config (in progress) -# TODO: disableButtons(), enableButtons() not working as intended due to the gnome-terminal thread handling the commands externally - -class cfg_gui(QWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.ui = Ui_Form() - self.ui.setupUi(self) - self.setFixedSize(655, 655) - self.setWindowTitle("NOS3 Igniter - Version 0.0.1") - self.setWindowIcon(QPixmap(f'{os.path.dirname(os.path.abspath(__file__))}/resources/nos3_original.png')) - - # globals - self.dateTimeEdit = QDateTimeEdit() - self.scConfigs = {} # Stores child configs {'index' : "filetext"} - self.prevButtonPressed = None # Tracks the last button pressed, used in buttonColor() - self.defaultStyleSheet = self.ui.pushButton_buildAll.styleSheet() # Saves default stylesheet to return button color to normal, buttonColor() - self.setup = 0 # Allows for switchConfig() to initially be called without calling saveText() - self.configNumTrack = 0 # Tracks the index of the previous SC config when switching to another index - self.defaultConfig = f'{os.path.dirname(os.path.abspath(__file__))}/../nos3-mission.xml' - self.config_path = self.defaultConfig - - # Config Tab - self.ui.pushButton_browse.clicked.connect(self.browseConfig) - self.ui.pushButton_save.clicked.connect(lambda: self.saveXML("save")) - self.ui.pushButton_saveAs.clicked.connect(lambda: self.saveXML("saveAs")) - self.ui.spinBox_configNumber.valueChanged.connect(lambda: self.switchConfig(self.ui.spinBox_configNumber.value())) - pixmap = QPixmap(f'{os.path.dirname(os.path.abspath(__file__))}/resources/JSTAR-transparent_original.png') - self.ui.label_jstarLogo.setPixmap(pixmap) - pixmap = QPixmap(f'{os.path.dirname(os.path.abspath(__file__))}/resources/nos3_original.png') - self.ui.label_nos3Logo.setPixmap(pixmap) - - # Build Tab - self.ui.pushButton_buildAll.clicked.connect(lambda: self.build("all", self.ui.pushButton_buildAll)) - self.ui.pushButton_cfgBuild.clicked.connect(lambda: self.build("config", self.ui.pushButton_cfgBuild)) - self.ui.pushButton_fswBuild.clicked.connect(lambda: self.build("fsw", self.ui.pushButton_fswBuild)) - self.ui.pushButton_gswBuild.clicked.connect(lambda: self.build("gsw", self.ui.pushButton_gswBuild)) - self.ui.pushButton_simBuild.clicked.connect(lambda: self.build("sim", self.ui.pushButton_simBuild)) - self.ui.pushButton_cleanAll.clicked.connect(lambda: self.clean("all", self.ui.pushButton_cleanAll)) - self.ui.pushButton_fswClean.clicked.connect(lambda: self.clean("fsw", self.ui.pushButton_fswClean)) - self.ui.pushButton_gswClean.clicked.connect(lambda: self.clean("gsw", self.ui.pushButton_gswClean)) - self.ui.pushButton_simClean.clicked.connect(lambda: self.clean("sim", self.ui.pushButton_simClean)) - - # Launch Tab (Time Driver controls disabled) - #self.ui.pushButton_play.clicked.connect(lambda: self.startBashProcess(self.ui.textEdit_launchConsole, ["-lc", "echo '>> Starting NOS3 Time Driver'"])) - self.ui.pushButton_play.setDisabled(1) - self.ui.pushButton_stop.clicked.connect(lambda: self.gnome_terminal(self.ui.textEdit_launchConsole, "make stop")) - #self.ui.pushButton_pause.clicked.connect(lambda: self.startBashProcess(self.ui.textEdit_launchConsole, ["-lc", "echo '>> Pausing NOS3 Time Driver'"])) - self.ui.pushButton_pause.setDisabled(1) - self.ui.pushButton_launch.clicked.connect(lambda: self.gnome_terminal(self.ui.textEdit_launchConsole, "make launch")) - #self.ui.comboBox_run.currentIndexChanged.connect(self.run_ForUntil) - self.ui.comboBox_run.setDisabled(1) - self.ui.lineEdit_secondsEntry.setDisabled(1) - - # Load Default Config - self.reloadConfig(self.defaultConfig) - - # Replaces the textbox on launch tab with a date/time box and vice versa - def run_ForUntil(self): - index = self.ui.comboBox_run.currentIndex() - if index == 0: - self.ui.horizontalLayout_runForUntil.itemAt(1).widget().setParent(None) - self.ui.horizontalLayout_runForUntil.insertWidget(1, self.ui.lineEdit_secondsEntry) - self.ui.lineEdit_secondsEntry.setPlaceholderText("") - elif index == 1: - self.ui.horizontalLayout_runForUntil.itemAt(1).widget().setParent(None) - self.ui.horizontalLayout_runForUntil.insertWidget(1, self.ui.lineEdit_secondsEntry) - self.ui.lineEdit_secondsEntry.setPlaceholderText("Seconds") - elif index == 2: - self.ui.horizontalLayout_runForUntil.itemAt(1).widget().setParent(None) - self.currentTime = datetime.datetime.now() - self.dateTimeEdit.setMinimumDateTime(QDateTime(self.currentTime.year, self.currentTime.month, self.currentTime.day, self.currentTime.hour, self.currentTime.minute, self.currentTime.second, 0, 0)) - self.ui.horizontalLayout_runForUntil.insertWidget(1, self.dateTimeEdit) - - # Updates the currently saved xml dictionary (not actual xml file) for the currently selected spacecraft config when edited. TODO: change name of function - def saveText(self, layout:QLayout, config_value:int): - text = self.scConfigs[config_value] - filename = text.split('\n')[0] - childXml = xmltodict.parse(text.split('\n', 2)[2]) - - # TODO: change to dynamically pull apps/components from xml file or directory, but how? - applications = ['cf', 'ds', 'fm', 'lc', 'sc'] - components = ['adcs', 'cam', 'css', 'eps', 'fss', 'gps', 'imu', 'mag', 'radio', 'rw', 'sample', 'st', 'syn', 'torquer', 'thruster'] - - i = 0 - while layout.itemAt(i) != None: - widget = layout.itemAt(i).widget() - - # Handle filename - if isinstance(widget, QTextEdit): - widget:QTextEdit - filename = widget.toPlainText() - if "Filename:" not in filename: - filename = f'Filename: {filename}' - if "Filename: " not in filename: - filename = f'Filename: {filename.split(":")[1]}' - - # Handle checkboxes - elif isinstance(widget, QCheckBox): - widget:QCheckBox - text = widget.text().split(' ')[0] - if text in applications: - childXml['sc-1-config']['applications'][text]['enable'] = str(widget.isChecked()).lower() - elif text in components: - childXml['sc-1-config']['components'][text]['enable'] = str(widget.isChecked()).lower() - elif text == 'gui': - childXml['sc-1-config'][text]['enable'] = str(widget.isChecked()).lower() - - # Handle orbits - elif isinstance(widget, QDoubleSpinBox): - widget:QDoubleSpinBox - prefix = widget.prefix().split(' ')[0] - childXml['sc-1-config']['orbit'][prefix] = str(widget.value()) - - # Increment index - i += 1 - - combined = filename + '\n\n' + xmltodict.unparse(childXml) - self.scConfigs[config_value] = combined - - # Saves the master/child XML's edited in the GUI - def saveXML(self, saveType:str): - # saveType = "save" (overwrite) or "saveAs" (new) - - if saveType == "saveAs": - savePath, _ = QFileDialog.getSaveFileName(None, 'Directory', './cfg') - elif saveType == "save": - savePath = self.config_path - - # Grab master and save to xml - masterXml = xmltodict.parse(self.ui.textEdit_masterConfig.toPlainText()) - self.convert2xml(masterXml, savePath) - - # Now handle children - self.saveText(self.layout_, self.ui.spinBox_configNumber.value()-1) - - for child in self.scConfigs: - text = str(self.scConfigs[child]) - filename = text.split('\n')[0].split(' ')[1] - childXml = text.split('\n', 2)[2] - childXml = xmltodict.parse(childXml) - - # save under same directory as masterXml using filename parsed from textEdit - self.convert2xml(childXml, savePath.rsplit('/', 1)[0]+f'/{filename}') - - self.reloadConfig(self.config_path) - - # Loads the child config into the Spacecraft Config textbox - def switchConfig(self, value:int): - # value : index of spacecraft config in the order listed in the master XML - # Note: Parameter indexing starts at 1 - - # save edits made to config before viewing next one - if self.setup == 1: - self.saveText(self.layout_, self.configNumTrack) - self.configNumTrack = value-1 - else: - self.setup = 1 - - # setup layout to add item to - self.ui.scrollArea.setWidgetResizable(True) - self.ui.scrollAreaWidgetContents.setLayout(QVBoxLayout().layout()) - self.layout_ = self.ui.scrollAreaWidgetContents.layout() - self.layout_.setSpacing(12) - - # remove all items from SC Config window when switching index - while self.layout_.itemAt(0) != None: - child = self.layout_.itemAt(0).widget().setParent(None) - - # Now parse the xml and convert to widgets - value = value-1 - if value in self.scConfigs: - fileName = self.scConfigs[value].split('\n')[0] - childXML = self.scConfigs[value].split('\n')[2::] - childXML = ''.join(childXML) - childDict = xmltodict.parse(childXML) - - # child = sc-xxx-cfg - for child in childDict: - configTag = QTextEdit() - configTag.setText((fileName)) - configTag.setMinimumHeight(30) - self.layout_.addWidget(configTag) - - # child2 = applications | components | gui | orbit - for child2 in childDict[child]: - tag = QLabel() - tag.setText(child2.upper()+": ") - format = QTextCharFormat() - format.setFontUnderline(True) - tag.setFont(format.font()) - tag.setMinimumHeight(18) - self.layout_.addWidget(tag) - - if child2 in ['applications', 'components']: - - # child 3 = cf | ds | ... | adcs | cam | ... - for child3 in childDict[child][child2]: - enableTag = QCheckBox() - enableTag.setText(child3 + " enable ") - enableTag.setChecked(childDict[child][child2][child3]['enable'] == 'true') - enableTag.setMinimumHeight(18) - enableTag.sizePolicy().setVerticalPolicy(QSizePolicy.Expanding) - self.layout_.addWidget(enableTag) - - elif child2 == 'gui': - enableTag = QCheckBox() - enableTag.setText(child2 + " enable ") - enableTag.setChecked(childDict[child][child2]['enable'] == 'true') - self.layout_.addWidget(enableTag) - - elif child2 == 'orbit': - - # child3 = tipoff_x/y/z - for child3 in childDict[child][child2]: - orbitSpinBox = QDoubleSpinBox() - orbitSpinBox.setMinimum(-99.00) - orbitSpinBox.setMaximum(99.00) - orbitSpinBox.setValue(float(childDict[child][child2][child3])) - orbitSpinBox.setPrefix(f'{child3} = ') - self.layout_.addWidget(orbitSpinBox) - else: - # No SC configs in master XML file, or selected a SC XML as master - tag = QLabel() - tag.setText("*ERROR*\n\nMake sure you chose a master configuration file\n\n*ERROR*") - self.layout_.addWidget(tag) - - # Converts a dictionary to XML file, saved under the given filename/path - def convert2xml(self, attrDict:dict, fileName:str): - # ensure file is saved as xml - if fileName[-4::] != ".xml": - fileName += ".xml" - - # unparse dictionary to xml - with open(fileName, "w") as f: - xmltodict.unparse(attrDict, f, pretty=True) - f.close() - - # Opens file selection menu and calls parseXML() on the selected file - def browseConfig(self): - - # Clear SC Config window when selecting a new master config - if "layout_" in self.__dict__: - while self.layout_.itemAt(0) != None: - self.layout_.itemAt(0).widget().setParent(None) - - self.config_path, _ = QFileDialog.getOpenFileName(None, 'File', './cfg', "XML Files [ *.xml ]") - if self.config_path != "": - self.config_name = self.config_path.split("/")[-1] - self.ui.lineEdit_curConfig.setText(self.config_name) - self.parseXml(self.config_path) - - # Reloads the whole config after clicking save, allows you to change "sc-x-cfg" xml file - def reloadConfig(self, config_path): - if "layout_" in self.__dict__: - while self.layout_.itemAt(0) != None: - self.layout_.itemAt(0).widget().setParent(None) - - if config_path != "": - config_name = config_path.split("/")[-1] - self.ui.lineEdit_curConfig.setText(config_name) - self.parseXml(config_path) - - self.ui.spinBox_configNumber.setValue(1) - - # Parses Master and child XML files from the given file, updates text boxes accordingly - def parseXml(self, config_path): - - # Read Master - with open(config_path, 'r') as f: - self.ui.textEdit_masterConfig.setText(f.read()) - f.close() - - # Parse number of SC and SC filenames from master - i = 1 - self.sc_cfg_files = [] - childDict = {} - self.master_root = ET.parse(config_path).getroot() - for child in self.master_root: - if child.tag == "number-spacecraft": - self.ui.spinBox_configNumber.setMaximum(int(child.text)) - if re.match("sc-[0-9]+-cfg", child.tag): - childDict[child.tag] = child.text - - # Check for duplicate xml's - if child.text in self.sc_cfg_files: - QMessageBox.critical(self, "Error", "Using duplicate Spacecraft Config Files, Changes will not be saved correctly") - print("Duplicate SC Config file") - - self.sc_cfg_files.append(child.text) - i+=1 - - # Read Children - config_dir = str(config_path.rsplit('/', 1)[0]) - for i, child in enumerate(childDict): - if Path(f'{config_dir}/{childDict[child]}').is_file(): - filePath = f'{config_dir}/{childDict[child]}' - else: - raise FileNotFoundError(childDict[child]) - - with open(filePath, 'r') as f: - self.scConfigs[i] = f'Filename: {childDict[child]}\n\n{f.read()}' - f.close() - - # Update Spacecraft Config Text to first SC config listed in master config - self.switchConfig(1) - - # Test for gnome-terminal instead of bash, also uses startCommand() instead of start() - def gnome_terminal(self, textbox:QTextEdit, command:str): - process = QProcess() - - # `read line` is to hold the terminal open after execution, allows errors to be seen - process.startCommand(f'gnome-terminal --tab -- bash -c "{command}; echo Done. Press ENTER to close.; read line" ') - - process.readyReadStandardOutput.connect(lambda: textbox.append(process.readAllStandardOutput().data().decode())) - process.readyReadStandardError.connect(lambda: textbox.append(process.readAllStandardError().data().decode())) - - process.waitForFinished(msecs=-1) - textbox.append(f'>> {command}...') - - # Placeholder clean command - def clean(self, software:str, button:QPushButton): - textbox = self.ui.textEdit_buildConsole - if software == 'all': - command = f'make clean' - else: - command = f'make clean-{software}' - - self.buttonColor(button) - t1 = threading.Thread(target=self.thread_gnome(textbox, button, command), name='t1') - t1.start() - - # Placeholder build command, assumes make prep already ran, same with clean commands - def build(self, software:str, button:QPushButton): - textbox = self.ui.textEdit_buildConsole - if software == 'all': - command = f'make' - else: - command = f'make {software}' - - self.buttonColor(button) - t1 = threading.Thread(target=self.thread_gnome(textbox, button, command), name='t1') - t1.start() - - # Button/Bash function wrapper for threads - def thread_gnome(self, textbox:QTextEdit, button:QPushButton, command:str): - self.disableButtons(button) - self.gnome_terminal(textbox, command) - self.enableButtons(button) - - # Changes the color of the most recently pressed button to green and the last pressed button to default - def buttonColor(self, button:QPushButton): - if self.prevButtonPressed is not None: - self.prevButtonPressed.setStyleSheet(self.defaultStyleSheet) - button.setStyleSheet('QPushButton {background-color: green;}') - self.prevButtonPressed = button - - # Disable build/clean buttons while another is being ran (not working) - def disableButtons(self, button:QPushButton): - index = self.ui.gridLayout_buildCleanButtons.count()-1 - while index >= 0: - widget = self.ui.gridLayout_buildCleanButtons.itemAt(index).widget() - if widget != button: - widget.setDisabled(1) - index -= 1 - - # Enable build/clean buttons after process is done running (not working) - def enableButtons(self, button:QPushButton): - index = self.ui.gridLayout_buildCleanButtons.count()-1 - while index >= 0: - widget = self.ui.gridLayout_buildCleanButtons.itemAt(index).widget() - if widget != button: - widget.setEnabled(1) - index -= 1 - - -def main(): - app = QApplication(sys.argv) - win = cfg_gui() - win.show() - sys.exit(app.exec()) - -main() \ No newline at end of file diff --git a/cfg/gui/cfg_gui_ui.py b/cfg/gui/cfg_gui_ui.py deleted file mode 100644 index 32b50d49..00000000 --- a/cfg/gui/cfg_gui_ui.py +++ /dev/null @@ -1,431 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'cfg_gui.ui' -## -## Created by: Qt User Interface Compiler version 6.7.0 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout, - QGroupBox, QHBoxLayout, QLabel, QLayout, - QLineEdit, QPushButton, QScrollArea, QSizePolicy, - QSpinBox, QTabWidget, QTextEdit, QVBoxLayout, - QWidget) - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(658, 655) - palette = QPalette() - brush = QBrush(QColor(0, 0, 0, 255)) - brush.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.WindowText, brush) - brush1 = QBrush(QColor(255, 255, 255, 255)) - brush1.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Button, brush1) - palette.setBrush(QPalette.Active, QPalette.Light, brush1) - palette.setBrush(QPalette.Active, QPalette.Midlight, brush1) - brush2 = QBrush(QColor(127, 127, 127, 255)) - brush2.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Dark, brush2) - brush3 = QBrush(QColor(170, 170, 170, 255)) - brush3.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.Mid, brush3) - palette.setBrush(QPalette.Active, QPalette.Text, brush) - palette.setBrush(QPalette.Active, QPalette.BrightText, brush1) - palette.setBrush(QPalette.Active, QPalette.ButtonText, brush) - palette.setBrush(QPalette.Active, QPalette.Base, brush1) - palette.setBrush(QPalette.Active, QPalette.Window, brush1) - palette.setBrush(QPalette.Active, QPalette.Shadow, brush) - palette.setBrush(QPalette.Active, QPalette.AlternateBase, brush1) - brush4 = QBrush(QColor(255, 255, 220, 255)) - brush4.setStyle(Qt.SolidPattern) - palette.setBrush(QPalette.Active, QPalette.ToolTipBase, brush4) - palette.setBrush(QPalette.Active, QPalette.ToolTipText, brush) - brush5 = QBrush(QColor(0, 0, 0, 127)) - brush5.setStyle(Qt.SolidPattern) -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - palette.setBrush(QPalette.Active, QPalette.PlaceholderText, brush5) -#endif - palette.setBrush(QPalette.Active, QPalette.Accent, brush1) - palette.setBrush(QPalette.Inactive, QPalette.WindowText, brush) - palette.setBrush(QPalette.Inactive, QPalette.Button, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Light, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Midlight, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Dark, brush2) - palette.setBrush(QPalette.Inactive, QPalette.Mid, brush3) - palette.setBrush(QPalette.Inactive, QPalette.Text, brush) - palette.setBrush(QPalette.Inactive, QPalette.BrightText, brush1) - palette.setBrush(QPalette.Inactive, QPalette.ButtonText, brush) - palette.setBrush(QPalette.Inactive, QPalette.Base, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Window, brush1) - palette.setBrush(QPalette.Inactive, QPalette.Shadow, brush) - palette.setBrush(QPalette.Inactive, QPalette.AlternateBase, brush1) - palette.setBrush(QPalette.Inactive, QPalette.ToolTipBase, brush4) - palette.setBrush(QPalette.Inactive, QPalette.ToolTipText, brush) -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - palette.setBrush(QPalette.Inactive, QPalette.PlaceholderText, brush5) -#endif - palette.setBrush(QPalette.Inactive, QPalette.Accent, brush1) - palette.setBrush(QPalette.Disabled, QPalette.WindowText, brush2) - palette.setBrush(QPalette.Disabled, QPalette.Button, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Light, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Midlight, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Dark, brush2) - palette.setBrush(QPalette.Disabled, QPalette.Mid, brush3) - palette.setBrush(QPalette.Disabled, QPalette.Text, brush2) - palette.setBrush(QPalette.Disabled, QPalette.BrightText, brush1) - palette.setBrush(QPalette.Disabled, QPalette.ButtonText, brush2) - palette.setBrush(QPalette.Disabled, QPalette.Base, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Window, brush1) - palette.setBrush(QPalette.Disabled, QPalette.Shadow, brush) - palette.setBrush(QPalette.Disabled, QPalette.AlternateBase, brush1) - palette.setBrush(QPalette.Disabled, QPalette.ToolTipBase, brush4) - palette.setBrush(QPalette.Disabled, QPalette.ToolTipText, brush) - brush6 = QBrush(QColor(127, 127, 127, 127)) - brush6.setStyle(Qt.SolidPattern) -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - palette.setBrush(QPalette.Disabled, QPalette.PlaceholderText, brush6) -#endif - palette.setBrush(QPalette.Disabled, QPalette.Accent, brush1) - Form.setPalette(palette) - self.tabWidget = QTabWidget(Form) - self.tabWidget.setObjectName(u"tabWidget") - self.tabWidget.setGeometry(QRect(0, 0, 661, 661)) - self.tabWidget.setTabShape(QTabWidget.TabShape.Rounded) - self.tab = QWidget() - self.tab.setObjectName(u"tab") - self.lineEdit_curConfig = QLineEdit(self.tab) - self.lineEdit_curConfig.setObjectName(u"lineEdit_curConfig") - self.lineEdit_curConfig.setGeometry(QRect(130, 20, 421, 26)) - self.lineEdit_curConfig.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.lineEdit_curConfig.setReadOnly(True) - self.pushButton_browse = QPushButton(self.tab) - self.pushButton_browse.setObjectName(u"pushButton_browse") - self.pushButton_browse.setGeometry(QRect(550, 20, 94, 26)) - self.label_curConfig = QLabel(self.tab) - self.label_curConfig.setObjectName(u"label_curConfig") - self.label_curConfig.setGeometry(QRect(10, 20, 121, 21)) - self.groupBox_scConfig = QGroupBox(self.tab) - self.groupBox_scConfig.setObjectName(u"groupBox_scConfig") - self.groupBox_scConfig.setGeometry(QRect(10, 260, 631, 321)) - self.horizontalLayoutWidget_6 = QWidget(self.groupBox_scConfig) - self.horizontalLayoutWidget_6.setObjectName(u"horizontalLayoutWidget_6") - self.horizontalLayoutWidget_6.setGeometry(QRect(0, 20, 631, 301)) - self.horizontalLayout_6 = QHBoxLayout(self.horizontalLayoutWidget_6) - self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") - self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0) - self.scrollArea = QScrollArea(self.horizontalLayoutWidget_6) - self.scrollArea.setObjectName(u"scrollArea") - sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy) - self.scrollArea.setAutoFillBackground(True) - self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) - self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.scrollArea.setWidgetResizable(True) - self.scrollAreaWidgetContents = QWidget() - self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 613, 68)) - sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth()) - self.scrollAreaWidgetContents.setSizePolicy(sizePolicy1) - self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents) - self.verticalLayout_3.setObjectName(u"verticalLayout_3") - self.verticalLayout_3.setSizeConstraint(QLayout.SizeConstraint.SetDefaultConstraint) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - - self.horizontalLayout_6.addWidget(self.scrollArea, 0, Qt.AlignmentFlag.AlignTop) - - self.spinBox_configNumber = QSpinBox(self.groupBox_scConfig) - self.spinBox_configNumber.setObjectName(u"spinBox_configNumber") - self.spinBox_configNumber.setGeometry(QRect(150, -1, 48, 21)) - self.spinBox_configNumber.setMinimum(1) - self.groupBox_masterConfig = QGroupBox(self.tab) - self.groupBox_masterConfig.setObjectName(u"groupBox_masterConfig") - self.groupBox_masterConfig.setGeometry(QRect(10, 60, 631, 181)) - self.horizontalLayoutWidget_5 = QWidget(self.groupBox_masterConfig) - self.horizontalLayoutWidget_5.setObjectName(u"horizontalLayoutWidget_5") - self.horizontalLayoutWidget_5.setGeometry(QRect(0, 20, 631, 161)) - self.horizontalLayout_5 = QHBoxLayout(self.horizontalLayoutWidget_5) - self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") - self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) - self.textEdit_masterConfig = QTextEdit(self.horizontalLayoutWidget_5) - self.textEdit_masterConfig.setObjectName(u"textEdit_masterConfig") - self.textEdit_masterConfig.setReadOnly(False) - - self.horizontalLayout_5.addWidget(self.textEdit_masterConfig) - - self.pushButton_save = QPushButton(self.tab) - self.pushButton_save.setObjectName(u"pushButton_save") - self.pushButton_save.setGeometry(QRect(220, 590, 94, 26)) - self.pushButton_saveAs = QPushButton(self.tab) - self.pushButton_saveAs.setObjectName(u"pushButton_saveAs") - self.pushButton_saveAs.setGeometry(QRect(330, 590, 94, 26)) - self.label_nos3Logo = QLabel(self.tab) - self.label_nos3Logo.setObjectName(u"label_nos3Logo") - self.label_nos3Logo.setGeometry(QRect(50, 585, 111, 41)) - self.label_nos3Logo.setScaledContents(True) - self.label_jstarLogo = QLabel(self.tab) - self.label_jstarLogo.setObjectName(u"label_jstarLogo") - self.label_jstarLogo.setGeometry(QRect(480, 585, 131, 41)) - self.label_jstarLogo.setScaledContents(True) - self.label_jstarLogo.setAlignment(Qt.AlignmentFlag.AlignLeading|Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter) - self.tabWidget.addTab(self.tab, "") - self.tab_2 = QWidget() - self.tab_2.setObjectName(u"tab_2") - self.groupBox_8 = QGroupBox(self.tab_2) - self.groupBox_8.setObjectName(u"groupBox_8") - self.groupBox_8.setGeometry(QRect(10, 120, 631, 491)) - self.groupBox_8.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.groupBox_8.setFlat(False) - self.groupBox_8.setCheckable(False) - self.textEdit_buildConsole = QTextEdit(self.groupBox_8) - self.textEdit_buildConsole.setObjectName(u"textEdit_buildConsole") - self.textEdit_buildConsole.setGeometry(QRect(0, 20, 631, 471)) - palette1 = QPalette() - palette1.setBrush(QPalette.Active, QPalette.Text, brush1) - palette1.setBrush(QPalette.Active, QPalette.Base, brush) - palette1.setBrush(QPalette.Inactive, QPalette.Text, brush1) - palette1.setBrush(QPalette.Inactive, QPalette.Base, brush) - self.textEdit_buildConsole.setPalette(palette1) - self.textEdit_buildConsole.setReadOnly(True) - self.frame_2 = QFrame(self.tab_2) - self.frame_2.setObjectName(u"frame_2") - self.frame_2.setGeometry(QRect(10, 20, 641, 81)) - self.frame_2.setFrameShape(QFrame.Shape.StyledPanel) - self.frame_2.setFrameShadow(QFrame.Shadow.Raised) - self.frame = QFrame(self.frame_2) - self.frame.setObjectName(u"frame") - self.frame.setGeometry(QRect(79, 0, 561, 80)) - self.frame.setFrameShape(QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QFrame.Shadow.Raised) - self.gridLayoutWidget_2 = QWidget(self.frame) - self.gridLayoutWidget_2.setObjectName(u"gridLayoutWidget_2") - self.gridLayoutWidget_2.setGeometry(QRect(0, 0, 561, 80)) - self.gridLayout_buildCleanButtons = QGridLayout(self.gridLayoutWidget_2) - self.gridLayout_buildCleanButtons.setObjectName(u"gridLayout_buildCleanButtons") - self.gridLayout_buildCleanButtons.setContentsMargins(0, 0, 0, 0) - self.pushButton_cleanAll = QPushButton(self.gridLayoutWidget_2) - self.pushButton_cleanAll.setObjectName(u"pushButton_cleanAll") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_cleanAll, 1, 0, 1, 1) - - self.pushButton_buildAll = QPushButton(self.gridLayoutWidget_2) - self.pushButton_buildAll.setObjectName(u"pushButton_buildAll") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_buildAll, 0, 0, 1, 1) - - self.pushButton_fswBuild = QPushButton(self.gridLayoutWidget_2) - self.pushButton_fswBuild.setObjectName(u"pushButton_fswBuild") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_fswBuild, 0, 2, 1, 1) - - self.pushButton_cfgBuild = QPushButton(self.gridLayoutWidget_2) - self.pushButton_cfgBuild.setObjectName(u"pushButton_cfgBuild") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_cfgBuild, 0, 1, 1, 1) - - self.pushButton_simClean = QPushButton(self.gridLayoutWidget_2) - self.pushButton_simClean.setObjectName(u"pushButton_simClean") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_simClean, 1, 4, 1, 1) - - self.pushButton_simBuild = QPushButton(self.gridLayoutWidget_2) - self.pushButton_simBuild.setObjectName(u"pushButton_simBuild") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_simBuild, 0, 4, 1, 1) - - self.pushButton_gswBuild = QPushButton(self.gridLayoutWidget_2) - self.pushButton_gswBuild.setObjectName(u"pushButton_gswBuild") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_gswBuild, 0, 3, 1, 1) - - self.pushButton_fswClean = QPushButton(self.gridLayoutWidget_2) - self.pushButton_fswClean.setObjectName(u"pushButton_fswClean") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_fswClean, 1, 2, 1, 1) - - self.pushButton_gswClean = QPushButton(self.gridLayoutWidget_2) - self.pushButton_gswClean.setObjectName(u"pushButton_gswClean") - - self.gridLayout_buildCleanButtons.addWidget(self.pushButton_gswClean, 1, 3, 1, 1) - - self.frame_3 = QFrame(self.frame_2) - self.frame_3.setObjectName(u"frame_3") - self.frame_3.setGeometry(QRect(0, 0, 81, 41)) - self.frame_3.setFrameShape(QFrame.Shape.StyledPanel) - self.frame_3.setFrameShadow(QFrame.Shadow.Raised) - self.verticalLayoutWidget = QWidget(self.frame_3) - self.verticalLayoutWidget.setObjectName(u"verticalLayoutWidget") - self.verticalLayoutWidget.setGeometry(QRect(0, 0, 81, 41)) - self.verticalLayout = QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout.setObjectName(u"verticalLayout") - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.label_4 = QLabel(self.verticalLayoutWidget) - self.label_4.setObjectName(u"label_4") - self.label_4.setAlignment(Qt.AlignmentFlag.AlignCenter) - - self.verticalLayout.addWidget(self.label_4) - - self.frame_4 = QFrame(self.frame_2) - self.frame_4.setObjectName(u"frame_4") - self.frame_4.setGeometry(QRect(0, 40, 81, 41)) - self.frame_4.setFrameShape(QFrame.Shape.StyledPanel) - self.frame_4.setFrameShadow(QFrame.Shadow.Raised) - self.verticalLayoutWidget_2 = QWidget(self.frame_4) - self.verticalLayoutWidget_2.setObjectName(u"verticalLayoutWidget_2") - self.verticalLayoutWidget_2.setGeometry(QRect(0, 0, 81, 41)) - self.verticalLayout_2 = QVBoxLayout(self.verticalLayoutWidget_2) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) - self.label_5 = QLabel(self.verticalLayoutWidget_2) - self.label_5.setObjectName(u"label_5") - self.label_5.setAlignment(Qt.AlignmentFlag.AlignCenter) - - self.verticalLayout_2.addWidget(self.label_5) - - self.tabWidget.addTab(self.tab_2, "") - self.tab_3 = QWidget() - self.tab_3.setObjectName(u"tab_3") - self.groupBox_control = QGroupBox(self.tab_3) - self.groupBox_control.setObjectName(u"groupBox_control") - self.groupBox_control.setGeometry(QRect(10, 10, 631, 611)) - self.horizontalLayoutWidget = QWidget(self.groupBox_control) - self.horizontalLayoutWidget.setObjectName(u"horizontalLayoutWidget") - self.horizontalLayoutWidget.setGeometry(QRect(10, 570, 611, 31)) - self.horizontalLayout = QHBoxLayout(self.horizontalLayoutWidget) - self.horizontalLayout.setSpacing(45) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.pushButton_play = QPushButton(self.horizontalLayoutWidget) - self.pushButton_play.setObjectName(u"pushButton_play") - icon = QIcon() - icon.addFile(u"../../../../../../../../usr/share/icons/Humanity/actions/24/gtk-media-play-ltr.svg", QSize(), QIcon.Normal, QIcon.Off) - self.pushButton_play.setIcon(icon) - - self.horizontalLayout.addWidget(self.pushButton_play) - - self.pushButton_pause = QPushButton(self.horizontalLayoutWidget) - self.pushButton_pause.setObjectName(u"pushButton_pause") - icon1 = QIcon() - icon1.addFile(u"../../../../../../../../usr/share/icons/Humanity/actions/24/media-playback-pause.svg", QSize(), QIcon.Normal, QIcon.Off) - self.pushButton_pause.setIcon(icon1) - - self.horizontalLayout.addWidget(self.pushButton_pause) - - self.horizontalLayoutWidget_2 = QWidget(self.groupBox_control) - self.horizontalLayoutWidget_2.setObjectName(u"horizontalLayoutWidget_2") - self.horizontalLayoutWidget_2.setGeometry(QRect(10, 10, 611, 41)) - self.horizontalLayout_2 = QHBoxLayout(self.horizontalLayoutWidget_2) - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.pushButton_launch = QPushButton(self.horizontalLayoutWidget_2) - self.pushButton_launch.setObjectName(u"pushButton_launch") - - self.horizontalLayout_2.addWidget(self.pushButton_launch) - - self.pushButton_stop = QPushButton(self.horizontalLayoutWidget_2) - self.pushButton_stop.setObjectName(u"pushButton_stop") - - self.horizontalLayout_2.addWidget(self.pushButton_stop) - - self.groupBox_9 = QGroupBox(self.groupBox_control) - self.groupBox_9.setObjectName(u"groupBox_9") - self.groupBox_9.setGeometry(QRect(10, 60, 611, 451)) - self.groupBox_9.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.groupBox_9.setFlat(False) - self.groupBox_9.setCheckable(False) - self.textEdit_launchConsole = QTextEdit(self.groupBox_9) - self.textEdit_launchConsole.setObjectName(u"textEdit_launchConsole") - self.textEdit_launchConsole.setGeometry(QRect(0, 20, 611, 431)) - palette2 = QPalette() - palette2.setBrush(QPalette.Active, QPalette.Text, brush1) - palette2.setBrush(QPalette.Active, QPalette.Base, brush) - palette2.setBrush(QPalette.Inactive, QPalette.Text, brush1) - palette2.setBrush(QPalette.Inactive, QPalette.Base, brush) - self.textEdit_launchConsole.setPalette(palette2) - self.textEdit_launchConsole.setReadOnly(True) - self.horizontalLayoutWidget_3 = QWidget(self.groupBox_control) - self.horizontalLayoutWidget_3.setObjectName(u"horizontalLayoutWidget_3") - self.horizontalLayoutWidget_3.setGeometry(QRect(190, 520, 261, 41)) - self.horizontalLayout_runForUntil = QHBoxLayout(self.horizontalLayoutWidget_3) - self.horizontalLayout_runForUntil.setObjectName(u"horizontalLayout_runForUntil") - self.horizontalLayout_runForUntil.setContentsMargins(0, 0, 0, 0) - self.comboBox_run = QComboBox(self.horizontalLayoutWidget_3) - self.comboBox_run.addItem("") - self.comboBox_run.addItem("") - self.comboBox_run.addItem("") - self.comboBox_run.setObjectName(u"comboBox_run") - - self.horizontalLayout_runForUntil.addWidget(self.comboBox_run) - - self.lineEdit_secondsEntry = QLineEdit(self.horizontalLayoutWidget_3) - self.lineEdit_secondsEntry.setObjectName(u"lineEdit_secondsEntry") - self.lineEdit_secondsEntry.setAlignment(Qt.AlignmentFlag.AlignCenter) - - self.horizontalLayout_runForUntil.addWidget(self.lineEdit_secondsEntry) - - self.tabWidget.addTab(self.tab_3, "") - - self.retranslateUi(Form) - - self.tabWidget.setCurrentIndex(0) - - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) - self.lineEdit_curConfig.setPlaceholderText(QCoreApplication.translate("Form", u"None", None)) - self.pushButton_browse.setText(QCoreApplication.translate("Form", u"Browse...", None)) - self.label_curConfig.setText(QCoreApplication.translate("Form", u"Current Config:", None)) - self.groupBox_scConfig.setTitle(QCoreApplication.translate("Form", u"Spacecraft Config", None)) - self.groupBox_masterConfig.setTitle(QCoreApplication.translate("Form", u"Master Config", None)) - self.pushButton_save.setText(QCoreApplication.translate("Form", u"Save", None)) - self.pushButton_saveAs.setText(QCoreApplication.translate("Form", u"Save As...", None)) - self.label_nos3Logo.setText("") - self.label_jstarLogo.setText("") - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QCoreApplication.translate("Form", u"Config", None)) - self.groupBox_8.setTitle(QCoreApplication.translate("Form", u"Console Output", None)) - self.pushButton_cleanAll.setText(QCoreApplication.translate("Form", u"All", None)) - self.pushButton_buildAll.setText(QCoreApplication.translate("Form", u"All", None)) - self.pushButton_fswBuild.setText(QCoreApplication.translate("Form", u"FSW", None)) - self.pushButton_cfgBuild.setText(QCoreApplication.translate("Form", u"CFG", None)) - self.pushButton_simClean.setText(QCoreApplication.translate("Form", u"SIM", None)) - self.pushButton_simBuild.setText(QCoreApplication.translate("Form", u"SIM", None)) - self.pushButton_gswBuild.setText(QCoreApplication.translate("Form", u"GSW", None)) - self.pushButton_fswClean.setText(QCoreApplication.translate("Form", u"FSW", None)) - self.pushButton_gswClean.setText(QCoreApplication.translate("Form", u"GSW", None)) - self.label_4.setText(QCoreApplication.translate("Form", u"Build", None)) - self.label_5.setText(QCoreApplication.translate("Form", u"Clean", None)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("Form", u"Build", None)) - self.groupBox_control.setTitle("") - self.pushButton_play.setText(QCoreApplication.translate("Form", u"Play", None)) - self.pushButton_pause.setText(QCoreApplication.translate("Form", u"Pause", None)) - self.pushButton_launch.setText(QCoreApplication.translate("Form", u"Launch", None)) - self.pushButton_stop.setText(QCoreApplication.translate("Form", u"Stop", None)) - self.groupBox_9.setTitle(QCoreApplication.translate("Form", u"NOS3 Time Driver", None)) - self.comboBox_run.setItemText(0, "") - self.comboBox_run.setItemText(1, QCoreApplication.translate("Form", u"Run For", None)) - self.comboBox_run.setItemText(2, QCoreApplication.translate("Form", u"Run Until", None)) - - self.lineEdit_secondsEntry.setText("") - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QCoreApplication.translate("Form", u"Launch", None)) - # retranslateUi - diff --git a/cfg/gui/datetime_dialog.py b/cfg/gui/datetime_dialog.py new file mode 100644 index 00000000..9862c34b --- /dev/null +++ b/cfg/gui/datetime_dialog.py @@ -0,0 +1,223 @@ +import customtkinter as ctk +import tkinter as tk +from tkinter import messagebox +from datetime import datetime, timezone + +class DateTimeDialog: + def __init__(self, parent, title): + self.result = None + + # Create dialog window + self.dialog = ctk.CTkToplevel(parent) + self.dialog.title(title) + self.dialog.geometry("500x350") + self.dialog.transient(parent) + + # Center the dialog + self.dialog.update_idletasks() + x = (self.dialog.winfo_screenwidth() // 2) - (500 // 2) + y = (self.dialog.winfo_screenheight() // 2) - (350 // 2) + self.dialog.geometry(f"500x350+{x}+{y}") + + # Setup widgets + self.setup_dialog() + + # Schedule grab_set and focus_set to happen after dialog is fully created + self.dialog.after(100, self._set_grab_and_focus) + + # Wait for dialog to close + parent.wait_window(self.dialog) + + def _set_grab_and_focus(self): + """Set grab and focus after the dialog is fully visible""" + try: + self.dialog.grab_set() + self.year_entry.focus_set() + except Exception as e: + print(f"Warning: Could not set grab: {e}") + + def setup_dialog(self): + # Main frame + main_frame = ctk.CTkFrame(self.dialog) + main_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Title label + ctk.CTkLabel(main_frame, text="Set Mission Date and Time", + font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(0, 20)) + + # Date section + date_frame = ctk.CTkFrame(main_frame) + date_frame.pack(fill="x", pady=10) + + ctk.CTkLabel(date_frame, text="Date:").pack(side="left", padx=10) + + # Year + ctk.CTkLabel(date_frame, text="Year:").pack(side="left", padx=(10, 0)) + self.year_entry = ctk.CTkEntry(date_frame, width=70) + self.year_entry.pack(side="left", padx=(0, 10)) + self.year_entry.insert(0, str(datetime.now().year)) + + # Month + ctk.CTkLabel(date_frame, text="Month:").pack(side="left", padx=(10, 0)) + + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.month_var = tk.StringVar() + self.month_var.set(months[datetime.now().month - 1]) + self.month_combo = ctk.CTkComboBox(date_frame, values=months, variable=self.month_var, width=70) + self.month_combo.pack(side="left", padx=(0, 10)) + + # Day + ctk.CTkLabel(date_frame, text="Day:").pack(side="left", padx=(10, 0)) + self.day_entry = ctk.CTkEntry(date_frame, width=50) + self.day_entry.pack(side="left", padx=(0, 10)) + self.day_entry.insert(0, str(datetime.now().day)) + + # Time section + time_frame = ctk.CTkFrame(main_frame) + time_frame.pack(fill="x", pady=10) + + ctk.CTkLabel(time_frame, text="Time (UTC):").pack(side="left", padx=10) + + # Hour + ctk.CTkLabel(time_frame, text="Hour:").pack(side="left", padx=(10, 0)) + self.hour_entry = ctk.CTkEntry(time_frame, width=50) + self.hour_entry.pack(side="left", padx=(0, 10)) + self.hour_entry.insert(0, "12") + + # Minute + ctk.CTkLabel(time_frame, text="Min:").pack(side="left", padx=(10, 0)) + self.minute_entry = ctk.CTkEntry(time_frame, width=50) + self.minute_entry.pack(side="left", padx=(0, 10)) + self.minute_entry.insert(0, "00") + + # Second + ctk.CTkLabel(time_frame, text="Sec:").pack(side="left", padx=(10, 0)) + self.second_entry = ctk.CTkEntry(time_frame, width=50) + self.second_entry.pack(side="left", padx=(0, 10)) + self.second_entry.insert(0, "00") + + # Current time button + current_btn = ctk.CTkButton(main_frame, text="Use Current Date/Time", + command=self.use_current_time) + current_btn.pack(pady=10) + + # Preview section + preview_frame = ctk.CTkFrame(main_frame) + preview_frame.pack(fill="x", pady=10) + + self.preview_label = ctk.CTkLabel(preview_frame, text="", wraplength=450) + self.preview_label.pack(pady=10) + + # Update preview initially + self.update_preview() + + # Bind events to update preview + self.year_entry.bind('', lambda e: self.update_preview()) + self.day_entry.bind('', lambda e: self.update_preview()) + self.hour_entry.bind('', lambda e: self.update_preview()) + self.minute_entry.bind('', lambda e: self.update_preview()) + self.second_entry.bind('', lambda e: self.update_preview()) + self.month_var.trace_add("write", lambda *args: self.update_preview()) + + # Buttons + button_frame = ctk.CTkFrame(main_frame) + button_frame.pack(fill="x", pady=(20, 0)) + + cancel_btn = ctk.CTkButton(button_frame, text="Cancel", command=self.cancel, width=100) + cancel_btn.pack(side="right", padx=(10, 0)) + + ok_btn = ctk.CTkButton(button_frame, text="OK", command=self.ok, width=100) + ok_btn.pack(side="right") + + # Bind Enter and Escape keys + self.dialog.bind('', lambda e: self.ok()) + self.dialog.bind('', lambda e: self.cancel()) + + def use_current_time(self): + """Fill in current date and time""" + now = datetime.now(timezone.utc) + + self.year_entry.delete(0, 'end') + self.year_entry.insert(0, str(now.year)) + + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.month_var.set(months[now.month - 1]) + + self.day_entry.delete(0, 'end') + self.day_entry.insert(0, str(now.day)) + + self.hour_entry.delete(0, 'end') + self.hour_entry.insert(0, f"{now.hour:02d}") + + self.minute_entry.delete(0, 'end') + self.minute_entry.insert(0, f"{now.minute:02d}") + + self.second_entry.delete(0, 'end') + self.second_entry.insert(0, f"{now.second:02d}") + + self.update_preview() + + def update_preview(self): + """Update the preview of the selected date/time""" + try: + year = int(self.year_entry.get()) + + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + month_name = self.month_var.get() + month = months.index(month_name) + 1 + + day = int(self.day_entry.get()) + hour = int(self.hour_entry.get()) + minute = int(self.minute_entry.get()) + second = int(self.second_entry.get()) + + # Create datetime object + dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc) + + # Update preview + preview_text = f"Date: {dt.strftime('%d %b %Y %H:%M:%S UTC')}" + self.preview_label.configure(text=preview_text) + + except (ValueError, IndexError): + self.preview_label.configure(text="Invalid date/time") + + def ok(self): + try: + year = int(self.year_entry.get()) + + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + month_name = self.month_var.get() + month = months.index(month_name) + 1 + + day = int(self.day_entry.get()) + hour = int(self.hour_entry.get()) + minute = int(self.minute_entry.get()) + second = int(self.second_entry.get()) + + # Validate ranges + if not (1 <= month <= 12): + raise ValueError("Invalid month") + if not (1 <= day <= 31): + raise ValueError("Invalid day") + if not (0 <= hour <= 23): + raise ValueError("Invalid hour") + if not (0 <= minute <= 59): + raise ValueError("Invalid minute") + if not (0 <= second <= 59): + raise ValueError("Invalid second") + + # Create datetime object + dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc) + + self.result = dt + self.dialog.destroy() + + except (ValueError, IndexError) as e: + messagebox.showerror("Error", f"Invalid date/time: {str(e)}", parent=self.dialog) + + def cancel(self): + self.dialog.destroy() \ No newline at end of file diff --git a/cfg/gui/igniter_entrypoint.py b/cfg/gui/igniter_entrypoint.py new file mode 100644 index 00000000..942734ba --- /dev/null +++ b/cfg/gui/igniter_entrypoint.py @@ -0,0 +1,6 @@ +from nos3_gui import NOS3ConfigGUI + + +if __name__ == "__main__": + app = NOS3ConfigGUI() + app.run() \ No newline at end of file diff --git a/cfg/gui/nos3_gui.py b/cfg/gui/nos3_gui.py new file mode 100644 index 00000000..ecdcf40f --- /dev/null +++ b/cfg/gui/nos3_gui.py @@ -0,0 +1,1794 @@ +import customtkinter as ctk +import tkinter as tk +from tkinter import filedialog, messagebox +import xml.etree.ElementTree as ET +from xml.dom import minidom +import os +from datetime import datetime, timedelta, timezone +from PIL import Image + +from add_dialog import SimpleNameDialog +from datetime_dialog import DateTimeDialog + +J2000_EPOCH = datetime(2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc) +J2000_TIMESTAMP = J2000_EPOCH.timestamp() + +class NOS3ConfigGUI: + # Update the __init__ method to initialize mission data + def __init__(self): + # Set appearance mode and color theme + ctk.set_appearance_mode("dark") # or "light" + ctk.set_default_color_theme("./cfg/gui/resources/orange.json") # or "green", "dark-blue" + + # Create main window + self.root = ctk.CTk() + self.root.title("NOS3 IGNITER") + self.root.geometry("1200x800") + self.root.minsize(800, 600) + + # Set window icon + self.set_window_icon() + + # Data structures + # Data structures + self.current_mission_file = None + self.current_spacecraft_file = None + self.spacecraft_config_path = None # Full path to spacecraft config + self.apps_data = {} # Dictionary of app_name: enabled + self.components_data = {} # Dictionary of component_name: enabled + self.mission_data = {} # Mission configuration data + self.additional_data = {} + self.modified = False + + # setup options selections + self.default_mission_config = "./cfg/nos3-mission.xml" + self.fsw_options = ["cfs", "fprime"] + self.gsw_options = ["cosmos", "openc3", "fprime", "yamcs"] + config_dir = "./cfg/spacecraft/" + self.config_filenames = { + filename for filename in os.listdir(config_dir) if os.path.isfile(os.path.join(config_dir, filename)) + } + + # Initialize mission variables (these will be used by the mission config tab) + self.gsw_var = None + self.fsw_var = None + self.sc_count_var = None + self.sc1_config_var = None + + self.setup_ui() + + def set_window_icon(self): + """Set the window icon with proper processing""" + try: + icon_path = "./cfg/gui/resources/nos3_original.png" + + if os.path.exists(icon_path): + # Open with PIL and process + with Image.open(icon_path) as img: + # Resize to standard icon size (use Image.LANCZOS instead of Image.Resampling.LANCZOS) + img = img.resize((64, 64), Image.LANCZOS) + + # Convert RGBA to RGB with white background if it has transparency + if img.mode in ('RGBA', 'LA'): + background = Image.new('RGB', img.size, (255, 255, 255)) # type: ignore + if img.mode == 'RGBA': + background.paste(img, mask=img.split()[-1]) + else: + background.paste(img) + img = background + + # Save processed image temporarily + temp_path = "./cfg/gui/resources/temp_icon.png" + img.save(temp_path, "PNG") + + # Load with tkinter + icon = tk.PhotoImage(file=temp_path) + self.root.iconphoto(False, icon) + + # Clean up + os.remove(temp_path) + + else: + print(f"Icon file not found: {icon_path}") + + except Exception as e: + print(f"Could not set window icon: {e}") + + def setup_ui(self): + # Create main frame + self.main_frame = ctk.CTkFrame(self.root) + self.main_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Setup logos first + self.setup_logos() + self.add_header_logo() + + # Create menu bar + self.create_menu_bar() + + # Create notebook for tabs + self.notebook = ctk.CTkTabview(self.main_frame) + self.notebook.pack(fill="both", expand=True, padx=10, pady=(50, 10)) + + # Create tabs + self.mission_tab = self.notebook.add("Mission Config") + self.apps_tab = self.notebook.add("Applications") + self.components_tab = self.notebook.add("Components") + self.additional_tab = self.notebook.add("Additional Options") + self.preview_tab = self.notebook.add("XML Preview") + + self.setup_mission_tab() + self.add_tab_logos + self.setup_apps_tab() + self.setup_components_tab() + self.setup_additional_tab() + self.setup_preview_tab() + + # Status bar + self.status_frame = ctk.CTkFrame(self.main_frame, height=30) + self.status_frame.pack(fill="x", padx=10, pady=(0, 10)) + self.status_frame.pack_propagate(False) + + self.status_label = ctk.CTkLabel(self.status_frame, text="Ready - Open a mission configuration file to begin") + self.status_label.pack(side="left", padx=10, pady=5) + + self.root.after(100, self.open_mission, True) + + def setup_additional_tab(self): + """Setup the Additional Options tab for GUI, Orbit, and Sim configurations""" + # Create main frame for the additional options tab + self.additional_frame = ctk.CTkFrame(self.additional_tab) + self.additional_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create a scrollable frame + self.additional_scrollable = ctk.CTkScrollableFrame(self.additional_frame) + self.additional_scrollable.pack(fill="both", expand=True, padx=10, pady=10) + + # Header + header_label = ctk.CTkLabel(self.additional_scrollable, text="Additional Spacecraft Options", + font=ctk.CTkFont(size=16, weight="bold")) + header_label.pack(pady=(5, 20)) + + # GUI Section + self.setup_gui_section() + + # Orbit Section + self.setup_orbit_section() + + # Sim Section + self.setup_sim_section() + + def setup_gui_section(self): + """Setup the GUI configuration section""" + # GUI Frame + gui_frame = ctk.CTkFrame(self.additional_scrollable) + gui_frame.pack(fill="x", pady=10, padx=5) + + gui_label = ctk.CTkLabel(gui_frame, text="GUI Configuration:", + font=ctk.CTkFont(size=14, weight="bold")) + gui_label.pack(anchor="w", padx=10, pady=(10, 5)) + + # GUI Enable checkbox + self.gui_enabled_var = tk.BooleanVar(value=True) + self.gui_enabled_check = ctk.CTkCheckBox(gui_frame, text="Enable GUI", + variable=self.gui_enabled_var, + command=self.on_gui_change) + self.gui_enabled_check.pack(anchor="w", padx=20, pady=5) + + def setup_orbit_section(self): + """Setup the Orbit configuration section""" + # Orbit Frame + orbit_frame = ctk.CTkFrame(self.additional_scrollable) + orbit_frame.pack(fill="x", pady=10, padx=5) + + orbit_label = ctk.CTkLabel(orbit_frame, text="Orbit Configuration:", + font=ctk.CTkFont(size=14, weight="bold")) + orbit_label.pack(anchor="w", padx=10, pady=(10, 5)) + + # Orbit parameters in a grid-like layout + params_frame = ctk.CTkFrame(orbit_frame) + params_frame.pack(fill="x", padx=10, pady=5) + + # Tipoff X + tipoff_x_frame = ctk.CTkFrame(params_frame) + tipoff_x_frame.pack(fill="x", pady=2) + + ctk.CTkLabel(tipoff_x_frame, text="Tipoff X:", width=100).pack(side="left", padx=10, pady=5) + self.tipoff_x_entry = ctk.CTkEntry(tipoff_x_frame, width=100, placeholder_text="0.2") + self.tipoff_x_entry.pack(side="left", padx=5, pady=5) + self.tipoff_x_entry.insert(0, "0.2") + self.tipoff_x_entry.bind('', lambda e: self.set_modified()) + + ctk.CTkLabel(tipoff_x_frame, text="(degrees/second)").pack(side="left", padx=5, pady=5) + + # Tipoff Y + tipoff_y_frame = ctk.CTkFrame(params_frame) + tipoff_y_frame.pack(fill="x", pady=2) + + ctk.CTkLabel(tipoff_y_frame, text="Tipoff Y:", width=100).pack(side="left", padx=10, pady=5) + self.tipoff_y_entry = ctk.CTkEntry(tipoff_y_frame, width=100, placeholder_text="2.0") + self.tipoff_y_entry.pack(side="left", padx=5, pady=5) + self.tipoff_y_entry.insert(0, "2.0") + self.tipoff_y_entry.bind('', lambda e: self.set_modified()) + + ctk.CTkLabel(tipoff_y_frame, text="(degrees/second)").pack(side="left", padx=5, pady=5) + + # Tipoff Z + tipoff_z_frame = ctk.CTkFrame(params_frame) + tipoff_z_frame.pack(fill="x", pady=2) + + ctk.CTkLabel(tipoff_z_frame, text="Tipoff Z:", width=100).pack(side="left", padx=10, pady=5) + self.tipoff_z_entry = ctk.CTkEntry(tipoff_z_frame, width=100, placeholder_text="-2.0") + self.tipoff_z_entry.pack(side="left", padx=5, pady=5) + self.tipoff_z_entry.insert(0, "-2.0") + self.tipoff_z_entry.bind('', lambda e: self.set_modified()) + + ctk.CTkLabel(tipoff_z_frame, text="(degrees/second)").pack(side="left", padx=5, pady=5) + + def setup_sim_section(self): + """Setup the Simulation configuration section""" + # Sim Frame + sim_frame = ctk.CTkFrame(self.additional_scrollable) + sim_frame.pack(fill="x", pady=10, padx=5) + + sim_label = ctk.CTkLabel(sim_frame, text="Simulation Configuration:", + font=ctk.CTkFont(size=14, weight="bold")) + sim_label.pack(anchor="w", padx=10, pady=(10, 5)) + + # Sim Truth Interface + sim_truth_frame = ctk.CTkFrame(sim_frame) + sim_truth_frame.pack(fill="x", padx=10, pady=5) + + ctk.CTkLabel(sim_truth_frame, text="Sim Truth Interface:", width=150).pack(side="left", padx=10, pady=5) + + self.sim_truth_var = tk.BooleanVar(value=True) + self.sim_truth_check = ctk.CTkCheckBox(sim_truth_frame, text="Enable", + variable=self.sim_truth_var, + command=self.on_sim_change) + self.sim_truth_check.pack(side="left", padx=10, pady=5) + + # Add methods to handle changes + def on_gui_change(self): + """Handle GUI configuration changes""" + self.set_modified() + + def on_sim_change(self): + """Handle Sim configuration changes""" + self.set_modified() + + # Add this method to the NOS3ConfigGUI class + def setup_mission_tab(self): + # Create main frame for the mission tab + self.mission_frame = ctk.CTkFrame(self.mission_tab) + self.mission_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create a scrollable frame for mission configuration + self.mission_scrollable = ctk.CTkScrollableFrame(self.mission_frame) + self.mission_scrollable.pack(fill="both", expand=True, padx=10, pady=10) + + # Mission Configuration Header + header_label = ctk.CTkLabel(self.mission_scrollable, text="NOS3 Mission Configuration", + font=ctk.CTkFont(size=16, weight="bold")) + header_label.pack(pady=(5, 20)) + + # Mission Start Time Section + time_frame = ctk.CTkFrame(self.mission_scrollable) + time_frame.pack(fill="x", pady=10, padx=5) + + time_label = ctk.CTkLabel(time_frame, text="Mission Start Time:", + font=ctk.CTkFont(size=14, weight="bold")) + time_label.pack(anchor="w", padx=10, pady=(5, 0)) + + time_help = ctk.CTkLabel(time_frame, text="J2000 format: seconds since Jan 1, 2000, 12:00:00 UTC") + time_help.pack(anchor="w", padx=10, pady=(0, 5)) + + # Time input row + time_input_frame = ctk.CTkFrame(time_frame) + time_input_frame.pack(fill="x", padx=10, pady=5) + + # Time entry + ctk.CTkLabel(time_input_frame, text="J2000 Time:").pack(side="left", padx=(0, 5)) + self.mission_time_entry = ctk.CTkEntry(time_input_frame, width=200) + self.mission_time_entry.pack(side="left", padx=(0, 10)) + + # Current time button + current_time_btn = ctk.CTkButton(time_input_frame, text="Use Current Time", + command=self.set_current_mission_time, + width=130) + current_time_btn.pack(side="left", padx=5) + + # Set date button + set_date_btn = ctk.CTkButton(time_input_frame, text="Set Date...", + command=self.set_mission_date, + width=100) + set_date_btn.pack(side="left", padx=5) + + # Display time in human-readable format + self.time_display_label = ctk.CTkLabel(time_frame, text="", wraplength=800) + self.time_display_label.pack(anchor="w", padx=10, pady=5) + + # Ground Software Section + gsw_frame = ctk.CTkFrame(self.mission_scrollable) + gsw_frame.pack(fill="x", pady=10, padx=5) + + gsw_label = ctk.CTkLabel(gsw_frame, text="Ground Software:", + font=ctk.CTkFont(size=14, weight="bold")) + gsw_label.pack(anchor="w", padx=10, pady=5) + + # Ground software options + self.gsw_var = tk.StringVar() + + # Radio buttons for ground software + for option in self.gsw_options: + rb = ctk.CTkRadioButton(gsw_frame, text=option, variable=self.gsw_var, value=option) + rb.pack(anchor="w", padx=20, pady=2) + + # Flight Software Section + fsw_frame = ctk.CTkFrame(self.mission_scrollable) + fsw_frame.pack(fill="x", pady=10, padx=5) + + fsw_label = ctk.CTkLabel(fsw_frame, text="Flight Software:", + font=ctk.CTkFont(size=14, weight="bold")) + fsw_label.pack(anchor="w", padx=10, pady=5) + + # Flight software options + self.fsw_var = tk.StringVar() + + # Radio buttons for flight software + for option in self.fsw_options: + rb = ctk.CTkRadioButton(fsw_frame, text=option, variable=self.fsw_var, value=option) + rb.pack(anchor="w", padx=20, pady=2) + + # Number of Spacecraft Section + sc_frame = ctk.CTkFrame(self.mission_scrollable) + sc_frame.pack(fill="x", pady=10, padx=5) + + sc_label = ctk.CTkLabel(sc_frame, text="Number of Spacecraft: (Multiple spacecraft not fully implemented)", + font=ctk.CTkFont(size=14, weight="bold")) + sc_label.pack(anchor="w", padx=10, pady=5) + + # Number of spacecraft input + sc_input_frame = ctk.CTkFrame(sc_frame) + sc_input_frame.pack(fill="x", padx=10, pady=5) + + self.sc_count_var = tk.StringVar() + sc_count_values = [str(i) for i in range(1, 11)] # 1-10 spacecraft + + sc_count_dropdown = ctk.CTkComboBox(sc_input_frame, values=sc_count_values, + variable=self.sc_count_var, + width=100) + sc_count_dropdown.pack(side="left", padx=5) + + # Spacecraft Configuration Section + sc_config_frame = ctk.CTkFrame(self.mission_scrollable) + sc_config_frame.pack(fill="x", pady=10, padx=5) + + sc_config_label = ctk.CTkLabel(sc_config_frame, text="Spacecraft Configuration:", + font=ctk.CTkFont(size=14, weight="bold")) + sc_config_label.pack(anchor="w", padx=10, pady=5) + + # Frame to hold configuration options + self.sc_config_options_frame = ctk.CTkFrame(sc_config_frame) + self.sc_config_options_frame.pack(fill="x", padx=10, pady=5) + + # Configuration options + self.sc1_config_var = tk.StringVar() + + # Radio buttons for spacecraft 1 config + self.sc1_config_label = ctk.CTkLabel(self.sc_config_options_frame, text="Spacecraft 1:") + self.sc1_config_label.pack(anchor="w", padx=10, pady=(5, 0)) + + for filename in self.config_filenames: + rb = ctk.CTkRadioButton(self.sc_config_options_frame, text=filename, + variable=self.sc1_config_var, value=filename) + rb.pack(anchor="w", padx=20, pady=2) + + self.sc1_config_var.trace_add("write", self.on_spacecraft_config_change_wrapper) + + # Initialize mission configuration data + self.mission_data = { + "start_time": "", + "gsw": "", + "fsw": "", + "num_spacecraft": "1", + "sc1_config": "", + "scN_config": "" # For spacecraft N config if more than 1 + } + + # Button to refresh the displayed info when spacecraft count changes + self.sc_count_var.trace_add("write", self.update_spacecraft_config_display) + + # Set up change tracking for mission configuration + self.setup_mission_change_tracking() + + # Add a wrapper function for the trace callback + def on_spacecraft_config_change_wrapper(self, *args): + """Wrapper for spacecraft config change to handle trace callback format""" + self.on_spacecraft_config_change() + + # Update the update_spacecraft_config_display method to include command binding + def update_spacecraft_config_display(self, *args): + """Update the spacecraft configuration display based on the number of spacecraft""" + # Clear existing widgets + for widget in self.sc_config_options_frame.winfo_children(): + widget.destroy() + + num_spacecraft = int(self.sc_count_var.get() if self.sc_count_var.get() else "1") # type: ignore + + # Add configuration for each spacecraft + for sc_num in range(1, num_spacecraft + 1): + sc_label = ctk.CTkLabel(self.sc_config_options_frame, + text=f"Spacecraft {sc_num}:", + font=ctk.CTkFont(weight="bold")) + sc_label.pack(anchor="w", padx=10, pady=(10, 0)) + + # Create a StringVar for this spacecraft + sc_var_name = f"sc{sc_num}_config_var" + if not hasattr(self, sc_var_name): + setattr(self, sc_var_name, tk.StringVar()) + + sc_var = getattr(self, sc_var_name) + + # Radio buttons for spacecraft config + for filename in self.config_filenames: + rb = ctk.CTkRadioButton(self.sc_config_options_frame, text=filename, + variable=sc_var, value=filename, command=self.load_xml_file(f"./cfg/spacecraft/{filename}")) + rb.pack(anchor="w", padx=20, pady=2) + + # Force update of the frame + self.sc_config_options_frame.update_idletasks() + + def set_current_mission_time(self): + """Set the mission time to the current time in J2000 format""" + current_time = datetime.now(timezone.utc) + j2000_seconds = (current_time - J2000_EPOCH).total_seconds() + + self.mission_time_entry.delete(0, 'end') + self.mission_time_entry.insert(0, f"{j2000_seconds:.1f}") + self.update_time_display() + + def set_mission_date(self): + """Open a dialog to set the mission date""" + dialog = DateTimeDialog(self.root, "Set Mission Date") + if dialog.result: + # dialog.result is a datetime object + j2000_seconds = (dialog.result - J2000_EPOCH).total_seconds() + self.mission_time_entry.delete(0, 'end') + self.mission_time_entry.insert(0, f"{j2000_seconds:.1f}") + self.update_time_display() + + def update_time_display(self): + """Update the display of the mission time in human-readable format""" + try: + j2000_str = self.mission_time_entry.get().strip() + if not j2000_str: + self.time_display_label.configure(text="") + return + + j2000_seconds = float(j2000_str) + + # Convert J2000 seconds to datetime + mission_time = J2000_EPOCH + timedelta(seconds=j2000_seconds) + + # Format the display + formatted_date = mission_time.strftime("%d %b %Y %H:%M:%S UTC") + + # Calculate years since J2000 epoch + years_since_j2000 = j2000_seconds / (365.25 * 24 * 3600) + + display_text = (f"Date: {formatted_date}\n" + f"Years since J2000 Epoch: {years_since_j2000:.2f}") + + self.time_display_label.configure(text=display_text) + + except ValueError: + self.time_display_label.configure(text="Invalid J2000 timestamp format") + + # Update the load_xml_file method to handle both mission and spacecraft config XML files + def load_xml_file(self, filename): + tree = ET.parse(filename) + root = tree.getroot() + + # Check if this is a mission config or spacecraft config file + if root.tag == 'nos3-mission-cfg': + self.load_mission_config(tree) + self.notebook.set("Mission Config") # Switch to mission config tab + elif root.tag == 'sc-1-config': + # Clear existing data + self.apps_data = {} + self.components_data = {} + + # Load applications + apps_element = root.find('applications') + if apps_element is not None: + for app_element in apps_element: + app_name = app_element.tag + enable_element = app_element.find('enable') + enabled = True # Default to True if not specified + if enable_element is not None: + enabled = enable_element.text.lower() == 'true' # type: ignore + self.apps_data[app_name] = enabled + + # Load components + components_element = root.find('components') + if components_element is not None: + for comp_element in components_element: + comp_name = comp_element.tag + enable_element = comp_element.find('enable') + enabled = True # Default to True if not specified + if enable_element is not None: + enabled = enable_element.text.lower() == 'true' # type: ignore + self.components_data[comp_name] = enabled + + self.refresh_displays() + # self.notebook.set("Applications") # Switch to applications tab + else: + messagebox.showerror("Error", "Unknown XML format") + + # Update the save_xml_file method to handle both mission and spacecraft config files + def save_xml_file(self, filename): + # Determine if we're saving a mission config or spacecraft config + is_mission_config = filename.endswith('nos3-mission.xml') or self.notebook.get() == "Mission Config" + + if is_mission_config: + # Create mission config XML + root = ET.Element('nos3-mission-cfg') + + # Add XML comments as in the example + comment1 = ET.Comment(" Mission Start Time (12000 UTC) ") + root.append(comment1) + + # Get the timestamp value + time_str = self.mission_time_entry.get().strip() + if time_str: + try: + timestamp = float(time_str) + dt = datetime.fromtimestamp(timestamp) + date_str = dt.strftime("%d %b %Y") + comment2 = ET.Comment(f" Default time: {time_str}, {date_str} ") + root.append(comment2) + except ValueError: + pass + + # Add start time + start_time = ET.SubElement(root, 'start-time') + start_time.text = self.mission_time_entry.get().strip() + + # Add ground software section + gsw_comment = ET.Comment(" Ground Software ") + root.append(gsw_comment) + + options_comment = ET.Comment(" cosmos (default), openc3, fprime, or yamcs ") + root.append(options_comment) + + gsw = ET.SubElement(root, 'gsw') + gsw.text = self.gsw_var.get() # type: ignore + + # Add flight software section + fsw_comment = ET.Comment(" Flight Software ") + root.append(fsw_comment) + + fsw_options_comment = ET.Comment(" cfs (default) or fprime ") + root.append(fsw_options_comment) + + fsw = ET.SubElement(root, 'fsw') + fsw.text = self.fsw_var.get() # type: ignore + + # Add number of spacecraft + num_sc_comment = ET.Comment(" Number of spacecraft ") + root.append(num_sc_comment) + + experimental_comment = ET.Comment(" Note this is experimental and not ready for use beyond proof of concept ") + root.append(experimental_comment) + + num_sc = ET.SubElement(root, 'number-spacecraft') + num_sc.text = self.sc_count_var.get() # type: ignore + + # Add spacecraft configurations + sc_num = int(self.sc_count_var.get()) # type: ignore + + # Add SC1 configuration + sc1_comment = ET.Comment(" Spacecraft 1 Configuration - options are as follows ") + root.append(sc1_comment) + + for filename in self.config_filenames: + root.append(ET.Comment(f" {filename} ")) + + sc1_cfg = ET.SubElement(root, 'sc-1-cfg') + selected_option = self.sc1_config_var.get() # type: ignore + if selected_option in self.config_filenames: + for filename in self.config_filenames: + if filename == selected_option: + sc1_cfg.text = filename + + # Add SCN configuration if more than one spacecraft + if sc_num > 1: + scn_comment = ET.Comment(" Spacecraft N Configuration ") + root.append(scn_comment) + + scn_cfg_comment = ET.Comment(" sc-minimal-config.xml ") + root.append(scn_cfg_comment) + + # For simplicity, we'll use the same config for all additional spacecraft + # In a real implementation, you might want to handle each spacecraft separately + + # XML formatting + xml_str = '\n' + rough_string = ET.tostring(root, 'utf-8') + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + + # Remove the XML declaration that parseString adds since we'll add our own + pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) + + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_str + pretty_xml) + else: + # Create spacecraft config XML + root = ET.Element('sc-1-config') + + # Add applications + apps_element = ET.SubElement(root, 'applications') + for app_name, enabled in self.apps_data.items(): + app_element = ET.SubElement(apps_element, app_name) + enable_element = ET.SubElement(app_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # Add components + components_element = ET.SubElement(root, 'components') + for comp_name, enabled in self.components_data.items(): + comp_element = ET.SubElement(components_element, comp_name) + enable_element = ET.SubElement(comp_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # XML formatting + xml_str = '\n' + rough_string = ET.tostring(root, 'utf-8') + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + + # Remove the XML declaration that parseString adds since we'll add our own + pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) + + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_str + pretty_xml) + + # Add a refresh_preview method that handles the active tab + def refresh_preview(self): + try: + active_tab = self.notebook.get() + + if active_tab == "Mission Config": + # Generate mission XML preview + root = ET.Element('nos3-mission-cfg') + + # Add comments as in the example + comment1 = ET.Comment(" Mission Start Time (12000 UTC) ") + root.append(comment1) + + time_str = self.mission_time_entry.get().strip() + if time_str: + try: + timestamp = float(time_str) + dt = datetime.fromtimestamp(timestamp) + date_str = dt.strftime("%d %b %Y") + comment2 = ET.Comment(f" Default time: {time_str}, {date_str} ") + root.append(comment2) + except ValueError: + pass + + # Add start time + start_time = ET.SubElement(root, 'start-time') + start_time.text = self.mission_time_entry.get().strip() + + # Add ground software section + gsw_comment = ET.Comment(" Ground Software ") + root.append(gsw_comment) + + options_comment = ET.Comment(" cosmos (default), openc3, fprime, or yamcs ") + root.append(options_comment) + + gsw = ET.SubElement(root, 'gsw') + gsw.text = self.gsw_var.get() # type: ignore + + # Add flight software section + fsw_comment = ET.Comment(" Flight Software ") + root.append(fsw_comment) + + fsw_options_comment = ET.Comment(" cfs (default) or fprime ") + root.append(fsw_options_comment) + + fsw = ET.SubElement(root, 'fsw') + fsw.text = self.fsw_var.get() # type: ignore + + # Add number of spacecraft + num_sc_comment = ET.Comment(" Number of spacecraft ") + root.append(num_sc_comment) + + experimental_comment = ET.Comment(" Note this is experimental and not ready for use beyond proof of concept ") + root.append(experimental_comment) + + num_sc = ET.SubElement(root, 'number-spacecraft') + num_sc.text = self.sc_count_var.get() # type: ignore + + # Add spacecraft configurations + sc_num = int(self.sc_count_var.get()) # type: ignore + + # Add SC1 configuration + sc1_comment = ET.Comment(" Spacecraft 1 Configuration - options are as follows ") + root.append(sc1_comment) + + for filename in self.config_filenames: + root.append(ET.Comment(f" {filename} ")) + + sc1_cfg = ET.SubElement(root, 'sc-1-cfg') + selected_option = self.sc1_config_var.get() # type: ignore + if selected_option in self.config_filenames: + sc1_cfg.text = selected_option + + # Add SCN configuration if more than one spacecraft + if sc_num > 1: + scn_comment = ET.Comment(" Spacecraft N Configuration ") + root.append(scn_comment) + + scn_cfg_comment = ET.Comment(" sc-minimal-config.xml ") + root.append(scn_cfg_comment) + else: + # Generate spacecraft XML preview + root = ET.Element('sc-1-config') + + # Add applications + apps_element = ET.SubElement(root, 'applications') + for app_name, enabled in self.apps_data.items(): + app_element = ET.SubElement(apps_element, app_name) + enable_element = ET.SubElement(app_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # Add components + components_element = ET.SubElement(root, 'components') + for comp_name, enabled in self.components_data.items(): + comp_element = ET.SubElement(components_element, comp_name) + enable_element = ET.SubElement(comp_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # Add GUI section + gui_element = ET.SubElement(root, 'gui') + gui_enable = ET.SubElement(gui_element, 'enable') + if hasattr(self, 'gui_enabled_var'): + gui_enable.text = 'true' if self.gui_enabled_var.get() else 'false' + else: + gui_enable.text = 'true' + + # Add Orbit section + orbit_element = ET.SubElement(root, 'orbit') + + tipoff_x = ET.SubElement(orbit_element, 'tipoff_x') + if hasattr(self, 'tipoff_x_entry'): + tipoff_x.text = self.tipoff_x_entry.get() if self.tipoff_x_entry.get() else "0.2" + else: + tipoff_x.text = "0.2" + + tipoff_y = ET.SubElement(orbit_element, 'tipoff_y') + if hasattr(self, 'tipoff_y_entry'): + tipoff_y.text = self.tipoff_y_entry.get() if self.tipoff_y_entry.get() else "2.0" + else: + tipoff_y.text = "2.0" + + tipoff_z = ET.SubElement(orbit_element, 'tipoff_z') + if hasattr(self, 'tipoff_z_entry'): + tipoff_z.text = self.tipoff_z_entry.get() if self.tipoff_z_entry.get() else "-2.0" + else: + tipoff_z.text = "-2.0" + + # Add Sim section + sim_element = ET.SubElement(root, 'sim') + sim_truth = ET.SubElement(sim_element, 'sim_truth_interface') + if hasattr(self, 'sim_truth_var'): + sim_truth.text = 'true' if self.sim_truth_var.get() else 'false' + else: + sim_truth.text = 'true' + + # Format XML + xml_str = '\n' + rough_string = ET.tostring(root, 'utf-8') + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + + # Remove the XML declaration that parseString adds since we'll add our own + pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) + + self.preview_text.delete('1.0', 'end') + self.preview_text.insert('1.0', xml_str + pretty_xml) + + except Exception as e: + self.preview_text.delete('1.0', 'end') + self.preview_text.insert('1.0', f"Error generating preview: {str(e)}") + + def create_menu_bar(self): + # Create menu frame + self.menu_frame = ctk.CTkFrame(self.main_frame, height=40) + self.menu_frame.pack(fill="x", padx=10, pady=(10, 0)) + self.menu_frame.pack_propagate(False) + + # File operations + self.new_btn = ctk.CTkButton(self.menu_frame, text="New Mission", command=self.new_mission, width=100) + self.new_btn.pack(side="left", padx=5, pady=5) + + self.open_btn = ctk.CTkButton(self.menu_frame, text="Open Mission", command=self.open_mission, width=100) + self.open_btn.pack(side="left", padx=5, pady=5) + + self.save_btn = ctk.CTkButton(self.menu_frame, text="Save All", command=self.save_all, width=80) + self.save_btn.pack(side="left", padx=5, pady=5) + + # Current files label + self.files_frame = ctk.CTkFrame(self.menu_frame) + self.files_frame.pack(side="right", padx=10, pady=5) + + self.mission_file_label = ctk.CTkLabel(self.files_frame, text="Mission: No file loaded") + self.mission_file_label.pack(pady=2) + + self.spacecraft_file_label = ctk.CTkLabel(self.files_frame, text="Spacecraft: No file loaded") + self.spacecraft_file_label.pack(pady=2) + + def setup_apps_tab(self): + # Create frame for the applications list + self.apps_frame = ctk.CTkFrame(self.apps_tab) + self.apps_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Header + header_label = ctk.CTkLabel(self.apps_frame, text="Application Configuration", + font=ctk.CTkFont(size=16, weight="bold")) + header_label.pack(pady=(10, 20)) + + # Create a scrollable frame + self.apps_scrollable = ctk.CTkScrollableFrame(self.apps_frame) + self.apps_scrollable.pack(fill="both", expand=True, padx=10, pady=10) + + # No applications loaded message + self.no_apps_label = ctk.CTkLabel(self.apps_scrollable, text="No applications loaded. Open a configuration file.") + self.no_apps_label.pack(pady=20) + + # Dictionary to keep track of app checkboxes + self.app_checkboxes = {} + + def setup_components_tab(self): + # Create frame for the components list + self.components_frame = ctk.CTkFrame(self.components_tab) + self.components_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Header + header_label = ctk.CTkLabel(self.components_frame, text="Component Configuration", + font=ctk.CTkFont(size=16, weight="bold")) + header_label.pack(pady=(10, 20)) + + # Create a scrollable frame + self.components_scrollable = ctk.CTkScrollableFrame(self.components_frame) + self.components_scrollable.pack(fill="both", expand=True, padx=10, pady=10) + + # No components loaded message + self.no_components_label = ctk.CTkLabel(self.components_scrollable, text="No components loaded. Open a configuration file.") + self.no_components_label.pack(pady=20) + + # Dictionary to keep track of component checkboxes + self.component_checkboxes = {} + + def setup_preview_tab(self): + # XML preview + preview_frame = ctk.CTkFrame(self.preview_tab) + preview_frame.pack(fill="both", expand=True, padx=10, pady=10) + + preview_label = ctk.CTkLabel(preview_frame, text="XML Preview", + font=ctk.CTkFont(size=16, weight="bold")) + preview_label.pack(pady=(10, 5)) + + # Create textbox for XML preview + self.preview_text = ctk.CTkTextbox(preview_frame, font=ctk.CTkFont(family="Courier", size=12)) + self.preview_text.pack(fill="both", expand=True, padx=10, pady=10) + + # Refresh button + refresh_btn = ctk.CTkButton(preview_frame, text="Refresh Preview", command=self.refresh_preview) + refresh_btn.pack(pady=(5, 10)) + + def new_mission(self): + if self.check_unsaved_changes(): + self.apps_data = {} + self.components_data = {} + self.mission_data = {} + self.additional_data = {} + self.current_mission_file = None + self.current_spacecraft_file = None + self.spacecraft_config_path = None + self.modified = False + + # Clear mission form fields + self.clear_mission_fields() + self.clear_additional_fields() # Add this line + self.refresh_displays() + self.update_status("New mission created - configure mission settings first") + self.update_file_labels() + self.notebook.set("Mission Config") + + def clear_additional_fields(self): + """Clear additional options fields""" + if hasattr(self, 'gui_enabled_var'): + self.gui_enabled_var.set(True) + if hasattr(self, 'tipoff_x_entry'): + self.tipoff_x_entry.delete(0, 'end') + self.tipoff_x_entry.insert(0, "0.2") + if hasattr(self, 'tipoff_y_entry'): + self.tipoff_y_entry.delete(0, 'end') + self.tipoff_y_entry.insert(0, "2.0") + if hasattr(self, 'tipoff_z_entry'): + self.tipoff_z_entry.delete(0, 'end') + self.tipoff_z_entry.insert(0, "-2.0") + if hasattr(self, 'sim_truth_var'): + self.sim_truth_var.set(True) + + def setup_mission_change_tracking(self): + """Setup change tracking for mission configuration fields""" + # Bind change events to track modifications + if hasattr(self, 'mission_time_entry'): + self.mission_time_entry.bind('', lambda e: self.set_modified()) + self.mission_time_entry.bind('', lambda e: self.update_time_display()) + + # Trace variable changes + if hasattr(self, 'gsw_var'): + self.gsw_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore + if hasattr(self, 'fsw_var'): + self.fsw_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore + if hasattr(self, 'sc_count_var'): + self.sc_count_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore + if hasattr(self, 'sc1_config_var'): + self.sc1_config_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore + + def open_mission(self, startup = False): + if self.check_unsaved_changes(): + if not startup: + filename = filedialog.askopenfilename( + title="Open NOS3 Mission Configuration", + filetypes=[("Mission XML files", "*mission*.xml"), ("XML files", "*.xml"), ("All files", "*.*")] + ) + else: + filename = self.default_mission_config + + if filename: + try: + self.load_mission_file(filename) + self.current_mission_file = filename + self.modified = False + self.update_status(f"Loaded mission: {os.path.basename(filename)}") + self.update_file_labels() + self.notebook.set("Mission Config") + except Exception as e: + messagebox.showerror("Error", f"Failed to load mission file: {str(e)}") + + def save_all(self): + """Save both mission and spacecraft configuration files""" + if not self.current_mission_file: + # Need to save mission file first + filename = filedialog.asksaveasfilename( + title="Save NOS3 Mission Configuration", + defaultextension=".xml", + filetypes=[("Mission XML files", "*mission*.xml"), ("XML files", "*.xml"), ("All files", "*.*")] + ) + if filename: + self.current_mission_file = filename + else: + return + + try: + # Save mission configuration + self.save_mission_file(f"/cfg/{self.current_mission_file}") + + # Determine spacecraft config file path from mission config + spacecraft_config = self.get_spacecraft_config_filename() + if spacecraft_config: + # Get the directory of the mission file + mission_dir = os.path.dirname(self.current_mission_file) + + # Handle relative path in spacecraft config + if not os.path.isabs(spacecraft_config): + self.spacecraft_config_path = os.path.join(mission_dir, f"spacecraft/{spacecraft_config}") + else: + self.spacecraft_config_path = spacecraft_config + + # Save spacecraft configuration + self.save_spacecraft_file(self.spacecraft_config_path) + self.current_spacecraft_file = self.spacecraft_config_path + + self.modified = False + self.update_status(f"Saved mission and spacecraft configurations") + self.update_file_labels() + else: + messagebox.showerror("Error", "No spacecraft configuration selected in mission config") + + except Exception as e: + messagebox.showerror("Error", f"Failed to save files: {str(e)}") + + def load_mission_file(self, filename): + """Load mission configuration and associated spacecraft config""" + tree = ET.parse(filename) + root = tree.getroot() + + if root.tag != 'nos3-mission-cfg': + raise ValueError("This is not a valid NOS3 mission configuration file") + + # Clear existing data + self.apps_data = {} + self.components_data = {} + self.mission_data = {} + + # Load mission configuration + self.load_mission_config(tree) + + # Try to load associated spacecraft configuration + spacecraft_config = self.get_spacecraft_config_filename() + if spacecraft_config: + mission_dir = os.path.dirname(filename) + + # Handle relative path + if not os.path.isabs(spacecraft_config): + self.spacecraft_config_path = os.path.join(f"{mission_dir}/spacecraft/", spacecraft_config) + else: + self.spacecraft_config_path = spacecraft_config + + # Try to load spacecraft config + if os.path.exists(self.spacecraft_config_path): + try: + self.load_spacecraft_file(self.spacecraft_config_path) + self.current_spacecraft_file = self.spacecraft_config_path + except Exception as e: + messagebox.showwarning("Warning", + f"Could not load spacecraft configuration '{spacecraft_config}': {str(e)}\n\n" + "You can still edit the mission configuration, but spacecraft apps/components will be empty.") + else: + messagebox.showwarning("Warning", + f"Spacecraft configuration file not found: {spacecraft_config}\n\n" + "You can still edit the mission configuration, but spacecraft apps/components will be empty.") + + self.refresh_displays() + + def load_spacecraft_file(self, filename): + """Load spacecraft configuration from XML file""" + tree = ET.parse(filename) + root = tree.getroot() + + if root.tag != 'sc-1-config': + raise ValueError("This is not a valid spacecraft configuration file") + + # Clear existing spacecraft data + self.apps_data = {} + self.components_data = {} + + # Load applications + apps_element = root.find('applications') + if apps_element is not None: + for app_element in apps_element: + app_name = app_element.tag + enable_element = app_element.find('enable') + enabled = True + if enable_element is not None: + enabled = enable_element.text.lower() == 'true' # type: ignore + self.apps_data[app_name] = enabled + + # Load components + components_element = root.find('components') + if components_element is not None: + for comp_element in components_element: + comp_name = comp_element.tag + enable_element = comp_element.find('enable') + enabled = True + if enable_element is not None: + enabled = enable_element.text.lower() == 'true' # type: ignore + self.components_data[comp_name] = enabled + + # Load additional sections + self.load_additional_sections(root) + + # Refresh the additional options display + self.refresh_additional_display() + + def load_additional_sections(self, root): + """Load GUI, Orbit, and Sim sections from XML""" + # Load GUI section + gui_element = root.find('gui') + if gui_element is not None: + enable_element = gui_element.find('enable') + if enable_element is not None: + self.additional_data['gui_enabled'] = enable_element.text.lower() == 'true' + else: + self.additional_data['gui_enabled'] = True + else: + self.additional_data['gui_enabled'] = True + + # Load Orbit section + orbit_element = root.find('orbit') + if orbit_element is not None: + tipoff_x = orbit_element.find('tipoff_x') + tipoff_y = orbit_element.find('tipoff_y') + tipoff_z = orbit_element.find('tipoff_z') + + self.additional_data['tipoff_x'] = tipoff_x.text if tipoff_x is not None else "0.2" + self.additional_data['tipoff_y'] = tipoff_y.text if tipoff_y is not None else "2.0" + self.additional_data['tipoff_z'] = tipoff_z.text if tipoff_z is not None else "-2.0" + else: + self.additional_data['tipoff_x'] = "0.2" + self.additional_data['tipoff_y'] = "2.0" + self.additional_data['tipoff_z'] = "-2.0" + + # Load Sim section + sim_element = root.find('sim') + if sim_element is not None: + sim_truth = sim_element.find('sim_truth_interface') + if sim_truth is not None: + self.additional_data['sim_truth'] = sim_truth.text.lower() == 'true' + else: + self.additional_data['sim_truth'] = True + else: + self.additional_data['sim_truth'] = True + + def refresh_additional_display(self): + """Refresh the additional options display with loaded data""" + if hasattr(self, 'gui_enabled_var') and 'gui_enabled' in self.additional_data: + self.gui_enabled_var.set(self.additional_data['gui_enabled']) + + if hasattr(self, 'tipoff_x_entry') and 'tipoff_x' in self.additional_data: + self.tipoff_x_entry.delete(0, 'end') + self.tipoff_x_entry.insert(0, self.additional_data['tipoff_x']) + + if hasattr(self, 'tipoff_y_entry') and 'tipoff_y' in self.additional_data: + self.tipoff_y_entry.delete(0, 'end') + self.tipoff_y_entry.insert(0, self.additional_data['tipoff_y']) + + if hasattr(self, 'tipoff_z_entry') and 'tipoff_z' in self.additional_data: + self.tipoff_z_entry.delete(0, 'end') + self.tipoff_z_entry.insert(0, self.additional_data['tipoff_z']) + + if hasattr(self, 'sim_truth_var') and 'sim_truth' in self.additional_data: + self.sim_truth_var.set(self.additional_data['sim_truth']) + + def get_spacecraft_config_filename(self): + """Get the spacecraft configuration filename from the selected option""" + selected_option = self.sc1_config_var.get() if hasattr(self, 'sc1_config_var') else "" # type: ignore + if selected_option and selected_option in self.config_filenames: + for filename in self.config_filenames: + if filename == selected_option: + return filename + return None + + def save_mission_file(self, filename): + """Save mission configuration to XML file""" + # Create root element with the exact format + root = ET.Element('nos3-mission-cfg') + + # Add comments as in the example + comment1 = ET.Comment(" Mission Start Time (J2000 format) ") + root.append(comment1) + + time_str = self.mission_time_entry.get().strip() + if time_str: + try: + j2000_seconds = float(time_str) + mission_time = J2000_EPOCH + timedelta(seconds=j2000_seconds) + date_str = mission_time.strftime("%d %b %Y") + comment2 = ET.Comment(f" Default time: {time_str}, {date_str} ") + root.append(comment2) + except ValueError: + pass + + # Add start time + start_time = ET.SubElement(root, 'start-time') + start_time.text = self.mission_time_entry.get().strip() + + # Add ground software section + gsw_comment = ET.Comment(" Ground Software ") + root.append(gsw_comment) + + options_comment = ET.Comment(" cosmos (default), openc3, fprime, or yamcs ") + root.append(options_comment) + + gsw = ET.SubElement(root, 'gsw') + gsw.text = self.gsw_var.get() # type: ignore + + # Add flight software section + fsw_comment = ET.Comment(" Flight Software ") + root.append(fsw_comment) + + fsw_options_comment = ET.Comment(" cfs (default) or fprime ") + root.append(fsw_options_comment) + + fsw = ET.SubElement(root, 'fsw') + fsw.text = self.fsw_var.get() # type: ignore + + # Add number of spacecraft + num_sc_comment = ET.Comment(" Number of spacecraft ") + root.append(num_sc_comment) + + experimental_comment = ET.Comment(" Note this is experimental and not ready for use beyond proof of concept ") + root.append(experimental_comment) + + num_sc = ET.SubElement(root, 'number-spacecraft') + num_sc.text = self.sc_count_var.get() # type: ignore + + # Add spacecraft configurations + sc1_comment = ET.Comment(" Spacecraft 1 Configuration - options are as follows ") + root.append(sc1_comment) + + for filename_part in self.config_filenames: + root.append(ET.Comment(f" {filename_part} ")) + + sc1_cfg = ET.SubElement(root, 'sc-1-cfg') + selected_option = self.sc1_config_var.get() # type: ignore + if selected_option in self.config_filenames: + for filename in self.config_filenames: + if filename == selected_option: + sc1_cfg.text = f"spacecraft/{filename}" + + # Format and save XML + xml_str = '\n' + rough_string = ET.tostring(root, 'utf-8') + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) + + with open("./cfg/nos3-mission.xml", 'w', encoding='utf-8') as f: + f.write(xml_str + pretty_xml) + + def save_spacecraft_file(self, filename): + """Save spacecraft configuration to XML file""" + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # Create spacecraft config XML + root = ET.Element('sc-1-config') + + # Add applications + if self.apps_data: + apps_element = ET.SubElement(root, 'applications') + for app_name, enabled in self.apps_data.items(): + app_element = ET.SubElement(apps_element, app_name) + enable_element = ET.SubElement(app_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # Add components + if self.components_data: + components_element = ET.SubElement(root, 'components') + for comp_name, enabled in self.components_data.items(): + comp_element = ET.SubElement(components_element, comp_name) + enable_element = ET.SubElement(comp_element, 'enable') + enable_element.text = 'true' if enabled else 'false' + + # Add GUI section + gui_element = ET.SubElement(root, 'gui') + gui_enable = ET.SubElement(gui_element, 'enable') + gui_enable.text = 'true' if self.gui_enabled_var.get() else 'false' + + # Add Orbit section + orbit_element = ET.SubElement(root, 'orbit') + + tipoff_x = ET.SubElement(orbit_element, 'tipoff_x') + tipoff_x.text = self.tipoff_x_entry.get() + + tipoff_y = ET.SubElement(orbit_element, 'tipoff_y') + tipoff_y.text = self.tipoff_y_entry.get() + + tipoff_z = ET.SubElement(orbit_element, 'tipoff_z') + tipoff_z.text = self.tipoff_z_entry.get() + + # Add Sim section + sim_element = ET.SubElement(root, 'sim') + sim_truth = ET.SubElement(sim_element, 'sim_truth_interface') + sim_truth.text = 'true' if self.sim_truth_var.get() else 'false' + + # Format and save XML + xml_str = '\n' + rough_string = ET.tostring(root, 'utf-8') + reparsed = minidom.parseString(rough_string) + pretty_xml = reparsed.toprettyxml(indent=" ") + pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) + + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_str + pretty_xml) + + # Add/update the on_spacecraft_config_change method + def on_spacecraft_config_change(self): + """Handle when user changes spacecraft configuration selection""" + + if not hasattr(self, 'sc1_config_var') or not self.sc1_config_var.get(): # type: ignore + return + + selected_config = self.sc1_config_var.get() # type: ignore + + if selected_config not in self.config_filenames: + return + + # Clear existing spacecraft data + self.apps_data = {} + self.components_data = {} + + # Get the spacecraft config filename + spacecraft_config_file = self.sc1_config_var.get() # type: ignore + + # Try to find and load the spacecraft config file + spacecraft_path = None + + if self.current_mission_file: + # Look relative to mission file + mission_dir = os.path.dirname(self.current_mission_file) + + # Try different possible locations + possible_paths = [ + os.path.join(mission_dir, "spacecraft", spacecraft_config_file), + os.path.join(mission_dir, spacecraft_config_file), + os.path.join(mission_dir, "..", "spacecraft", spacecraft_config_file), + ] + + for path in possible_paths: + if os.path.exists(path): + spacecraft_path = path + break + else: + # If no mission file is loaded, try current directory + possible_paths = [ + os.path.join("spacecraft", spacecraft_config_file), + spacecraft_config_file, + os.path.join("..", "spacecraft", spacecraft_config_file), + ] + + for path in possible_paths: + if os.path.exists(path): + spacecraft_path = os.path.abspath(path) + break + + if spacecraft_path and os.path.exists(spacecraft_path): + try: + self.load_spacecraft_file(spacecraft_path) + self.current_spacecraft_file = spacecraft_path + self.spacecraft_config_path = spacecraft_path + + self.update_status(f"Loaded spacecraft config: {spacecraft_config_file}") + + # Refresh displays to show the loaded apps and components + self.refresh_apps_display() + self.refresh_components_display() + + except Exception as e: + messagebox.showerror("Error", f"Failed to load spacecraft configuration '{spacecraft_config_file}':\n{str(e)}") + self.update_status(f"Failed to load spacecraft config: {spacecraft_config_file}") + else: + # Config file not found - create default structure + self.create_default_spacecraft_config(spacecraft_config_file) + + # Still refresh displays to show empty state + self.refresh_apps_display() + self.refresh_components_display() + + self.update_status(f"Spacecraft config '{spacecraft_config_file}' not found - will create when saving") + + # Update file labels + self.update_file_labels() + + # Mark as modified + self.set_modified() + + def create_default_spacecraft_config(self, config_filename): + """Create default spacecraft configuration based on the selected type""" + + # Default configurations for different types + default_configs = { + "sc-minimal-config.xml": { + "apps": ["cf", "ds", "fm", "lc", "sc"], + "components": ["sample"] + }, + "sc-mission-config.xml": { + "apps": ["cf", "ds", "fm", "lc", "sc"], + "components": ["adcs", "css", "eps", "fss", "gps", "imu", "mag", "mgr", "radio", "rw", "sample", "st", "tourqer"] + }, + "sc-research-config.xml": { + "apps": ["cf", "ds", "fm", "lc", "sbn", "sc"], + "components": ["adcs", "cam", "css", "eps", "fss", "gps", "imu", "mag", "mgr", "onair", "radio", "rw", "sample", "st", "syn", "tourqer", "thruster"] + }, + "sc-fprime-config.xml": { + "apps": [], + "components": ["cam", "css", "eps", "fss", "imu", "mag", "radio", "rw", "sample", "st", "tourqer", "thruster"] + } + } + + if config_filename in default_configs: + config = default_configs[config_filename] + + # Set default apps (all enabled) + for app in config["apps"]: + self.apps_data[app] = True + + # Set default components (all enabled) + for component in config["components"]: + self.components_data[component] = True + else: + # Generic default + self.apps_data = { + "cf": True, + "ds": True, + "fm": True, + "lc": True, + "sc": True + } + self.components_data = { + "adcs" : True, + "css" : True, + "eps" : True, + "fss" : True, + "gps" : True, + "imu" : True, + "mag" : True, + "mgr" : True, + "radio" : True, + "rw" : True, + "sample" : True, + "st" : True, + "tourqer" : True + } + + # Update the load_mission_config method to properly set the spacecraft config + def load_mission_config(self, tree): + """Load mission configuration from XML tree""" + root = tree.getroot() + + # Clear existing mission data + self.mission_data = { + "start_time": "", + "gsw": "", + "fsw": "", + "num_spacecraft": "1", + "sc1_config": "", + "scN_config": "" + } + + # Extract start time + start_time_elem = root.find('start-time') + if start_time_elem is not None and start_time_elem.text: + self.mission_data["start_time"] = start_time_elem.text + self.mission_time_entry.delete(0, 'end') + self.mission_time_entry.insert(0, start_time_elem.text) + self.update_time_display() + + # Extract ground software + gsw_elem = root.find('gsw') + if gsw_elem is not None and gsw_elem.text: + self.mission_data["gsw"] = gsw_elem.text + self.gsw_var.set(gsw_elem.text) # type: ignore + + # Extract flight software + fsw_elem = root.find('fsw') + if fsw_elem is not None and fsw_elem.text: + self.mission_data["fsw"] = fsw_elem.text + self.fsw_var.set(fsw_elem.text) # type: ignore + + # Extract number of spacecraft + num_sc_elem = root.find('number-spacecraft') + if num_sc_elem is not None and num_sc_elem.text: + self.mission_data["num_spacecraft"] = num_sc_elem.text + self.sc_count_var.set(num_sc_elem.text) # type: ignore + + # Extract spacecraft 1 configuration + sc1_elem = root.find('sc-1-cfg') + if sc1_elem is not None and sc1_elem.text: + config_file = sc1_elem.text + + # Remove "spacecraft/" prefix if present + if config_file.startswith("spacecraft/"): + config_file = config_file[11:] # Remove "spacecraft/" prefix + + # Find the matching option + for filename in self.config_filenames: + if filename == config_file: + self.sc1_config_var.set(filename) # type: ignore + self.mission_data["sc1_config"] = filename + break + + # Update display for multiple spacecraft if needed + self.update_spacecraft_config_display() + + # Don't trigger spacecraft config change during loading to avoid conflicts + # The spacecraft config will be loaded by the main load_mission_file method + + # Add a method to manually trigger spacecraft config loading after mission is loaded + def load_initial_spacecraft_config(self): + """Load initial spacecraft config after mission config is loaded""" + if hasattr(self, 'sc1_config_var') and self.sc1_config_var.get(): # type: ignore + # Small delay to ensure UI is ready + self.root.after(600, self.on_spacecraft_config_change) + + def update_file_labels(self): + """Update the file labels in the menu bar""" + if self.current_mission_file: + self.mission_file_label.configure(text=f"Mission: {os.path.basename(self.current_mission_file)}") + else: + self.mission_file_label.configure(text="Mission: No file loaded") + + if self.current_spacecraft_file: + self.spacecraft_file_label.configure(text=f"Spacecraft: {os.path.basename(self.current_spacecraft_file)}") + else: + self.spacecraft_file_label.configure(text="Spacecraft: No file loaded") + + # Add modification indicator + if self.modified: + if self.current_mission_file: + self.mission_file_label.configure(text=f"Mission: {os.path.basename(self.current_mission_file)}") + + def clear_mission_fields(self): + """Clear all mission configuration fields""" + if hasattr(self, 'mission_time_entry'): + self.mission_time_entry.delete(0, 'end') + if hasattr(self, 'gsw_var'): + self.gsw_var.set("cosmos") # Default value # type: ignore + if hasattr(self, 'fsw_var'): + self.fsw_var.set("cfs") # Default value # type: ignore + if hasattr(self, 'sc_count_var'): + self.sc_count_var.set("1") # type: ignore + if hasattr(self, 'sc1_config_var'): + self.sc1_config_var.set("") # type: ignore + if hasattr(self, 'time_display_label'): + self.time_display_label.configure(text="") + + def set_modified(self): + """Mark the configuration as modified""" + self.modified = True + self.update_file_labels() + + def check_unsaved_changes(self): + """Check for unsaved changes and prompt user""" + if self.modified: + result = messagebox.askyesnocancel("Unsaved Changes", "Save changes before continuing?") + if result is True: # Yes + self.save_all() + return True + elif result is False: # No + return True + else: # Cancel + return False + return True + + def update_status(self, message): + """Update the status bar message""" + self.status_label.configure(text=message) + + # App and component operations + def add_app(self): + dialog = SimpleNameDialog(self.root, "Add Application", "Enter application name:") + if dialog.result: + app_name = dialog.result + if app_name in self.apps_data: + messagebox.showerror("Error", f"Application '{app_name}' already exists.") + return + self.apps_data[app_name] = True # Default to enabled + self.refresh_apps_display() + self.set_modified() + + def add_component(self): + dialog = SimpleNameDialog(self.root, "Add Component", "Enter component name:") + if dialog.result: + comp_name = dialog.result + if comp_name in self.components_data: + messagebox.showerror("Error", f"Component '{comp_name}' already exists.") + return + self.components_data[comp_name] = True # Default to enabled + self.refresh_components_display() + self.set_modified() + + def toggle_app_enabled(self, app_name, var): + self.apps_data[app_name] = var.get() + self.set_modified() + + def toggle_component_enabled(self, comp_name, var): + self.components_data[comp_name] = var.get() + self.set_modified() + + def delete_app(self, app_name): + if messagebox.askyesno("Confirm Delete", f"Delete application '{app_name}'?"): + del self.apps_data[app_name] + self.refresh_apps_display() + self.set_modified() + + def delete_component(self, comp_name): + if messagebox.askyesno("Confirm Delete", f"Delete component '{comp_name}'?"): + del self.components_data[comp_name] + self.refresh_components_display() + self.set_modified() + + # Display refresh methods + def refresh_displays(self): + self.refresh_apps_display() + self.refresh_components_display() + self.refresh_additional_display() + self.refresh_preview() + + def refresh_apps_display(self): + # Clear existing app widgets + for widget in self.apps_scrollable.winfo_children(): + widget.destroy() + + # Clear checkbox dictionary + self.app_checkboxes = {} + + if not self.apps_data: + self.no_apps_label = ctk.CTkLabel(self.apps_scrollable, text="No applications loaded. Open a configuration file.") + self.no_apps_label.pack(pady=20) + return + + # Add header row + header_frame = ctk.CTkFrame(self.apps_scrollable) + header_frame.pack(fill="x", pady=(0, 5)) + + ctk.CTkLabel(header_frame, text="Application", width=200, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + ctk.CTkLabel(header_frame, text="Enabled", width=80, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + ctk.CTkLabel(header_frame, text="Actions", width=80, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + + # Add apps + for app_name, enabled in sorted(self.apps_data.items()): + app_frame = ctk.CTkFrame(self.apps_scrollable) + app_frame.pack(fill="x", pady=2) + + # App name label + name_label = ctk.CTkLabel(app_frame, text=app_name, width=200) + name_label.pack(side="left", padx=10, pady=5) + + # App enabled checkbox + var = tk.BooleanVar(value=enabled) + checkbox = ctk.CTkCheckBox(app_frame, text="", variable=var, onvalue=True, offvalue=False, + command=lambda n=app_name, v=var: self.toggle_app_enabled(n, v)) + checkbox.pack(side="left", padx=10) + self.app_checkboxes[app_name] = checkbox + + # Delete button + delete_btn = ctk.CTkButton(app_frame, text="Delete", width=60, fg_color="#D32F2F", hover_color="#B71C1C", + command=lambda n=app_name: self.delete_app(n)) + delete_btn.pack(side="left", padx=10) + + # Add button at the bottom + add_btn_frame = ctk.CTkFrame(self.apps_scrollable) + add_btn_frame.pack(fill="x", pady=10) + + add_btn = ctk.CTkButton(add_btn_frame, text="Add Application", command=self.add_app) + add_btn.pack(pady=5) + + def refresh_components_display(self): + # Clear existing component widgets + for widget in self.components_scrollable.winfo_children(): + widget.destroy() + + # Clear checkbox dictionary + self.component_checkboxes = {} + + if not self.components_data: + self.no_components_label = ctk.CTkLabel(self.components_scrollable, text="No components loaded. Open a configuration file.") + self.no_components_label.pack(pady=20) + return + + # Add header row + header_frame = ctk.CTkFrame(self.components_scrollable) + header_frame.pack(fill="x", pady=(0, 5)) + + ctk.CTkLabel(header_frame, text="Component", width=200, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + ctk.CTkLabel(header_frame, text="Enabled", width=80, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + ctk.CTkLabel(header_frame, text="Actions", width=80, font=ctk.CTkFont(weight="bold")).pack(side="left", padx=10) + + # Add components + for comp_name, enabled in sorted(self.components_data.items()): + comp_frame = ctk.CTkFrame(self.components_scrollable) + comp_frame.pack(fill="x", pady=2) + + # Component name label + name_label = ctk.CTkLabel(comp_frame, text=comp_name, width=200) + name_label.pack(side="left", padx=10, pady=5) + + # Component enabled checkbox + var = tk.BooleanVar(value=enabled) + checkbox = ctk.CTkCheckBox(comp_frame, text="", variable=var, onvalue=True, offvalue=False, + command=lambda n=comp_name, v=var: self.toggle_component_enabled(n, v)) + checkbox.pack(side="left", padx=10) + self.component_checkboxes[comp_name] = checkbox + + # Delete button + delete_btn = ctk.CTkButton(comp_frame, text="Delete", width=60, fg_color="#D32F2F", hover_color="#B71C1C", + command=lambda n=comp_name: self.delete_component(n)) + delete_btn.pack(side="left", padx=10) + + # Add button at the bottom + add_btn_frame = ctk.CTkFrame(self.components_scrollable) + add_btn_frame.pack(fill="x", pady=10) + + add_btn = ctk.CTkButton(add_btn_frame, text="Add Component", command=self.add_component) + add_btn.pack(pady=5) + + def run(self): + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + self.root.mainloop() + + def on_closing(self): + if self.check_unsaved_changes(): + self.root.destroy() + + def setup_logos(self): + """Setup logos in the application""" + try: + # Path to the logo file + logo_path = "cfg/gui/resources/nos3_original.png" + + if not os.path.exists(logo_path): + print(f"Logo file not found: {logo_path}") + return + + # Open the logo image with PIL + original_logo = Image.open(logo_path) + + # Create header logo (larger) using CTkImage + header_size = (200, 80) # Adjust size as needed + self.header_logo_img = ctk.CTkImage(original_logo, size=header_size) + + # Create smaller logo for tabs using CTkImage + tab_size = (120, 50) # Adjust size as needed + self.tab_logo_img = ctk.CTkImage(original_logo, size=tab_size) + + except Exception as e: + print(f"Error setting up logos: {e}") + + def add_header_logo(self): + """Add logo to the header of the application""" + # Create a header frame above the menu + self.header_frame = ctk.CTkFrame(self.main_frame, height=100) + self.header_frame.pack(fill="x", padx=10, pady=(10, 0)) + self.header_frame.pack_propagate(False) # Don't shrink + + # Add logo to the left side using CTkLabel + self.header_logo_label = ctk.CTkLabel(self.header_frame, image=self.header_logo_img, text="") + self.header_logo_label.pack(side="left", padx=20, pady=10) + + # Add title text + title_text = "NOS³ Configuration Manager" + self.title_label = ctk.CTkLabel(self.header_frame, text=title_text, + font=ctk.CTkFont(family="Helvetica", size=24, weight="bold")) + self.title_label.pack(side="left", padx=20, pady=10) + + # Add theme toggle + self.add_theme_toggle() + + def add_tab_logos(self): + """Add logos to the tabs""" + # Add logo to mission tab + self.mission_logo_frame = ctk.CTkFrame(self.mission_scrollable) + self.mission_logo_frame.pack(fill="x", pady=(0, 20)) + + self.mission_logo_label = ctk.CTkLabel(self.mission_logo_frame, image=self.tab_logo_img, text="") + self.mission_logo_label.pack(side="right", padx=10, pady=5) + + # Add logo to applications tab + self.apps_logo_frame = ctk.CTkFrame(self.apps_frame) + self.apps_logo_frame.pack(fill="x", pady=(0, 10)) + + self.apps_logo_label = ctk.CTkLabel(self.apps_logo_frame, image=self.tab_logo_img, text="") + self.apps_logo_label.pack(side="right", padx=10, pady=5) + + # Add logo to components tab + self.components_logo_frame = ctk.CTkFrame(self.components_frame) + self.components_logo_frame.pack(fill="x", pady=(0, 10)) + + self.components_logo_label = ctk.CTkLabel(self.components_logo_frame, image=self.tab_logo_img, text="") + self.components_logo_label.pack(side="right", padx=10, pady=5) + + def add_theme_toggle(self): + """Add a theme toggle button to the header""" + # Create a toggle button + self.theme_button = ctk.CTkButton( + self.header_frame, + text="🌓 Toggle Theme", + command=self.toggle_theme, + width=120 + ) + self.theme_button.pack(side="right", padx=20, pady=10) + + def toggle_theme(self): + """Toggle between light and dark themes""" + current_theme = ctk.get_appearance_mode() + new_theme = "Light" if current_theme == "Dark" else "Dark" + ctk.set_appearance_mode(new_theme) diff --git a/cfg/gui/resources/orange.json b/cfg/gui/resources/orange.json new file mode 100644 index 00000000..bd286906 --- /dev/null +++ b/cfg/gui/resources/orange.json @@ -0,0 +1,155 @@ +{ + "CTk": { + "fg_color": ["gray92", "gray14"] + }, + "CTkToplevel": { + "fg_color": ["gray92", "gray14"] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray86", "gray17"], + "top_fg_color": ["gray81", "gray20"], + "border_color": ["gray65", "gray28"] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#FF8C42", "#FF6505"], + "hover_color": ["#E67320", "#CC5500"], + "border_color": ["#3E454A", "#949A9F"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": ["gray10", "#DCE4EE"] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "placeholder_text_color": ["gray52", "gray62"] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": ["#FF8C42", "#FF6505"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#FF8C42", "#FF6505"], + "checkmark_color": ["#DCE4EE", "gray90"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#FF8C42", "#FF6505"], + "button_color": ["gray36", "#D5D9DE"], + "button_hover_color": ["gray20", "gray100"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": ["#FF8C42", "#FF6505"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#E67320", "#CC5500"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#FF8C42", "#FF6505"], + "border_color": ["gray", "gray"] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["gray40", "#AAB0B5"], + "button_color": ["#FF8C42", "#FF6505"], + "button_hover_color": ["#E67320", "#CC5500"] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": ["#FF8C42", "#FF6505"], + "button_color": ["#E67320", "#CC5500"], + "button_hover_color": ["#CC5500", "#994411"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#F9F9FA", "#343638"], + "border_color": ["#979DA2", "#565B5E"], + "button_color": ["#979DA2", "#565B5E"], + "button_hover_color": ["#6E7174", "#7A848D"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray50", "gray45"] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": ["gray55", "gray41"], + "button_hover_color": ["gray40", "gray53"] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": ["#979DA2", "gray29"], + "selected_color": ["#FF8C42", "#FF6505"], + "selected_hover_color": ["#E67320", "#CC5500"], + "unselected_color": ["#979DA2", "gray29"], + "unselected_hover_color": ["gray70", "gray41"], + "text_color": ["#DCE4EE", "#DCE4EE"], + "text_color_disabled": ["gray74", "gray60"] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#F9F9FA", "#1D1E1E"], + "border_color": ["#979DA2", "#565B5E"], + "text_color":["gray10", "#DCE4EE"], + "scrollbar_button_color": ["gray55", "gray41"], + "scrollbar_button_hover_color": ["gray40", "gray53"] + }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, + "DropdownMenu": { + "fg_color": ["gray90", "gray20"], + "hover_color": ["gray75", "gray28"], + "text_color": ["gray10", "gray90"] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} diff --git a/scripts/cfg/igniter_launch.sh b/scripts/cfg/igniter_launch.sh index 418773ae..bf57f2d6 100755 --- a/scripts/cfg/igniter_launch.sh +++ b/scripts/cfg/igniter_launch.sh @@ -8,6 +8,6 @@ echo "" echo "" cd $BASE_DIR -python3 $BASE_DIR/cfg/gui/cfg_gui_main.py & +python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & echo "" echo "" \ No newline at end of file diff --git a/scripts/cfg/prepare.sh b/scripts/cfg/prepare.sh index cc9041bf..7c287174 100755 --- a/scripts/cfg/prepare.sh +++ b/scripts/cfg/prepare.sh @@ -49,7 +49,7 @@ echo "" echo "Prepare Igniter (optional)..." pip3 install pyside6 xmltodict cd $BASE_DIR -python3 $BASE_DIR/cfg/gui/cfg_gui_main.py & +python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & echo "" echo "" From c9e650b279b89e0ee8fc4c5b3b188189fa62d677 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Tue, 4 Nov 2025 22:48:15 +0000 Subject: [PATCH 2/7] [nasa/nos3#319] fix new mission and saving --- cfg/gui/nos3_gui.py | 98 ++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/cfg/gui/nos3_gui.py b/cfg/gui/nos3_gui.py index ecdcf40f..d64d73dc 100644 --- a/cfg/gui/nos3_gui.py +++ b/cfg/gui/nos3_gui.py @@ -978,7 +978,7 @@ def save_all(self): try: # Save mission configuration - self.save_mission_file(f"/cfg/{self.current_mission_file}") + self.save_mission_file(self.current_mission_file) # Determine spacecraft config file path from mission config spacecraft_config = self.get_spacecraft_config_filename() @@ -1220,9 +1220,9 @@ def save_mission_file(self, filename): sc1_cfg = ET.SubElement(root, 'sc-1-cfg') selected_option = self.sc1_config_var.get() # type: ignore if selected_option in self.config_filenames: - for filename in self.config_filenames: - if filename == selected_option: - sc1_cfg.text = f"spacecraft/{filename}" + for filename2 in self.config_filenames: + if filename2 == selected_option: + sc1_cfg.text = f"spacecraft/{filename2}" # Format and save XML xml_str = '\n' @@ -1231,7 +1231,7 @@ def save_mission_file(self, filename): pretty_xml = reparsed.toprettyxml(indent=" ") pretty_xml = '\n'.join(pretty_xml.split('\n')[1:]) - with open("./cfg/nos3-mission.xml", 'w', encoding='utf-8') as f: + with open(filename, 'w', encoding='utf-8') as f: f.write(xml_str + pretty_xml) def save_spacecraft_file(self, filename): @@ -1373,61 +1373,41 @@ def on_spacecraft_config_change(self): def create_default_spacecraft_config(self, config_filename): """Create default spacecraft configuration based on the selected type""" - - # Default configurations for different types - default_configs = { - "sc-minimal-config.xml": { - "apps": ["cf", "ds", "fm", "lc", "sc"], - "components": ["sample"] - }, - "sc-mission-config.xml": { - "apps": ["cf", "ds", "fm", "lc", "sc"], - "components": ["adcs", "css", "eps", "fss", "gps", "imu", "mag", "mgr", "radio", "rw", "sample", "st", "tourqer"] - }, - "sc-research-config.xml": { - "apps": ["cf", "ds", "fm", "lc", "sbn", "sc"], - "components": ["adcs", "cam", "css", "eps", "fss", "gps", "imu", "mag", "mgr", "onair", "radio", "rw", "sample", "st", "syn", "tourqer", "thruster"] - }, - "sc-fprime-config.xml": { - "apps": [], - "components": ["cam", "css", "eps", "fss", "imu", "mag", "radio", "rw", "sample", "st", "tourqer", "thruster"] - } + # Generic default + self.apps_data = { + "cf" : True, + "ds" : True, + "fm" : True, + "lc" : True, + "sbn" : False, + "sc" : True + } + self.components_data = { + "adcs" : True, + "cam" : False, + "css" : True, + "eps" : True, + "fss" : True, + "gps" : True, + "imu" : True, + "mag" : True, + "mgr" : True, + "onair" : False, + "radio" : True, + "rw" : True, + "sample" : True, + "st" : True, + "syn" : False, + "tourqer" : True, + "thruster" : False + } + self.additional_data = { + "gui_enabled" : True, + "tipoff_x" : 0.2, + "tipoff_y" : 2.0, + "tipoff_z" : -2.0, + "sim_truth" : True } - - if config_filename in default_configs: - config = default_configs[config_filename] - - # Set default apps (all enabled) - for app in config["apps"]: - self.apps_data[app] = True - - # Set default components (all enabled) - for component in config["components"]: - self.components_data[component] = True - else: - # Generic default - self.apps_data = { - "cf": True, - "ds": True, - "fm": True, - "lc": True, - "sc": True - } - self.components_data = { - "adcs" : True, - "css" : True, - "eps" : True, - "fss" : True, - "gps" : True, - "imu" : True, - "mag" : True, - "mgr" : True, - "radio" : True, - "rw" : True, - "sample" : True, - "st" : True, - "tourqer" : True - } # Update the load_mission_config method to properly set the spacecraft config def load_mission_config(self, tree): From 30ae5c5c6dce0f42c0f21158e99e13adc4047b13 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Tue, 4 Nov 2025 23:00:13 +0000 Subject: [PATCH 3/7] [nasa/nos3#319] move classes to own directory --- cfg/gui/{ => classes}/add_dialog.py | 0 cfg/gui/{ => classes}/datetime_dialog.py | 0 cfg/gui/{ => classes}/nos3_gui.py | 4 ++-- cfg/gui/igniter_entrypoint.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename cfg/gui/{ => classes}/add_dialog.py (100%) rename cfg/gui/{ => classes}/datetime_dialog.py (100%) rename cfg/gui/{ => classes}/nos3_gui.py (99%) diff --git a/cfg/gui/add_dialog.py b/cfg/gui/classes/add_dialog.py similarity index 100% rename from cfg/gui/add_dialog.py rename to cfg/gui/classes/add_dialog.py diff --git a/cfg/gui/datetime_dialog.py b/cfg/gui/classes/datetime_dialog.py similarity index 100% rename from cfg/gui/datetime_dialog.py rename to cfg/gui/classes/datetime_dialog.py diff --git a/cfg/gui/nos3_gui.py b/cfg/gui/classes/nos3_gui.py similarity index 99% rename from cfg/gui/nos3_gui.py rename to cfg/gui/classes/nos3_gui.py index d64d73dc..c53e20d6 100644 --- a/cfg/gui/nos3_gui.py +++ b/cfg/gui/classes/nos3_gui.py @@ -7,8 +7,8 @@ from datetime import datetime, timedelta, timezone from PIL import Image -from add_dialog import SimpleNameDialog -from datetime_dialog import DateTimeDialog +from classes.add_dialog import SimpleNameDialog +from classes.datetime_dialog import DateTimeDialog J2000_EPOCH = datetime(2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc) J2000_TIMESTAMP = J2000_EPOCH.timestamp() diff --git a/cfg/gui/igniter_entrypoint.py b/cfg/gui/igniter_entrypoint.py index 942734ba..d041696d 100644 --- a/cfg/gui/igniter_entrypoint.py +++ b/cfg/gui/igniter_entrypoint.py @@ -1,4 +1,4 @@ -from nos3_gui import NOS3ConfigGUI +from classes.nos3_gui import NOS3ConfigGUI if __name__ == "__main__": From 37a4b85252eef1371b24094d4d60f2346713f894 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Wed, 19 Nov 2025 15:43:13 -0500 Subject: [PATCH 4/7] [nasa/nos3#319] Containerized, fix typo --- cfg/gui/classes/nos3_gui.py | 2 +- scripts/cfg/igniter_launch.sh | 2 +- scripts/cfg/prepare.sh | 3 +-- scripts/env.sh | 3 ++- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cfg/gui/classes/nos3_gui.py b/cfg/gui/classes/nos3_gui.py index c53e20d6..11ea9437 100644 --- a/cfg/gui/classes/nos3_gui.py +++ b/cfg/gui/classes/nos3_gui.py @@ -1398,7 +1398,7 @@ def create_default_spacecraft_config(self, config_filename): "sample" : True, "st" : True, "syn" : False, - "tourqer" : True, + "torquer" : True, "thruster" : False } self.additional_data = { diff --git a/scripts/cfg/igniter_launch.sh b/scripts/cfg/igniter_launch.sh index bf57f2d6..80ec4b4e 100755 --- a/scripts/cfg/igniter_launch.sh +++ b/scripts/cfg/igniter_launch.sh @@ -8,6 +8,6 @@ echo "" echo "" cd $BASE_DIR -python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & +$DFLAGS_NOIT -v $BASE_DIR:$BASE_DIR -v ~/.fonts/:/home/jstar/.fonts -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY=$DISPLAY -w $BASE_DIR --name "nos3-igniter" $DBOX python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & echo "" echo "" \ No newline at end of file diff --git a/scripts/cfg/prepare.sh b/scripts/cfg/prepare.sh index 7c287174..df2f1e95 100755 --- a/scripts/cfg/prepare.sh +++ b/scripts/cfg/prepare.sh @@ -47,9 +47,8 @@ echo "" echo "" echo "Prepare Igniter (optional)..." -pip3 install pyside6 xmltodict cd $BASE_DIR -python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & +sh ./scripts/cfg/igniter_launch echo "" echo "" diff --git a/scripts/env.sh b/scripts/env.sh index ff33bd0d..25aa9808 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -43,12 +43,13 @@ INFLUXDB_ADMIN_PASSWORD=admin_password #else DCALL="docker" DFLAGS="docker run --rm -it -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -u $(id -u $(stat -c '%U' $SCRIPT_DIR/env.sh)):$(getent group $(stat -c '%G' $SCRIPT_DIR/env.sh) | cut -d: -f3)" + DFLAGS_NOIT="docker run --rm -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -u $(id -u $(stat -c '%U' $SCRIPT_DIR/env.sh)):$(getent group $(stat -c '%G' $SCRIPT_DIR/env.sh) | cut -d: -f3)" DFLAGS_CPUS="$DFLAGS --cpus=$NUM_CPUS" DCREATE="docker create --rm -it" DNETWORK="docker network" #fi -DBOX="ivvitc/nos3-64:20250514" +DBOX="ivvitc/nos3-64:dev" # Radio Config RADIO_TX_FSW_PORT=5010 From acad221479bf4b256f82b10a4d8add795bad9842 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Wed, 19 Nov 2025 16:19:20 -0500 Subject: [PATCH 5/7] [nasa/nos3#319] fix typo --- scripts/cfg/prepare.sh | 2 +- scripts/env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cfg/prepare.sh b/scripts/cfg/prepare.sh index df2f1e95..9a65dd6e 100755 --- a/scripts/cfg/prepare.sh +++ b/scripts/cfg/prepare.sh @@ -48,7 +48,7 @@ echo "" echo "Prepare Igniter (optional)..." cd $BASE_DIR -sh ./scripts/cfg/igniter_launch +./scripts/cfg/igniter_launch.sh echo "" echo "" diff --git a/scripts/env.sh b/scripts/env.sh index 25aa9808..f2181052 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -49,7 +49,7 @@ INFLUXDB_ADMIN_PASSWORD=admin_password DNETWORK="docker network" #fi -DBOX="ivvitc/nos3-64:dev" +DBOX="ivvitc/nos3-64:igniter" # Radio Config RADIO_TX_FSW_PORT=5010 From 12ff136d9d505ccdab8e28b9737da3848abd26f8 Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Wed, 19 Nov 2025 17:02:12 -0500 Subject: [PATCH 6/7] [nasa/nos3#319] Fix XML preview, add scenario selections --- cfg/gui/classes/nos3_gui.py | 238 ++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 134 deletions(-) diff --git a/cfg/gui/classes/nos3_gui.py b/cfg/gui/classes/nos3_gui.py index 11ea9437..882a08e7 100644 --- a/cfg/gui/classes/nos3_gui.py +++ b/cfg/gui/classes/nos3_gui.py @@ -18,7 +18,7 @@ class NOS3ConfigGUI: def __init__(self): # Set appearance mode and color theme ctk.set_appearance_mode("dark") # or "light" - ctk.set_default_color_theme("./cfg/gui/resources/orange.json") # or "green", "dark-blue" + ctk.set_default_color_theme("./cfg/gui/resources/orange.json") # Create main window self.root = ctk.CTk() @@ -29,7 +29,6 @@ def __init__(self): # Set window icon self.set_window_icon() - # Data structures # Data structures self.current_mission_file = None self.current_spacecraft_file = None @@ -44,6 +43,7 @@ def __init__(self): self.default_mission_config = "./cfg/nos3-mission.xml" self.fsw_options = ["cfs", "fprime"] self.gsw_options = ["cosmos", "openc3", "fprime", "yamcs"] + self.scenario_options = ["STF1", "Gateway"] config_dir = "./cfg/spacecraft/" self.config_filenames = { filename for filename in os.listdir(config_dir) if os.path.isfile(os.path.join(config_dir, filename)) @@ -164,7 +164,7 @@ def setup_gui_section(self): gui_frame = ctk.CTkFrame(self.additional_scrollable) gui_frame.pack(fill="x", pady=10, padx=5) - gui_label = ctk.CTkLabel(gui_frame, text="GUI Configuration:", + gui_label = ctk.CTkLabel(gui_frame, text="42 GUI Configuration:", font=ctk.CTkFont(size=14, weight="bold")) gui_label.pack(anchor="w", padx=10, pady=(10, 5)) @@ -338,6 +338,22 @@ def setup_mission_tab(self): for option in self.fsw_options: rb = ctk.CTkRadioButton(fsw_frame, text=option, variable=self.fsw_var, value=option) rb.pack(anchor="w", padx=20, pady=2) + + # Scenario Section + scenario_frame = ctk.CTkFrame(self.mission_scrollable) + scenario_frame.pack(fill="x", pady=10, padx=5) + + scenario_label = ctk.CTkLabel(scenario_frame, text="Scenario:", + font=ctk.CTkFont(size=14, weight="bold")) + scenario_label.pack(anchor="w", padx=10, pady=5) + + # Scenario options + self.scenario_var = tk.StringVar() + + # Radio buttons for scenario + for option in self.scenario_options: + rb = ctk.CTkRadioButton(scenario_frame, text=option, variable=self.scenario_var, value=option) + rb.pack(anchor="w", padx=20, pady=2) # Number of Spacecraft Section sc_frame = ctk.CTkFrame(self.mission_scrollable) @@ -390,6 +406,7 @@ def setup_mission_tab(self): "start_time": "", "gsw": "", "fsw": "", + "scenario": "", "num_spacecraft": "1", "sc1_config": "", "scN_config": "" # For spacecraft N config if more than 1 @@ -574,6 +591,16 @@ def save_xml_file(self, filename): fsw = ET.SubElement(root, 'fsw') fsw.text = self.fsw_var.get() # type: ignore + # Add flight software section + scenario_comment = ET.Comment(" Scenario ") + root.append(scenario_comment) + + scenario_options_comment = ET.Comment(" STF1 (default) or Gateway ") + root.append(scenario_options_comment) + + scenario = ET.SubElement(root, 'scenario') + scenario.text = self.scenario_var.get() # type: ignore + # Add number of spacecraft num_sc_comment = ET.Comment(" Number of spacecraft ") root.append(num_sc_comment) @@ -653,141 +680,63 @@ def save_xml_file(self, filename): with open(filename, 'w', encoding='utf-8') as f: f.write(xml_str + pretty_xml) - # Add a refresh_preview method that handles the active tab + # Add a refresh_preview method def refresh_preview(self): - try: - active_tab = self.notebook.get() - - if active_tab == "Mission Config": - # Generate mission XML preview - root = ET.Element('nos3-mission-cfg') - - # Add comments as in the example - comment1 = ET.Comment(" Mission Start Time (12000 UTC) ") - root.append(comment1) - - time_str = self.mission_time_entry.get().strip() - if time_str: - try: - timestamp = float(time_str) - dt = datetime.fromtimestamp(timestamp) - date_str = dt.strftime("%d %b %Y") - comment2 = ET.Comment(f" Default time: {time_str}, {date_str} ") - root.append(comment2) - except ValueError: - pass - - # Add start time - start_time = ET.SubElement(root, 'start-time') - start_time.text = self.mission_time_entry.get().strip() - - # Add ground software section - gsw_comment = ET.Comment(" Ground Software ") - root.append(gsw_comment) - - options_comment = ET.Comment(" cosmos (default), openc3, fprime, or yamcs ") - root.append(options_comment) - - gsw = ET.SubElement(root, 'gsw') - gsw.text = self.gsw_var.get() # type: ignore - - # Add flight software section - fsw_comment = ET.Comment(" Flight Software ") - root.append(fsw_comment) - - fsw_options_comment = ET.Comment(" cfs (default) or fprime ") - root.append(fsw_options_comment) - - fsw = ET.SubElement(root, 'fsw') - fsw.text = self.fsw_var.get() # type: ignore - - # Add number of spacecraft - num_sc_comment = ET.Comment(" Number of spacecraft ") - root.append(num_sc_comment) - - experimental_comment = ET.Comment(" Note this is experimental and not ready for use beyond proof of concept ") - root.append(experimental_comment) - - num_sc = ET.SubElement(root, 'number-spacecraft') - num_sc.text = self.sc_count_var.get() # type: ignore - - # Add spacecraft configurations - sc_num = int(self.sc_count_var.get()) # type: ignore - - # Add SC1 configuration - sc1_comment = ET.Comment(" Spacecraft 1 Configuration - options are as follows ") - root.append(sc1_comment) - - for filename in self.config_filenames: - root.append(ET.Comment(f" {filename} ")) - - sc1_cfg = ET.SubElement(root, 'sc-1-cfg') - selected_option = self.sc1_config_var.get() # type: ignore - if selected_option in self.config_filenames: - sc1_cfg.text = selected_option - - # Add SCN configuration if more than one spacecraft - if sc_num > 1: - scn_comment = ET.Comment(" Spacecraft N Configuration ") - root.append(scn_comment) - - scn_cfg_comment = ET.Comment(" sc-minimal-config.xml ") - root.append(scn_cfg_comment) - else: - # Generate spacecraft XML preview - root = ET.Element('sc-1-config') + try: + # Generate spacecraft XML preview + root = ET.Element('sc-1-config') # Add applications - apps_element = ET.SubElement(root, 'applications') - for app_name, enabled in self.apps_data.items(): - app_element = ET.SubElement(apps_element, app_name) - enable_element = ET.SubElement(app_element, 'enable') - enable_element.text = 'true' if enabled else 'false' + apps_element = ET.SubElement(root, 'applications') + for app_name, enabled in self.apps_data.items(): + app_element = ET.SubElement(apps_element, app_name) + enable_element = ET.SubElement(app_element, 'enable') + enable_element.text = 'true' if enabled else 'false' - # Add components - components_element = ET.SubElement(root, 'components') - for comp_name, enabled in self.components_data.items(): - comp_element = ET.SubElement(components_element, comp_name) - enable_element = ET.SubElement(comp_element, 'enable') - enable_element.text = 'true' if enabled else 'false' - - # Add GUI section - gui_element = ET.SubElement(root, 'gui') - gui_enable = ET.SubElement(gui_element, 'enable') - if hasattr(self, 'gui_enabled_var'): - gui_enable.text = 'true' if self.gui_enabled_var.get() else 'false' - else: - gui_enable.text = 'true' - - # Add Orbit section - orbit_element = ET.SubElement(root, 'orbit') - - tipoff_x = ET.SubElement(orbit_element, 'tipoff_x') - if hasattr(self, 'tipoff_x_entry'): - tipoff_x.text = self.tipoff_x_entry.get() if self.tipoff_x_entry.get() else "0.2" - else: - tipoff_x.text = "0.2" - - tipoff_y = ET.SubElement(orbit_element, 'tipoff_y') - if hasattr(self, 'tipoff_y_entry'): - tipoff_y.text = self.tipoff_y_entry.get() if self.tipoff_y_entry.get() else "2.0" - else: - tipoff_y.text = "2.0" - - tipoff_z = ET.SubElement(orbit_element, 'tipoff_z') - if hasattr(self, 'tipoff_z_entry'): - tipoff_z.text = self.tipoff_z_entry.get() if self.tipoff_z_entry.get() else "-2.0" - else: - tipoff_z.text = "-2.0" + # Add components + components_element = ET.SubElement(root, 'components') + for comp_name, enabled in self.components_data.items(): + comp_element = ET.SubElement(components_element, comp_name) + enable_element = ET.SubElement(comp_element, 'enable') + enable_element.text = 'true' if enabled else 'false' - # Add Sim section - sim_element = ET.SubElement(root, 'sim') - sim_truth = ET.SubElement(sim_element, 'sim_truth_interface') - if hasattr(self, 'sim_truth_var'): - sim_truth.text = 'true' if self.sim_truth_var.get() else 'false' - else: - sim_truth.text = 'true' + # Add GUI section + gui_element = ET.SubElement(root, 'gui') + gui_enable = ET.SubElement(gui_element, 'enable') + if hasattr(self, 'gui_enabled_var'): + gui_enable.text = 'true' if self.gui_enabled_var.get() else 'false' + else: + gui_enable.text = 'true' + + # Add Orbit section + orbit_element = ET.SubElement(root, 'orbit') + + tipoff_x = ET.SubElement(orbit_element, 'tipoff_x') + if hasattr(self, 'tipoff_x_entry'): + tipoff_x.text = self.tipoff_x_entry.get() if self.tipoff_x_entry.get() else "0.2" + else: + tipoff_x.text = "0.2" + + tipoff_y = ET.SubElement(orbit_element, 'tipoff_y') + if hasattr(self, 'tipoff_y_entry'): + tipoff_y.text = self.tipoff_y_entry.get() if self.tipoff_y_entry.get() else "2.0" + else: + tipoff_y.text = "2.0" + + tipoff_z = ET.SubElement(orbit_element, 'tipoff_z') + if hasattr(self, 'tipoff_z_entry'): + tipoff_z.text = self.tipoff_z_entry.get() if self.tipoff_z_entry.get() else "-2.0" + else: + tipoff_z.text = "-2.0" + # Add Sim section + sim_element = ET.SubElement(root, 'sim') + sim_truth = ET.SubElement(sim_element, 'sim_truth_interface') + if hasattr(self, 'sim_truth_var'): + sim_truth.text = 'true' if self.sim_truth_var.get() else 'false' + else: + sim_truth.text = 'true' + # Format XML xml_str = '\n' rough_string = ET.tostring(root, 'utf-8') @@ -936,6 +885,8 @@ def setup_mission_change_tracking(self): self.gsw_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore if hasattr(self, 'fsw_var'): self.fsw_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore + if hasattr(self, 'scenario_var'): + self.fsw_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore if hasattr(self, 'sc_count_var'): self.sc_count_var.trace_add("write", lambda *args: self.set_modified()) # type: ignore if hasattr(self, 'sc1_config_var'): @@ -1200,6 +1151,16 @@ def save_mission_file(self, filename): fsw = ET.SubElement(root, 'fsw') fsw.text = self.fsw_var.get() # type: ignore + # Add flight software section + scenario_comment = ET.Comment(" Scenario ") + root.append(scenario_comment) + + scenario_options_comment = ET.Comment(" STF1 (default) or Gateway ") + root.append(scenario_options_comment) + + scenario = ET.SubElement(root, 'scenario') + scenario.text = self.scenario_var.get() # type: ignore + # Add number of spacecraft num_sc_comment = ET.Comment(" Number of spacecraft ") root.append(num_sc_comment) @@ -1419,6 +1380,7 @@ def load_mission_config(self, tree): "start_time": "", "gsw": "", "fsw": "", + "scenario": "", "num_spacecraft": "1", "sc1_config": "", "scN_config": "" @@ -1443,6 +1405,12 @@ def load_mission_config(self, tree): if fsw_elem is not None and fsw_elem.text: self.mission_data["fsw"] = fsw_elem.text self.fsw_var.set(fsw_elem.text) # type: ignore + + # Extract scenario + scenario_elem = root.find('scenario') + if scenario_elem is not None and scenario_elem.text: + self.mission_data["scenario"] = scenario_elem.text + self.scenario_var.set(scenario_elem.text) # type: ignore # Extract number of spacecraft num_sc_elem = root.find('number-spacecraft') @@ -1504,6 +1472,8 @@ def clear_mission_fields(self): self.gsw_var.set("cosmos") # Default value # type: ignore if hasattr(self, 'fsw_var'): self.fsw_var.set("cfs") # Default value # type: ignore + if hasattr(self, 'scenario_var'): + self.fsw_var.set("STF1") # Default value # type: ignore if hasattr(self, 'sc_count_var'): self.sc_count_var.set("1") # type: ignore if hasattr(self, 'sc1_config_var'): @@ -1541,7 +1511,7 @@ def add_app(self): if app_name in self.apps_data: messagebox.showerror("Error", f"Application '{app_name}' already exists.") return - self.apps_data[app_name] = True # Default to enabled + self.apps_data[app_name] = False # Default to disabled self.refresh_apps_display() self.set_modified() @@ -1552,7 +1522,7 @@ def add_component(self): if comp_name in self.components_data: messagebox.showerror("Error", f"Component '{comp_name}' already exists.") return - self.components_data[comp_name] = True # Default to enabled + self.components_data[comp_name] = False # Default to disabled self.refresh_components_display() self.set_modified() From b05045719802443308487f9197cb791dc02b7a3d Mon Sep 17 00:00:00 2001 From: Donnie-Ice Date: Thu, 20 Nov 2025 11:20:02 -0500 Subject: [PATCH 7/7] [nasa/nos3#319] Run make config after saving --- cfg/gui/classes/nos3_gui.py | 3 +++ scripts/cfg/igniter_launch.sh | 2 +- scripts/env.sh | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cfg/gui/classes/nos3_gui.py b/cfg/gui/classes/nos3_gui.py index 882a08e7..a1fb120e 100644 --- a/cfg/gui/classes/nos3_gui.py +++ b/cfg/gui/classes/nos3_gui.py @@ -950,6 +950,9 @@ def save_all(self): self.modified = False self.update_status(f"Saved mission and spacecraft configurations") self.update_file_labels() + + # Update config in NOS3 on save + os.system("make config") else: messagebox.showerror("Error", "No spacecraft configuration selected in mission config") diff --git a/scripts/cfg/igniter_launch.sh b/scripts/cfg/igniter_launch.sh index 80ec4b4e..77320f4d 100755 --- a/scripts/cfg/igniter_launch.sh +++ b/scripts/cfg/igniter_launch.sh @@ -8,6 +8,6 @@ echo "" echo "" cd $BASE_DIR -$DFLAGS_NOIT -v $BASE_DIR:$BASE_DIR -v ~/.fonts/:/home/jstar/.fonts -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY=$DISPLAY -w $BASE_DIR --name "nos3-igniter" $DBOX python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & +$DFLAGS_NOINT -v $BASE_DIR:$BASE_DIR -v ~/.fonts/:/home/jstar/.fonts -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY=$DISPLAY -w $BASE_DIR --name "nos3-igniter" $DBOX python3 $BASE_DIR/cfg/gui/igniter_entrypoint.py & echo "" echo "" \ No newline at end of file diff --git a/scripts/env.sh b/scripts/env.sh index f2181052..d27957fc 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -43,7 +43,7 @@ INFLUXDB_ADMIN_PASSWORD=admin_password #else DCALL="docker" DFLAGS="docker run --rm -it -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -u $(id -u $(stat -c '%U' $SCRIPT_DIR/env.sh)):$(getent group $(stat -c '%G' $SCRIPT_DIR/env.sh) | cut -d: -f3)" - DFLAGS_NOIT="docker run --rm -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -u $(id -u $(stat -c '%U' $SCRIPT_DIR/env.sh)):$(getent group $(stat -c '%G' $SCRIPT_DIR/env.sh) | cut -d: -f3)" + DFLAGS_NOINT="docker run --rm -t -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -u $(id -u $(stat -c '%U' $SCRIPT_DIR/env.sh)):$(getent group $(stat -c '%G' $SCRIPT_DIR/env.sh) | cut -d: -f3)" DFLAGS_CPUS="$DFLAGS --cpus=$NUM_CPUS" DCREATE="docker create --rm -it" DNETWORK="docker network"