Tighten Textual dashboard controls and panel layout
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user