Skip to main content

Controls — Recipes

FieldDetail
Primary artifact(s)controls/water/recipes/recipes.json, controls/water/recipes/recipes.csv, controls/water/st/delta-recipe.st
SpecRO-SPEC-001 §5.5 ; WIRE-001

Summary

Ten named HMI presets (indices 09RCP_0_*RCP_9_* in tags.csv). Indices 03 mirror the Example Factory-Loaded Presets table in system overview §5.5; indices 47 are JCWS service profiles; indices 89 are operator NV slots (Custom 1 / Custom 2). Dosing rates are illustrative until concentrate bench calibration.

idxiddisplay_nameMP-101MP-102MP-103TDS§5.5
0sca_gold_cupSCA Gold Cup2.43.11.8120yes
1light_roast_high_extLight Roast — High Extraction3.61.52.0100yes
2medium_roast_balancedMedium Roast — Balanced2.02.52.2130yes
3rao_perger_waterRao/Perger Water3.00.01.585yes
4espresso_standardEspresso — Standard JC2.43.11.8120
5espresso_perla_negraEspresso — Perla Negra3.03.82.6135
6matcha_waterMatcha Service Water1.92.22.895
7dw_still_serviceStill Drinking Water1.22.82.0110
8custom_1Custom 1NV
9custom_2Custom 2NV

Barista ppm deltas (RCP_DELTA_PPM_ADJ_*) convert to mL/gal offsets in delta-recipe.st via dimensionless gains kMg / kCa / kNa (placeholders 0.02 / 0.018 / 0.015); dosing.st applies the same factors per pump when computing stroke words.

Test

Import JSON or CSV into CM5 NV / PLC retain array; run controls/water/sim-tests/sim-delta-recipe-propagation.md and sim-recipe-change-mid-fill.md.

Offline validation (from repo root):

python3 - <<'PY'
import csv, json
from pathlib import Path

json_path = Path("controls/water/recipes/recipes.json")
csv_path = Path("controls/water/recipes/recipes.csv")

recipes = json.loads(json_path.read_text(encoding="utf-8"))
assert len(recipes) == 10, len(recipes)

fields = (
"id", "display_name",
"mp101_ml_per_gal", "mp102_ml_per_gal", "mp103_ml_per_gal",
"target_tds_ppm", "tds_tolerance_ppm", "notes",
)
with csv_path.open(encoding="utf-8") as f:
rows = list(csv.DictReader(f))
assert len(rows) == 10
for i, (r, j) in enumerate(zip(rows, recipes)):
assert int(r["idx"]) == i
for k in fields:
got = r[k] if k.startswith("mp") or k.endswith("_ppm") else r[k]
if k.startswith("mp"):
assert float(got) == j[k]
elif k.endswith("_ppm"):
assert int(float(got)) == j[k]
else:
assert got == j[k]
ids = [r["id"] for r in recipes]
assert len(ids) == len(set(ids))
print("OK: 10 presets, JSON/CSV mirror, unique ids")
PY

Changelog

  • 2026-05-24 — Reordered presets: §5.5 factory rows at indices 0–3; JC service profiles 4–7; custom_1 / custom_2 at 8–9. Expanded delta-recipe.st per-mineral mL/gal math and lab-cal placeholder comments.

Open items

  • Lab calibrate all dosing rates and kMg / kCa / kNa gains against Levitt concentrate prep (metering pumps §6)
  • Wire RCP_APPLY_TRIG when HMI commissioning wires Apply button (see hmi/screens.md)

Reviewer sign-off

  • Recipe preset table reviewed against RO-SPEC-001 §5.5 — _______________