Two types of plugins can be added to the Virtual Device runner:
- Item Plugins - for generating primitive values.
- Sample Plugins - for generating complete sample payloads including their primitive item values.
Plugins are jar files with a main class extending one of two classes:
ItemGenPlugin
for Item plugins. The key method from this class that a plugin must override is:genData()
- handles the generation of primitive values.
SamplePlugin
for Sample plugins. Extended classes will need to override or implement the following methods.<? extends SamplePlugin> create(<? extends SamplePluginConf> conf)
- a static factory method used to generate a plugin instance.upate()
- called to update the sample.toJson()
- serializes the sample contents to the JSON payload.onLoad()
- to initialize a new instance of the sample, e.g. for setting special fields or reading additional files.applyProps()
- to apply any special properties from plugin properties.
Plugins must also contain a plugin.props
file. At a minimum this file needs to define the following properties:
plugin.main
- the name of the main class extendingItemGenPlugin
orSamplePlugin
.plugin.name
- a string value for working with the class elsewhere, for example in an Item Configuration.plugin.description
- what the plugin does.plugin.version
- developmental iteration of the plugin.plugin.type
- one ofItem
orSample
, needed byPluginLoader
.plugin.resultType
- for Sample Plugins this can be an object serialized as aJson
string. For Item Plugins this can be the primitive type returned by thegenData()
method:Double
,Long
,String
.
plugin.label
- default label to be used when serializing the results of an Item Plugin as part of a sample. This value can be overridden on YAML configuration files.
For primitive results of type Double
a precision property plugin.dec.prec
of type integer can be added, to specify globally for the plugin the floating point decimal precision of a value to be used during serialization. This can also be set in configuration files through the field prec
.
Additional properties can be defined for use in specific plugins.
When the default device runner starts it scans the plugins/
directory for any jar files and attempts to load them into the environment. Subdirectories will not be scanned. They are therefore an ideal location for storing examples, that can be activated by being copied into the plugins/
directory before starting the runner. The copies can also be removed from the directory to remove them from the environment on a subsequent device runner load and run.
When first loaded the plugin class is generated and saved into a registry. Later, new instances get generated and bound to samples or items based on their YAML configurations. During the remainder of runtime the plugin needs to generate data based on the plugin type.
- ItemPlugin - generates primitive data to be used in sample payload fields. This is done through the overloaded
genData()
method. It must generate and return a value of the type specified by the propertyplugin.resultType
. - SamplePlugin - generates a complete JSON sample payload. This is done through the overloaded
update()
method.
These methods will be repeatedly called based on the interval
property of the device to which they are attached.
Plugins include an enabled
field, whch can be used to switch on or off data generation as needed. It depends upon the plugin implementation. enabled
should be set to true
in either the onLoad
or create
method.
Plugin instances last until they and the items or samples to which they are bound get garbage collected. Generally this means for the life of the device runner. Class references in the registry last for the life of the device runner.
Once an Item Plugin is loaded it can be referenced in an Item configuration.
- name: "speed"
label: "speed"
type: "Plugin"
pluginName: "AcceleratorPlugin"
resultType: "Double"
Any Item instance created from this configuration will look for the plugin class and its properties by pluginName
in the registry. In this case "AcceleratorPlugin". The value of pluginName
must match the property plugin.name
from the plugin.props
file. The factory class ItemPluginMill
wraps a registry and can generate a new instance of the ItemPlugin. This instance then gets assigned as the data generator for that item instance.
ItemGenPlugin
The abstract class ItemGenPlugin
, which extends DataGenPlugin
, provides the basis for any ItemPlugin to be loaded into the Virtual Device runtime. Two abstract methods need to be implemented.
onLoad()
- intended to be used to set up any background or global values needed by a plugin instance. For example, it is a good place to set theenabled
property totrue
.genData()
- returns an object. The return value needs to be of the type defined inplugin.resultType
.
An Example Item Plugin
AcceleratorPlugin.java
package io.bonitoo.virdev.plugin;
import io.bonitoo.qa.conf.data.ItemConfig;
import io.bonitoo.qa.plugin.item.ItemGenPlugin;
import io.bonitoo.qa.plugin.PluginProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
public class AcceleratorPlugin extends ItemGenPlugin {
static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static double INITIAL_SPEED = 1.0;
private static double INITIAL_ACCEL = 1.0;
private static double SPEED_LIMIT = 60.0;
double speed; // m/s
double accel; // m/s**
long lastRecordStamp;
public AcceleratorPlugin() {
this.speed = INITIAL_SPEED;
this.accel = INITIAL_ACCEL;
this.lastRecordStamp = System.currentTimeMillis();
}
@Override
public Double getCurrentVal() {
return speed;
}
public AcceleratorPlugin(PluginProperties props, ItemConfig config, boolean enabled, double speed, double accel) {
super(props, config, enabled);
this.speed = speed;
this.accel = accel;
this.lastRecordStamp = System.currentTimeMillis();
}
@Override
public void onLoad() {
this.speed = props.getProperties().getProperty("initial.speed") == null ?
INITIAL_SPEED : Double.parseDouble(props.getProperties().getProperty("initial.speed"));
this.accel = props.getProperties().getProperty("initial.accel") == null ?
INITIAL_ACCEL : Double.parseDouble(props.getProperties().getProperty("initial.accel"));
this.enabled = true;
}
@Override
public Object genData() {
accel = changeAccel(accel, speed);
long currTimeStamp = System.currentTimeMillis();
double timeFactor = currTimeStamp - lastRecordStamp;
lastRecordStamp = currTimeStamp;
speed += (accel * (timeFactor / 1000));
logger.info(String.format("speed: %.5f", speed));
return speed;
}
private double changeAccel(double curAccel, double curSpeed) {
double delta = Math.random() * 2;
if (curSpeed < 0) {
if (curSpeed < SPEED_LIMIT / -3) {
return curAccel + delta;
} else {
return curAccel - delta;
}
} else {
if (curSpeed > SPEED_LIMIT / 3) {
return curAccel - delta;
} else {
return curAccel + delta;
}
}
}
}
plugin.props for the above
plugin.main=io.bonitoo.virdev.plugin.AcceleratorPlugin
plugin.name=AcceleratorPlugin
plugin.description=A toy plugin for oscillating speed values
plugin.version=0.1
plugin.type=Item
plugin.resultType=Double
plugin.label=speed
initial.speed=3.0
initial.accel=0.5
A configuration using the plugin
- name: "speed"
label: "speed"
type: "Plugin"
pluginName: "AcceleratorPlugin"
resultType: "Double"
It may occur that an item plugin needs to handle more values in its configuration than those made available by default. In this case a custom configuration extending the ItemPluginConfig
class can be added to the plugin. This new configuration needs to be declared through the annotation @ItemPluginConfigClass(conf = <Configuration.Class>)
used at the start of the definition of the plugin class. Furthermore, it needs an associated deserializer extending the class ItemConfigDeserializer
. See the SimpleMovingAverage plugin for more insight.
A Sample Plugin requires the extension of the following class:
SamplePlugin
- e.g.InfluxLpSamplePlugin
. This handles the core work of updating and generating data. Extended SamplePlugin main classes should also leverage the class level annotation@SamplePluginConfigClass
, whose fieldconf
should be set to the plugin configuration class (see below). e.g.@SamplePluginConfigClass(conf = InfluxLPSamplePluginConf.class)
.
Additionally, it is recommended for more complex Sample Plugins that the following be extended or created.
- A serializer for the sample plugin extending Jackson
StdSerializer
- e.g.InfluxLPSampleSerializer
. This converts data to a JSON payload string and should be leveraged in thetoJson()
method. - A configuration for the plugin extending
SamplePluginConf
- e.g.InfluxLPSamplePluginConf
. This adds extra parameters to the configuration, if necessary, and is passed to factory methods, whenever a new instance of the SamplePlugin is needed. - A configuration deserializer extending
SampleConfigDeserializer
- e.g.InfluxLPSamplePluginConfDeserializer
. This is necessary to generate the above configuration file from a YAML representation.
Failure to implement these additional classes means that their default SamplePlugin
equivalents are used and the results may be unpredictable.
SamplePlugin extended class main
The class referred to by the plugin property plugin.main
needs to override or implement the following methods.
public static Class<? extends SamplePlugin> create(Class<? extends SamplePluginConfig> conf)
- this factory style method works like a callback inSamplePluginMill
to generate new instances of the plugin.public Sample update()
- called by the device to update the internal state of the plugin, in other words to advance a counter or to generate new values for fields.public void onLoad()
- needed to set up the values for the initial state of the plugin instance. For example, setenabled=true
.public void applyProps(PluginProperties props)
- needed to set any special values defined in theplugin.props
file.public String toJson()
- needed to serialize the current state of the plugin to a JSON payload.
An Example Sample Plugin
SamplePlugin example
@Getter
@SamplePluginConfigClass(conf = LPFileReaderPluginConf.class)
@JsonSerialize(using = LPFileReaderPluginSerializer.class)
public class LPFileReaderPlugin extends SamplePlugin {
String lpFile;
List<LineProtocol> lines;
int index;
public static LPFileReaderPlugin create(LPFileReaderPluginConf conf){
// should have been loaded into SamplePluginMill by PluginLoader
return new LPFileReaderPlugin(SamplePluginMill.getPluginProps(conf.getPlugin()), conf);
}
public LPFileReaderPlugin(PluginProperties props, LPFileReaderPluginConf conf) {
super(props, conf);
lines = new ArrayList<>();
lpFile = conf.getSource() != null ? conf.getSource() : (String) props.getProperties().get("default.lp.file");
index = 0;
}
@Override
public LPFileReaderPlugin update(){
index++;
if(index >= lines.size()){
index = 0;
}
lines.get(index).setTimestamp(System.currentTimeMillis());
return this;
}
@Override
public String toJson() throws JsonProcessingException {
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
return ow.writeValueAsString(this);
}
protected File resolveSourceFile() throws URISyntaxException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL fileUrl = loader.getResource(lpFile);
File result;
if( fileUrl != null){ // located as resource
return new File(Objects.requireNonNull(loader.getResource(lpFile)).toURI());
}
return new File(lpFile);
}
@Override
public void onLoad(){
super.onLoad();
try {
File inputFile = resolveSourceFile();
try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
String line;
while((line = br.readLine()) != null){
LineProtocol lp = LineProtocol.parseLP(line);
if(lp != null) {
lines.add(LineProtocol.parseLP(line));
}
}
} catch (IOException e) {
throw new LPFileReaderPluginException(e);
}
} catch (URISyntaxException e) {
throw new LPFileReaderPluginException(e);
}
}
@Override
public void applyProps(PluginProperties pluginProperties) {
// holder
}
public LineProtocol get(int ndx){
return lines.get(ndx);
}
public LineProtocol getCurrent(){
return get(this.index);
}
}
plugin.props for the above
plugin.main=io.bonitoo.virdev.plugin.LPFileReaderPlugin
plugin.name=LPFileReader
plugin.description=Reads Line Protocol files and sends to MQTT
plugin.version=0.1
plugin.type=Sample
plugin.resultType=Json
plugin.label=lp
default.lp.file=test.lp
A Sample configuration using the above
- id: "random"
name: "LPFileReaderConf"
topic: "test/linep"
items: [ ]
plugin: "LPFileReader"
source: "./plugins/examples/lpFileReader/data/myTestLP.lp"
Note above that an empty array of Items
is needed. This is required by the default sample configuration deserializer. Failure to include it will result in an exception.