PhloemDocs

Acceptance recipes

Use a recipe to pass or fail a unit. It runs on a setup, steps it through a profile, takes measurements and records a verdict per bound. Each time you run it is a run.

Setup, recipe, run

What it isHolds
setup(...)your test setup: the document you Deploychannels, stop conditions, interlocks, safe values, the page
recipe(...)a procedure that runs on a setupsteps, measurements, bounds
runone execution of a recipemeasurements, verdicts, the event log. Recorded, never edited

A file with neither setup(...) nor recipe(...) first is a notebook.

Safety lives in the setup. A recipe can't declare an interlock or a stop condition, so it can't weaken its setup.

Write the setup first and Deploy it. The recipe names it in setup= and reads only channels the setup declares. If that setup isn't deployed, Start run offers to deploy it. One setup runs at a time; a recipe for a different setup offers to replace the deployed one and start.

Replacing a setup stops the running one first:

  1. A confirmation shows what will move, before you agree.
  2. Every output it controls goes to its safe value, as with Stop.
  3. The new setup deploys once Computer confirms those safe values.

If Computer never confirms, nothing is replaced. The old setup stays stopped, sending safe values, and the message names the outputs to check before you press Reset. A new revision of the same setup that keeps all its outputs stops nothing.

A recipe

recipe("unit_accept", setup="load_test", unit="serial")

ch = channels()
output = ch["/dut/output_v"]
temp   = ch["/dut/temp_c"]

# Bounds — each declares WHAT is judged, then HOW.
output_v = value("output_at_load", unit="V")
require(output_v.within(11.8, 12.2))

warm = value("temp_after_load", unit="degC")
require(warm.below(60))

# Steps — actions first, measurements after.
step("load",
     set(ch["/dut/enable"], True),
     set(ch["/dut/load_a"], 2.0),
     hold("30s"))

step("verify",
     measure(output_v, output),
     measure(warm, temp))

step("stow", set(ch["/dut/load_a"], 0.0), set(ch["/dut/enable"], False))

plan(steps=["load", "verify", "stow"])

page("Unit acceptance",
     layout=col(row(readout(output), readout(temp)),
                row(verifications("Bounds"), col(alarms(), log()))))
recipe(name, setup=, unit=)the header. setup= names the setup it runs on; unit= names what identifies the unit under test ("serial")
ch["/path"]a channel the setup declares, by its path on Live
value(name, unit=)a named measurement the run records
require(v.below(x))the bound that judges it: above, below, within(lo, hi), outside(lo, hi)
step(name, ...actions)one ordered step: set, ramp, wait, hold, wait_until, confirm, exec, then measure
measure(v, channel)capture the channel's reading into the value, during that step
plan(steps=[...])the order the steps run in
page(title, layout=)the run's live panel: a readout per measured channel, verifications() for the bounds as they are judged, alarms(), log(). Without one, the panel is empty

Actions

ActionDoes
set(ch["/out/enable"], True)command an output the setup declared with ch.write
ramp(ch["/x"], from_=0, to=100, over="20s")move an output smoothly. from_ is required: a ramp never guesses where it starts
wait("30s")let time pass. Nothing is checked meanwhile
hold("2m")stay at the current commands. Recorded as part of the test, not dead time
wait_until(lambda: ch["/p"] > 900, timeout="30s")wait for a condition. timeout= is required; if it expires, the step faults
confirm("Complete the manual action", timeout="10m")a sign-off gate; the run keeps the signature. hold=True waits with no limit
exec("python3 read.py --source running")run a script and wait (see Run your own scripts)

Durations are strings: "250ms", "30s", "2m".

Verdicts

A bound judges a measurement, never the setup. A failed bound fails that measurement, and the run reports it. It does not stop the test; that is the setup's interlock.

The run saysMeaningIs the unit scrap?
passevery bound heldno
faila measurement was outside its boundyes
unavailablea measurement couldn't be taken: its channel published nothingno, but not a pass either
passed_config_pendingonly a persists=True bound failed. The unit is fine; a change didn't survive its save or power cycleno. Redo the save

value(..., persists=True) marks a bound that checks whether a change survived, not whether the unit is good. Usually you put two bounds on one quantity: one read live, one read after a power cycle.

Change, then measure, in separate steps

A step that acts (exec, set, ramp) measures nothing; it either completes or is interrupted. The verdict comes from a later step that reads the unit independently.

recipe("unit_configure_and_verify", setup="load_test", unit="serial")

ch = channels()
setting = ch["/dut/setting"]
drift   = ch["/dut/config_diff_lines"]

applied = value("setting_applied")
require(applied.within(41.5, 42.5))

clean = value("config_drift", unit="lines")
require(clean.below(0.5))

kept = value("config_drift_after_restart", unit="lines", persists=True)
require(kept.below(0.5))

# The change. Nothing is measured here.
step("configure", exec("python3 configure.py --setting 42"))

# An independent read of what is live now, and the verdict.
step("verify",
     exec("python3 read.py --source running"),
     measure(applied, setting),
     measure(clean, drift))

# Did it survive: someone restarts the unit, then the read repeats.
step("restart",
     confirm("Restart the unit, then continue", hold=True),
     wait("30s"),
     exec("python3 read.py --source startup"),
     measure(kept, drift))

plan(steps=["configure", "verify", "restart"])
Note

A bound on a channel nothing publishes still compiles, but every run reports unavailable. Name only channels the setup declares and that something is sending. Check the list on Live.

Updated