Consolidate config.json.sim into config.json.templ

config.json.templ's component names already default to "sim" for every
backend (sensor/heater/stirrer/plant), making the separate .sim template
mostly redundant - it only differed in dt/sim_warp_factor/PID gain tuning,
which .templ now adopts directly. Removes config.json.sim, updates
brewpi.py's CLI args (-m/--logdir replaces the unused -d/--model/--sim
flags), and refreshes the README/TODO references that pointed at the now-
removed file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
2026-06-22 21:27:58 +02:00
co-authored by Claude Sonnet 4.6
parent 1f146dbc0d
commit 4d98dacb49
6 changed files with 41 additions and 68 deletions
+20 -6
View File
@@ -91,8 +91,10 @@ sude/ Mash schedules ("Sud" = brew/wort), each a
per-step stirrer timing, and optional pause
for user confirmation.
config.json.templ Configuration template (real hardware).
config.json.sim Configuration template for simulation mode.
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.
```
### Data flow
@@ -168,10 +170,11 @@ pip install -r client/requirements.txt
## Running
1. Copy a config template to `config.json` next to `brewpi.py` and adjust it.
Use `config.json.sim` to run entirely in simulation (no hardware needed),
or `config.json.templ` as a starting point for real hardware (set the
heater/stirrer serial ports and sensor type).
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.
2. Start the server:
```bash
@@ -311,6 +314,17 @@ re-anchoring itself to match whatever's actually happening would no longer
be a meaningful baseline to compare reality against. It's computed once,
up front, and left alone.
`SudTask._send_forecast()` thins the curve to at most `MAX_FORECAST_POINTS`
(1000) before broadcasting it - a fine enough `dt` over a multi-hour brew
can otherwise produce a single `Forecast` message of several MB, large
enough to exceed the `websockets` library's default 1 MiB `max_size` and
get the connection closed outright (code 1009). `SudTask.forecast_t`/
`forecast_theta` themselves stay at full simulated resolution (used for
the exact-match truncation in `_continue_forecast_after_confirm()` below,
and for `SudLogTask`'s full-fidelity `logs/forecast_*.json` - see "Logging
& analysis") - only the copy actually sent to clients is thinned, which
the GUI's few-hundred-pixel-wide plot can't show the difference from anyway.
The one wrinkle is a step with `user_wait_for_continue`: a human's response
time genuinely can't be forecast. Rather than stall the whole estimate
there, `SudForecastEstimator.estimate()` models it as a zero-delay
+4 -4
View File
@@ -58,10 +58,10 @@ git history for `temp_controller.py`/`temp_controller_smith.py`).
schema/dataclass validation layer at config-load would surface
errors immediately instead of mid-`__init__` — and would have caught
the dead `TempCtrl.Kalman` / `Model.kn` / `Plant.kn` keys that used
to sit unused in `config.json.templ`/`.sim` (since deleted) and the
`Model.gain`/`Plant.gain` keys still doing the same in
`config.json.sim` right now`Pot` dropped `gain` entirely, see
`components/plant/TODO.md`.
to sit unused in `config.json.templ`/`.sim` (since deleted), and the
`Model.gain`/`Plant.gain` keys that did the same in `config.json.sim`
before that file was removed entirely`Pot` dropped `gain`
entirely, see `components/plant/TODO.md`.
- [ ] **`kalman.py` is now dead code in production.** Neither
`temp_controller.py` nor `temp_controller_smith.py` uses `Kalman`
+4 -3
View File
@@ -46,9 +46,10 @@ the current `delay`-line rewrite of `Pot`.
and partially regressed: `Pot` also dropped its `gain` (heater
efficiency) factor entirely — `process()` now applies 100% of
commanded power with no loss, whereas before `params['gain']` could
model e.g. an 80% efficient transfer. `Model.gain`/`Plant.gain` are
now dead keys in `config.json.sim` (already absent from `.templ`)
see `components/pid/TODO.md`'s config-validation item. Still no
model e.g. an 80% efficient transfer. `Model.gain`/`Plant.gain` were
dead keys in `config.json.sim` (already absent from `.templ`) before
that file was removed entirely — see `components/pid/TODO.md`'s
config-validation item. Still no
heater power saturation enforced inside `Pot` itself (handled ad hoc
in demo harnesses), no evaporation/lid heat loss, and no varying
thermal mass `M` as volume changes (e.g. boil-off, adding
-41
View File
@@ -1,41 +0,0 @@
{
"ambient_temperature": 20,
"Controller" : {
"dt": 0.1,
"sim_warp_factor": 10.0,
"stirrer_name": "sim",
"heater_name": "sim",
"sensor_name": "sim",
"plant_name": "sim",
"pid_type": "Smith"
},
"TempCtrl": {
"Hold": {
"kp": 0.25,
"ki": 0.0,
"kd": 0.0,
"kt": 0.0
},
"Heat": {
"kp": 0.05,
"ki": 0.01,
"kd": 0.0,
"kt": 1.5
},
"Cool": {
"kp": 0.05,
"ki": 0.01,
"kd": 0.0,
"kt": 1.5
}
},
"Heater": {
"port": "/dev/ttyUSB0",
"speed": "115200"
},
"Stirrer": {
"port": "/dev/ttyACM0",
"speed": "115200"
}
}
+5 -5
View File
@@ -2,7 +2,7 @@
"ambient_temperature": 20,
"Controller" : {
"dt": 1,
"sim_warp_factor": 10.0,
"sim_warp_factor": 50.0,
"stirrer_name": "sim",
"heater_name": "sim",
"sensor_name": "sim",
@@ -17,14 +17,14 @@
"kt": 0.0
},
"Heat": {
"kp": 0.05,
"ki": 0.01,
"kp": 0.08,
"ki": 0.02,
"kd": 0.0,
"kt": 1.5
},
"Cool": {
"kp": 0.05,
"ki": 0.01,
"kp": 0.08,
"ki": 0.02,
"kd": 0.0,
"kt": 1.5
},
+8 -9
View File
@@ -37,20 +37,19 @@ class Tee:
if __name__ == '__main__':
os.makedirs("logs", exist_ok=True)
log_path = "logs/brewpi.{}.log".format(time.strftime("%Y%m%d%H%M%S", time.localtime()))
parser = ap.ArgumentParser()
parser.add_argument("-c", "--config", default="config.json")
parser.add_argument("-m", "--logdir", default="logs")
args = parser.parse_args()
os.makedirs(args.logdir, exist_ok=True)
log_path = f"{args.logdir}/brewpi.{time.strftime("%Y%m%d%H%M%S", time.localtime())}.log"
log_file = open(log_path, "a", buffering=1)
sys.stdout = Tee(sys.stdout, log_file)
sys.stderr = Tee(sys.stderr, log_file)
print("Logging to {}".format(log_path))
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)