5.2. Mapping between the Model Definition Files and Coffeescript source code#
5.2.1. Introduction#
The following guide describes how the model definitions are mapped into a Coffeescript programming language implementation that executes in the nodejs platform. The OCS SDK provides a code generator (as part of the gds tool that thansforms the model specification into a set of programs that take advantage of the core framework API to access the OCS distributed infrastructure as well as the OCS Core Services. In addition to execute in the nodejs runtime, the generated programs run also in the electron-js environment as long as the core framework API is used on the node side.
The nodejs OCS Core Framework takes advantage of the nodejs asynchronous event loop and is well suited for (a) components that required distributed asynchronous communication (e.g. the OCS Core Services are implemented this way); (b) components that are part of the OCS user interface that runs in the electron platform; (c) applications that don’t require high performace computations or communications and that will benefit from an implementation in a dynamic typed language.
The mapping between the model definition of a Component and its implementation in the nodejs/Coffeescript platform is the most direct transformation of the platforms supported by the OCS development system (nodejs, cpp, python). Most of the modeling concepts have a one to one correspondace in the Core Framework. The cases where there are discrepancies are noted in this document.
Note
The use of nodejs allows access to a rich ecosystem of node modules, however any node module not distributed as part of the OCS SDK should require approval from the GMT PO.
5.2.2. NodeJS file tree structure#
As described in the gds gen command a coffeescript implementation
for those components whose target list contains coffee
in its target attribute
is generated. The source files generated are created under
the <MODULE_DIR>/src/coffee
directory, which has the following structure:
<MODULE_DIR>/
|-- ...
|-- src/
|-- ...
|-- bin/
| |-- <app_1>
| |-- ...
| |-- <app_n>
|
|-- coffee/
| |-- <pk_1_pkg>/
| |-- ...
| |-- <pk_n_pkg>/
| |
| |-- Makefile
| |-- webpack.config.coffee
|--etc/conf/
| |-- <pk_1_pkg>/
| |-- <module_name>_ld.coffee
Model elements are processed differently depending on the value of their
build
attribute and on the model element class.
When then model element class is
Application
and thebuild
attribute value isapp
an application bootstrap file will be created in<MODULE_DIR>/src/bin
and an application skeleton file will be created in the corresponding package directory<MODULE_DIR>/src/coffee/<pkg>
. By default, the application skeleton creates an instance of every Component in the package. The section Applications includes an example of a generated application.When the model element class or superclass is
Component
and thebuild
attribute value isdist
two files will be created in<MODULE_DIR>/src/coffee/<pkg>/<component>
:A Component source file with an skeleton of the component implementation
A demo Application that creates an instance of the component and that allows to start and test the component. Section Section 4.7.3 includes an example of a generated component.
When the model element class or superclass is
Component
and thebuild
attribute value istest
two files will be created in<MODULE_DIR>/src/coffee/<pkg>/<component>
:A Component source file with an skeleton of the component implementation
A demo Application that creates an instance of the component and that allows to start and test the component. Section Section 4.7.3 includes an example of a generated component. TODO
When the model element class or superclass is
Component
and thebuild
attribute value isexample
two files will be created in<MODULE_DIR>/src/coffee/<pkg>/<component>
:A Component source file with an skeleton of the component implementation
A demo Application that creates an instance of the component and that allows to start and test the component. Section Section 4.7.3 includes an example of a generated component. TODO
5.2.2.1. Webpack files#
Webpack is used to process and optimize Coffeescript files. The behavior of the webpack tool is taylored by means of a configuration file: webpack.config.coffee that is created by the code generator. The webpack configuration file works in the following way:
A library is created for each Package and installed in
$GMT_LOCAL/lib/js
An application executable is created for each Application and is installed in
$GMT_LOCAL/bin
Tests defined in the directory
<module_name>/test
are installed inGMT_LOCAL/test/<module_name>
The following code shows an example of the webpack configuration file
path = require 'path'
_ = require 'lodash'
local_path = process.env.GMT_LOCAL
global_path = process.env.GMT_GLOBAL
[base_config] = require "#{global_path}/etc/webpack/webpack.base"
project_name = "ocs"
module _name = "isample_dcs"
lib_config =
name: "lib"
entry:
isample_ctrl_pkg: "./isample_ctrl_pkg/isample_ctrl_pkg.coffee"
output:
filename: "#{project_name}_#{module_name}_lib.js"
path: path.resolve local_path, 'lib/js'
libraryTarget: 'umd'
library: module_name
_.extend lib_config, base_config
app_config =
name: "app"
entry:
isample_main_app: "./isample_ctrl_pkg/IsampleMainApp.coffee"
output:
filename: "[name]_lib.js"
path: path.resolve local_path, 'lib/js'
_.extend app_config, base_config
module.exports = [lib_config, app_config]
5.2.3. Data Types Mapping#
Coffeescript is a dynamically typed language that doesn’t provide type declarations. Primitive Types are the same ones as the Javascript language. The code generator creates an skeleton for the following data types:
StructType types in the model are mapped into a class. An individual file is created for each StructType and it is saved in the
DataTypes
folder.Enum types in the model are mapped into a class. An individual file is created for each Enum and it is saved in the
DataTypes
folder.For those Component features whose model type is StateMachine the code generator creates an skeleton of the StateMachine implementation. An individual file is created for each StateMachine and it is saved in the
Behaviors
folder.
The following code shows an example of a generated StructType
class IsampleHmiLeds
constructor: (def = {}) ->
{
@pilot = null
@emergency_light = null
@heartbeat = null
@counter = null
} = def
module.exports = { IsampleHmiLeds }
5.2.4. Component Features#
Components are the main building block of the OCS architecture. The following diagram shows an overall view of the Core Framework main classes.
A Component has the following features:
Properties: Collection of Properties. See the properties section in the model specification guide for the attributes of a Property
Inputs: Collection of DataIO. See the input section in the model specification guide for the attributes of an Input
Outputs: Collection of DataIO. See the output section in the model specification guide for the attributes of an Output
State Variables: Collection of State variables. See the state variables section in the model specification guide for the attributes of a StateVariable
Faults: Collection of Faults. See the faults section in the model specification guide for the attributes of a Fault
Alarms: Collection of Alarms. See the alarms section in the model specification guide for the attributes of an Alarm
Each Component implementation class inherits from the Core Framework Component class. Each of the features is implemented as a Collection. (see diagram). All the collections include partially applied functions to manipulate each collection. The following fragment of code shows some examples.
# prints the name of all the inputs
@inputs.each (i) -> console.log i.name
# returns an array with the names of all the state variables that are controllable
@state_vars
.filter (s) -> s.is_controllable
.map (s) -> s.name
# returns true if there is at least one fault active, otherwise returns false
@faults
.some (f) -> f.is_active
The implementation of these methods is based on the lodash library and includes the most common methods to support functional programming.
5.2.5. Communication between Components#
Components can communicate with each other by means of Connectors. A Connector specification includes the definition of the Connector endpoints.
If the blocking_mode
of a Connector is ‘sync’ the information will be transmitted from
the one endpoint to the other endpoint at the rate specified by the nom_rate
attribute of
the connector.
In case no Connectors are defined that involve a given Component instance is still possible to communicate such a Component using service ports. Service ports are created by default for every Component and allow to exchange information with it. The grs set, grs get and grs inspect commands use Service Ports to access the information of a running Component.
If the blocking_mode
of a port is ‘async’ the Core Framework provides the ComponentProxy class to
allow communicating with a remote Component. The following segment of code demonstrates the use
of the ComponentProxy methods.
# Imports ComponentProxy from the core framework
{ComponentProxy} = require 'ocs_core_fwk'
proxy = new ComponentProxy {name: 'position_ctrl'}
position = proxy.get 'state_vars/position/value'
proxy.set 'state_vars/position/goal', 200
5.2.6. ComponentProxy#
The core framework includes a ComponentProxy class that allows comunicating with other Components: The ComponentProxy constructor:
The ComponentProxy exports the following methods:
ping: (timeout)
Sends a ping message to the remote object and returns a Promise. If the Promise is resolved the ping method returns an object with the transaction timestamps. If the Promise is rejected an timeour error is generated. If
timeout
is omitted the default value is 400 msset: (path, data)
Sends a message to change the feature of the remote object defined by
path
with thedata
valueget: (path = "", slice = "value", timeout)
Returns a Promise with the
slice
of the feature of the remote object defined bypath
. By default the value of theslice
is the attribute value. Other possibilities are desc, units,… If the Promise is resolved before the timeout it returns the value of the remote feature, otherwise generates a timeout error. Iftimeout
is omitted the default value is 400 ms
5.2.7. Component Behaviors#
Components implement their control, monitoring and supervisory
functions by specializing their step function.
The step function can be considered as a non side effect function fstep: (I, S) -> O
taking the current inputs and state (as represented by state variables)
of a Component and generating a new set of outputs that produce
an effect on the context of a Component (e.g. other connected
components or the system under control via the hardware adapters).
In many cases the step function may be enough to implement all the functions allocated
to a Component. However, in some cases it may be useful to organize
the functions of a Component in a modular way, specially if the
functions are complex. A Behavior is the modular functional unit
to organize the complexity of a Component. Behaviors must implement
an apply
function. The apply
function is invoked by the
Core Framework at the scan rate of the Component (as defined by the scan_rate
property. In case the Behavior requires to be executed at a different rate from the scan rate
the PeriodicBehavior class can be used, in which case a rate attribute
can be defined when created an instance (see following example).
# Imports Behavior from the core framework
{Behavior, PeriodicBehavior} = require 'ocs_core_fwk'
class NewBehavior extends Behavior
apply: (d, dt) ->
# Behavior function
class NewPeriodicBehavior extends PeriodicBehavior
apply: (d, dt) ->
# Behavior function
bh = new NewBehavior {name: 'behavior_name'}
bhp = new NewPeriodicBehavior {name: 'behavior_name', rate: 10}
@state_vars.position.behaviors.add bh
@state_vars.position.behaviors.add bhp
bh.setup()
bhp.setup()
It’s possible to define input and output parameters as part of a behavior specification. This allows to compose them following the structure that suits the decomposition of the problem by connecting the Behaviors inputs and outputs. A common structure is a control or supervisory hierarchy.
Behaviors can be attached to the Component context or to any of the Component features (e.g. state variable, faults, alarms). The control functions may be of discrete (e.g. a state machine) or continuous nature (e.g. a PID loop). In hybrid control systems both types of functions are present.
Continuous functions will often involve the application of a set of mathematical operations and are mostly specific to the application’ domain.
5.2.8. State Machines#
In case of discrete functions that could be modeled as an state machine, the core framework includes a StateMachine class with the following characteristics:
Supports Moore and Mealy machines
Reactive evaluation like other Component Behaviors
Full access to Component state and Core Services through
@ctx
referenceCan be composed to create arbitrary large hierarchical and parallel machines (StateCharts)
Allow each individual State Machine to be tested individually
Arbitrary composition of hybrid control architectures (continuous and discrete behavior) inside each component
The StateMachine section in the model specification guide describes how to define an StateMachine in the model. State Machines can be implemented by specializing the StateMachine class from the core framework by the following means:
Defining a transition function
ft: (I,S) -> S
that takes as arguments the inputs and current state and determines the next stateDefining an ouput function
fo: (I,S) -> O
that takes as arguments the inputs and current state and updated the output of the StateMachineDefining the following functions for each of the states:
An
entry
function that is executed when the state is enteredAn
exit
function that is executed when the state is exitedAn
on
function that is executed every time the StateMachine is evaluated and is in the present state
The following diagram shows some of the core framework builtin state machines.
5.2.8.1. Fault management#
The previous section describes some strategies to implement the control, monitoring and supervisory functions of a Component. In addition to these functions control systems that require to exhibit robustness and reliability must address the detection and management of non-nominal operating conditions. Often the strategy for addressing fault management doesn’t necessary follows the structure of the control function, in which case it is convenient to have an independant implementation.
The Core Framework allows the definition of the faults that a Component may
handle. For each Fault an eval
function can be defined to detect if the
fault condition is active. Additionally, in cases in which an strategy can be
establised to handle the fault condition a recover
function can be defined.
Each Fault is associated by default with a fault state machine (FaultFSM
)
that governs the transitions between fault states as described
in the following diagraman.
As described in the Faults section of the model specification guide Faults can be organized as a fault tree to model more complex fault states. The next segment of code shows an example of a fault specification.
# prints the name of all the inputs
faults:
axis_fault:
name: 'axis_fault'
kind: 'or'
parent: ""
level: 'CRITICAL'
default_value: 'NOT_ACTIVE'
desc: 'Axis Controller not operational'}
detection_latency: 1
motor_over_heat:
name: 'motor_over_heat'
kind: 'primary'
parent: 'axis_fault'
level: 'CRITICAL'
default_value: 'NOT_ACTIVE'
desc: 'Motor Overheat'
detection_latency: 1
encoder_fault:
name: 'encoder_fault'
kind: 'primary'
level: 'CRITICAL'
parent: 'axis_fault'
default_value: 'NOT_ACTIVE'
desc: 'Encoder not responding'
detection_latency: 1
motor_fault:
name: 'motor_fault'
kind: 'primary'
level: 'CRITICAL'
parent: 'axis_fault'
default_value: 'NOT_ACTIVE'
desc: 'Motor not responding'
detection_latency: 1
5.2.8.2. Alarm management#
Alarms are used to notify operators of operating conditions that
require their attention. The Core Framework allows to define which
Alarms are associated with a Component. For each Alarm an
eval
function has to be defined to determine if an alarm condition
is active. As described in the Alarms section
of the model specification guide Alarms
can be arranged, grouped and connected in a similar way to fault trees.
Although the implementation mechanism is similar, Faults and Alarms have different functions are their logic is independed, although in some cases the occurence of a Fault condition of the inability to recover from a Fault condition may require the activation of an Alarm condition. Once an Alarm condition occurs the alarm follows a life-cycle similar to the one defined in the IEC62682 *Management of alarm system for the process industries* standard.
In the Core Framework the life-cycle of an alarm is implemented in the AlarmFSM state machine as described in the following diagram.
5.2.8.3. Component Operational State#
Components include an operational state StateVariable that governs
the life-cycle of each Component. This life-cycle is implemented in the
StateMachine Behavior OpStateFSM
as described in the following diagram:
The following table describes the behavior of the Component in each possible state.
Method
Description
OFF
Initial state
STARTING
Auto transition to ON if auto_start is true
ON
Enables input and output behaviors
INITIALIZING
Enables property, state_var, alarms and fault behaviors
RUNNING
Set state variables following_mode to ‘FOLLOWING’
HALTING
Set state variables following_mode to ‘NOT_FOLLOWING’
SHUTTING_DOWN
Disables input and output behaviors
FAULT
State variables are set to ‘NOT_FOLLOWING’
HALTING
Disables property, state_var, alarms and fault behaviors
5.2.8.4. Component Health Supervision#
The core framework includes a default health supervisory behavior HealthSupervisingBehavior that implements a basic supervisory function which allows:
Detecting if a supervised Component is able to respond to a
ping
message, otherwise it will assert the fault<supervisee>_not_responding
Detecting if a supervised Component
op_state
state variable isRUNNING
, otherwise it will assert the fault<supervisee>_not_operational
The following segment of code shows an example of how to define a health supervisory behavior
# Example of HealthSupervisingBehavior declaration
stage_sup.behaviors.add new HealthSupervisingBehavior {name: 'sup_bh'}
stage_sup.add_supervisee {name: 'x_ctrl', conf: x_ctrl.file_conf}
stage_sup.add_supervisee {name: 'y_ctrl', conf: y_ctrl.file_conf}
stage_sup.add_supervisee {name: 'z_ctrl', conf: z_ctrl.file_conf}
stage_sup.add_supervisee {name: 'cartesian_ctrl', conf: cartesian_ctrl.file_conf}
stage_sup.add_supervisee {name: 'thermal_ctrl', conf: thermal_ctrl.file_conf}
5.2.9. Applications#
The Core Framework allows implementing end applications, which eventually will be transformed in an executable using the CoreApplication class. The CoreApplication class has the following characteristics:
Instantiates a CoreContainer that allows the Component to use the OCS Core Services
Instantiates the indicated Component instances
The CoreCLIApplication class in addition allows the management of command line options.
The following code shows an example of an application as produced by the code generator.
{ CoreContainer
Supervisor
HealthSupervisingBehavior
CoreCLIApplication } = require 'ocs_core_fwk'
{IsampleCtrlSuper} = require './isample_ctrl_super/IsampleCtrlSuper'
{IsampleTempCtrl} = require './isample_temp_ctrl/IsampleTempCtrl'
{IsampleFocusCtrl} = require './isample_focus_ctrl/IsampleFocusCtrl'
{IsampleFilterWheelCtrl} = require './isample_filter_wheel_ctrl/IsampleFilterWheelCtrl'
{IsampleHwAdapter} = require './isample_hw_adapter/IsampleHwAdapter'
class IsampleCtrlPkgApp extends CoreCLIApplication
setup: ->
@ctnr = new CoreContainer @, null,
name: "isample_ctrl_pkg_app_container"
scope: @properties.scope.value
logging: @properties.logging.value
@ctnr.create_adapters()
@isample_ctrl_super = new IsampleCtrlSuper @ctnr, null,
name: 'isample_ctrl_super'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_cryo_internal_temp_ctrl = new IsampleTempCtrl @ctnr, null,
name: 'isample_cryo_internal_temp_ctrl'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_cryo_external_temp_ctrl = new IsampleTempCtrl @ctnr, null,
name: 'isample_cryo_external_temp_ctrl'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_focus1_ctrl = new IsampleFocusCtrl @ctnr, null,
name: 'isample_focus1_ctrl'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_fw1_ctrl = new IsampleFilterWheelCtrl @ctnr, null,
name: 'isample_fw1_ctrl'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_fw2_ctrl = new IsampleFilterWheelCtrl @ctnr, null,
name: 'isample_fw2_ctrl'
scope: @properties.scope.value
logging: @properties.logging.value
@isample_hw1_adapter = new IsampleHwAdapter @ctnr, null,
name: 'isample_hw1_adapter'
scope: @properties.scope.value
logging: @properties.logging.value
@sup = new Supervisor @ctnr, null, {name: "isample_ctrl_pkg_super"} # Default supervisor, substitute by pkg supervisor
@sup.behaviors.add new HealthSupervisingBehavior {name: 'sup_bh'}
@sup.add_supervisee {name: 'isample_ctrl_super'}
@sup.add_supervisee {name: 'isample_cryo_internal_temp_ctrl'}
@sup.add_supervisee {name: 'isample_cryo_external_temp_ctrl'}
@sup.add_supervisee {name: 'isample_focus1_ctrl'}
@sup.add_supervisee {name: 'isample_fw1_ctrl'}
@sup.add_supervisee {name: 'isample_fw2_ctrl'}
@sup.add_supervisee {name: 'isample_hw1_adapter'}
super() if super.setup
app = new IsampleCtrlPkgApp null,
name: "isample_ctrl_pkg_app"
scope: "local"
logging: "info"
app.setup()
app.start()
5.2.9.1. Command line options#
The Core Framework makes every property of a Component available
as a command line option. The following example shows an example
in which the logging
property of an application is propagated
to other instances created by the application.
{ Component
CoreContainer
Supervisor
HealthSupervisingBehavior
CoreCLIApplication } = require 'ocs_core_fwk'
test_sup_conf =
properties:
uri: { name: 'uri', default_value: 'gmt://127.0.0.1:12100/core_fwk/tests/test_supervisor', type: 'string', desc: ""}
name: { name: 'name', default_value: 'test_supervisor', type: 'string', desc: ""}
host: { name: "host", default_value: '127.0.0.1', type: 'string', desc: ""}
port: { name: "port", default_value: 12100, type: 'integer', desc: ""}
scan_rate: { name: 'scan_rate', default_value: 1, type: 'integer', desc: ""}
acl: { name: 'acl', default_value: {users: ['*']}, type: 'ACL', desc: ""}
faults:
not_operational: { name: 'not_operational', default_value: 'NOT_ACTIVE', level: 'CRITICAL', detection_latency: 1, kind: 'or', parent: "" }
my_component_not_responding: { name: 'my_component_not_responding', default_value: 'NOT_ACTIVE', level: 'CRITICAL', detection_latency: 1, kind: 'primary', parent: 'not_operational' }
my_component_not_operational: { name: 'my_component_not_operational', default_value: 'NOT_ACTIVE', level: 'CRITICAL', detection_latency: 1, kind: 'primary', parent: 'not_operational' }
class TestApp extends CoreCLIApplication
setup: ->
@ctnr = new CoreContainer @, null,
name: "test_app_container"
scope: @properties.scope.value
logging: @properties.logging.value
@ctnr.create_adapters()
@comp = new Component @ctnr, null,
name: 'my_component'
scope: @properties.scope.value
logging: @properties.logging.value
@sup = new Supervisor @ctnr, test_sup_conf,
name: "my_super"
scope: @properties.scope.value
logging: @properties.logging.value
@sup.behaviors.add new HealthSupervisingBehavior {name: 'super_sup_bh'}
@sup.add_supervisee {name: 'my_component'}
super() if super.setup
app = new TestApp null,
name: "test_app"
scope: "global"
logging: "metric"
app.setup()
app.start()
# when invoking the application we will enter in the shell:
> my_app --logging debug
5.2.10. Core Service adapter API#
Components run inside a container that manages their life-cycle and provides access to the interface with the Core Services. The Core Framework includes two default container classes:
CoreContainer: Implements the complete interfaces with the Core Services, but instead of sending the information to the Core Service servers it is directed to the terminal standar ouput. This is useful for quick feedback on the developement of a Component as there is no need to have an instance of the Core Services running
CoreServiceContainer: Implements the complete interface with the Core Services and integrates the information generated by the Component with them.
The interface with the Core Services is implemented by means of a set of service adapters
Although all services can be accesed from a component (e.g. @log
, @tele
).
The most common use cases are already implemented by the defatul behaviors of
the Core Framework:
The SamplingBehavior samples the Component features at the defined rate and sends the information to the Telemetry Service
The FaultFSM and AlarmFSM state machines send information to the corresponding services when the associated Fault and Alarm state variables transition from one state to another.
The Configuration Service adapter sends a configuration event message when properties change once the execution of a Component has started.
The Logging Service is the most likely beeing used by the developer when implementing the functions of a component
5.2.10.1. Logging Service Adapter#
The following segment of code demonstrate the use of the Logging Service adapter interface. The logging functions are consisten with the logging levels as defined in the grs command.
The syntax of the loggin API is:
<log_adapter_reference>.<logging function> <contex>, <message>
# Log adapter info example
@log.fatal @, "Fatal log message"
@log.error @, "Error log message"
@log.warning @, "Warning log message"
@log.info @, "Informative log message"
@log.debug @, "Debug log message"
@log.trace @, "Trace log message"
@log.metric @, "Metric log message"
When setting the logging
property of a Component only
those messages whose level of detail is the same or lower will
be directed to the Logging Service. For example if logging = 'info'
only messages of type fatal
, error
, warning
and info
will be send.