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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions app_window.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while we're working on a redesign, consider licensing the gui.

the primary library in use is SDEC, which uses the BSD-3-clause

Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import tkinter as tk
from PIL import Image, ImageTk

import gauge as SDR_gauge
import sensor as SDR_sensor
import engine_display as SDR_engine_display
from button_factory import ButtonFactory

# Valve state constants
VALVE_OPEN = True
VALVE_CLOSED = False

# Layout constant
_GRID_LEN = 85


class DashboardWindow:
"""Encapsulates all Tkinter details for the engine dashboard.

The rest of the application never imports tkinter directly; it interacts
with the GUI exclusively through this class's public interface:

update_valve_states(valve_states) — sync valve button visuals
update() — tick the GUI event loop
close() — destroy both windows

Widget collections are exposed as plain attributes:
gauges : list of SDR_gauge.gauge, in _GAUGE_CONFIGS order
valve_buttons : dict[str, SDR_valve.Buttons], keyed by valve-state key
"""

def __init__( self, callbacks, valve_buttons ):
"""
Parameters
----------
callbacks : CallbackHandler — provides all button callbacks
valve_buttons : dict — shared mutable dict; populated here
so external code (TelemetryProcessor,
CallbackHandler closures) can reference it
"""
self._callbacks = callbacks
self.valve_buttons = valve_buttons # populated during _build_widgets
self.gauges = []

self._root = self._build_root()
self._frames = self._build_frames()
self.plumbing = SDR_engine_display.Engine_Display(_GRID_LEN)
self._build_widgets()
self._layout()

# -------------------------------------------------------------------------
# Public interface
# -------------------------------------------------------------------------

def update_valve_states( self, valve_states ):
"""Sync every valve button's visual state with the latest telemetry."""
for key, btn in self.valve_buttons.items():
state_str = valve_states.get(key)
if state_str == "OPEN":
btn.symbol.setState(VALVE_OPEN); btn.state = VALVE_OPEN
elif state_str == "CLOSED":
btn.symbol.setState(VALVE_CLOSED); btn.state = VALVE_CLOSED
btn.updateText()
btn.updateColor()
btn.configButton()

def update( self ):
"""Advance the Tkinter and P&ID event loops by one tick."""
self.plumbing.updatePipeStatus()
self._root.update()
self.plumbing.getWindow().update()

def close( self ):
"""Destroy both the dashboard window and the P&ID window."""
self._root.destroy()
self.plumbing.win.destroy()

# -------------------------------------------------------------------------
# Private construction helpers
# -------------------------------------------------------------------------

def _build_root( self ):
root = tk.Tk()
root.title("Engine Dashboard")
root.configure( background="black", borderwidth=10 )
root.geometry("900x1000")
root.protocol( "WM_DELETE_WINDOW", self._callbacks.close_window )

# Load logo images (kept on self to prevent garbage collection)
self._sdr_logo = tk.PhotoImage(file='images/SDRLogo5.png')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: bad (non-descriptive) file and variable naming here w/ sdr_logo vs sdr_img

sdr_img = Image.open("images/SDRlogont2.png")
sdr_img = sdr_img.resize(
(int(0.8*140), int(0.8*125)), Image.Resampling.LANCZOS
)
self._sdr_photo = ImageTk.PhotoImage(sdr_img)
return root

def _build_frames( self ):
r = self._root
valve_frame = tk.Label( r, bg='black' )
frames = {
"main_title" : tk.Label( r, bg='black' ),
"valve_button" : valve_frame,
"valve_col1" : tk.Label( valve_frame, bg='black' ),
"valve_col2" : tk.Label( valve_frame, bg='black' ),
"valve_col3" : tk.Label( valve_frame, bg='black' ),
"valve_col4" : tk.Label( valve_frame, bg='black' ),
"sequence_row1" : tk.Label( r, bg='black' ),
"sequence_row2" : tk.Label( r, bg='black' ),
"gauge_row1" : tk.Label( r, bg='black' ),
"gauge_row2" : tk.Label( r, bg='black' ),
}
return frames

def _build_widgets( self ):
f = self._frames
cb = self._callbacks

# --- Valve buttons ---------------------------------------------------
# (valve_state_key, label, col_frame_key, plumbing_sym, seq_fn_name)
_VALVE_CONFIGS = [
("oxPress", "LOX Pressure (1)", "valve_col1", self.plumbing.one, "manual_lox_press" ),
("oxVent", "LOX Vent (2)", "valve_col1", self.plumbing.two, "manual_lox_vent" ),
("oxPurge", "LOX Purge (5)", "valve_col1", self.plumbing.five, "manual_lox_purge" ),
("oxMain", "LOX Main", "valve_col1", self.plumbing.s2, "manual_lox_main" ),
("fuelPress", "Fuel Pressure (3)", "valve_col2", self.plumbing.three, "manual_fuel_press"),
("fuelVent", "Fuel Vent (4)", "valve_col2", self.plumbing.four, "manual_fuel_vent" ),
("fuelPurge", "Fuel Purge (6)", "valve_col2", self.plumbing.six, "manual_fuel_purge"),
("fuelMain", "Fuel Main Valve", "valve_col2", self.plumbing.s1, "manual_fuel_main" ),
]
for key, label, col_key, sym, seq_fn in _VALVE_CONFIGS:
self.valve_buttons[key] = ButtonFactory.valve_button(
f[col_key], label, sym, cb.make_valve_callback(key, seq_fn)
)

# --- Sequence / control buttons --------------------------------------
# (dict_key, label, frame_key, callback, fg, outline, text_color)
_BUTTON_CONFIGS = [
("pre_fire_purge", "Pre-Fire Purge", "sequence_row1", cb.pre_fire_purge, 'white', 'white', 'white'),
("fill_chill", "Fill/Chill", "sequence_row1", cb.fill_and_chill, 'white', 'white', 'white'),
("standby", "Standby", "sequence_row1", cb.standby, 'white', 'white', 'white'),
("ignite", "Ignite", "sequence_row1", cb.fire_engine, 'white', 'white', 'white'),
("stop_hotfire", "Stop Hotfire", "sequence_row2", cb.stop_hotfire, 'white', 'white', 'white'),
("stop_purge", "Disarm", "sequence_row2", cb.stop_purge, 'white', 'white', 'white'),
("lox_purge", "LOX Purge", "sequence_row2", cb.lox_purge, 'white', 'white', 'white'),
("kbottle_close", "KBottle Close", "sequence_row2", cb.kbottle_close, 'white', 'white', 'white'),
("getstate", "Get State", "valve_col3", cb.get_state, 'white', 'white', 'white'),
("manual", "Manual", "valve_col3", cb.manual_mode, 'white', 'white', 'white'),
("abort", "ABORT", "valve_col4", cb.hotfire_abort, 'red', 'red', 'red' ),
("reset", "Reset", "valve_col4", cb.reset, 'white', 'white', 'white'),
]
self._sequence_buttons = {}
for name, label, frame_key, callback, fg, outline, text in _BUTTON_CONFIGS:
self._sequence_buttons[name] = ButtonFactory.sequence_button(
f[frame_key], label, callback,
fg_color=fg, outline_color=outline, text_color=text
)

# --- Sensor gauges ---------------------------------------------------
# (sensor_key, display_label, row_frame_key)
_GAUGE_CONFIGS = [
("pt7", "Fuel Tank Pressure", "gauge_row1"),
("ffr", "Fuel Flow Rate", "gauge_row1"),
("pt2", "None", "gauge_row1"),
("lc", "Thrust", "gauge_row1"),
("pt0", "LOX Pressure", "gauge_row2"),
("oxfr", "LOX Flow Rate", "gauge_row2"),
("pt4", "Engine Pressure", "gauge_row2"),
("tc", "LOX Temperature", "gauge_row2"),
]
for sensor_key, label, row_key in _GAUGE_CONFIGS:
g = SDR_gauge.gauge(
f[row_key],
background = 'black',
max_sensor_val = SDR_sensor.max_sensor_vals[sensor_key]
)
g.setText("Nan", label)
self.gauges.append(g)

def _layout( self ):
f = self._frames
sb = self._sequence_buttons

f["main_title"].pack()

f["valve_col1"].pack( side='left' )
f["valve_col2"].pack( side='left' )
f["valve_col3"].pack( side='left' )
f["valve_col4"].pack( side='left' )
f["valve_button"].pack()

f["sequence_row1"].pack()
f["sequence_row2"].pack()

sb["pre_fire_purge"].pack( side="left", padx=30 )
sb["fill_chill" ].pack( side="left", padx=30 )
sb["standby" ].pack( side="left", padx=30 )
sb["ignite" ].pack( side="left", padx=30 )
sb["getstate" ].pack( side="top", padx=30 )
sb["manual" ].pack( side="top", padx=30 )
sb["abort" ].pack( side="top", padx=30 )
sb["stop_hotfire" ].pack( side="left", padx=30 )
sb["stop_purge" ].pack( side="left", padx=30 )
sb["lox_purge" ].pack( side="left", padx=30 )
sb["kbottle_close" ].pack( side="left", padx=30 )
sb["reset" ].pack( side="top", padx=30 )

f["gauge_row1"].pack()
f["gauge_row2"].pack()
for g in self.gauges:
g.getWidget().pack(side='left')
## DashboardWindow ##
32 changes: 32 additions & 0 deletions button_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import valve as SDR_valve
import buttons as SDR_buttons


class ButtonFactory:
"""Abstract factory that centralises construction of valve and sequence buttons.

Having widget constructor details in one place means a single edit propagates
to every button rather than hunting through the initialisation block."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[too nitpicky, feel free to ignore, no change necessary] okay, i suppose this is a valid reason, but you could also use variables in the initialization block like "valve_initialization_fg_color" or something. the factory seems like a bit of a roundabout way to do it.


@staticmethod
def valve_button(parent, label, plumbing_sym, callback):
"""Create an SDR_valve.Buttons widget."""
return SDR_valve.Buttons(parent, label, 'top', plumbing_sym,
f_callback=callback)

@staticmethod
def sequence_button(parent, label, callback,
fg_color='white', outline_color='white',
text_color='white', size=(135, 45)):
"""Create an SDR_buttons.Button widget with the dashboard's default styling."""
return SDR_buttons.Button(
parent,
text = label,
bg_color = 'black',
fg_color = fg_color,
outline_color = outline_color,
text_color = text_color,
size = size,
f_callback = callback,
)
## ButtonFactory ##
101 changes: 101 additions & 0 deletions callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import sequence as SDR_sequence


class CallbackHandler:
"""Owns every button callback in the dashboard.

Dependencies are injected at construction so callbacks are not coupled to
module-level globals. The window reference is set separately via
set_window() to break the initialization cycle:
callbacks → window (needs callbacks to build buttons)
window → callbacks (needs window reference to close it)
"""

def __init__( self, engine_state, terminal, valve_buttons, exit_flag ):
"""
Parameters
----------
engine_state : Liquid_Engine_State

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider using actual strong typing for these parameters if you know what they're expected to be

terminal : sdec.terminalData
valve_buttons : dict — shared mutable dict populated by DashboardWindow
exit_flag : list — single-element mutable bool, e.g. [False]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this a list?

"""
self._engine_state = engine_state
self._terminal = terminal
self._valve_buttons = valve_buttons
self._exit_flag = exit_flag
self._window = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be careful with this -- very easy way to get an AttributeError if set_window isn't called before you call other methods. there's no handling for issues that could come from _window being None


def set_window( self, window ):
"""Late-bind the window reference after DashboardWindow is created."""
self._window = window

# -------------------------------------------------------------------------
# Window lifecycle
# -------------------------------------------------------------------------

def close_window( self ):
self._window.close()
self._exit_flag[0] = True

# -------------------------------------------------------------------------
# Sequence / control callbacks
# -------------------------------------------------------------------------

def pre_fire_purge( self ):
SDR_sequence.pre_fire_purge( self._engine_state, self._terminal )

def fill_and_chill( self ):
SDR_sequence.fill_and_chill( self._engine_state, self._terminal )

def standby( self ):
SDR_sequence.standby ( self._engine_state, self._terminal )

def fire_engine( self ):
SDR_sequence.fire_engine ( self._engine_state, self._terminal )

def hotfire_abort( self ):
SDR_sequence.hotfire_abort ( self._engine_state, self._terminal )

def get_state( self ):
SDR_sequence.get_state ( self._engine_state, self._terminal )

def stop_hotfire( self ):
SDR_sequence.stop_hotfire ( self._engine_state, self._terminal )

def stop_purge( self ):
SDR_sequence.stop_purge ( self._engine_state, self._terminal )

def lox_purge( self ):
SDR_sequence.lox_purge ( self._engine_state, self._terminal )

def kbottle_close( self ):
SDR_sequence.kbottle_close ( self._engine_state, self._terminal )

def manual_mode( self ):
SDR_sequence.manual ( self._engine_state, self._terminal )

def reset( self ):
pass

# -------------------------------------------------------------------------
# Valve callback factory
# -------------------------------------------------------------------------

def make_valve_callback( self, valve_key, seq_fn_name ):
"""Return a closure that toggles a valve button and fires the matching
sequence function. Captured variables are resolved at click-time so
valve_buttons can be populated after the closure is created."""
def callback():
btn = self._valve_buttons[valve_key]
getattr(SDR_sequence, seq_fn_name)(
self._engine_state, self._terminal, btn.state
)
if btn.symbol is not None and self._engine_state.state == "Manual State":
btn.state = not btn.state
btn.symbol.setState(btn.state)
btn.updateText()
btn.updateColor()
btn.configButton()
return callback
## CallbackHandler ##
19 changes: 9 additions & 10 deletions engine_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@
###############################################################
# Project Modules #
###############################################################
import component_template
import solenoid as SDR_solenoid
import ball_valve as SDR_ball_valve
import orifice as SDR_orifice
import pressure_sensor as SDR_pressure_sensor
import temp_sensor as SDR_temp_sensor
import tank as SDR_tank
import pipe as SDR_pipe
import nozzle as SDR_nozzle
import main
from plumbing import component_template
from plumbing import solenoid as SDR_solenoid
from plumbing import ball_valve as SDR_ball_valve
from plumbing import orifice as SDR_orifice
from plumbing import pressure_sensor as SDR_pressure_sensor
from plumbing import temp_sensor as SDR_temp_sensor
from plumbing import tank as SDR_tank
from plumbing import pipe as SDR_pipe
from plumbing import nozzle as SDR_nozzle


###############################################################
Expand Down
12 changes: 12 additions & 0 deletions liquid_engine_state.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider something like a strenum for this so you're not doing string comparisons as much:

from enum import StrEnum, auto

class HttpMethod(StrEnum):
    GET = auto()     # Value is "get"
    POST = "POST"    # Value is "POST"
    DELETE = auto()  # Value is "delete"

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Liquid_Engine_State:
"""Holds and transitions the current engine state string."""

def __init__( self ):
self.state = "Initialization State"

def get_engine_state( self ):
return self.state

def set_engine_state( self, new_engine_state ):
self.state = new_engine_state
## Liquid_Engine_State ##
Loading