server: always write log_{date_time}.json for the full server session
Adds ServerLogTask (tasks/server_log.py) which records temp/power samples from startup to shutdown regardless of Sud state, in the same Name/Samples JSON format as SudLogTask so analyze_log.py and replay_sim.py can consume it directly. The file is written on graceful exit (Ctrl-C) from brewpi.py's finally block, just before the heater is closed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -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, SudLogTask
|
from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, SudTask, SudLogTask, ServerLogTask
|
||||||
|
|
||||||
import argparse as ap
|
import argparse as ap
|
||||||
|
|
||||||
@@ -140,6 +140,11 @@ if __name__ == '__main__':
|
|||||||
# tasks/sud_log.py.
|
# tasks/sud_log.py.
|
||||||
taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK))
|
taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK))
|
||||||
|
|
||||||
|
# Continuous server-session log - records from startup to shutdown
|
||||||
|
# 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)
|
||||||
|
taskmgr.add(server_log_task)
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -203,6 +208,7 @@ if __name__ == '__main__':
|
|||||||
task.cancel()
|
task.cancel()
|
||||||
if pending:
|
if pending:
|
||||||
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
server_log_task.write()
|
||||||
# Belt-and-suspenders: explicitly close the heater connection even if
|
# Belt-and-suspenders: explicitly close the heater connection even if
|
||||||
# the task cleanup above failed to reach HeaterHendi.activate(False).
|
# the task cleanup above failed to reach HeaterHendi.activate(False).
|
||||||
heater.close()
|
heater.close()
|
||||||
|
|||||||
@@ -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.sud_log import SudLogTask
|
from tasks.sud_log import SudLogTask
|
||||||
|
from tasks.server_log import ServerLogTask
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from tasks import ATask
|
||||||
|
from components import APid, AHeater
|
||||||
|
|
||||||
|
|
||||||
|
class ServerLogTask(ATask):
|
||||||
|
"""Continuously records temp/power samples from server start to shutdown.
|
||||||
|
|
||||||
|
Writes a single log_{date_time}.json on close() in the same format as
|
||||||
|
SudLogTask (Name/Samples) so analyze_log.py and replay_sim.py can
|
||||||
|
consume it directly. Unlike SudLogTask, which only records during an
|
||||||
|
active Sud run, this log covers the full server session regardless of
|
||||||
|
Sud state — useful for verifying controller and heater behaviour
|
||||||
|
outside of a scheduled brew."""
|
||||||
|
|
||||||
|
def __init__(self, tc: APid, heater: AHeater, heater_task, interval, path='./logs'):
|
||||||
|
ATask.__init__(self, interval)
|
||||||
|
self.tc = tc
|
||||||
|
self.heater = heater
|
||||||
|
self.heater_task = heater_task
|
||||||
|
self.path = path
|
||||||
|
self._samples = []
|
||||||
|
self._t0 = time.monotonic()
|
||||||
|
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
||||||
|
|
||||||
|
async def on_process(self):
|
||||||
|
while True:
|
||||||
|
power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
|
||||||
|
self._samples.append({
|
||||||
|
'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)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
if not self._samples:
|
||||||
|
return
|
||||||
|
os.makedirs(self.path, exist_ok=True)
|
||||||
|
filename = 'log_{}.json'.format(self._run_id)
|
||||||
|
path = os.path.join(self.path, filename)
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump({'Name': '', 'Samples': self._samples}, f, indent='\t')
|
||||||
|
print('Server log: wrote {} samples to {}'.format(len(self._samples), filename))
|
||||||
Reference in New Issue
Block a user