diff --git a/cfg/gui/cfg_gui.ui b/cfg/gui/cfg_gui.ui
deleted file mode 100644
index 02234ade7..000000000
--- 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 6af780233..000000000
--- 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 0e49b8f32..000000000
--- 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 32b50d49f..000000000
--- 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/classes/add_dialog.py b/cfg/gui/classes/add_dialog.py
new file mode 100644
index 000000000..6f5772ed4
--- /dev/null
+++ b/cfg/gui/classes/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/classes/datetime_dialog.py b/cfg/gui/classes/datetime_dialog.py
new file mode 100644
index 000000000..9862c34b3
--- /dev/null
+++ b/cfg/gui/classes/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/classes/nos3_gui.py b/cfg/gui/classes/nos3_gui.py
new file mode 100644
index 000000000..a1fb120e9
--- /dev/null
+++ b/cfg/gui/classes/nos3_gui.py
@@ -0,0 +1,1747 @@
+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 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()
+
+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")
+
+ # 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
+ 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"]
+ 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))
+ }
+
+ # 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="42 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)
+
+ # 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)
+ 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": "",
+ "scenario": "",
+ "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 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)
+
+ 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
+ def refresh_preview(self):
+ 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'
+
+ # 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, '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'):
+ 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(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()
+
+ # Update config in NOS3 on save
+ os.system("make config")
+ 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 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)
+
+ 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 filename2 in self.config_filenames:
+ if filename2 == selected_option:
+ sc1_cfg.text = f"spacecraft/{filename2}"
+
+ # 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)
+
+ 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"""
+ # 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,
+ "torquer" : True,
+ "thruster" : False
+ }
+ self.additional_data = {
+ "gui_enabled" : True,
+ "tipoff_x" : 0.2,
+ "tipoff_y" : 2.0,
+ "tipoff_z" : -2.0,
+ "sim_truth" : 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": "",
+ "scenario": "",
+ "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 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')
+ 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, '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'):
+ 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] = False # Default to disabled
+ 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] = False # Default to disabled
+ 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/igniter_entrypoint.py b/cfg/gui/igniter_entrypoint.py
new file mode 100644
index 000000000..d041696d2
--- /dev/null
+++ b/cfg/gui/igniter_entrypoint.py
@@ -0,0 +1,6 @@
+from classes.nos3_gui import NOS3ConfigGUI
+
+
+if __name__ == "__main__":
+ app = NOS3ConfigGUI()
+ app.run()
\ No newline at end of file
diff --git a/cfg/gui/resources/orange.json b/cfg/gui/resources/orange.json
new file mode 100644
index 000000000..bd2869063
--- /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/fsw/fprime/CMakeLists.txt b/fsw/fprime/CMakeLists.txt
deleted file mode 100644
index fc9eaefeb..000000000
--- a/fsw/fprime/CMakeLists.txt
+++ /dev/null
@@ -1,69 +0,0 @@
-cmake_minimum_required(VERSION 2.6.4)
-
-project (sample_checkout)
-
-if (NOT DEFINED TGTNAME)
- message(FATAL_ERROR "TGTNAME must be defined on the cmake command line (e.g. \"-DTGTNAME=cpu1\")")
-endif()
-
-# if(${TGTNAME} STREQUAL cpu1)
-# SET(CMAKE_C_FLAGS_INIT "-m32" CACHE STRING "C Flags required by platform")
-# SET(CMAKE_C_FLAGS "-m32" CACHE STRING "C Flags required by platform")
-# endif()
-
-include(../../ComponentSettings.cmake)
-
-if(${TGTNAME} STREQUAL cpu1)
- find_path(_ITC_CMAKE_MODULES_
- NAMES FindITC_Common.cmake
- PATHS ${ITC_CMAKE_MODULES}
- ${ITC_DEV_ROOT}/cmake/modules
- $ENV{ITC_DEV_ROOT}/cmake/modules
- /usr/local/cmake/modules
- /usr/cmake/modules)
- if(NOT _ITC_CMAKE_MODULES_)
- message(WARNING "Unable to find ITC CMake Modules")
- endif()
- set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${_ITC_CMAKE_MODULES_})
-
- find_package(NOSENGINE REQUIRED QUIET COMPONENTS common transport client uart can i2c spi)
-endif()
-
-include_directories("./")
-include_directories("../fsw/platform_inc")
-include_directories("../fsw/src")
-include_directories("../../../fsw/apps/hwlib/fsw/public_inc")
-
-set(sample_checkout_src
- test.cpp
- ../fsw/src/sample_device.c
-)
-
-if(${TGTNAME} STREQUAL cpu1)
- include_directories("../../../fsw/apps/hwlib/sim/inc")
- set(sample_checkout_src
- ${sample_checkout_src}
- ../../../fsw/apps/hwlib/sim/src/libuart.c
- ../../../fsw/apps/hwlib/sim/src/libcan.c
- ../../../fsw/apps/hwlib/sim/src/libi2c.c
- ../../../fsw/apps/hwlib/sim/src/libspi.c
- ../../../fsw/apps/hwlib/sim/src/nos_link.c
- )
- set(sample_checkout_libs
- ${ITC_Common_LIBRARIES}
- ${NOSENGINE_LIBRARIES}
- )
-endif()
-if(${TGTNAME} STREQUAL cpu2)
- set(sample_checkout_src
- ${sample_checkout_src}
- ../../../fsw/apps/hwlib/fsw/linux/libuart.c
- )
-endif()
-
-add_executable(sample_checkout ${sample_checkout_src})
-target_link_libraries(sample_checkout ${sample_checkout_libs})
-
-if(${TGTNAME} STREQUAL cpu1)
- set_target_properties(sample_checkout PROPERTIES COMPILE_FLAGS " -g" LINK_FLAGS " -g")
-endif()
diff --git a/scripts/cfg/igniter_launch.sh b/scripts/cfg/igniter_launch.sh
index 418773aed..77320f4d1 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 &
+$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/cfg/prepare.sh b/scripts/cfg/prepare.sh
index cc9041bf0..9a65dd6e6 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/cfg_gui_main.py &
+./scripts/cfg/igniter_launch.sh
echo ""
echo ""
diff --git a/scripts/env.sh b/scripts/env.sh
index 124e1048e..d27957fc6 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_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"
#fi
-DBOX="ivvitc/nos3-64:20251107"
+DBOX="ivvitc/nos3-64:igniter"
# Radio Config
RADIO_TX_FSW_PORT=5010