diff --git a/server/brewpi.py b/server/brewpi.py index 32f36e4..30ef0d2 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -131,7 +131,9 @@ if __name__ == '__main__': # Records every Sud run's measured data and forecast to their own # JSON files under logs/, for later offline analysis - see # tasks/sud_log.py. - taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK)) + sud_log_task = SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK, config=config) + sud_task.set_on_plant_params(sud_log_task.log_plant_params) + taskmgr.add(sud_log_task) # Continuous server-session log - records from startup to shutdown # regardless of Sud state; written to logs/log_{date_time}.json on exit. diff --git a/tasks/sud.py b/tasks/sud.py index 6c86e64..38083cc 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -111,8 +111,14 @@ class SudTask(ATask): # when the current step has temperature=None (inherits from a prior step). self._paused_temp_soll = None self._on_end = None + self._on_plant_params = None msg_handler.set_recv_handler(self.recv) + def set_on_plant_params(self, callback): + """Register a callback invoked whenever apply_plant_params() fires, + so external observers (e.g. SudLogTask) can record each change.""" + self._on_plant_params = callback + def apply_plant_params(self, grain_mass, water_mass): """Keeps the real plant's and the controller's internal model's plant params in sync with this Sud's own doc - L/Td come straight @@ -129,6 +135,8 @@ class SudTask(ATask): params = self.sud.derive_plant_params(grain_mass, water_mass) self.pot.set_plant_params(params) self.tc.set_model_plant_params(params) + if self._on_plant_params: + self._on_plant_params(self.sud.elapsed, params) def apply_stirrer(self, phase): stirrer_cfg = phase.get('stirrer', {}) diff --git a/tasks/sud_log.py b/tasks/sud_log.py index cc76ae2..40ddf27 100644 --- a/tasks/sud_log.py +++ b/tasks/sud_log.py @@ -43,7 +43,7 @@ class SudLogTask(ATask): and the real controller keeps actively driving toward its target throughout WAIT_USER too).""" - def __init__(self, sud: Sud, tc: APid, heater: AHeater, heater_task, sud_task, interval, path='./logs'): + def __init__(self, sud: Sud, tc: APid, heater: AHeater, heater_task, sud_task, interval, path='./logs', config=None): ATask.__init__(self, interval) self.sud = sud self.tc = tc @@ -51,8 +51,12 @@ class SudLogTask(ATask): self.heater_task = heater_task self.sud_task = sud_task self.path = path + self.config = config # None while no run is in progress - see on_sud_state_changed(). self._samples = None + # Plant param changes during a run: [{t, params}, ...] - recorded + # whenever apply_plant_params() fires (once per step transition). + self._param_events = None # Identifies this run's pair of files (log__.json # and forecast__.json) - fixed at the moment the run # starts, not when it's written out, so the name reflects when @@ -67,21 +71,37 @@ class SudLogTask(ATask): was_running = self._samples is not None if self._is_running(state) and not was_running: self._samples = [] + self._param_events = [] self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime()) elif not self._is_running(state) and was_running: self._write() self._samples = None + self._param_events = None self._run_id = None + def log_plant_params(self, elapsed, params): + """Called by SudTask.apply_plant_params() on every step transition + so the log captures which plant params were active at each point + in the run.""" + if self._param_events is not None: + self._param_events.append({'t': elapsed, 'params': params}) + def _write(self): if not self._samples: return os.makedirs(self.path, exist_ok=True) name_part = _safe_filename_part(self.sud.name) + log_data = {'Name': self.sud.name} + if self.config is not None: + log_data['Config'] = self.config + if self._param_events: + log_data['PlantParams'] = self._param_events + log_data['Samples'] = self._samples + samples_filename = "log_{}_{}.json".format(self._run_id, name_part) with open(os.path.join(self.path, samples_filename), "w") as f: - json.dump({'Name': self.sud.name, 'Samples': self._samples}, f, indent='\t') + json.dump(log_data, f, indent='\t') print("Sud log: wrote {} samples to {}".format(len(self._samples), samples_filename)) forecast_filename = "forecast_{}_{}.json".format(self._run_id, name_part)