Lesson 3: Accommodating Hardware Changes¶
Let’s suppose that a hardware change is made to the valve hardware interface requiring a one-time setup of valve
parameters, such as hardware port number, before the valve can be moved. Furthermore, let’s assume that the ValveHALBase
component will check the parameters and reject them if they are invalid. This introduces a new requirement:
| R7: | No valve movement is allowed unless and until a correct set of valve parameters has been accepted. Furthermore, once a set of parameters has been accepted, it is not permitted to change them. |
|---|
In this lesson, we will adapt our software to implement the new requirement R7.
We decide that the valve parameters must be supplied to the ValveHALBase by its client, ValveControllerImpl, as a
parameter to its setup(…) function and passed on to the ValveHALBase in order to satisfy R7. In order to
satisfy the requirements R4 and R5, the ValveController has to be able to work with different types of valves,
and must not be dependent on specifics such as the details of the parameters; we just want ValveController to be a
conduit, passing the parameters as an undefined block of data.
Following the Coco workflow, we will implement requirements R4, R5 and R7 by:
- Extending the
ValveHALport to require the valve parameters. - Modifying the
SimulatorImplcomponent to implement the changes made toValveHAL. - Extending the
ValveControllerport to accept valve parameters. - Verifying all the ports and components to identify the changes necessary to the
ValveControllerImplcomponent so that it uses its required ports correctly, and implements its provided port. - Fixing the
ValveControllercomponent according to the verification results.
We will be introducing the following new language features in this lesson:
- Unverifiable external types.
- Function parameter passing.
- Guarded
event transitions. - The
@representsattribute.
Step 1: Extending ValveHAL¶
We start by extending ValveHAL port to implement these requirements as follows:
- Declare a new external type for the valve parameter data.
- Declare a new function to set the valve parameters.
- Extend the state machine so that the parameters are set before the call to move the valve is allowed.
- Indicate whether the parameters are valid or not.
Declaring an External Type for the Valve Parameters¶
The format and semantics of the valve parameters do not need to be known by the clients of the ValveHALBase component,
namely the ValveControllerImpl. The ValveControllerImpl is simply acting as a conduit that feeds the data as an
opaque block from its clients through to the ValveHALBase. In Coco, such data are declared as an
external type in the provided port ValveHAL of ValveHALBase.
Note
An external type declaration allows users to refer to a type that is not defined within a Coco program,
but is defined in the same programming language as the code to be generated from Coco. It enables Coco generated code
to integrate with code not generated by Coco. In our case, we are passing parameters to the C++ ValveHALBase via a Coco
component without any knowledge of their format or semantics.
An external type is completely opaque to Coco and other than type checking, nothing about it can be
verified by Coco. This means an instance of an external type can be declared and passed as a parameter,
and assigned a value of the same type, but because it is opaque to Coco, Coco code cannot use instances of these types
to influence control flow and they cannot appear as an operand in any Boolean or pattern, for example
in a match expression or event source.
In order to be able to generate C++ code later in the tutorial, we need to specify the mapping between this external type and C++ code.
Add the following line to import the C++ language mappings:
import CPP
Add the following external type declaration labelled with the @CPP.mapToType attribute:
The @CPP.mapToType attribute specifies that the external type ValveParameters declared in Coco is
mapped to the C++ type ValveHALTypes::Params specified in the header file called ValveHALTypes.h. In function calls,
it is passed as a const &. This is one of several Coco features for specifying mappings between Coco and C++, which
are explored in more details later on in the tutorial.
Adding a New Function¶
Declare a new function interface in ValveHAL for setting the parameters as follows:
function setParameters(prms : ValveParameters) : Bool
This function takes an instance of ValveParameters, and returns true if they are valid, and
false otherwise.
Extending the State Machine¶
The ValveHALBase software is a simple piece of handwritten code necessary to make the hardware/software interface directly
callable by the code generated from a Coco component. It is important that it is simple because there is no automated
method of ensuring that handwritten code implements its Coco port specification; this has to be checked by manual
review. To simplify this process, it is good practice to ensure that the structure of the Coco port is aligned as
closely as possible to that of the handwritten code.
Therefore, instead of changing the current port state machine into one with several user-defined states,
the new behaviour will be added to Main using a boolean variable to keep track of whether parameters have been
specified or not, and to guard the new event transitions. This should ensure that
the Coco port will closely align to the handwritten ValveHALBase code.
The new requirement is that a valve movement is only allowed after a correct set of valve parameters has been
accepted by ValveHALBase. Furthermore, once a set of parameters has been accepted, it is not allowed to change them.
Declare a new boolean variable in the state machine as follows:
var paramsSet : Bool
By default, boolean variables are initialised to false.
Add the following event transition for the newly declared function setParameters(…) in
Main:
if (!paramsSet) setParameters(_ : ValveParameters) = paramsSet = nondet {
true,
false,
}
This transition is guarded, and is therefore only enabled when the value of paramsSet is false. The function is
called with _ as its argument, which is the wildcard pattern, and means that it matches any value. The
value passed as an argument to this function cannot be referenced in the port state machine. This is required because
the parameter is an unverifiable type.
The body of the event handler for this transition is a block expression with the following effect:
- The
nondetexpression is evaluated totrueorfalse, the choice being made nondeterministically; - The resulting value is assigned to the Boolean variable
paramsSet; - The value assigned, namely the value of the assignment expression, is returned as the value of the
setParameters(…)function call.
Note
This port specifies that the parameters passed to the setParameters(…) function can be accepted or rejected
nondeterministically. This does not mean that the component implementing it must reject the parameters; it simply
states that it is allowed to do so. However, it does mean that any client using this port must be able to handle both
cases.
Modelling nondeterminism in port state machines highlights a fundamental difference between ports and components. Ports abstract away the implementation details of how functions are implemented, and simply specify a partial view of the visible behaviour of a component on a specific boundary. In contrast, a component state machine is not allowed to be nondeterministic, as it has to specify the complete implementation of the component from which code can be generated. In this example, the component would have to specify the logic necessary to determine whether the parameters are valid or not.
Finally, we must amend the event transition for the function move(…) by adding the following guard:
if (paramsSet) move(_ : Cmd) = nil
This transition is now only enabled when paramsSet evaluates to true.
After making these changes, your ValveHAL port should look like the following:
import CPP
port ValveHAL {
enum Cmd {
case Open
case Close
}
@CPP.mapToType("ValveHALTypes::Params", .ConstReference, .User("ValveHALTypes.h"))
external type ValveParameters
function setParameters(prms : ValveParameters) : Bool
function move(cmd : Cmd) : Nil
machine M {
var paramsSet : Bool
// accept or reject valve parameters and remember result
if (!paramsSet) setParameters(_ : ValveParameters) = paramsSet = nondet {
true,
false,
}
// Move valve if valid parameters set
if (paramsSet) move(_ : Cmd) = nil
}
}
The complete Coco for this port can be found in Example_03B/src/ValveHAL.coco.
Step 2: Extending SimulatorImpl¶
We have just added a new function, setParameters(…) to ValveHAL and before going further, we should implement
this in SimulatorImpl as it has to implement ValveHAL as its provided port. To do this, we will modify its state
machine by adding a transition to implement the setParameters(…) function as follows:
machine M {
client.setParameters(_ : ValveHAL.ValveParameters) = true
client.move(_ : ValveHAL.Cmd) = chronoSleep(seconds(10))
}
The SimulatorImpl assumes the parameters are always valid.
The complete Coco for this component is in Example_03B/src/SimulatorImpl.coco.
Verifying your Changes (Example_03B)¶
Run the verification on the modified Coco files in this example. The Coco Platform finds errors in both the SimulatorImpl
and the ValveControllerImpl. Let’s start by analysing the error found in the SimulatorImpl component.
The SimulatorImpl is failing to implement its provided port with the following
counterexample:
Analysis¶
The SimulatorImpl component fails to implement its provided port correctly, because it allows move(…)
to be called before setParameters(…) has been called with valid parameters, which is not allowed by ValveHAL.
In the counterexample above, the move(…) function can be called as the first interaction with the SimulatorImpl.
Modifying the SimulatorImpl¶
Inspecting the ValveHAL port we can see the following:
- When
setParameters(…)has been called and returnstrue, it cannot be called again. - The
move(…)function can only be called after a successful call tosetParameters(…).
To make SimulatorImpl comply with these requirements, we need to modify it to remember when a successful call has been
made to the setParameters(…) function. Before this has occurred, the move(…) function must not be allowed.
After this has occurred, the move(…) function must be allowed, but all further calls on setParameters(…) must
be forbidden.
To achieve this, we are going to use transition guards. Modify the SimulatorImpl state machine
as follows:
The following line declares a variable called parmsSet of type Bool, which is initialised to false by
default:
var parmsSet : Bool
The following line defines a guarded transition for the setParameters(…) function; it is only enabled if parmsSet is
false; its handler always returns true, and sets parmsSet to true:
if (!parmsSet) client.setParameters(_ : ValveHAL.ValveParameters) = parmsSet = true
The following line defines a guarded transition for the move(…) function so that the transition is only enabled when
parmsSet is true:
if (parmsSet) client.move(_ : ValveHAL.Cmd) = chronoSleep(seconds(10))
The complete Coco for this component is in Example_03C/src/SimulatorImpl.coco.
Verifying your Changes (Example_03C)¶
After modifying SimulatorImpl, re-run the verification. All of the assertions for your SimulatorImpl component
should now pass.
However, the Coco Platform finds an error in one of the assertions for the ValveControllerImpl component. ValveHAL
is also a required port of the ValveControllerImpl component, and with the changes we made to it above, the
ValveControllerImpl no longer uses this required port correctly. The Coco Platform gives the following counterexample:
Analysis¶
The assertion fails because the ValveControllerImpl component has not yet been modified to handle the changes
made to its required port hal (which is an instance of the port ValveHAL). In particular, the counterexample
shows how the ValveControllerImpl can call the function move(…) on its required port hal before the setParameters(…)
function has been called with valid parameters, which is not allowed by ValveHAL.
In order to implement its provided port, the ValveControllerImpl component needs to obtain the valve parameters it must
supply on to ValveHALBase. We decide that the ValveControllerImpl client will supply these as a parameter to the
setup(…) call.
We will take the following steps to implement these changes:
- Extend the
ValveControllerImplinterface by extending its provided port,ValveHAL. - Extend the
ValveControllerImplcomponent to adopt the changed interface, and callsetParameters(…)on theValveHALport.
Step 3: Extending ValveHAL¶
The modified ValveHAL port above requires parameters to be set before the first valve movement is allowed. These will
be supplied by the client of the ValveControllerImpl component as part of the initialisation process. This requires
extending the ValveControllerImpl interface, which we achieve by making the following changes to its provided
port ValveController:
- Import the
ValveHALmodule to bring the declaration ofValveParametersinto scope. - Extend the
setup(…)function interfacedeclaration so that it has an input parameter, and returns aBool. - Modify the state machine so that the
setup(…)transition’s handler returnstrueif the parameters are valid, andfalse, otherwise.
Importing ValveHAL Module Declarations¶
Add the following line to bring the declaration of ValveParameters into scope:
import unqualified ValveHAL
Extending the setup(…) Function with Parameters¶
Amend the setup(…) function interface declaration so that it has an input parameter, and returns a
boolean, which reflects whether the parameters are valid or not:
function setup(prms : ValveHAL.ValveParameters) : Bool
Modifying the state machine¶
Modify the state machine to reflect the changes made to the initialisation process. Specifically, in state
Uninitialised, the transition specified for the function setup(_ : ValveHAL.ValveParameters) needs to reflect
the possibility that the parameters might be rejected. The reasons for rejection are unknown by the port, not least
because ValveParameters is an external data type, and therefore opaque to Coco. However, the port can reflect this
behaviour by modelling it as nondeterministic choice between the parameters being accepted as valid or rejected as
invalid.
Modify the transition as follows:
setup(_ : ValveHAL.ValveParameters) = nondet {
@represents("Invalid parameters")
false,
@represents("Valid parameters")
{
setNextState(Operational);
true
},
}
The body of the event handler is now a nondet expression with the following two cases:
false, indicating invalid parameters, in which case the state machine remains in the same state, and
true, indicating valid parameters, and the state machine transitions to the state Operational.
Each nondet case is labelled with a @represents attribute, which annotates these
actions in any verification counterexamples that are generated.
The resulting ValveController port is as follows:
import unqualified ValveHAL
port ValveController {
function setup(prms : ValveHAL.ValveParameters) : Bool
function open() : Nil
function close() : Nil
function isOpen() : Bool
function isClosed() : Bool
function terminate() : Nil
machine M {
enum ValvePosition {
case Open
case Closed
case Unknown
}
var position : ValvePosition = ValvePosition.Unknown
isOpen() = position == ValvePosition.Open
isClosed() = position == ValvePosition.Closed
terminate() = setNextState(Terminated)
state Uninitialised {
setup(_ : ValveHAL.ValveParameters) = nondet {
@represents("Invalid parameters")
false,
@represents("Valid parameters")
{
setNextState(Operational);
true
},
}
}
state Operational {
open() = {
position = ValvePosition.Open;
}
close() = {
position = ValvePosition.Closed;
}
}
}
}
The complete Coco for this port is in Example_04A/src/ValveController.coco.
Step 4: Extending ValveControllerImpl¶
The Coco Platform will highlight the fact that ValveControllerImpl component is no longer well-formed, because the
setup(…) function signature in the ValveController port differs from how it is used in ValveControllerImpl.
The ValveControllerImpl component needs to be modified to work correctly with the changes that we have made to its
provided and required ports.
We will start by editing ValveControllerImpl to make it well-formed again, and then re-verify it to determine
what modifications are needed. Check your changes by re-verifying the component as you make them.
Your new ValveControllerImpl component should look like the following:
import unqualified ValveController
import unqualified ValveHAL
@runtime(.MultiThreaded)
component ValveControllerImpl {
val client : Provided<ValveController>
val hal : Required<ValveHAL>
machine M {
client.isOpen() = false
client.isClosed() = false
client.terminate() = setNextState(Terminated)
state Uninit {
client.setup(vParms : ValveHAL.ValveParameters) = if (hal.setParameters(vParms)) {
setNextState(Unknown);
true
} else false
}
// Valve position unknown until first command received
state Unknown {
client.open() = setNextState(Opened)
client.close() = setNextState(Closed)
}
state Opened {
entry() = hal.move(ValveHAL.Cmd.Open)
client.isOpen() = true
client.open() = {}
client.close() = setNextState(Closed)
}
state Closed {
entry() = hal.move(ValveHAL.Cmd.Close)
client.isClosed() = true
client.open() = setNextState(Opened)
client.close() = {}
}
}
}
The complete Coco for this component is in Example_04A/src/ValveControllerImpl.coco.
Verifying your Changes (Example_04A)¶
All of the updated ports and components verify successfully.