Files
brewpi/brewpi/brewpi.py
T
jensandClaude Sonnet 4.6 7e131df4ca Add mash-schedule (Sud) automation: temp/rate sequencing + stirrer switching
brewpi.py had a dead -m/--model CLI arg and sude/*.json schedules were
pure data with no code to drive them, despite the README documenting
them as if they were already wired up.

Add components/sud.py's Sud, which loads a sude/*.json schedule and
steps through its Rasten (ramp -> hold -> optional wait-for-user ->
next rest), and tasks/sud.py's SudTask, which drives the temperature
controller's theta_soll/heatrate_soll from the current rest and
switches the stirrer between continuous (ramping) and intermittent
(holding, via stirrSpeedRast/stirrDutyRast/stirrCycleTime) operation.
Exposes progress on a new "Sud" WebSocket channel and accepts
Start/Confirm commands. Enabled via a new optional top-level "sud"
config key (see config.json.templ/.sim).

"Reached target" is detected via abs(theta_ist - theta_soll_set) <
tolerance rather than tc.state == HOLD, since the latter can still
read HOLD left over from the previous rest for one tick after a new
target is pushed (tc.state only updates on the controller's own,
independently-scheduled process() tick).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:54:42 +02:00

94 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
import asyncio
import json
from utils import ChangedFloat
from ws.message import MessageDispatcher
from ws.server.ws_server_multi_user import WsServerMultiUser
from components.sensor import TempSensorFactory
from components.pid import PidFactory
from components.plant import Pot
from components.actor import HeaterFactory, StirrerFactory
from components.sud import Sud
from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, TracerTask, SudTask
from tracer import Tracer
import argparse as ap
if __name__ == '__main__':
parser = ap.ArgumentParser()
parser.add_argument("-d", "--debug", action="store_true")
parser.add_argument("-c", "--config", default="config.json")
parser.add_argument("-m", "--model", default="model.json")
parser.add_argument("-s", "--sim", action="store_true")
args = parser.parse_args()
config = json.load(open(args.config))
dispatcher = MessageDispatcher()
server = WsServerMultiUser(listener=dispatcher)
taskmgr = TaskManager()
DT = config['Controller']['dt']
DT_TASK = 1.0 / config['Controller']['sim_warp_factor']
DT_TASK_TRACER = DT_TASK / DT
theta_amb = config['ambient_temperature']
# Sensor
sensor = TempSensorFactory.create(config['Controller']['sensor_name'], temp_offset=-0.15, variance=0.01)
sensor_task = TempSensorTask(sensor, DT_TASK, dispatcher.msgio_get("Sensor"))
taskmgr.add(sensor_task)
# Plant
pot_params = config['Plant']
pot = Pot(DT, pot_params, theta_amb)
taskmgr.add(PotTask(pot, DT_TASK, dispatcher.msgio_get("Pot")))
# Heater
heater = HeaterFactory.create(config['Controller']['heater_name'], config['Heater'])
heater.set_on_changed("power_eff", ChangedFloat(pot.set_power, prec=0).set)
heater_task = HeaterTask(heater, DT_TASK, dispatcher.msgio_get("Heater"))
taskmgr.add(heater_task)
# Temperature Controller
tc = PidFactory.create(config['Controller']['pid_type'], DT, config['TempCtrl'], config['Model'], theta_amb=theta_amb)
tc_task = TcTask(tc, DT_TASK, dispatcher.msgio_get("TempCtrl"))
taskmgr.add(tc_task)
tc_trace_vars = []
trace_tc = Tracer(tc, tc_trace_vars, name='tc_trace')
# Stirrer
stirrer = StirrerFactory.create(config['Controller']['stirrer_name'], DT, config['Stirrer'])
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, DT_TASK, dispatcher.msgio_get("Sud")))
# Tracer
taskmgr.add(TracerTask(sensor, heater, tc, trace_tc, DT_TASK_TRACER, dispatcher.msgio_get("Tracer")))
# Assign data flow
# Assign tc control value to heater
tc.set_on_changed("y", heater_task.actor)
# Assign temp. sensor readings to tc
sensor_task.set_on_changed("temp", ChangedFloat(tc.set_theta_ist, prec=2).set)
# Assign heater power set to tc model
heater.set_on_changed("power_set", tc.set_model_power)
# For simulation
if "sim" in config['Controller']['sensor_name']:
pot.set_on_changed("temp", ChangedFloat(sensor.set_fake_temp, prec=3).set)
sensor.set_fake_temp(pot.temp)
# Message dispatcher
h_dispatcher = taskmgr.start()
h_server = server.listen("0.0.0.0", 8765)
asyncio.gather(h_dispatcher, h_server)
server.run_forever()