Build the heater/pot/sensor rig as one unit, picked by plant_name alone
heater_name/sensor_name/plant_name were three independent config keys, each picked via its own factory - in practice sim and real hardware are never actually mixed and matched, so this could (nonsensically) disagree, e.g. a real heater paired with a simulated sensor. Add PlantFactory (components/plant/plant_factory.py): plant_name alone now builds the whole rig together. "sim" gets HeaterSim/Pot (the modeled plant, as before)/TempSensorSim. Anything else gets HeaterHendi/PotReal/TempSensor_max31865 - PotReal is a new, deliberately unmodeled Pot for the real, physical kettle (components/plant/pot_real.py): the real temperature comes straight from the real sensor, not from a model, so it just accepts and ignores set_plant_params()/ set_ambient_temperature()/initial() and reports no temperature of its own, satisfying PotTask/SudTask's interface with nothing to actually simulate. heater_name/sensor_name are gone from config.json.templ; server/brewpi.py delegates to HeaterFactory/TempSensorFactory lazily from inside PlantFactory, same as before, so real hardware's spidev/pyserial deps still aren't needed just to import the module.
This commit is contained in:
@@ -92,22 +92,26 @@ sude/ Mash schedules ("Sud" = brew/wort), each a
|
||||
for user confirmation.
|
||||
|
||||
config.json.templ Configuration template - the "sim" component
|
||||
names (sensor/heater/stirrer/plant) already
|
||||
default to simulation; point Heater/Stirrer
|
||||
at real serial ports to switch to hardware.
|
||||
names (stirrer/plant) already default to
|
||||
simulation; point Stirrer/Heater at real
|
||||
serial ports and plant_name at a real
|
||||
backend to switch to hardware.
|
||||
```
|
||||
|
||||
### Data flow
|
||||
|
||||
The server (`server/brewpi.py`) loads `config.json`, builds the configured
|
||||
sensor/heater/stirrer/controller via factories (`*Factory.create(name, ...)`)
|
||||
based on the `Controller` section (`sensor_name`, `heater_name`,
|
||||
`stirrer_name`, `pid_type`, or `"sim"` for any of them), and connects their
|
||||
outputs to each other's inputs via `set_on_changed` callbacks, e.g.:
|
||||
stirrer/controller via factories (`*Factory.create(name, ...)`), and the
|
||||
heater/pot/sensor trio together as one consistent rig via `PlantFactory`
|
||||
(`components/plant/plant_factory.py`) - based on the `Controller` section
|
||||
(`stirrer_name`, `pid_type`, `plant_name`, or `"sim"` for any of them) -
|
||||
and connects their outputs to each other's inputs via `set_on_changed`
|
||||
callbacks, e.g.:
|
||||
|
||||
- sensor temperature → temperature controller's `theta_ist`
|
||||
- temperature controller output `y` → heater power
|
||||
- heater effective power → pot model power (simulation only)
|
||||
- heater effective power → pot model power (simulation only - PotReal, the
|
||||
real, unmodeled kettle, has no model to feed)
|
||||
|
||||
Each component runs inside its own `ATask` at a configurable interval
|
||||
(`Controller.dt`, scaled by `sim_warp_factor`) and pushes state changes onto a
|
||||
@@ -172,9 +176,11 @@ pip install -r client/requirements.txt
|
||||
|
||||
1. Copy `config.json.templ` to `config.json` next to `brewpi.py` - it
|
||||
already runs entirely in simulation (no hardware needed) as-is; switch
|
||||
any of the `sensor_name`/`heater_name`/`stirrer_name`/`plant_name`
|
||||
`"sim"` values to a real backend, and set the corresponding serial
|
||||
port, to use real hardware.
|
||||
`stirrer_name`/`plant_name`'s `"sim"` value(s) to a real backend, and
|
||||
set the corresponding serial port, to use real hardware. `plant_name`
|
||||
alone picks the heater/pot/sensor trio together (see `PlantFactory`) -
|
||||
`"sim"` gets `HeaterSim`/`Pot`/`TempSensorSim`, anything else gets
|
||||
`HeaterHendi`/`PotReal`/`TempSensor_max31865`.
|
||||
2. Start the server:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from components.plant.pot import *
|
||||
from components.plant.pot_real import *
|
||||
from components.plant.plant_factory import *
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from components.actor import HeaterFactory
|
||||
from components.sensor import TempSensorFactory
|
||||
from components.plant.pot import Pot
|
||||
from components.plant.pot_real import PotReal
|
||||
|
||||
|
||||
class PlantFactory:
|
||||
"""Builds the heater/pot/sensor trio together, as one consistent rig,
|
||||
rather than each picked independently via its own *_name config key -
|
||||
sim and real hardware are never actually mixed and matched in
|
||||
practice, so Controller.plant_name alone now decides all three (see
|
||||
server/brewpi.py).
|
||||
|
||||
Delegates the actual construction to HeaterFactory/TempSensorFactory
|
||||
rather than importing HeaterHendi/TempSensor_max31865 directly, so
|
||||
those classes' hardware-only deps (spidev, pyserial) stay lazily
|
||||
imported - only actually needed when plant_name picks the real rig."""
|
||||
|
||||
@staticmethod
|
||||
def create(plant_name, dt, heater_config):
|
||||
if "sim" in plant_name:
|
||||
heater = HeaterFactory.create("sim", heater_config)
|
||||
pot = Pot(dt)
|
||||
sensor = TempSensorFactory.create("sim", temp_offset=-0.15, variance=0.01)
|
||||
return heater, pot, sensor
|
||||
|
||||
heater = HeaterFactory.create("Hendi", heater_config)
|
||||
pot = PotReal()
|
||||
sensor = TempSensorFactory.create("max31865", temp_offset=-0.15)
|
||||
return heater, pot, sensor
|
||||
@@ -0,0 +1,46 @@
|
||||
from components.aplant import APlant
|
||||
|
||||
|
||||
class PotReal(APlant):
|
||||
"""The real kettle - unlike Pot (components/plant/pot.py), this doesn't
|
||||
model the plant's thermal behavior at all: the real kettle does its
|
||||
own physics, and the real temperature comes straight from the real
|
||||
temperature sensor, not from here (see PlantFactory). Exists purely so
|
||||
a real rig still satisfies the same informal interface PotTask/
|
||||
SudTask expect from a Pot - set_plant_params()/set_ambient_temperature()/
|
||||
initial() are accepted and ignored, and get_temperature() has nothing
|
||||
to report."""
|
||||
|
||||
def __init__(self):
|
||||
APlant.__init__(self)
|
||||
self.p_in = 0
|
||||
|
||||
def is_configured(self):
|
||||
return True
|
||||
|
||||
def set_plant_params(self, params):
|
||||
pass
|
||||
|
||||
def set_ambient_temperature(self, theta_amb):
|
||||
pass
|
||||
|
||||
def initial(self, temp):
|
||||
pass
|
||||
|
||||
def activate(self, enable):
|
||||
pass
|
||||
|
||||
def is_activated(self):
|
||||
return True
|
||||
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
def set_power(self, power):
|
||||
self.p_in = power
|
||||
|
||||
def get_power(self):
|
||||
return round(self.p_in, 1)
|
||||
|
||||
def get_temperature(self):
|
||||
return None
|
||||
@@ -4,8 +4,6 @@
|
||||
"dt": 1,
|
||||
"sim_warp_factor": 50.0,
|
||||
"stirrer_name": "sim",
|
||||
"heater_name": "sim",
|
||||
"sensor_name": "sim",
|
||||
"plant_name": "sim",
|
||||
"pid_type": "Smith"
|
||||
},
|
||||
|
||||
+21
-16
@@ -7,10 +7,9 @@ import time
|
||||
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.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
|
||||
@@ -64,24 +63,29 @@ if __name__ == '__main__':
|
||||
# several schedules onto it later (see components/sud.py's Sud).
|
||||
sud = Sud()
|
||||
|
||||
# Sensor
|
||||
sensor = TempSensorFactory.create(config['Controller']['sensor_name'], temp_offset=-0.15, variance=0.01)
|
||||
# Heater/Pot/Sensor - built together as one consistent rig by a single
|
||||
# Controller.plant_name, rather than three independently-named
|
||||
# components that in practice are never actually mixed and matched
|
||||
# (see components/plant/plant_factory.py): "sim" gets HeaterSim/Pot
|
||||
# (the modeled plant)/TempSensorSim; anything else gets HeaterHendi/
|
||||
# PotReal (no model - the real kettle does its own physics)/
|
||||
# TempSensor_max31865.
|
||||
heater, pot, sensor = PlantFactory.create(config['Controller']['plant_name'], DT, config['Heater'])
|
||||
|
||||
sensor_task = TempSensorTask(sensor, DT_TASK, dispatcher.msgio_get("Sensor"))
|
||||
taskmgr.add(sensor_task)
|
||||
|
||||
# Plant - seeded with the not-yet-loaded sud's own (now physically
|
||||
# sane, see Sud.EMPTY_SUD) plant params right away, same as ambient
|
||||
# below, so the simulated Pot already runs - manual heater
|
||||
# power/Sud disabled or not - instead of sitting inert until a real
|
||||
# Sud loads. A real Sud's own doc still overrides these the moment
|
||||
# one is loaded (see tasks/sud.py's SudTask.apply_plant_params()).
|
||||
pot = Pot(DT)
|
||||
# Pot - seeded with the not-yet-loaded sud's own (now physically sane,
|
||||
# see Sud.EMPTY_SUD) plant params right away, same as ambient below, so
|
||||
# a simulated Pot already runs - manual heater power/Sud disabled or
|
||||
# not - instead of sitting inert until a real Sud loads (PotReal just
|
||||
# ignores this - see there). A real Sud's own doc still overrides
|
||||
# these the moment one is loaded (see tasks/sud.py's SudTask.
|
||||
# apply_plant_params()).
|
||||
pot.set_ambient_temperature(theta_amb)
|
||||
pot.set_plant_params(sud.derive_plant_params(sud.grain_mass, sud.water_mass))
|
||||
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)
|
||||
@@ -128,8 +132,9 @@ if __name__ == '__main__':
|
||||
if hasattr(tc, "set_model_power"):
|
||||
heater.set_on_changed("power_set", tc.set_model_power)
|
||||
|
||||
# For simulation
|
||||
if "sim" in config['Controller']['sensor_name']:
|
||||
# For simulation - the sim sensor has no real plant to read, so it
|
||||
# just reports back whatever the modeled Pot's own temperature is.
|
||||
if plant_sim:
|
||||
pot.set_on_changed("temp", ChangedFloat(sensor.set_fake_temp, prec=3).set)
|
||||
sensor.set_fake_temp(pot.temp)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user