Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. Inputs, variables and flow

So far every step has taken nothing and returned a fixed value. Real steps take parameters — which channel, which range — and later steps depend on earlier results.

Create Bench/Dmm.cs and Bench/Unit.cs, then restart the executor:

using Anvil.Step;

/// <summary>A multimeter with four input channels.</summary>
public static class Dmm
{
    /// <summary>Measures the voltage on a channel.</summary>
    /// <param name="channel">The input channel, 1 to 4.</param>
    /// <param name="range">The measurement range.</param>
    [Step]
    public static Outcome MeasureVoltage(double channel, string range = "auto") =>
        channel switch
        {
            1 => Outcome.Measured(1.1, "range " + range),
            2 => Outcome.Measured(1.8, "range " + range),
            3 => Outcome.Measured(3.3, "range " + range),
            4 => Outcome.Measured(5.0, "range " + range),
            _ => Outcome.Errored("the multimeter has no such channel"),
        };
}
using Anvil.Step;

/// <summary>The unit's own identity.</summary>
public static class Unit
{
    /// <summary>Reads the serial number from the unit's memory.</summary>
    [Step]
    public static Outcome ReadSerial() =>
        Outcome.Passed("serial read").Output("serial", "SN-0042");
}

Inputs

A method’s parameters are the step’s inputs, named in snake_case like the step. channel has no default, so it is required; range has one, so it is optional. A parameter can be a double, a string, a bool, or a reference (chapter 8); anything else does not compile.

--list shows that signature — dmm/measure_voltage(channel: number, range?: text) — and Anvil checks every sequence against it.

Variables and results

name: data

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  channel: 2
  rail: 0.0
  led_ok: false

main:
  - name: unit/read_serial
    type: action
    module: unit/read_serial
    executor: bench

  - name: dmm/measure_voltage
    type: numeric_limit
    module: dmm/measure_voltage
    executor: bench
    inputs:
      channel: '${locals.channel}'
      range: "10V"
    limit: { comparison: GELE, low: 1.7, high: 1.9 }
    assign:
      rail: result.measured_value

  - name: board/check_led
    type: pass_fail
    module: board/check_led
    executor: bench
    assign:
      led_ok: 'result.status == "pass"'

  - name: rail_in_spec
    type: pass_fail
    condition: 'locals.rail > 1.75 && locals.led_ok'
$ anvil sequences/data.yseq --json data.json 2>/dev/null
=== data: pass ===
  [done] unit/read_serial: serial read
  [pass] dmm/measure_voltage: range 10V
  [pass] board/check_led: 
  [pass] rail_in_spec: condición cumplida
$ cat data.json
{
  "sequence": "data",
  "status": "pass",
  "skipped_steps": 0,
  "total_steps": 4,
  "steps": [
    {
      "name": "unit/read_serial",
      "module": "unit/read_serial",
      "status": "done",
      "phase": "main",
      "message": "serial read",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "comparison": null,
      "units": null,
      "inputs": {},
      "outputs": {
        "serial": "SN-0042"
      }
    },
    {
      "name": "dmm/measure_voltage",
      "module": "dmm/measure_voltage",
      "status": "pass",
      "phase": "main",
      "message": "range 10V",
      "measured_value": 1.8,
      "limit_min": 1.7,
      "limit_max": 1.9,
      "expected_value": null,
      "operator": null,
      "comparison": "GELE",
      "units": null,
      "inputs": {
        "channel": 2.0,
        "range": "10V"
      },
      "outputs": {}
    },
    {
      "name": "board/check_led",
      "module": "board/check_led",
      "status": "pass",
      "phase": "main",
      "message": "",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "comparison": null,
      "units": null,
      "inputs": {},
      "outputs": {}
    },
    {
      "name": "rail_in_spec",
      "module": null,
      "status": "pass",
      "phase": "main",
      "message": "condición cumplida",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "comparison": null,
      "units": null,
      "inputs": {},
      "outputs": {}
    }
  ]
}

What happens there:

  • locals declares the sequence’s variables with their initial values. The type comes from the value: 2 and 0.0 are numbers, false is a boolean, "" would be text.
  • inputs gives a step its parameters. A plain value is sent as it is ("10V"); a value in ${...} is an expression, evaluated before the step runs (${locals.channel} sends 2).
  • assign stores something from the step’s result in a local after its module answers: result.measured_value, or an expression such as result.status == "pass". It runs before the step is judged, so it sees the module’s own status, not the limit’s verdict.
  • A pass_fail step with a condition and no module calls no executor. Anvil evaluates its condition and the step passes or fails on it — the place for a verdict that combines several measurements. With a module as well, the condition can read the module’s result too.

The JSON report (chapter 10) records what each step was given in inputs and what it returned in outputs. That is what makes a result reconstructible later: two runs on different channels produce different reports.

Outputs from a C# step cannot be assigned yet

unit/read_serial returns a named output, serial, and the report above shows it. But reading it into a variable does not work in 0.5.0:

name: serial

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  serial: ""

main:
  - name: unit/read_serial
    type: action
    module: unit/read_serial
    executor: bench
    assign:
      serial: result.outputs.serial
$ anvil sequences/serial.yseq 2>&1 | tail -n 3
1 paso(s) comprobados contra el catálogo de su ejecutor
la secuencia no casa con lo que ofrecen los ejecutores (1 problema(s)):
  - step 'unit/read_serial' (bench): 'assign' reads result.outputs.serial and the step does not return it (it returns: none)

Anvil checks assign against the outputs a step declares in its catalog, and the C# SDK has no way yet to declare them — only the open step of chapter 8 declares one (#77). So the check refuses the sequence before it runs. Until that is fixed, use named outputs from C# for the report only, and pass values between steps through result.measured_value or result.status.

Flow

name: flow

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

file_globals:
  station: "bench-3"

locals:
  channel: 1

main:
  - name: pick_channel
    type: statement
    statement: 'locals.channel = locals.channel + 2'

  - name: dmm/measure_voltage
    type: numeric_limit
    module: dmm/measure_voltage
    executor: bench
    inputs: { channel: '${locals.channel}' }
    limit: { comparison: GELE, low: 3.0, high: 3.6 }

  - name: board/measure_leakage
    type: numeric_limit
    module: board/measure_leakage
    executor: bench
    precondition: 'file_globals.station == "bench-1"'
    limit: { comparison: LE, low: 0.001 }

  - name: board/read_temperature
    type: action
    module: board/read_temperature
    executor: bench
    disable: true
$ anvil sequences/flow.yseq 2>/dev/null
=== flow: pass ===
  [done] pick_channel: statement ok
  [pass] dmm/measure_voltage: range auto
  [skipped] board/measure_leakage: precondición falsa
  [skipped] board/read_temperature: disable
  (2 de 4 pasos saltados)
  • type: statement runs an assignment inside the engine, without calling any executor: locals.channel becomes 3 before the measurement uses it.
  • file_globals are variables visible to every sequence in the file. Steps read them; writing to one is refused when the file loads.
  • precondition is an expression; when it is false the step is skipped without being called. Here the station is not bench-1.
  • disable: true skips a step without evaluating anything.

Look at the verdict: the sequence passed with two steps skipped. A skipped step is neither a pass nor a fail, and it does not fail the sequence. If a measurement must always happen, do not put a precondition on it.

Checking a sequence without running it

A typo in an input name, in sequences/typo.yseq:

name: typo

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: dmm/measure_voltage
    type: pass_fail
    module: dmm/measure_voltage
    executor: bench
    inputs: { chanel: 2 }
$ anvil sequences/typo.yseq --validate
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
$ anvil sequences/typo.yseq --validate --with-executors
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
1 paso(s) comprobados contra el catálogo de su ejecutor
la secuencia no casa con lo que ofrecen los ejecutores (2 problema(s)):
  - step 'dmm/measure_voltage' (bench): it takes no input called 'chanel' (it takes: channel, range)
  - step 'dmm/measure_voltage' (bench): the input 'channel' is required and the sequence does not send it

--validate loads the file and checks it — the schema, the expressions, the subsequences — without starting or connecting to anything, so it runs in CI with no bench. It cannot know what inputs a step takes, so it says the file is valid.

--validate --with-executors also asks each executor for its catalog, and catches both problems: there is no input chanel, and the required channel is missing. A real run makes the same check before its first step.

Do not treat a green --with-executors as a guarantee in 0.5.0: when an executor fails while describing itself, it still passes (#70).