Sud: server always starts empty; sude/*.json is purely a client concern
Several sude/*.json schedules can exist on disk; only one runs at a time, chosen by a client Load. Sud no longer takes a path at all - it starts empty (EMPTY_SUD) and the server config no longer names a specific schedule file, removing the implicit link to sud_0010.json in particular. Reading/writing sude/*.json is now entirely the client's job (e.g. the GUI's Load/Save file dialogs); the server's Sud only ever holds whatever was last Load()ed, in memory, until replaced or the server restarts. Updates demo_sud.py/demo_sud_save_load.py to construct an empty Sud() and load() a schedule explicitly, matching the new constructor.
This commit is contained in:
@@ -38,9 +38,10 @@ components/ Pluggable building blocks behind factories:
|
||||
actor/ heater and stirrer drivers: simulated, a
|
||||
Hendi induction hob (serial protocol), or a
|
||||
Pololu 1376 stirrer motor controller
|
||||
sud.py optional mash-schedule sequencer that steps
|
||||
through a sude/*.json schedule and drives
|
||||
the temperature controller from it
|
||||
sud.py mash-schedule sequencer; starts out empty,
|
||||
a client loads one of several sude/*.json
|
||||
schedules onto it and it drives the
|
||||
temperature controller from it
|
||||
|
||||
tasks/ Async tasks (one per component) that poll
|
||||
hardware/sim state at a fixed interval and
|
||||
@@ -168,21 +169,25 @@ only needs to specify the fields it overrides — anything it omits is filled
|
||||
in from `default.step`, recursively. A step is a ramp or a hold depending on
|
||||
which of those two keys it specifies; the other is not defaulted in.
|
||||
|
||||
Set the top-level `sud` config key to a path under `sude/` (see
|
||||
`config.json.sim`/`config.json.templ`) to have the server step through it
|
||||
automatically: `components/sud.py`'s `Sud` resolves each raw step against
|
||||
`default.step` and tracks the current (resolved) step, while
|
||||
`tasks/sud.py`'s `SudTask` drives the temperature controller's
|
||||
`theta_soll`/`heatrate_soll` from ramp steps (advancing once `theta_ist`
|
||||
settles close to the target), counts down hold steps' `duration`, and
|
||||
applies each step's `stirrer` block (`interval_time`/`on_ratio` map directly
|
||||
onto the stirrer's cycle time/duty cycle). It exposes progress (current
|
||||
step, remaining hold time, state, any `user_message`) on the `"Sud"`
|
||||
WebSocket channel and accepts `{"Sud": {"Start": true}}` to begin the
|
||||
schedule and `{"Sud": {"Confirm": true}}` to acknowledge a pause and move to
|
||||
the next step. Omit `sud` from the config to run without schedule
|
||||
automation (manual `theta_soll`/`heatrate_soll` control via the GUI, as
|
||||
before).
|
||||
The server always runs a `Sud` (`components/sud.py`), starting out empty (no
|
||||
schedule) - `sude/` can hold several schedule files, and a client loads one
|
||||
of them onto the running `Sud` (`{"Sud": {"Load": <sud.json contents>}}`),
|
||||
kept purely in memory until replaced by another Load or the server
|
||||
restarts; nothing is ever read from or written to `sude/*.json` by the
|
||||
server itself, that's on the client (e.g. the GUI's File menu). `Sud`
|
||||
resolves each raw step against `default.step` and tracks the current
|
||||
(resolved) step, while `tasks/sud.py`'s `SudTask` drives the temperature
|
||||
controller's `theta_soll`/`heatrate_soll` from ramp steps (advancing once
|
||||
`theta_ist` settles close to the target), counts down hold steps'
|
||||
`duration`, and applies each step's `stirrer` block (`interval_time`/
|
||||
`on_ratio` map directly onto the stirrer's cycle time/duty cycle). It
|
||||
exposes progress (current step, remaining hold time, state, any
|
||||
`user_message`) on the `"Sud"` WebSocket channel and accepts
|
||||
`{"Sud": {"Start": true}}` to begin the schedule and
|
||||
`{"Sud": {"Confirm": true}}` to acknowledge a pause and move to the next
|
||||
step. With no schedule loaded (or all its steps run), it just sits idle -
|
||||
manual `theta_soll`/`heatrate_soll` control via the GUI works the same as
|
||||
ever.
|
||||
|
||||
## Logging & analysis
|
||||
|
||||
|
||||
+4
-5
@@ -70,11 +70,10 @@ if __name__ == '__main__':
|
||||
stirrer_task = StirrerTask(stirrer, DT_TASK, dispatcher.msgio_get("Stirrer"))
|
||||
taskmgr.add(stirrer_task)
|
||||
|
||||
# Mash schedule (optional)
|
||||
sud_path = config.get('sud')
|
||||
if sud_path:
|
||||
sud = Sud(sud_path)
|
||||
taskmgr.add(SudTask(sud, tc, stirrer, pot, DT, DT_TASK, dispatcher.msgio_get("Sud")))
|
||||
# Mash schedule - starts out empty; a client loads one of sude/*.json's
|
||||
# several schedules onto it later (see components/sud.py's Sud).
|
||||
sud = Sud()
|
||||
taskmgr.add(SudTask(sud, tc, stirrer, pot, DT, DT_TASK, dispatcher.msgio_get("Sud")))
|
||||
|
||||
# Tracer
|
||||
taskmgr.add(TracerTask(sensor, heater, tc, trace_tc, DT_TASK_TRACER, dispatcher.msgio_get("Tracer")))
|
||||
|
||||
+24
-18
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from enum import Enum
|
||||
from utils.value import AttributeChange
|
||||
|
||||
@@ -46,16 +45,26 @@ def _build_step(number, defaults, raw_step):
|
||||
return step
|
||||
|
||||
|
||||
EMPTY_SUD = {
|
||||
'Name': '',
|
||||
'Description': '',
|
||||
'pot_mass': 0,
|
||||
'pot_material': None,
|
||||
'steps': [],
|
||||
}
|
||||
|
||||
|
||||
class Sud(AttributeChange):
|
||||
def __init__(self, path):
|
||||
def __init__(self):
|
||||
"""Starts out with no schedule loaded - one of potentially several
|
||||
sude/*.json files on disk is brought in later via load(), kept
|
||||
purely in memory (see load()) until replaced or the server
|
||||
restarts."""
|
||||
AttributeChange.__init__(self)
|
||||
self.path = path
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
self._data = data
|
||||
self._data = EMPTY_SUD
|
||||
|
||||
(self.name, self.description, self.schedule,
|
||||
self.pot_mass, self.pot_material) = self._parse_data(data)
|
||||
self.pot_mass, self.pot_material) = self._parse_data(self._data)
|
||||
|
||||
self._paused_from = None
|
||||
self._reset_run_state()
|
||||
@@ -88,20 +97,17 @@ class Sud(AttributeChange):
|
||||
self._paused_from = None
|
||||
|
||||
def save(self):
|
||||
"""Returns the currently running sud.json document, for a client to
|
||||
edit and hand back to load(). Purely in-memory - load() no longer
|
||||
touches disk, so this is just whatever was last loaded (the file
|
||||
given to __init__, or a later load()), not necessarily what's on
|
||||
disk at self.path anymore."""
|
||||
"""Returns the currently running sud.json document (EMPTY_SUD if
|
||||
none has been loaded yet), for a client to edit and hand back to
|
||||
load(). Purely in-memory - never read from or written to disk."""
|
||||
return self._data
|
||||
|
||||
def load(self, data):
|
||||
"""Replaces the running schedule with a new sud.json document (kept
|
||||
in memory only - never written to disk, so the file originally
|
||||
configured via self.path is never touched by this), resetting to
|
||||
IDLE as if freshly loaded. A client wanting to persist a schedule
|
||||
does so explicitly itself, e.g. by writing save()'s result wherever
|
||||
it chooses. Refused while a brew is in progress, or if data is
|
||||
"""Replaces the running schedule with a new sud.json document, kept
|
||||
in memory only (never written to disk - a client wanting to persist
|
||||
a schedule does so explicitly itself, e.g. by writing save()'s
|
||||
result to one of the sude/*.json files), resetting to IDLE as if
|
||||
freshly loaded. Refused while a brew is in progress, or if data is
|
||||
malformed - in either case the current schedule is left untouched."""
|
||||
if self.state not in (SudState.IDLE, SudState.DONE):
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"ambient_temperature": 20,
|
||||
"sud": "sude/sud_0010.json",
|
||||
"Controller" : {
|
||||
"dt": 0.1,
|
||||
"sim_warp_factor": 10.0,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"ambient_temperature": 20,
|
||||
"sud": "sude/sud_0010.json",
|
||||
"Controller" : {
|
||||
"dt": 1,
|
||||
"sim_warp_factor": 10.0,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import numpy as np
|
||||
from matplotlib.pyplot import plot, step, figure, subplot, grid, show, legend, yticks
|
||||
from components.sud import Sud, SudState
|
||||
@@ -33,7 +34,9 @@ if __name__ == '__main__':
|
||||
}
|
||||
}
|
||||
|
||||
sud = Sud("sude/sud_0010.json")
|
||||
sud = Sud()
|
||||
with open("sude/sud_0010.json") as f:
|
||||
sud.load(json.load(f))
|
||||
|
||||
first_step = sud.schedule[0]
|
||||
plant_params = {
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from components.sud import Sud, SudState
|
||||
|
||||
# Console-only walkthrough of Sud.save()/load() (no plot - there's no
|
||||
# numeric data here, just JSON plumbing). Runs against a scratch copy of a
|
||||
# real sud.json so sude/sud_0010.json itself is never touched.
|
||||
# numeric data here, just JSON plumbing). load()/save() are purely
|
||||
# in-memory - Sud itself never reads or writes anything under sude/.
|
||||
|
||||
if __name__ == '__main__':
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "sud.json"
|
||||
shutil.copy("sude/sud_0010.json", path)
|
||||
sud = Sud()
|
||||
print(f"Starts out empty: '{sud.name}', {len(sud.schedule)} steps")
|
||||
|
||||
sud = Sud(str(path))
|
||||
print(f"Loaded '{sud.name}', {len(sud.schedule)} steps")
|
||||
with open("sude/sud_0010.json") as f:
|
||||
doc = json.load(f)
|
||||
assert sud.load(doc) is True
|
||||
print(f"Loaded '{sud.name}', {len(sud.schedule)} steps")
|
||||
|
||||
doc = sud.save()
|
||||
assert doc['Name'] == sud.name
|
||||
print("save() returned the on-disk document")
|
||||
saved = sud.save()
|
||||
assert saved['Name'] == sud.name
|
||||
print("save() returned the running document")
|
||||
|
||||
sud.state = SudState.RAMPING
|
||||
assert sud.load(doc) is False
|
||||
print("load() refused while RAMPING")
|
||||
sud.state = SudState.RAMPING
|
||||
assert sud.load(saved) is False
|
||||
print("load() refused while RAMPING")
|
||||
|
||||
sud.state = SudState.IDLE
|
||||
doc['Name'] = 'Sud-Renamed'
|
||||
doc['steps'][0]['descr'] = 'Edited step'
|
||||
assert sud.load(doc) is True
|
||||
assert sud.name == 'Sud-Renamed'
|
||||
assert sud.schedule[0]['descr'] == 'Edited step'
|
||||
assert sud.state == SudState.IDLE
|
||||
print("load() applied the edit and reset to IDLE")
|
||||
sud.state = SudState.IDLE
|
||||
saved['Name'] = 'Sud-Renamed'
|
||||
saved['steps'][0]['descr'] = 'Edited step'
|
||||
assert sud.load(saved) is True
|
||||
assert sud.name == 'Sud-Renamed'
|
||||
assert sud.schedule[0]['descr'] == 'Edited step'
|
||||
assert sud.state == SudState.IDLE
|
||||
print("load() applied the edit, purely in memory")
|
||||
|
||||
on_disk = json.loads(path.read_text())
|
||||
assert on_disk['Name'] == 'Sud-Renamed'
|
||||
print("load() persisted the edit to disk")
|
||||
with open("sude/sud_0010.json") as f:
|
||||
on_disk = json.load(f)
|
||||
assert on_disk['Name'] != 'Sud-Renamed'
|
||||
print("sude/sud_0010.json on disk is untouched")
|
||||
|
||||
assert sud.load({'Name': 'broken'}) is False
|
||||
assert sud.name == 'Sud-Renamed'
|
||||
print("load() rejected malformed data, left schedule untouched")
|
||||
assert sud.load({'Name': 'broken'}) is False
|
||||
assert sud.name == 'Sud-Renamed'
|
||||
print("load() rejected malformed data, left schedule untouched")
|
||||
|
||||
print("All sud save/load checks passed")
|
||||
|
||||
Reference in New Issue
Block a user