Skip to content

Commit f356001

Browse files
committed
vtd: Add SCP Action
This allows SCP commands to be sent to VTD from Cloe triggers.
1 parent 4969e08 commit f356001

File tree

9 files changed

+483
-2
lines changed

9 files changed

+483
-2
lines changed

docs/develop/adding-a-new-action.rst

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
Addding a New Action
2+
====================
3+
4+
Trigger actions can be added in your Cloe plugin in the ``enroll`` method:
5+
6+
.. code-block:: cpp
7+
8+
class YourPlugin : public cloe::Controller {
9+
// ...
10+
11+
void enroll(cloe::Registrar& r) override {
12+
// This is where you add your calls to register various things with
13+
// the cloe engine, including actions.
14+
r.register_action<YourActionFactory>();
15+
}
16+
}
17+
18+
And it looks like what ``register_action`` wants is a ``cloe::ActionFactoryPtr``,
19+
but why? Well, trigger actions are most often created in stackfiles, as
20+
something compatible with JSON. The ``ActionFactory`` takes a JSON object or
21+
a string, and makes an ``Action`` out of it.
22+
23+
Defining an Action
24+
------------------
25+
26+
Let's look at a simple action defined in the runtime package in the file
27+
``cloe/trigger/example_actions.hpp``: ``Log``.
28+
29+
We can see there is an include for ``<cloe/trigger.hpp>``. This file can also
30+
be found in the runtime package, and you should have a look at the
31+
documentation there for the ``Action`` interface.
32+
33+
Then we come to the source for the ``Log`` action:
34+
35+
.. code-block:: cpp
36+
37+
class Log : public Action {
38+
public:
39+
Log(const std::string& name, LogLevel level, const std::string& msg)
40+
: Action(name), level_(level), msg_(msg) {}
41+
ActionPtr clone() const override { return std::make_unique<Log>(name(), level_, msg_); }
42+
void operator()(const Sync&, TriggerRegistrar&) override {
43+
logger()->log(level_, msg_.c_str());
44+
}
45+
bool is_significant() const override { return false; }
46+
47+
protected:
48+
void to_json(Json& j) const override {
49+
j = Json{
50+
{"level", logger::to_string(level_)},
51+
{"msg", msg_},
52+
};
53+
}
54+
55+
private:
56+
LogLevel level_;
57+
std::string msg_;
58+
};
59+
60+
The constructor is only really relevant for the ``LogFactory``, so we'll ignore
61+
that for now.
62+
63+
Then there is the ``clone()`` method. This should return another new instance
64+
of this action that can do the same thing again. This is important because the
65+
user can request that actions be repeated, which results in clones occurring.
66+
67+
Now we come to the ``operator()`` method. This is where the real work of the
68+
action occurs. It takes two arguments, which you will probably never use. If
69+
you need them, you'll know.
70+
Inside the body of this function you can do what the action says it will do.
71+
72+
For the ``is_significant()`` method, we tell Cloe whether this action can
73+
affect the simulation. If in doubt, answer yes. The ``Log`` action is one of
74+
the few actions that is not significant.
75+
76+
Finally, we need to implement the ``to_json()`` method. This should provide the
77+
extra fields in the JSON representation that are needed to recreate this object
78+
with the ``LogFactory``. Note that there is no name of this action. That's
79+
because that is added automatically by Cloe, in order to prevent errors from
80+
slipping in.
81+
82+
Defining the ActionFactory
83+
--------------------------
84+
85+
Defining the ``ActionFactory`` is often more work than the action itself. We
86+
need to think about how we want to allow the action to be created.
87+
At first glance, the implmentation looks simple:
88+
89+
.. code-block:: cpp
90+
91+
class LogFactory : public ActionFactory {
92+
public:
93+
using ActionType = Log;
94+
LogFactory() : ActionFactory("log", "log a message with a severity") {}
95+
TriggerSchema schema() const override;
96+
ActionPtr make(const Conf& c) const override;
97+
ActionPtr make(const std::string& s) const override;
98+
};
99+
100+
But then we see that the implementation for three methods is not inline. We'll
101+
have a look at each of these in turn, but first: the using statement.
102+
This statement defines what the output type of this action factory is, and is
103+
necessary for properly registering the action factory.
104+
105+
The constructor of the action factory simply calls the super-constructor and
106+
supplies the *default* name and the description of the *action*.
107+
108+
The ``schema()`` method is used to define the trigger schema, which among other
109+
things, lets Cloe validate input and also document the action.
110+
111+
.. code-block:: cpp
112+
113+
TriggerSchema LogFactory::schema() const {
114+
return TriggerSchema{
115+
this->name(),
116+
this->description(),
117+
InlineSchema("level and message to send", "[level:] msg", true),
118+
Schema{
119+
{"level", make_prototype<std::string>("logging level to use")},
120+
{"msg", make_prototype<std::string>("message to send").require()},
121+
},
122+
};
123+
}
124+
125+
The ``make(const Conf&)`` method takes the object configuration, and reads the
126+
variables that are necessary for configuration.
127+
128+
.. code-block:: cpp
129+
130+
ActionPtr LogFactory::make(const Conf& c) const {
131+
auto level = logger::into_level(c.get_or<std::string>("level", "info"));
132+
return std::make_unique<Log>(name(), level, c.get<std::string>("msg"));
133+
}
134+
135+
The ``make(const std::string&)`` method takes a string, and tries to parse this
136+
into something that it can fill into a Conf and pass on to the method above.
137+
This is often the most complex function, but it makes using triggers by
138+
hand much much easier. This just takes a string in the format: ``level:
139+
message`` and packs it into the JSON object structure the simpler ``make()``
140+
method needs.
141+
142+
.. code-block:: cpp
143+
144+
ActionPtr LogFactory::make(const std::string& s) const {
145+
auto level = spdlog::level::info;
146+
auto pos = s.find(":");
147+
std::string msg;
148+
if (pos != std::string::npos) {
149+
try {
150+
level = logger::into_level(s.substr(0, pos));
151+
if (s[++pos] == ' ') {
152+
++pos;
153+
}
154+
msg = s.substr(pos);
155+
} catch (...) {
156+
msg = s;
157+
}
158+
} else {
159+
msg = s;
160+
}
161+
162+
auto c = Conf{Json{
163+
{"level", logger::to_string(level)},
164+
{"msg", msg},
165+
}};
166+
if (msg.size() == 0) {
167+
throw TriggerInvalid(c, "cannot log an empty message");
168+
}
169+
return make(c);
170+
}

docs/reference/plugins/vtd.rst

+92
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ vehicles/<name>/components/<component_name>/override
123123
with the newly declared one or terminate with an error if no such component
124124
can be found.
125125

126+
scp_actions
127+
A dictionary defining keywords to SCP actions. These can then be referred
128+
to by the ``scp`` action. For example::
129+
130+
"scp_actions": {
131+
"stop": "<SimCtrl><Stop/></SimCtrl>"
132+
}
126133

127134
Ground Truth
128135
------------
@@ -167,6 +174,91 @@ The above is sensible only for a single agent simulation. In case of a multi
167174
agent scenario you should define your sensors and map your sensor components
168175
explicitly as described in :ref:`configuration`: section.
169176

177+
Triggers
178+
--------
179+
180+
The following actions are provided by the plugin. Plugin trigger action names
181+
are dependent on the given name of a plugin in a simulation. This is by default
182+
the name of the plugin itself; this default name is used in this documentation.
183+
184+
<vtd>/scp
185+
"""""""""
186+
The VTD plugin makes this action available to send SCP messages to the VTD
187+
simulator. The name of the action is prefixed by the given name of the plugin.
188+
189+
There are two variants of this action. Either a template is predefined in the
190+
configuration ``/scp_actions`` field or raw XML is provided. If a template is
191+
defined, then variables can be specified via this syntax: ``[[ var ]]``.
192+
193+
**XML Variant**
194+
195+
============== ========== ============== =====================================================
196+
Parameter Required Type Description
197+
============== ========== ============== =====================================================
198+
``xml`` yes string Raw XML data to send to simulator
199+
============== ========== ============== =====================================================
200+
201+
**Template Variant**
202+
203+
============== ========== ============== =====================================================
204+
Parameter Required Type Description
205+
============== ========== ============== =====================================================
206+
``template`` yes string | Switch to enable or disable
207+
``data`` no bool | Push button to cycle distance profiles.
208+
============== ========== ============== =====================================================
209+
210+
Inline short-form is supported as a reference to template to use. No data
211+
interpolation is supported in this form:
212+
213+
.. code:: text
214+
215+
vtd/scp=stop
216+
217+
**Examples**
218+
219+
Given the following simulator configuration:
220+
221+
.. code-block:: yaml
222+
223+
version: "4"
224+
defaults:
225+
simulators:
226+
binding: vtd
227+
args:
228+
scp_actions:
229+
simctrl: "<SimCtrl><[[cmd]]/></SimCtrl>"
230+
stop: >
231+
<SimCtrl>
232+
<Stop/>
233+
</SimCtrl>
234+
235+
The the following triggers are all identical:
236+
237+
.. code-block:: json
238+
239+
[
240+
{
241+
"event": "time=5",
242+
"action": "vtd/scp=stop"
243+
},
244+
{
245+
"event": "time=5",
246+
"action": {
247+
"name": "vtd/scp",
248+
"xml": "<SimCtrl><Stop/></SimCtrl>"
249+
}
250+
},
251+
{
252+
"event": "time=5",
253+
"action": {
254+
"name": "vtd/scp",
255+
"template": "simctrl",
256+
"data": {
257+
"cmd": "Stop"
258+
}
259+
}
260+
}
261+
]
170262
171263
Example
172264
-------

optional/vtd/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ cloe_add_plugin(
4646
TARGET ${PROJECT_NAME}
4747
OUTPUT_NAME simulator_vtd
4848
SOURCES
49+
src/scp_action.cpp
4950
src/vtd_binding.cpp
5051
LINK_LIBRARIES
5152
vtd-object-lib

optional/vtd/src/scp_action.cpp

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2022 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file scp_action.cpp
20+
* \see scp_action.hpp
21+
*/
22+
23+
#include "scp_action.hpp"
24+
25+
#include <cloe/trigger.hpp> // for TriggerSchema, ActionPtr
26+
#include <cloe/utility/inja.hpp> // for inja
27+
28+
namespace vtd {
29+
30+
cloe::TriggerSchema ScpActionFactory::schema() const {
31+
using namespace cloe;
32+
return TriggerSchema{
33+
this->name(),
34+
this->description(),
35+
cloe::InlineSchema("template reference as defined in scp_actions configuration"),
36+
schema::Variant{
37+
Schema{
38+
{"xml", make_prototype<std::string>("raw SCP text to send").require()}
39+
},
40+
Schema{
41+
{"template", make_prototype<std::string>("use predefined template").require()},
42+
{"data", make_prototype<Conf>("map of template parameters")},
43+
}
44+
},
45+
};
46+
}
47+
48+
cloe::ActionPtr ScpActionFactory::make(const cloe::Conf& c) const {
49+
if (c.has("xml")) {
50+
auto text = c.get<std::string>("xml");
51+
return std::make_unique<ScpAction>(name(), scp_client_, text);
52+
} else {
53+
auto key = c.get<std::string>("template");
54+
auto tmpl = predefined_.at(key);
55+
std::string output;
56+
if (c.has("data")) {
57+
cloe::Json data = *c.at("data");
58+
output = cloe::utility::inja_env().render(tmpl, data);
59+
} else {
60+
output = tmpl;
61+
}
62+
return std::make_unique<ScpAction>(name(), scp_client_, output);
63+
}
64+
}
65+
66+
cloe::ActionPtr ScpActionFactory::make(const std::string& s) const {
67+
return make(cloe::Conf{cloe::Json{
68+
{"template", s}
69+
}});
70+
}
71+
72+
} // namespace vtd

0 commit comments

Comments
 (0)