feat: re-add per-run sud log alongside the continuous server log
SudLogTask reuses ServerLogTask's sample format but only records while
a Sud run is active: starts on Play (fresh start only, not a Pause
resume) and writes logs/log_{date_time}_{sud_name}.log on Stop or
natural completion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
This commit is contained in:
+16
-6
@@ -15,7 +15,7 @@ from components.plant import PlantFactory
|
|||||||
from components.actor import StirrerFactory
|
from components.actor import StirrerFactory
|
||||||
from components.sud import Sud
|
from components.sud import Sud
|
||||||
from components.sud_forecast import SudForecastEstimator
|
from components.sud_forecast import SudForecastEstimator
|
||||||
from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, SudTask, ServerLogTask
|
from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, SudTask, ServerLogTask, SudLogTask
|
||||||
|
|
||||||
import argparse as ap
|
import argparse as ap
|
||||||
|
|
||||||
@@ -133,21 +133,31 @@ if __name__ == '__main__':
|
|||||||
# Continuous server-session log - records from startup to shutdown
|
# Continuous server-session log - records from startup to shutdown
|
||||||
# regardless of Sud state; written to logs/log_{date_time}.json on exit.
|
# regardless of Sud state; written to logs/log_{date_time}.json on exit.
|
||||||
server_log_task = ServerLogTask(tc, heater, heater_task, DT_TASK, args.logdir, config=config, dt=DT)
|
server_log_task = ServerLogTask(tc, heater, heater_task, DT_TASK, args.logdir, config=config, dt=DT)
|
||||||
sud_task.set_on_plant_params(server_log_task.log_plant_params)
|
|
||||||
if startup_params is not None:
|
if startup_params is not None:
|
||||||
server_log_task.log_plant_params(0, startup_params)
|
server_log_task.log_plant_params(0, startup_params)
|
||||||
taskmgr.add(server_log_task)
|
taskmgr.add(server_log_task)
|
||||||
|
|
||||||
|
# Per-run log - only records while a Sud is actually playing; written to
|
||||||
|
# logs/log_{date_time}_{sud_name}.log on Stop (or natural completion).
|
||||||
|
sud_log_task = SudLogTask(tc, heater, heater_task, sud, DT_TASK, args.logdir, config=config, dt=DT)
|
||||||
|
taskmgr.add(sud_log_task)
|
||||||
|
|
||||||
|
sud_task.set_on_plant_params(lambda elapsed, params: (
|
||||||
|
server_log_task.log_plant_params(elapsed, params),
|
||||||
|
sud_log_task.log_plant_params(elapsed, params)))
|
||||||
|
|
||||||
# Assign data flow
|
# Assign data flow
|
||||||
# Assign tc control value to heater
|
# Assign tc control value to heater
|
||||||
tc.set_on_changed("y", heater_task.actor)
|
tc.set_on_changed("y", heater_task.actor)
|
||||||
# HeaterTask owns TC enable: enabled in closed-loop mode, disabled in open-loop.
|
# HeaterTask owns TC enable: enabled in closed-loop mode, disabled in open-loop.
|
||||||
heater_task.set_on_closed_loop_changed(tc.set_enabled)
|
heater_task.set_on_closed_loop_changed(tc.set_enabled)
|
||||||
# Brew start: switch heater to closed-loop so the TC drives it regardless
|
# Brew start: switch heater to closed-loop so the TC drives it regardless
|
||||||
# of which client (browser, PyQt GUI, …) started the brew.
|
# of which client (browser, PyQt GUI, …) started the brew; also (re)starts
|
||||||
sud_task.set_on_start(heater_task.start_closed_loop)
|
# the per-run sud log (a Pause->resume Start is a no-op for the log).
|
||||||
# Brew end (DONE/IDLE): shut heater off and broadcast open-loop/power-0 to clients.
|
sud_task.set_on_start(lambda: (heater_task.start_closed_loop(), sud_log_task.start_run()))
|
||||||
sud_task.set_on_end(heater_task.shutdown)
|
# Brew end (DONE/IDLE): shut heater off, broadcast open-loop/power-0 to
|
||||||
|
# clients, and write out the per-run sud log.
|
||||||
|
sud_task.set_on_end(lambda: (heater_task.shutdown(), sud_log_task.stop_run()))
|
||||||
|
|
||||||
# Assign temp. sensor readings to tc
|
# Assign temp. sensor readings to tc
|
||||||
sensor_task.set_on_changed("temp", ChangedFloat(tc.set_theta_ist, prec=2).set)
|
sensor_task.set_on_changed("temp", ChangedFloat(tc.set_theta_ist, prec=2).set)
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ from tasks.pot import PotTask
|
|||||||
from tasks.tempctrl import TcTask
|
from tasks.tempctrl import TcTask
|
||||||
from tasks.sud import SudTask
|
from tasks.sud import SudTask
|
||||||
from tasks.server_log import ServerLogTask
|
from tasks.server_log import ServerLogTask
|
||||||
|
from tasks.sud_log import SudLogTask
|
||||||
|
|||||||
+22
-12
@@ -34,24 +34,34 @@ class ServerLogTask(ATask):
|
|||||||
|
|
||||||
async def on_process(self):
|
async def on_process(self):
|
||||||
while True:
|
while True:
|
||||||
power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
|
if self._should_sample():
|
||||||
self._samples.append({
|
self._samples.append(self._take_sample())
|
||||||
't': time.monotonic() - self._t0,
|
|
||||||
'timestamp': time.time(),
|
|
||||||
'temp_ist': self.tc.theta_ist,
|
|
||||||
'temp_soll': self.tc.theta_soll_set,
|
|
||||||
'rate_ist': self.tc.heatrate_ist,
|
|
||||||
'rate_soll': self.tc.heatrate_soll,
|
|
||||||
'power_set': power_set,
|
|
||||||
'power_eff': self.heater.power_eff,
|
|
||||||
})
|
|
||||||
await asyncio.sleep(self.interval)
|
await asyncio.sleep(self.interval)
|
||||||
|
|
||||||
|
def _should_sample(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _take_sample(self):
|
||||||
|
power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
|
||||||
|
return {
|
||||||
|
't': time.monotonic() - self._t0,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'temp_ist': self.tc.theta_ist,
|
||||||
|
'temp_soll': self.tc.theta_soll_set,
|
||||||
|
'rate_ist': self.tc.heatrate_ist,
|
||||||
|
'rate_soll': self.tc.heatrate_soll,
|
||||||
|
'power_set': power_set,
|
||||||
|
'power_eff': self.heater.power_eff,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _filename(self):
|
||||||
|
return 'log_{}.json'.format(self._run_id)
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
if not self._samples:
|
if not self._samples:
|
||||||
return
|
return
|
||||||
os.makedirs(self.path, exist_ok=True)
|
os.makedirs(self.path, exist_ok=True)
|
||||||
filename = 'log_{}.json'.format(self._run_id)
|
filename = self._filename()
|
||||||
path = os.path.join(self.path, filename)
|
path = os.path.join(self.path, filename)
|
||||||
log_data = {'Name': ''}
|
log_data = {'Name': ''}
|
||||||
if self.dt is not None:
|
if self.dt is not None:
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import time
|
||||||
|
from tasks.server_log import ServerLogTask
|
||||||
|
from components import APid, AHeater
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_filename_part(text):
|
||||||
|
return "".join(c if c.isalnum() or c in "-_" else "_" for c in text) or "Sud"
|
||||||
|
|
||||||
|
|
||||||
|
class SudLogTask(ServerLogTask):
|
||||||
|
"""Records temp/power samples for a single Sud run - same sample format
|
||||||
|
as ServerLogTask, but only while a run is active: recording starts on
|
||||||
|
Play and is written out on Stop (or natural completion), to
|
||||||
|
logs/log_{date_time}_{sud_name}.log. A Pause/resume doesn't start a new
|
||||||
|
file - only a fresh Play (from IDLE/DONE) does."""
|
||||||
|
|
||||||
|
def __init__(self, tc: APid, heater: AHeater, heater_task, sud, interval, path='./logs', config=None, dt=None):
|
||||||
|
ServerLogTask.__init__(self, tc, heater, heater_task, interval, path, config, dt)
|
||||||
|
self.sud = sud
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
def _should_sample(self):
|
||||||
|
return self._active
|
||||||
|
|
||||||
|
def _filename(self):
|
||||||
|
name_part = _safe_filename_part(self.sud.name or 'Sud')
|
||||||
|
return 'log_{}_{}.log'.format(self._run_id, name_part)
|
||||||
|
|
||||||
|
def start_run(self):
|
||||||
|
if self._active:
|
||||||
|
return
|
||||||
|
self._samples = []
|
||||||
|
self._t0 = time.monotonic()
|
||||||
|
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
||||||
|
self._active = True
|
||||||
|
|
||||||
|
def stop_run(self):
|
||||||
|
if not self._active:
|
||||||
|
return
|
||||||
|
self._active = False
|
||||||
|
self.write()
|
||||||
|
self._param_events = []
|
||||||
Reference in New Issue
Block a user