diff --git a/server/brewpi.py b/server/brewpi.py index d63e678..2286449 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -15,7 +15,7 @@ from components.plant import PlantFactory from components.actor import StirrerFactory from components.sud import Sud 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 @@ -140,6 +140,11 @@ if __name__ == '__main__': # tasks/sud_log.py. 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 tc control value to heater tc.set_on_changed("y", heater_task.actor) @@ -203,6 +208,7 @@ if __name__ == '__main__': task.cancel() if pending: loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + server_log_task.write() # Belt-and-suspenders: explicitly close the heater connection even if # the task cleanup above failed to reach HeaterHendi.activate(False). heater.close() diff --git a/tasks/__init__.py b/tasks/__init__.py index c0bf530..281e094 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -6,3 +6,4 @@ from tasks.pot import PotTask from tasks.tempctrl import TcTask from tasks.sud import SudTask from tasks.sud_log import SudLogTask +from tasks.server_log import ServerLogTask diff --git a/tasks/server_log.py b/tasks/server_log.py new file mode 100644 index 0000000..06e160c --- /dev/null +++ b/tasks/server_log.py @@ -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))