Steps and Input Profiles
A simulation is a use case: it runs the code under test through one situation and checks what it does. Inside it, a test is usually a sequence: reset the controller, drive it into a state, check what it does there, reset, try the next thing. The library schedules that sequence for you, so the step function doesn't need its own state machine of test phases.
- A step is a function run once per cycle for its duration. It returns whether the cycle passed.
- An input profile is a list of steps run one after the other: the sequence of inputs the simulation provides to the code under test.
- A step can also be a nested input profile, run whole. A short input profile, such as "bring the switch to operator present", is written once and reused wherever a test needs that state.
- A step can also be an inputs step: an input document whose inputs are read during the step, with an optional function checking each cycle (see Inputs steps).
Timing
- A step function gets the time since its step started: 0 at its first cycle.
- Each step lasts its duration rounded up to whole cycles of the simulation, and at least one cycle, so a 0 ms step runs exactly one cycle.
- An input profile's duration is the sum of its steps' durations. Create the simulation with that duration.
- The last step of the whole schedule continues to the end of the run, including the final cycle at the total time.
- Nothing is kept between cycles: the running step is found from the simulation time. A nested input profile needs no reset, and the same one can appear any number of times.
Step numbers and the run's result
Running an input profile records the running step's number as the output CppModel.Step. Steps are numbered from 1 across the whole flattened schedule, nested ones included. The graph of CppModel.Step in the workspace shows which step was running at any time.
A failed run's result, which is also the program's exit code, is the number of the first step that failed. So a failing test in CI already tells you where to look.
Running an input profile returns whether the running step passed, but doesn't set CppModel.StepResult itself: your step function sets it from that result, combined with any other checks. Verification rules still fail every step at which they find a violation.
In C
#include <cppmodel/CModel.h>
static uint8_t Reset(CModelSimulation_ts *self, unsigned long t) { /* ... */ return 1; }
static uint8_t PressActivate(CModelSimulation_ts *self, unsigned long t) { /* ... */ return 1; }
static uint8_t ReleaseActivate(CModelSimulation_ts *self, unsigned long t) { /* ... */ return 1; }
static uint8_t OpenDoor(CModelSimulation_ts *self, unsigned long t) { /* ... */ return !present; }
/* Bring the switch to operator present: reused below. */
static const CppModelStep_ts toPresentSteps[] = {
CMODEL_STEP(PressActivate, 10),
CMODEL_STEP(ReleaseActivate, 10),
};
static const CppModelInputProfile_ts toPresent = CMODEL_INPUT_PROFILE(toPresentSteps);
static const CppModelStep_ts testSteps[] = {
CMODEL_STEP(Reset, 100),
CMODEL_STEP_INPUT_PROFILE(toPresent),
CMODEL_STEP(OpenDoor, 100),
CMODEL_STEP(Reset, 100),
CMODEL_STEP_INPUT_PROFILE(toPresent),
CMODEL_STEP(NULL, 500), /* only waits */
};
static const CppModelInputProfile_ts test = CMODEL_INPUT_PROFILE(testSteps);
CMODEL_CYCLIC()
{
uint8_t passed = CppModel_runInputProfile(self, &test, currentTime_ms);
/* inputs and outputs of the code under test */
CppModel_setOutputU8(self, "CppModel.StepResult", passed);
}
CMODEL_SIMULATE("Safety switch", CppModel_inputProfileDuration(&test, 1), 1)
CMODEL_STEP(function, duration_ms)is a step; aNULLfunction only waits.CMODEL_STEP_INPUT_PROFILE(profile)nests an input profile.CppModel_inputProfileDuration(&profile, stepTime_ms)gives the profile's duration for that cycle time.CppModel_stepAt()gives the running step, its local time and its number without running it, andCppModel_inputProfileSteps()gives the number of steps.
In C++
#include <cppmodel/cppmodel.hpp>
class SafetySwitch : public cppmodel::Simulation
{
bool present = false;
bool Reset(unsigned long long t);
bool PressActivate(unsigned long long t);
bool ReleaseActivate(unsigned long long t);
bool OpenDoor(unsigned long long t);
cppmodel::InputProfile toPresent{
{&SafetySwitch::PressActivate, this, 10},
{&SafetySwitch::ReleaseActivate, this, 10},
};
cppmodel::InputProfile test{
{&SafetySwitch::Reset, this, 100},
toPresent,
{&SafetySwitch::OpenDoor, this, 100},
{[this](unsigned long long t) { return !present; }, 100}, // any function or lambda
{nullptr, 500}, // only waits
};
public:
SafetySwitch()
{
name = "Safety switch";
stepSize_ms = 1;
time_ms = test.Duration(stepSize_ms);
}
void RunCyclic(unsigned long long simulationTime_ms) override
{
outputs["CppModel.StepResult"] = RunInputProfile(test, simulationTime_ms);
}
};
A nested input profile is copied into the step, so declare it before the input profiles that use it. InputProfile::Add() appends a step, At() gives the running step's position without running it, and Count() gives the number of steps.
Input profiles work the same way in the Python and Node.js packages.
Inputs steps
An inputs step takes its stimuli from an input document instead of code. The document has the same format as one passed with --inputs, so the same file works both ways, for example a stretch of a field log:
{
"inputs": [
{ "label": "Start", "x": [0, 300], "y": [0, 0] },
{ "label": "GuardClosed", "x": [0, 100, 120, 300], "y": [1, 0, 1, 1] }
]
}
/* C: the document, a check function (or NULL), and a duration (0: the document's length) */
CMODEL_STEP_INPUTS("inputs/guard-flicker.json", GuardFlicker, 0),
// C++: the document, then optionally a check (function, lambda or member) and a duration
cppmodel::InputProfile::Inputs("inputs/guard-flicker.json", &SafetySwitch::GuardFlicker, this),
- Time:
xcounts from the step's start, like the time a step function gets. - Duration: the document's latest
x, unless the step sets its own. - Which inputs: an input in the document reads the document's value during the step, held between points. Outside the step, and for inputs the document doesn't have, the code's fallback (or the plant, in a closed loop) applies as usual.
- Precedence: inputs passed with
--inputsor set in the workspace win over a step's document for the whole run. - The record: the execution records the values the simulation actually read, so a run can be reproduced from its record.
- Parameters: a run's parameters are constant, so a document's parameters are ignored with a warning.
- The document: a file path, relative to the working directory, or the JSON itself when the text starts with
{. To run from any folder, pass an absolute path in from CMake withtarget_compile_definitions(). - Check function: it reads the inputs as usual and returns whether the cycle passed. Without one, the step only applies its inputs, and passes.
- Loading: running the input profile loads its documents at time 0. If your code reads inputs before that, in the init function, call
CppModel_loadInputProfile(self, &test)(C++:LoadInputProfile(test)) there. A document that can't be used fails the run.
Steps, inputs steps or --inputs
- Steps are code: an order of events, stimuli that react to the state of the code under test, or checks of their own.
- Inputs steps put a stretch of recorded or hand-written stimuli into such a sequence, for example a field log replayed after the controller has been brought to a state.
--inputsalone is enough when a test differs only in values for the whole run, such as a setpoint profile, a load or a tuned gain. Each such run of the same build is another use case, and no new code is needed.