Note

You are not reading the most recent version of this documentation. 1.4.7 is the latest version available.

Lesson 1: The Valve Controller Project

This tutorial provides a step-by-step introduction on how to build software in Coco, using the Coco Platform via Eclipse. As a running example, the tutorial uses a simple software component that operates a valve actuator, starting with a simple implementation in the first lesson, and incrementally extending it with more functionality as the tutorial progresses. Although this example is conceptually simple in the sense that a valve is either open or closed, it highlights some of the challenges arising in developing asynchronous systems, and how the Coco Platform supports developers in solving them.

Lesson 1 introduces the software to be developed, in terms of its functional requirements and architecture, and introduces the following topics required to get started:

  • Creating a new Coco project, and new Coco module.
  • Building simple port and component declarations.
  • Formally verifying ports and components, and understanding the results.
  • Generating the corresponding code in C++.

Whilst we recommend that you try to build the examples yourself, you can download the completed files and import them into Eclipse as follows:

  1. Download the example files and unzip them to a directory where you would like to store them.
  2. In Eclipse:
    1. Select File → Open Projects from File System.
    2. In the dialog that opens, click Directory, and select the coco_examples directory where you unzipped the example files.
    3. Ensure that Search for nested projects is ticked.
    4. Ensure that Detect and configure project natures is ticked.
    5. Click on the Finish button.

Warning

On more recent versions of Eclipse, the steps described above will import all the example projects into a top-level folder called coco_examples. Opening the folder reveals all of the individual Coco projects.

On some older versions of Eclipse, all the Coco projects are imported as top-level project folders, and should be used instead of the coco_examples folder and its subfolders, which can be deleted.

Software requirements

The objective of this tutorial is to build a software component that can be used by a control systems application for operating a valve actuator. The component has the following functional requirements:

R1:The valve controller must enable clients to open and close the valve.
R2:The valve controller must enable clients to query the valve position.
R3:The valve controller must be suitable for use by control systems. This means that all function calls on it must be non-blocking, and the valve controller shall always remain responsive to client requests.
R4:The valve controller must support different valve types, including fast action solenoid operated valves, and slower motor driven valves that can take several minutes to complete the movement instruction.
R5:The valve controller must not be dependent on hardware connections, or details of the hardware/software interface.
R6:The valve controller must indicate whether or not the client’s instructions have been successfully carried out.

We will follow an incremental software development approach to build this component, starting with the simple good weather behaviour, and then adding functionality to satisfy the requirements as the tutorial progresses.

Introducing the Coco Workflow

A fundamental building block in Coco is the concept of a component that describes a system or sub-system, and has defined interfaces with its environment. A component interacts with its environment by sending and receiving events across these interfaces via ports, where an event is either a synchronous function call or an asynchronous signal. Further, a component describes how these interactions are implemented in sufficient detail such that the Coco Platform can automatically generate the code, for example in C++. In contrast, a port describes the visible events that a component can perform with other components across a specific boundary. These events are either functions that the component implements or signals that it emits, which other components in the system can call or receive respectively. In addition to defining the interfaces, a port can also specify what sequences of function calls and signals are allowed across a component boundary. This only includes the events that are visible across the port and omitting any implementation details. A port declaration acts as a behavioural specification to the component implementing the port, and those using it.

This tutorial follows the normal workflow for developing software in Coco. The first step is to identify the scope of the software that will be developed using Coco, and define the software architecture in terms of components and ports. This includes identifying all other parts of the system that are outside of the scope of the software being developed, but that nonetheless interact with it. Coco provides an elegant way of modelling these as external components, and capturing the assumptions about the interactions between them and the software being developed. An example of this is provided in this tutorial.

Defining the Architecture

The valve controller project is broken down into three simple software components, as illustrated in the following architecture diagram.

ValveHALBase Application ValveControllerImpl client : ValveController client : ValveHAL valve hal

The provided port of each component is annotated with a label of the form x : X, where X is the port declaration, and x is an instance of X.

The topmost component called Application is a client of the valve controller component, and can therefore make calls on and receive signals from the valve controller. This component is outside the scope of this tutorial, but is included in the architecture to highlight the fact that there would typically be an application layer that uses the valve controller software being developed in this tutorial.

The component called ValveControllerImpl is the one being developed in this tutorial. It implements all of the hardware-independent logic, and provides all of the functions to the application layer. The partitioning of the software into the ValveControllerImpl and ValveHALBase components is designed to meet the requirement R4 and R5 above.

Finally, the component called ValveHALBase implements the hardware abstraction layer, which is responsible for abstracting away all of the hardware/software interface details about how the valve is connected and processes commands. Its implementation is written directly in C++, and is the minimum code needed to provide an interface to the hardware which is callable by a Coco component. We will see how Coco handles code developed in other languages, which interacts with the software being developed in Coco.

The components are connected together via ports as illustrated in the architecture diagram above. For example, the ValveControllerImpl component has a provided port called client (which is an instance of the port ValveController). This port specifies the functions and signals provided by this component to its clients. In contrast, ValveControllerImpl also has a required port called hal, which specifies the functions and signals implemented by the ValveHALBase component, and which ValveControllerImpl uses. These connections are declared in an encapsulating component and automatically established at runtime by the generated code.

Building Ports and Components

The next step is to start developing the ports and components defined in the software architecture above. Using the valve controller component as an example, the recommended steps for building this component are as follows:

  1. Identify the functions and signals that the valve controller component has to implement, and provide to other components in its environment.
  2. Define a port with these function and signal declarations, together with a precise description of how the valve controller component must behave across this port boundary. This port will become the provided port of the valve controller component, which it will have to implement.
  3. Identify the other components in the system that will be used by the valve controller component. In this case, there is only one, namely the HAL component. Follow steps 1 and 2 to define a port for the HAL, which describes the interactions between the HAL and its client (i.e. the valve controller). This port will become the provided port for the HAL, which it has to implement correctly, and the required port for the valve controller.
  4. Build the implementation of the valve controller as a Coco component.
  5. Make small incremental steps, and run the verification frequently. This is referred to as a verification-driven development approach, and supports developers by making full use of the automated verification to find all of the errors for them as they build their software.
  6. When all of the components are implemented and verify correctly, generate the corresponding C++ code.

Step 1: Creating a Coco Project

We start by creating a new project called ValveProject in Eclipse as follows:

  1. Select File → New → Coco Project.

    If this is not available, then select File → New → Other…, click on the Coco dropdown menu, and select Coco Project.

  2. Complete the project dialog as follows:

    ../_images/create_project.png
    1. Enter a project name, for example, ValveProject.
    2. Select Use default location, if not already selected by default.
    3. Select Create separate folders for source files, if not already selected. By default, a folder named src is created and all Coco source files must be created in this folder. All of the tutorial examples use this structure. If you prefer to have all Coco source files in your projects to be in the project folder itself and not in a subfolder, select Use project folder as root for source files instead.
    4. Click on the Finish button.

Step 2: Building a Port for the Valve Controller

A port defines the interfaces implemented by a component on a specific boundary, and is analogous to an abstract class in C++ or an interface in Java. It can also specify the ordering in which all interactions across the port can occur. These interactions are either function calls made on the port by other components in the system, or signals sent by the port to its environment. A port only specifies the interactions that are visible across the component boundary, and omits any implementation details. As a result, the behaviour of a port is often nondeterministic. This tutorial will introduce the different ways that nondeterminism can arise in ports as the lessons progress. Further details about the syntax and semantics of ports can be found in the sections Ports and State Machines of the Coco Language Reference.

This section goes through the steps of building your first simple port for the valve controller in Coco, starting with the creation of a new Coco module.

Creating a new Coco Module

Create a new module called ValveController as follows:

  1. Select the ValveProject project. If you selected Create separate folders for source files when creating the project, the Coco Platform expects all Coco files to be in the src subfolder and you must select this as the place where the new Coco source file will be created.

  2. Select File → New → Coco Module.

  3. Complete the module dialog box as follows:

    ../_images/create_pvalve.png
    1. Enter ValveController as the file name.
    2. Click on the Finish button.

Your new module will appear in the project explorer window under the ValveProject folder. You may need to expand the project folder to see your new Coco module. The newly created Coco file is automatically opened in the editor window.

Declaring a Port

To declare a port for the valve controller, add the following lines to the newly created ValveController module:

port ValveController {
}

This declares a port called ValveController, the body of which will be declared inside the braces.

Note

The Coco editor automatically formats your code as you type. You can request a complete reformat of the file at any time by using the keyboard shortcut Shift+Command+F on macOS, or Shift+Ctrl+F on Windows and Linux. Depending on your Coco preferences, the file is automatically reformatted each time it is saved.

Declaring Function Interfaces

This simple port will start with four function interface declarations to initialise, terminate, open, and close a valve. To declare these new functions, add the following declarations in the body of ValveController:

  function setup() : Nil
  function open() : Nil
  function close() : Nil
  function terminate() : Nil

None of the functions have parameters, and they all return Nil, which is analogous to void in C++.

Specifying Behaviour

In addition to declaring functions and signals, a port can also specify the ordering in which all interactions across the port can occur. This behaviour is specified by declaring a state machine in the port declaration. Port state machines are often nondeterministic as they only specify a partial view of how a component interacts on a given boundary, in terms of the events that are visible across that boundary, and omits any of the implementation details.

A provided port PC of some component C essentially acts as a contractual specification for how C will behave with respect to other components that use it. For example ValveController is the provided port of the ValveControllerImpl component, and will therefore specify how the ValveControllerImpl component will interact with the software application layer that uses it ValveController also acts as a specification for the ValveControllerImpl component, and the Coco Platform will formally verify whether ValveControllerImpl correctly implements ValveController. This will be discussed further in this lesson when all of the ports and components are verified by the Coco Platform.

To add the port behaviour to ValveController, extend your port by adding the following state machine declaration after the function interface declarations:

  machine M {
  }

Note

Every state machine has two built-in event states called Main and Terminated. The state Main is the ancestor of all user-defined states. All declarations in a state machine that are not within a user-defined state are declarations in Main. The state Terminated denotes the termination of a state machine. No declarations can be added to this state, and all incoming events are illegal. This means that a transition to Terminated is the final action of a state machine, and it cannot be restarted from this state. Components have to enter the Terminated state to ensure that they terminate correctly at runtime. Failure to do this can result in runtime errors when programs exit.

Upon startup, the state machine will enter an initial state, which is the first user-defined state in the state machine. If the state machine has no user-defined states, then the initial state is Main. This startup behaviour can be overridden by declaring an entry function in Main that calls the state transition function setNextState passing the desired initial state as the parameter. Examples of this will be illustrated later on in the tutorial.

The example state machine M above is empty, and therefore its initial state is Main. Although it does not have any transitions declared, it is still a valid state machine, and specifies that no functions are allowed to be called of ValveController.

The next step is to declare a new event state, called Uninitialised, as follows:

  machine M {
    state Uninitialised {
      setup() = setNextState(Operational)
    }
  }

Uninitialised is now the initial state of M. It has a single event transition, which says that when setup() is called, the state machine will transition to a state called Operational. The action setNextState(Operational) is a call to the built-in function setNextState(…) passing Operational as its parameter to specify the state to which that the state machine will change completing the transition.

Uninitialised does not declare transitions for any of the other functions in ValveController, which means that a component requiring this port is not permitted to call these other functions while the port is in this state.

Note

For an incoming event (i.e. function or signal) in a port or component state machine, if there is a matching enabled transition, then its handler gets executed. In a port state machine, there can be multiple enabled transitions for a given event, in which case one of them is chosen nondeterministically. If the handler is illegal, then it means that this event is not allowed in the given state. If no such matching enabled transition exists, then the illegal handler is executed by default. For example, the following two ports are equivalent:

port Explicit {
  function start() : Nil
  function stop() : Nil

  machine M {
    start() = {}
    stop() = illegal
  }
}

port Implicit {
  function start() : Nil
  function stop() : Nil

  machine M {
    start() = {}
  }
}

In both example ports above, the function stop() results in the illegal handler being executed. In this case, we say that the function stop() is illegal. The same applies to incoming signals in component state machines. As we will see later on, verification checks whether components and ports agree when function calls and incoming signals are illegal.

The Coco editor will show a static error in your current version of ValveController, because Operational has not been declared yet. This error will disappear once this state has been added as follows:

  machine M {
    state Uninitialised {
      setup() = setNextState(Operational)
    }
    state Operational {
      open() = {}
      close() = {}
    }
  }

In Operational, the functions open() and close() can be repeatedly called, and in both cases the state machine will simply remain in the same state.

Note

The opening and closing movements are specified in ValveController as synchronous actions, namely when the function open() or close() returns to the calling client, the valve movement has finished. Consequently, the client is blocked, and unresponsive while the valve is moving and this specification fails to meet R3.

The final step in completing ValveController is to add the termination behaviour:

  machine M {
    terminate() = setNextState(Terminated)

    state Uninitialised {
      setup() = setNextState(Operational)
    }
    state Operational {
      open() = {}
      close() = {}
    }
  }

The highlighted transition specifies that when the function terminate() is called, the state machine will transit to Terminated. This transition has been declared directly in Main only, which means that it will fire when terminate() is called in any of the user-defined states in Main.

The complete Coco for this port declaration is in Example_01A/src/ValveController.coco.

Note

Coco supports hierarchical state machines. This means that every event state, except for Terminated, can have nested states to any depth, forming a rooted tree of parent-child relationships with Main as the root. For any event, such as the terminate() function, event transitions can be declared in multiple states across the hierarchy. For details on which transitions are executed in this case, see the semantics section in the Coco Language Reference. This topic will also be explored later on in the tutorial.

Step 3: Modelling the Hardware Abstraction Layer

In this tutorial, we use a hardware abstraction layer (HAL) as the interfacing code between the valve controller software being implemented, and the physical hardware itself. The implementation of the HAL falls outside of the scope of what is being developed in Coco, and is implemented directly in C++. However, it interacts with the valve controller being developed in Coco, and is therefore specified as an external component.

Note

An external component in Coco is one that describes some part of the system that is not implemented in Coco, but that nonetheless interacts with the Coco components being developed. They are used to capture the assumptions of how the external environment interacts with Coco components for the purposes of verification, and to support the integration of the generated code into existing code that was developed manually or using other tools.

The first step towards building an external component of the HAL implementation is to specify its provided port, which describes how it interacts with the valve controller software being developed in Coco. For this tutorial, we use a very simple HAL abstraction.

Declaring a HAL Port

Create a new module called ValveHAL, and add the following port declaration:

port ValveHAL {
  enum Cmd {
    case Open
    case Close
  }
}

ValveHAL declares an enum called Cmd with named values Open and Close, which will be used to indicate which way the valve should be moved. It is declared locally within the ValveHAL port, and references to it will have to be qualified by the port name.

Note

Modules, ports and components are all namespaces. It is common to declare entities that form part of a component’s interface within the namespace of its provided port. The enum called Cmd is part of the interface to any component implementing ValveHAL as its provided port.

Add the following function interface declaration:

  function move(cmd : Cmd) : Nil

This function takes a parameter of type Cmd to indicate which way the valve is being requested to move.

Complete ValveHAL by adding the following state machine declaration:

port ValveHAL {
  enum Cmd {
    case Open
    case Close
  }
  function move(cmd : Cmd) : Nil

  machine M {
    move(_ : Cmd) = {}
  }
}

This port is stateless, and allows move(Open) or move(Close) to be repeatedly called by its client to open or close the valve respectively. In this example, its client will be the valve controller software being developed in this tutorial.

Note the following line in the state machine:

    move(_ : Cmd) = {}

ValveHAL does not use the parameter cmd. In order to prevent Coco warning us about this, the parameter name is specified as the wildcard _. The complete Coco for this port declaration is in Example_01A/src/ValveHAL.coco.

Declaring an external component for the HAL

Create a new module called ValveHALBase, and add the following external component declaration:

import unqualified ValveHAL

@runtime(.MultiThreaded)
external component ValveHALBase {
  val client : Provided<ValveHAL>
}

The external component ValveHALBase has a provided port called client, which is an instance of the port ValveHAL declared above. This component will be used when code is generated for the other Coco components being implemented.

Note

Each module has its own namespace, so a single name may be used in several different modules, with a distinct meaning in each. In order to use a name declared in another module, the module must be imported.

A module can either be imported with a different name using the as keyword followed by a new module name, or it can be imported unqualified, using the keyword unqualified, as illustrated in the example of ValveHALBase above. In this case, names declared in the imported module may be used unqualified in the current module.

The complete Coco for this external component declaration is in Example_01A/src/ValveHALBase.coco.

Step 4: Building a Component for the Valve Controller

You should now have the following three modules:

  • ValveController containing the port declaration ValveController;
  • ValveHAL containing the port declaration ValveHAL; and
  • ValveHALBase containing the external component declaration ValveHALBase.

The next step is to build the implementation of the valve controller software in Coco. This will be achieved by declaring an implementaton component, called ValveControllerImpl, which specifies a complete description of the functionality of a component from which runtime code is generated.

Note

An implementaton component describes the complete behaviour of a component in terms of the functions and signals declared on its provided port, and the functions and signals it can call and receive respectively on its required ports. It must implement the functions and signals in accordance with its provided port specification, and may call any of the functions on its required ports. It must also be able to handle all of the signals that it can receive from its required ports. An implementation component is sufficiently detailed that the corresponding runtime code can be automatically generated from it.

As illustrated in the architecture diagram at the start of this lesson, the component ValveControllerImpl has a provided port of type ValveController and a required port of type ValveHAL, both of which have been declared above.

Create a new module called ValveControllerImpl, and add the following lines to import the port declarations for ValveController and ValveHAL from their respective modules:

import unqualified ValveController
import unqualified ValveHAL

Declare a new component called ValveControllerImpl by adding the following lines:

@runtime(.MultiThreaded)
component ValveControllerImpl {
}

A component declaration is analogous to a class in Java or C++.

Add the following field declarations to specify the provided and required port instances:

@runtime(.MultiThreaded)
component ValveControllerImpl {
  val client : Provided<ValveController>
  val hal : Required<ValveHAL>
}

The first field means that this component provides a port instance client of type ValveController to all other components in the system that use this component. The example provided in the architecture diagram at the start of this tutorial is a software application layer represented by the Application component. The second field specifies that the component requires access to some component instance that provides a port of type ValveHAL, and declares a local instance of it called hal.

Add the following state machine declaration to specify the component’s behaviour:

  machine M {
    client.terminate() = setNextState(Terminated)

    state Uninit {
      client.setup() = setNextState(Unknown)
    }
    // Valve position unknown until first command received
    state Unknown {
      client.open() = {
        hal.move(ValveHAL.Cmd.Open);
        setNextState(Opened);
      }
      client.close() = {
        hal.move(ValveHAL.Cmd.Close);
        setNextState(Closed);
      }
    }

    state Opened {
      client.open() = {}
      client.close() = {
        hal.move(ValveHAL.Cmd.Close);
        setNextState(Closed);
      }
    }

    state Closed {
      client.open() = {
        hal.move(ValveHAL.Cmd.Open);
        setNextState(Opened);
      }
      client.close() = {}
    }
  }

The events in a component state machine must be qualified by the event source from which they originate. All events originating from the provided port are qualified with the instance name client, for example, client.setup(). Likewise, all events on the required port are qualified with the instance name hal, for example, hal.move(ValveHAL.Cmd.Open).

This simple component state machine illustrates how ValveControllerImpl responds to some of the incoming function calls made on its provided port by calling functions on its required port. For example, in state Opened, when it receives a call on client.close(), ValveControllerImpl will call hal.move(ValveHAL.Cmd.Close) on its required port, and transition to the state Closed.

The complete Coco for this component declaration is in Example_01A/src/ValveControllerImpl.coco.

Note

Component state machines differ from port state machines in a number of ways. Firstly, a component state machine must be complete and deterministic. This means that for every possible incoming event, there must be precisely one enabled matching event transition. If no such transition is specified, then the event is defined to be illegal by default. Incoming events can either be function calls made via its provided port, or signals sent to the component by one of its required ports. Secondly, component state machines cannot have spontaneous transitions, as these are abstractions used only in ports to model the consequences of actions that are invisible across a component boundary. Finally, component state machines can implement timer transitions, as will be illustrated later on in the tutorial.

For further details on port and component state machines, see the State Machines section in the Coco Language Reference. This is an ideal point in the tutorial to read through this section!

Step 5: Verifying your Ports and Components

The Coco Platform formally verifies Coco components and ports for a broad range of properties, ranging from runtime errors through to advanced liveness properties. When verifying the correctness of a Coco port, the Coco Platform verifies whether the port state machine is well-formed, usable, and implements the base port correctly in the case that it inherits a base port. A Coco component is verified within the context of its environment, namely the required ports that it is connected to, and the specified runtime. When verifying the correctness of a Coco component, the Coco Platform checks that the component state machine is well-formed, and that it implements its provided port correctly.

Note

For details about the properties that are formally verified for ports and components, together with examples of how the errors are represented as graphical counterexamples, see the Verification section in the Coco Language Reference. This is an ideal point in the tutorial to read through this section!

Now that you have built your initial set of components and ports, you can formally verify them automatically in the Coco Platform. The list of checks that will be performed by the Coco Platform are listed in the Assertions window. Assertions that have not yet been verified are listed with a blue icon. You can either select an individual assertion to be verified, or verify all of them using the Run All (green) icon at the top of the Assertions window. When an assertion is verified, it will lead to one of the following results:

  • The assertion successfully satisfies the property being verified. This is denoted by a green icon.
  • The assertion has failed the verification, which is denoted by a red icon. This means that the port or component being verified does not satisfy the property being checked. Users can double-click on the failed assertion to get a graphical counterexample of the error found.
  • The verification has raised a warning, which is denoted by a yellow icon. This means that the assertion has failed the verification, but this does not necessarily imply that it is an error in the port or component. For example, a failure to satisfy properties such as the reachability of states and transitions are denoted as warnings instead of errors, as this may well reflect the intention of the port or component specification. Users can double-click on an assertion that failed with a warning to get feedback on the error found. This will either be in the form of a graphical counterexample, or a textual summary.

Note

Developers are encouraged to follow a verification-driven development approach when building software in Coco, where the verification is run frequently as they build their software. This means that errors are found quickly and early, which typically makes them easier and faster to fix. This approach also works very well alongside an incremental development approach, which is commonly followed by developers using Coco.

Results (Example_01A)

All of the ports and components we have built so far in this tutorial verify correctly. Assuming the manually-written C++ implementation of the ValveHALBase component correctly implements its provided port, then this means that the code generated by the Coco Platform for the ValveControllerImpl component will compile and execute as expected, without any errors.

If you are interested in provoking the Coco Platform to find a verification error in order to see what a graphical counterexample looks like, then you can make the following amendment to the state machine declaration in the ValveControllerImpl component to comment out the termination behaviour, and rerun the verification:

// client.terminate() = setNextState(Terminated)

The verification will find an error in one of the assertions for the ValveControllerImpl component, because it no longer correctly implements its provided port.

Alternatively, there are plenty of counterexamples presented in the Verification section in the Coco Language Reference, and more will be generated throughout the rest of the tutorial.

Step 6: Refactoring your Code

The state machine declared in the ValveControllerImpl component can be refactored to remove some of the repetitive actions defined in the open() and close() transitions, which make calls on the ValveHALBase to move the valve each time.

Instead of calling ValveHALBase to move the valve in these transitions in the Unknown, Opened and Closed states, this move request can be factored out by using entry functions in the corresponding states as follows:

  machine M {
    client.terminate() = setNextState(Terminated)

    state Uninit {
      client.setup() = setNextState(Unknown)
    }
    // 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.open() = {}
      client.close() = setNextState(Closed)
    }

    state Closed {
      entry() = hal.move(ValveHAL.Cmd.Close)
      client.open() = setNextState(Opened)
      client.close() = {}
    }
  }

Note

An entry function is defined by an expression, which is executed upon entry into a state S in which it is declared. The expression is allowed to have a state transition function only if the target state is a direct descendant of S, and does not cause S to be re-entered.

The complete Coco for this component is in Example_02A/src/ValveController.coco.

Step 7: Running the Generated Code

Having developed the ValveControllerImpl, the next step is to generate the C++ code, and run it. For this, the following additional components are required:

  1. A hardware simulator for the valve.
  2. An encapsulating component that composes the components together, and specifies how they are connected to one another.
  3. A package file specifying code generation parameters.
  4. A test application.

Warning

This section is still to be completed, and currently only provides the steps for building the simulator component, and the encapsulating component.

Building a SimulatorImpl Component

The hardware simulator is a simple Coco component that implements the ValveHAL as its provided port. When called to move, it simply sleeps for 10 seconds to simulate a blocking valve hardware that takes 10 seconds to operate. The SimulatorImpl component is declared as follows:

import CPP
import unqualified ValveHAL

// Access to the stdlib function std::this_thread::sleep_for
@CPP.mapToValue("std::this_thread::sleep_for", .System("thread"))
external function chronoSleep(t : Duration) : Nil = {}

@runtime(.MultiThreaded)
component SimulatorImpl {
  val client : Provided<ValveHAL>

  machine M {
    client.move(_ : ValveHAL.Cmd) = chronoSleep(seconds(10))
  }
}

The function chronoSleep(t : Duration) is declared as an external function, and is labelled with the @CPP.mapToValue attribute. This maps the external function to the C++ signature of the standard library function std::this_thread::sleep_for(t : Duration), and specifies the system header file to be included in the generated code.

The built-in Coco type Duration is automatically mapped onto the standard library type std::chrono::duration. This means that when Coco encounters a reference to chronoSleep(t), it will output std::this_thread::sleep_for(t) in the corresponding generated C++.

The state machine has a single transition declared in Main:

    client.move(_ : ValveHAL.Cmd) = chronoSleep(seconds(10))

When the function client.move(cmd : ValveHAL.Cmd) is called, it calls the function chronoSleep(seconds(10)) defined above.

This is the entire behaviour for the simulator.

The complete Coco for this component is in Example_02A/src/SimulatorImpl.coco.

Declaring an Encapsulating Component

An encapsulating component is a component that defines a collection of components, and specifies how they are connected together. It can have zero or more provided or required ports, but it cannot have a state machine. It must declare instances of all the components it owns and have a constructor that includes connector functions, specifying the connections between the encapsulating component’s ports and those of the component instances it owns.

Note

A Coco constructor specifies how instances of components declared in the encapsulating component are connected together. It is defined as a block expression, which can consist of connector functions, shared access functions, and the queue control functions (with the exception of the purge() functions), depending on the component’s type.

Declare the following encapsulating component:

import unqualified SimulatorImpl
import unqualified ValveController
import unqualified ValveControllerImpl
import unqualified ValveHAL

@runtime(.MultiThreaded)
component TestValveGroup {
  val client : Provided<ValveController>

  val valve : ValveControllerImpl
  val hal : SimulatorImpl

  init() = {
    connect(client, valve.client);
    connect(valve.hal, hal.client);
  }
}

The following architecture diagram is a graphical representation of TestValveGroup:

TestValveGroup valve : ValveControllerImpl hal : SimulatorImpl client : ValveController client : ValveController client : ValveHAL hal ENC

TestValveGroup is an encapsulating component that provides one port client, and owns two component instances, valve of type ValveControllerImpl and hal of type SimulatorImpl. The init() declaration is its constructor, which specifies how the component instances are connected together. In this case, init() specifies that:

  1. The TestValveGroup component’s own provided port client is connected to the provided port client of the component instance valve.
  2. The required port hal in component instance valve is connected to the client port of the component instance hal.

The complete Coco for this encapsulating component is in Example_02A/src/TestValveGroup.coco.