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
ValveHAL
port to require the valve parameters. - Modifying the
SimulatorImpl
component to implement the changes made toValveHAL
. - Extending the
ValveController
port to accept valve parameters. - Verifying all the ports and components to identify the changes necessary to the
ValveControllerImpl
component so that it uses its required ports correctly, and implements its provided port. - Fixing the
ValveController
component 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
@represents
attribute.
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
nondet
expression is evaluated totrue
orfalse
, 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
ValveControllerImpl
interface by extending its provided port,ValveHAL
. - Extend the
ValveControllerImpl
component to adopt the changed interface, and callsetParameters(…)
on theValveHAL
port.
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
ValveHAL
module to bring the declaration ofValveParameters
into scope. - Extend the
setup(…)
function interface
declaration so that it has an input parameter, and returns aBool
. - Modify the state machine so that the
setup(…)
transition’s handler returnstrue
if 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.