Files
brewpi/tasks/sud_log.py
jensandClaude Sonnet 5 a495e758fd feat: always write a fixed-name copy of server/sud logs, use .json for sud logs
Alongside the dated log files, ServerLogTask/SudLogTask now also write
(and overwrite) a fixed-name copy on every write() - logs/log_latest.json
and logs/log_latest_sud.json - so a dashboard/tail-style consumer can
point at one unchanging path instead of tracking the current run's
timestamped filename.

Also switches SudLogTask's own dated/latest filenames from .log to .json,
matching what they've always actually contained. This incidentally fixes
utils/analyze_log.py's two-argument CLI form, which already assumed a
.json extension for sud logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDeoTKBDkXRhPfnyDEoBt
2026-07-01 20:23:16 +02:00

87 lines
3.1 KiB
Python

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}.json. 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, log_interval=None, sim_warp_factor=None):
ServerLogTask.__init__(self, tc, heater, heater_task, interval, path, config, dt, log_interval, sim_warp_factor)
self.sud = sud
self._active = False
self._doc = None
self._reanchors = []
def _should_sample(self):
return self._active
def _filename(self):
name_part = _safe_filename_part(self.sud.name or 'Sud')
return 'log_{}_{}.json'.format(self._run_id, name_part)
def _latest_filename(self):
# Fixed regardless of which schedule is running, unlike _filename()
# above - a dashboard/tail-style consumer can point at one
# unchanging path across different Suds, not just different runs
# of the same one.
return 'log_latest_sud.json'
def _log_name(self):
return self.sud.name
def _elapsed(self):
# The Sud's own tick-counted ground truth (see components/sud.py's
# Sud.elapsed) rather than the base class's own tick counter -
# already resets to 0 on every fresh Play (Sud.start()), so samples
# line up with SudForecastEstimator's t=0 the same way Sud.elapsed
# does everywhere else it's consumed (e.g. SudTask's forecast
# re-anchoring).
return self.sud.elapsed
def log_reanchor(self, elapsed, index, theta_ist):
# One entry per real _reanchor_forecast() trigger (see SudTask.
# set_on_reanchor()) - lets utils/analyze_log.py replay the exact
# same splice-per-transition forecast correction a live client
# would have seen, instead of just the single, uncorrected
# cold-start estimate() run - see build_forecast() there.
self._reanchors.append({'t': elapsed, 'index': index, 'theta_ist': theta_ist})
def _extra_log_data(self):
# The raw sud.json doc this run was playing, exactly as loaded (see
# Sud.save()) - lets utils/analyze_log.py re-simulate this run's own
# SudForecastEstimator forecast for comparison against Samples,
# without depending on the sude/*.json file it came from still
# existing or being unchanged.
data = {}
if self._doc is not None:
data['Doc'] = self._doc
if self._reanchors:
data['ForecastAnchors'] = self._reanchors
return data
def start_run(self):
if self._active:
return
self._samples = []
self._last_write = time.monotonic()
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
self._doc = self.sud.save()
self._active = True
def stop_run(self):
if not self._active:
return
self._active = False
self.write()
self._param_events = []
self._reanchors = []