Tighten Textual dashboard controls and panel layout

This commit is contained in:
Codex Agent
2025-11-28 18:28:12 +01:00
parent d626007fae
commit 7ffb2a8ce0

View File

@@ -9,8 +9,8 @@ from pathlib import Path
from typing import Optional from typing import Optional
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Horizontal, VerticalScroll from textual.containers import Grid, Horizontal, HorizontalScroll, Vertical, VerticalScroll
from textual.widgets import Static, Header, Footer from textual.widgets import Button, Footer, Header, Static
from textual.timer import Timer from textual.timer import Timer
from . import constants from . import constants
@@ -33,8 +33,12 @@ class TextualDashboard(App):
CSS = """ CSS = """
Screen { layout: vertical; } Screen { layout: vertical; }
.col { width: 1fr; } #controls { padding: 0 0; height: auto; }
.panel { padding: 0 1; border: solid gray; } #controls .row { height: auto; }
#controls Button { min-width: 8; padding: 0 1; }
.columns { height: 1fr; }
.col { width: 1fr; height: 1fr; }
.panel { padding: 0 1; border: round $primary; }
""" """
BINDINGS = [ BINDINGS = [
@@ -61,6 +65,7 @@ class TextualDashboard(App):
("c", "toggle_consumer", "Consumer"), ("c", "toggle_consumer", "Consumer"),
("a", "toggle_auto_rods", "Auto rods"), ("a", "toggle_auto_rods", "Auto rods"),
("f12", "snapshot", "Snapshot"), ("f12", "snapshot", "Snapshot"),
# Maintenance is mouse-only here; keyboard remains in curses.
] ]
timestep: float = 1.0 timestep: float = 1.0
@@ -104,27 +109,30 @@ class TextualDashboard(App):
self.help_panel = Static(classes="panel") self.help_panel = Static(classes="panel")
self.log_panel = Static(classes="panel") self.log_panel = Static(classes="panel")
self.status_panel = Static(classes="panel") self.status_panel = Static(classes="panel")
self.controls_panel: Grid | None = None
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
with Horizontal(): controls = self._build_controls()
yield controls
with Horizontal(classes="columns"):
with VerticalScroll(classes="col"): with VerticalScroll(classes="col"):
yield self.core_panel yield self.core_panel
yield self.trend_panel yield self.trend_panel
yield self.poison_panel yield self.poison_panel
yield self.primary_panel yield self.primary_panel
yield self.secondary_panel yield self.secondary_panel
yield self.power_panel
yield self.generator_panel
yield self.status_panel
with VerticalScroll(classes="col"): with VerticalScroll(classes="col"):
yield self.turbine_panel yield self.turbine_panel
yield self.generator_panel
yield self.power_panel
yield self.hx_panel yield self.hx_panel
yield self.protection_panel yield self.protection_panel
yield self.maintenance_panel yield self.maintenance_panel
yield self.health_panel yield self.health_panel
yield self.log_panel yield self.log_panel
yield self.help_panel yield self.help_panel
yield self.status_panel
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:
@@ -206,6 +214,37 @@ class TextualDashboard(App):
def action_toggle_auto_rods(self) -> None: def action_toggle_auto_rods(self) -> None:
self._enqueue(ReactorCommand(rod_manual=not self.reactor.control.manual_control)) self._enqueue(ReactorCommand(rod_manual=not self.reactor.control.manual_control))
# Maintenance (mouse-driven)
def action_maintain_core(self) -> None:
self._enqueue(ReactorCommand.maintain("core"))
def action_maintain_primary_p1(self) -> None:
self._enqueue(ReactorCommand.maintain("primary_pump_1"))
def action_maintain_primary_p2(self) -> None:
self._enqueue(ReactorCommand.maintain("primary_pump_2"))
def action_maintain_secondary_p1(self) -> None:
self._enqueue(ReactorCommand.maintain("secondary_pump_1"))
def action_maintain_secondary_p2(self) -> None:
self._enqueue(ReactorCommand.maintain("secondary_pump_2"))
def action_maintain_turbine_1(self) -> None:
self._enqueue(ReactorCommand.maintain("turbine_1"))
def action_maintain_turbine_2(self) -> None:
self._enqueue(ReactorCommand.maintain("turbine_2"))
def action_maintain_turbine_3(self) -> None:
self._enqueue(ReactorCommand.maintain("turbine_3"))
def action_maintain_generator_1(self) -> None:
self._enqueue(ReactorCommand.maintain("generator_1"))
def action_maintain_generator_2(self) -> None:
self._enqueue(ReactorCommand.maintain("generator_2"))
def action_snapshot(self) -> None: def action_snapshot(self) -> None:
self._save_snapshot(auto=False) self._save_snapshot(auto=False)
@@ -229,40 +268,89 @@ class TextualDashboard(App):
if self.snapshot_at is not None and not self.snapshot_done and self.state.time_elapsed >= self.snapshot_at: if self.snapshot_at is not None and not self.snapshot_done and self.state.time_elapsed >= self.snapshot_at:
self._save_snapshot(auto=True) self._save_snapshot(auto=True)
def _build_controls(self) -> Vertical:
row1 = Horizontal(
Button("SCRAM", id="scram"),
Button("P1", id="p1"),
Button("P2", id="p2"),
Button("S1", id="s1"),
Button("S2", id="s2"),
Button("RelP", id="relief_pri"),
Button("RelS", id="relief_sec"),
Button("Turb", id="turbines"),
Button("T1", id="t1"),
Button("T2", id="t2"),
Button("T3", id="t3"),
Button("G1", id="g1"),
Button("G2", id="g2"),
Button("Grid", id="consumer"),
Button("AutoR", id="auto_rods"),
Button("+Rod", id="rods_plus"),
Button("-Rod", id="rods_minus"),
classes="row",
)
row2 = Horizontal(
Button("D+50", id="demand_up"),
Button("D-50", id="demand_down"),
Button("SP+250", id="sp_up"),
Button("SP-250", id="sp_down"),
Button("Snap", id="snapshot"),
classes="row",
)
row3 = Horizontal(
Button("MCore", id="m_core"),
Button("MP1", id="m_p1"),
Button("MP2", id="m_p2"),
Button("MS1", id="m_s1"),
Button("MS2", id="m_s2"),
Button("MT1", id="m_t1"),
Button("MT2", id="m_t2"),
Button("MT3", id="m_t3"),
Button("MG1", id="m_g1"),
Button("MG2", id="m_g2"),
classes="row",
)
return Vertical(
HorizontalScroll(row1, classes="row"),
HorizontalScroll(row2, classes="row"),
HorizontalScroll(row3, classes="row"),
id="controls",
)
def _render_panels(self) -> None: def _render_panels(self) -> None:
self._trend_history.append((self.state.time_elapsed, self.state.core.fuel_temperature, self.state.core.power_output_mw)) self._trend_history.append((self.state.time_elapsed, self.state.core.fuel_temperature, self.state.core.power_output_mw))
self.core_panel.update( self.core_panel.update(
f"[bold cyan]Core[/bold cyan]\\n" "[bold cyan]Core[/bold cyan]\n"
f"Power {self.state.core.power_output_mw:6.1f} MW (Nom {constants.NORMAL_CORE_POWER_MW:4.0f}/Max {constants.TEST_MAX_POWER_MW:4.0f})\\n" f"Power {self.state.core.power_output_mw:6.1f} MW (Nom {constants.NORMAL_CORE_POWER_MW:4.0f}/Max {constants.TEST_MAX_POWER_MW:4.0f})\n"
f"Fuel {self.state.core.fuel_temperature:6.1f} K Rods {self.reactor.control.rod_fraction:.3f} ({'AUTO' if not self.reactor.control.manual_control else 'MAN'})\\n" f"Fuel {self.state.core.fuel_temperature:6.1f} K Rods {self.reactor.control.rod_fraction:.3f} ({'AUTO' if not self.reactor.control.manual_control else 'MAN'})\n"
f"Setpoint {self.reactor.control.setpoint_mw:5.0f} MW Reactivity {self.state.core.reactivity_margin:+.4f}\\n" f"Setpoint {self.reactor.control.setpoint_mw:5.0f} MW Reactivity {self.state.core.reactivity_margin:+.4f}\n"
f"DNB {self.state.core.dnb_margin:4.2f} Subcool {self.state.core.subcooling_margin:4.1f}K" f"DNB {self.state.core.dnb_margin:4.2f} Subcool {self.state.core.subcooling_margin:4.1f}K"
) )
self.trend_panel.update(self._trend_text()) self.trend_panel.update(self._trend_text())
self.poison_panel.update(self._poison_text()) self.poison_panel.update(self._poison_text())
self.primary_panel.update( self.primary_panel.update(
f"[bold cyan]Primary Loop[/bold cyan]\\n" "[bold cyan]Primary Loop[/bold cyan]\n"
f"Flow {self.state.primary_loop.mass_flow_rate:7.0f}/{self.reactor.primary_pump.nominal_flow * len(self.reactor.primary_pump_units):.0f} kg/s\\n" f"Flow {self.state.primary_loop.mass_flow_rate:7.0f}/{self.reactor.primary_pump.nominal_flow * len(self.reactor.primary_pump_units):.0f} kg/s\n"
f"Level {self.state.primary_loop.level*100:6.1f}%\\n" f"Level {self.state.primary_loop.level*100:6.1f}%\n"
f"Tin {self.state.primary_loop.temperature_in:7.1f} K Tout {self.state.primary_loop.temperature_out:7.1f} K (Target {constants.PRIMARY_OUTLET_TARGET_K:4.0f})\\n" f"Tin {self.state.primary_loop.temperature_in:7.1f} K Tout {self.state.primary_loop.temperature_out:7.1f} K (Target {constants.PRIMARY_OUTLET_TARGET_K:4.0f})\n"
f"P {self.state.primary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa Pressurizer {self.reactor.pressurizer_level*100:6.1f}% @ {constants.PRIMARY_PRESSURIZER_SETPOINT_MPA:4.1f} MPa\\n" f"P {self.state.primary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa Pressurizer {self.reactor.pressurizer_level*100:6.1f}% @ {constants.PRIMARY_PRESSURIZER_SETPOINT_MPA:4.1f} MPa\n"
f"Relief {'OPEN' if self.reactor.primary_relief_open else 'CLOSED'} Pumps {[p.status for p in self.state.primary_pumps]}" f"Relief {'OPEN' if self.reactor.primary_relief_open else 'CLOSED'} Pumps {[p.status for p in self.state.primary_pumps]}"
) )
self.secondary_panel.update( self.secondary_panel.update(
f"[bold cyan]Secondary Loop[/bold cyan]\\n" "[bold cyan]Secondary Loop[/bold cyan]\n"
f"Flow {self.state.secondary_loop.mass_flow_rate:7.0f}/{self.reactor.secondary_pump.nominal_flow * len(self.reactor.secondary_pump_units):.0f} kg/s\\n" f"Flow {self.state.secondary_loop.mass_flow_rate:7.0f}/{self.reactor.secondary_pump.nominal_flow * len(self.reactor.secondary_pump_units):.0f} kg/s\n"
f"Level {self.state.secondary_loop.level*100:6.1f}%\\n" f"Level {self.state.secondary_loop.level*100:6.1f}%\n"
f"Tin {self.state.secondary_loop.temperature_in:7.1f} K Tout {self.state.secondary_loop.temperature_out:7.1f} K (Target {constants.SECONDARY_OUTLET_TARGET_K:4.0f})\\n" f"Tin {self.state.secondary_loop.temperature_in:7.1f} K Tout {self.state.secondary_loop.temperature_out:7.1f} K (Target {constants.SECONDARY_OUTLET_TARGET_K:4.0f})\n"
f"P {self.state.secondary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa q {self.state.secondary_loop.steam_quality:5.2f}/1.00\\n" f"P {self.state.secondary_loop.pressure:5.2f}/{constants.MAX_PRESSURE:4.1f} MPa q {self.state.secondary_loop.steam_quality:5.2f}/1.00\n"
f"Relief {'OPEN' if self.reactor.secondary_relief_open else 'CLOSED'} Pumps {[p.status for p in self.state.secondary_pumps]}" f"Relief {'OPEN' if self.reactor.secondary_relief_open else 'CLOSED'} Pumps {[p.status for p in self.state.secondary_pumps]}"
) )
self.turbine_panel.update(self._turbine_text()) self.turbine_panel.update(self._turbine_text())
self.generator_panel.update(self._generator_text()) self.generator_panel.update(self._generator_text())
self.power_panel.update(self._power_text()) self.power_panel.update(self._power_text())
self.hx_panel.update( self.hx_panel.update(
f"[bold cyan]Heat Exchanger[/bold cyan]\\n" "[bold cyan]Heat Exchanger[/bold cyan]\n"
f"ΔT (pri-sec) {self.state.primary_to_secondary_delta_t:4.0f} K\\n" f"ΔT (pri-sec) {self.state.primary_to_secondary_delta_t:4.0f} K\n"
f"Efficiency {self.state.heat_exchanger_efficiency*100:5.1f}%" f"Efficiency {self.state.heat_exchanger_efficiency*100:5.1f}%"
) )
self.protection_panel.update(self._protection_text()) self.protection_panel.update(self._protection_text())
@@ -272,15 +360,15 @@ class TextualDashboard(App):
self.help_panel.update(self._help_text()) self.help_panel.update(self._help_text())
failures = ", ".join(self.reactor.health_monitor.failure_log) if self.reactor.health_monitor.failure_log else "None" failures = ", ".join(self.reactor.health_monitor.failure_log) if self.reactor.health_monitor.failure_log else "None"
self.status_panel.update( self.status_panel.update(
f"[bold cyan]Status[/bold cyan]\\n" "[bold cyan]Status[/bold cyan]\n"
f"Time {self.state.time_elapsed:6.1f}s\\n" f"Time {self.state.time_elapsed:6.1f}s\n"
f"Consumer {'ON' if (self.reactor.consumer and self.reactor.consumer.online) else 'OFF'} Demand {self.reactor.consumer.demand_mw if self.reactor.consumer else 0.0:5.1f} MW\\n" f"Consumer {'ON' if (self.reactor.consumer and self.reactor.consumer.online) else 'OFF'} Demand {self.reactor.consumer.demand_mw if self.reactor.consumer else 0.0:5.1f} MW\n"
f"Failures: {failures}" f"Failures: {failures}"
) )
def _trend_text(self) -> str: def _trend_text(self) -> str:
if len(self._trend_history) < 2: if len(self._trend_history) < 2:
return "[bold cyan]Trends[/bold cyan]\\nFuel Temp Δ n/a\\nCore Power Δ n/a" return "[bold cyan]Trends[/bold cyan]\nFuel Temp Δ n/a\nCore Power Δ n/a"
start_t, start_temp, start_power = self._trend_history[0] start_t, start_temp, start_power = self._trend_history[0]
end_t, end_temp, end_power = self._trend_history[-1] end_t, end_temp, end_power = self._trend_history[-1]
duration = max(1.0, end_t - start_t) duration = max(1.0, end_t - start_t)
@@ -289,8 +377,8 @@ class TextualDashboard(App):
temp_rate = temp_delta / duration temp_rate = temp_delta / duration
power_rate = power_delta / duration power_rate = power_delta / duration
return ( return (
"[bold cyan]Trends[/bold cyan]\\n" "[bold cyan]Trends[/bold cyan]\n"
f"Fuel Temp Δ {end_temp:7.1f} K (Δ{temp_delta:+6.1f} / {duration:4.0f}s, {temp_rate:+5.2f}/s)\\n" f"Fuel Temp Δ {end_temp:7.1f} K (Δ{temp_delta:+6.1f} / {duration:4.0f}s, {temp_rate:+5.2f}/s)\n"
f"Core Power Δ {end_power:7.1f} MW (Δ{power_delta:+6.1f} / {duration:4.0f}s, {power_rate:+5.2f}/s)" f"Core Power Δ {end_power:7.1f} MW (Δ{power_delta:+6.1f} / {duration:4.0f}s, {power_rate:+5.2f}/s)"
) )
@@ -302,11 +390,11 @@ class TextualDashboard(App):
iodine = inventory.get("I", 0.0) iodine = inventory.get("I", 0.0)
xe_drho = getattr(self.state.core, "reactivity_margin", 0.0) xe_drho = getattr(self.state.core, "reactivity_margin", 0.0)
return ( return (
"[bold cyan]Key Poisons / Emitters[/bold cyan]\\n" "[bold cyan]Key Poisons / Emitters[/bold cyan]\n"
f"Xe (xenon): {xe:9.2e}\\n" f"Xe (xenon): {xe:9.2e}\n"
f"Sm (samarium): {sm:9.2e}\\n" f"Sm (samarium): {sm:9.2e}\n"
f"I (iodine): {iodine:9.2e}\\n" f"I (iodine): {iodine:9.2e}\n"
f"Xe Δρ: {xe_drho:+.4f}\\n" f"Xe Δρ: {xe_drho:+.4f}\n"
f"Neutrons (src): {particles.get('neutrons', 0.0):.2e}" f"Neutrons (src): {particles.get('neutrons', 0.0):.2e}"
) )
@@ -331,7 +419,7 @@ class TextualDashboard(App):
lines.append(f"Electrical {self.state.total_electrical_output():7.1f} MW Load {self._total_load_supplied(self.state):7.1f}/{self._total_load_demand(self.state):7.1f} MW") lines.append(f"Electrical {self.state.total_electrical_output():7.1f} MW Load {self._total_load_supplied(self.state):7.1f}/{self._total_load_demand(self.state):7.1f} MW")
if self.reactor.consumer: if self.reactor.consumer:
lines.append(f"Consumer {'ONLINE' if self.reactor.consumer.online else 'OFF'} Demand {self.reactor.consumer.demand_mw:7.1f} MW") lines.append(f"Consumer {'ONLINE' if self.reactor.consumer.online else 'OFF'} Demand {self.reactor.consumer.demand_mw:7.1f} MW")
return "\\n".join(lines) return "\n".join(lines)
def _generator_text(self) -> str: def _generator_text(self) -> str:
lines = ["[bold cyan]Generators[/bold cyan]"] lines = ["[bold cyan]Generators[/bold cyan]"]
@@ -340,7 +428,7 @@ class TextualDashboard(App):
if g.starting: if g.starting:
status = "START" status = "START"
lines.append(f"Gen{idx+1}: {status:5} {g.power_output_mw:5.1f} MW batt {g.battery_charge*100:5.1f}%") lines.append(f"Gen{idx+1}: {status:5} {g.power_output_mw:5.1f} MW batt {g.battery_charge*100:5.1f}%")
return "\\n".join(lines) return "\n".join(lines)
def _power_text(self) -> str: def _power_text(self) -> str:
draws = getattr(self.state, "aux_draws", {}) or {} draws = getattr(self.state, "aux_draws", {}) or {}
@@ -352,9 +440,9 @@ class TextualDashboard(App):
gen_out = draws.get("generator_output", 0.0) gen_out = draws.get("generator_output", 0.0)
turb_out = draws.get("turbine_output", 0.0) turb_out = draws.get("turbine_output", 0.0)
return ( return (
"[bold cyan]Power Stats[/bold cyan]\\n" "[bold cyan]Power Stats[/bold cyan]\n"
f"Base Aux {base:5.1f} MW Prim Aux {prim:5.1f} MW Sec Aux {sec:5.1f} MW\\n" f"Base Aux {base:5.1f} MW Prim Aux {prim:5.1f} MW Sec Aux {sec:5.1f} MW\n"
f"Aux demand {demand:5.1f} MW supplied {supplied:5.1f} MW\\n" f"Aux demand {demand:5.1f} MW supplied {supplied:5.1f} MW\n"
f"Gen out {gen_out:5.1f} MW Turbine out {turb_out:5.1f} MW" f"Gen out {gen_out:5.1f} MW Turbine out {turb_out:5.1f} MW"
) )
@@ -375,17 +463,17 @@ class TextualDashboard(App):
lines.append(f"DNB margin {self.state.core.dnb_margin:4.2f}") lines.append(f"DNB margin {self.state.core.dnb_margin:4.2f}")
lines.append(f"Subcooling {self.state.core.subcooling_margin:5.1f} K") lines.append(f"Subcooling {self.state.core.subcooling_margin:5.1f} K")
lines.append(f"Reliefs pri={'OPEN' if self.reactor.primary_relief_open else 'CLOSED'} sec={'OPEN' if self.reactor.secondary_relief_open else 'CLOSED'}") lines.append(f"Reliefs pri={'OPEN' if self.reactor.primary_relief_open else 'CLOSED'} sec={'OPEN' if self.reactor.secondary_relief_open else 'CLOSED'}")
return "\\n".join(lines) return "\n".join(lines)
def _maintenance_text(self) -> str: def _maintenance_text(self) -> str:
active = list(self.reactor.maintenance_active) active = list(self.reactor.maintenance_active)
return "[bold cyan]Maintenance[/bold cyan]\\nActive: " + (", ".join(active) if active else "None") return "[bold cyan]Maintenance[/bold cyan]\nActive: " + (", ".join(active) if active else "None")
def _health_text(self) -> str: def _health_text(self) -> str:
lines = ["[bold cyan]Component Health[/bold cyan]"] lines = ["[bold cyan]Component Health[/bold cyan]"]
for name, comp in self.reactor.health_monitor.components.items(): for name, comp in self.reactor.health_monitor.components.items():
lines.append(_bar(name, comp.integrity)) lines.append(_bar(name, comp.integrity))
return "\\n".join(lines) return "\n".join(lines)
def _help_text(self) -> str: def _help_text(self) -> str:
tips = [ tips = [
@@ -395,7 +483,7 @@ class TextualDashboard(App):
"Use m/n/,/. in curses; mapped to j/k etc here.", "Use m/n/,/. in curses; mapped to j/k etc here.",
"F12 saves a snapshot; set FISSION_SNAPSHOT_AT for auto.", "F12 saves a snapshot; set FISSION_SNAPSHOT_AT for auto.",
] ]
return "[bold cyan]Controls & Tips[/bold cyan]\\n" + "\\n".join(f"- {t}" for t in tips) return "[bold cyan]Controls & Tips[/bold cyan]\n" + "\n".join(f"- {t}" for t in tips)
def _log_text(self) -> str: def _log_text(self) -> str:
lines = ["[bold cyan]Logs[/bold cyan]"] lines = ["[bold cyan]Logs[/bold cyan]"]
@@ -403,7 +491,7 @@ class TextualDashboard(App):
lines.append("No recent logs.") lines.append("No recent logs.")
else: else:
lines.extend(list(self.log_buffer)) lines.extend(list(self.log_buffer))
return "\\n".join(lines) return "\n".join(lines)
def _turbine_status_lines(self) -> list[str]: def _turbine_status_lines(self) -> list[str]:
if not self.reactor.turbine_unit_active: if not self.reactor.turbine_unit_active:
@@ -437,14 +525,19 @@ class TextualDashboard(App):
def _save_snapshot(self, auto: bool = False) -> None: def _save_snapshot(self, auto: bool = False) -> None:
try: try:
self.snapshot_path.parent.mkdir(parents=True, exist_ok=True) self.snapshot_path.parent.mkdir(parents=True, exist_ok=True)
self.snapshot_path.write_text("\\n".join(self._snapshot_lines())) self.snapshot_path.write_text("\n".join(self._snapshot_lines()))
self.snapshot_done = True self.snapshot_done = True
LOGGER.info("Saved dashboard snapshot to %s%s", self.snapshot_path, " (auto)" if auto else "") LOGGER.info("Saved dashboard snapshot to %s%s", self.snapshot_path, " (auto)" if auto else "")
except Exception as exc: # pragma: no cover except Exception as exc: # pragma: no cover
LOGGER.error("Failed to save snapshot: %s", exc) LOGGER.error("Failed to save snapshot: %s", exc)
def _install_log_capture(self) -> None: def _install_log_capture(self) -> None:
# Silence existing root handlers to avoid spewing logs over the UI.
root = logging.getLogger()
root.handlers = []
# Capture reactor_sim logs into the on-screen log buffer.
logger = logging.getLogger("reactor_sim") logger = logging.getLogger("reactor_sim")
logger.propagate = False
self._previous_handlers = list(logger.handlers) self._previous_handlers = list(logger.handlers)
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setLevel(logging.INFO) handler.setLevel(logging.INFO)
@@ -465,6 +558,45 @@ class TextualDashboard(App):
if self._log_handler and self._log_handler in logger.handlers: if self._log_handler and self._log_handler in logger.handlers:
logger.removeHandler(self._log_handler) logger.removeHandler(self._log_handler)
def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
mapping = {
"scram": self.action_scram,
"p1": self.action_toggle_primary_p1,
"p2": self.action_toggle_primary_p2,
"s1": self.action_toggle_secondary_p1,
"s2": self.action_toggle_secondary_p2,
"relief_pri": self.action_toggle_primary_relief,
"relief_sec": self.action_toggle_secondary_relief,
"turbines": self.action_toggle_turbine_bank,
"t1": lambda: self.action_toggle_turbine_unit("1"),
"t2": lambda: self.action_toggle_turbine_unit("2"),
"t3": lambda: self.action_toggle_turbine_unit("3"),
"g1": self.action_toggle_generator1,
"g2": self.action_toggle_generator2,
"consumer": self.action_toggle_consumer,
"auto_rods": self.action_toggle_auto_rods,
"rods_plus": self.action_rods_insert,
"rods_minus": self.action_rods_withdraw,
"demand_up": self.action_demand_up,
"demand_down": self.action_demand_down,
"sp_up": self.action_setpoint_up,
"sp_down": self.action_setpoint_down,
"snapshot": lambda: self._save_snapshot(auto=False),
"m_core": self.action_maintain_core,
"m_p1": self.action_maintain_primary_p1,
"m_p2": self.action_maintain_primary_p2,
"m_s1": self.action_maintain_secondary_p1,
"m_s2": self.action_maintain_secondary_p2,
"m_t1": self.action_maintain_turbine_1,
"m_t2": self.action_maintain_turbine_2,
"m_t3": self.action_maintain_turbine_3,
"m_g1": self.action_maintain_generator_1,
"m_g2": self.action_maintain_generator_2,
}
handler = mapping.get(event.button.id or "")
if handler:
handler()
def run_textual_dashboard(reactor: Reactor, start_state: Optional[PlantState], timestep: float, save_path: Optional[str]) -> None: def run_textual_dashboard(reactor: Reactor, start_state: Optional[PlantState], timestep: float, save_path: Optional[str]) -> None:
app = TextualDashboard(reactor, start_state, timestep=timestep, save_path=save_path) app = TextualDashboard(reactor, start_state, timestep=timestep, save_path=save_path)