Controls — Recipes
| Field | Detail |
|---|---|
| Primary artifact(s) | controls/water/recipes/recipes.json, controls/water/recipes/recipes.csv, controls/water/st/delta-recipe.st |
| Spec | RO-SPEC-001 §5.5 ; WIRE-001 — |
Summary
Ten named HMI presets (indices 0–9 → RCP_0_* … RCP_9_* in tags.csv). Indices 0–3 mirror the Example Factory-Loaded Presets table in system overview §5.5; indices 4–7 are JCWS service profiles; indices 8–9 are operator NV slots (Custom 1 / Custom 2). Dosing rates are illustrative until concentrate bench calibration.
| idx | id | display_name | MP-101 | MP-102 | MP-103 | TDS | §5.5 |
|---|---|---|---|---|---|---|---|
| 0 | sca_gold_cup | SCA Gold Cup | 2.4 | 3.1 | 1.8 | 120 | yes |
| 1 | light_roast_high_ext | Light Roast — High Extraction | 3.6 | 1.5 | 2.0 | 100 | yes |
| 2 | medium_roast_balanced | Medium Roast — Balanced | 2.0 | 2.5 | 2.2 | 130 | yes |
| 3 | rao_perger_water | Rao/Perger Water | 3.0 | 0.0 | 1.5 | 85 | yes |
| 4 | espresso_standard | Espresso — Standard JC | 2.4 | 3.1 | 1.8 | 120 | — |
| 5 | espresso_perla_negra | Espresso — Perla Negra | 3.0 | 3.8 | 2.6 | 135 | — |
| 6 | matcha_water | Matcha Service Water | 1.9 | 2.2 | 2.8 | 95 | — |
| 7 | dw_still_service | Still Drinking Water | 1.2 | 2.8 | 2.0 | 110 | — |
| 8 | custom_1 | Custom 1 | — | — | — | — | NV |
| 9 | custom_2 | Custom 2 | — | — | — | — | NV |
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_2at 8–9. Expandeddelta-recipe.stper-mineral mL/gal math and lab-cal placeholder comments.
Open items
- Lab calibrate all dosing rates and
kMg/kCa/kNagains against Levitt concentrate prep (metering pumps §6) - Wire
RCP_APPLY_TRIGwhen HMI commissioning wires Apply button (seehmi/screens.md)
Reviewer sign-off
- Recipe preset table reviewed against RO-SPEC-001 §5.5 — _______________