-
Notifications
You must be signed in to change notification settings - Fork 71
Device Models
Each Simulated Device belongs to a specific Device Type, which defines the simulation behavior, for example how frequently telemetry is sent, what kind of messages to send, which methods are supported, etc.
Device types are defined using a set of JSON configuration files, (one file for each device type), and a set of Javascript files containing the functions to generate the random data. Normally a simulation service should have:
- One JSON file for each device type (e.g. elevator.json)
- One Javascript file for each device type (e.g. elevator-state.js)
- One Javascript file for each device method (e.g. elevator-go-down.js)
The DeviceTypes folder contains some examples of these files, showing how to define a device type and how to simulate a device behavior.
Device Type JSON example
{
"SchemaVersion": "1.0.0",
"Id": "8dd272c0-514e-4a87-a1ad-be12a0b2809c",
"Version": "0.0.1",
"Name": "Elevator",
"Description": "Simulated Elevator with floor number sensor",
"Protocol": "AMQP",
"DeviceState": {
"Initial": {
"floor": 1
},
"SimulationInterval": "00:00:15",
"SimulationScript": {
"Type": "javascript",
"Path": "elevator-state.js"
}
},
"Telemetry": [
{
"Interval": "00:00:01",
"MessageTemplate": "{\"current_floor\": ${floor}}",
"MessageSchema": {
"Name": "elevator;v1",
"Format": "JSON",
"Fields": {
"current_floor": "integer"
}
}
}
],
"CloudToDeviceMethods": {
"Start": {
"Type": "javascript",
"Path": "TBD.js"
}
}
}Device Type Javascript simulation example
var state = {
floor: 1,
direction: "up"
};
function main(context, previousState) {
restoreState(previousState);
if (typeof(state.direction) === "undefined" || state.direction === null
|| (state.direction !== "up" && state.direction !== "down")) {
state.direction = "up";
}
if (state.direction === "up") state.floor++;
if (state.direction === "down") state.floor--;
if (state.floor < 1) {
state.floor = 1;
state.direction = "up";
}
if (state.floor > 10) {
state.floor = 10;
state.direction = "down";
}
return state;
}
function restoreState(previousState) {
if (typeof(previousState) !== "undefined" && previousState !== null) {
log("Using default state");
state = previousState;
}
}Thanks to these files, it is possible to add new device types and customize the simulation behavior, without rebuilding the service. To add new types or to change the simulated behavior, edit the relevant *.json and *.js files and redeploy the device simulation microservice.
Each device type file contains the definition of the simulated device type, including the following information:
- Device type name: string
- Protocol: AMQP | MQTT | HTTP
- The initial device state
- How often to refresh the device state
- Which Javascript file to use to generate the device state
- A list of telemetry messages to send, each with a specific frequency
- The schema of the telemetry messages, used by backend application to parse the telemetry received
- A list of supported methods and the Javascript file to use to simulate the method.
Check the DeviceTypes folder for some examples.
The schema version is always "1.0.0" and is specific to the format of this file:
"SchemaVersion": "1.0.0"The following properties describe the device type. Each type has a unique identifier, a semantic version, a name and a description:
"Id": "12345678-1234-1234-1234-123456789012",
"Version": "0.0.1",
"Name": "Chiller",
"Description": "Simulated Chiller with temperature and humidity sensors"IoT devices can connect using different protocols. The simulation allows to use either AMQP, MQTT or HTTP:
"Protocol": "AMQP"Each simulated device has an internal state, which needs to be defined. The state also defines the properties that can be reported in telemetry. For example an elevator might have an initial state like:
"Initial": {
"floor": 1
}
while a room with multiple sensors might have more properties, like:
"Initial": {
"temperature": 50.5,
"temperature_unit": "F",
"humidity": 50,
"humidity_unit": "%",
"lights_on": false
}
The device state is kept in memory by the simulation service, and provided in input to the Javascript function. The Javascript function can decide to ignore the state and generate some random data, or to update the device status in some realistic way, given a desired scenario.
The function generating the state receives in input also the Device Id, and the current time, so it is possible to generate different data by device and by time.
The simulation service can send multiple messages for each device, and each message can be sent at a different frequency. Typically, a telemetry will send a message including some data from the device state. For example, a simulated room might send information about temperature and humidity every 10 seconds, and lights status once per minute. Note the placeholders, which are automatically replaced with values from the device state:
"Telemetry": [
{
"Interval": "00:00:10",
"MessageTemplate":
"{\"t\":${temperature},\"t_u\":\"${temperature_unit}\",\"h\":\"${humidity}\"}",
"MessageSchema": {
"Name": "RoomComfort;v1",
"Format": "JSON",
"Fields": {
"t": "double",
"t_u": "text",
"h": "integer"
}
}
},
{
"Interval": "00:01:00",
"MessageTemplate": "{\"lights\":${lights_on}}",
"MessageSchema": {
"Name": "RoomLights;v1",
"Format": "JSON",
"Fields": {
"lights": "boolean"
}
}
}
],
The placeholders use a special syntax ${NAME} where NAME is a key from
the device state object returned by the Javascript main() function.
Each message type must have a well defined schema. The message schema is also published to IoT Hub, so that backend applications reuse this information to interpret the incoming telemetry.
The schema supports JSON format, which allows for easy parsing, transformation and analytics, across several systems and services.
The fields listed in the schema can be of the following types:
- Object - serialized using JSON
- Binary - serialized using base64
- Text
- Boolean
- Integer
- Double
- DateTime
Simulated devices can also react to method calls, in which case they will execute some logic and provide some response. Similarly to the simulation, the method logic is stored in a Javascript file, and can interact with the device state. For example:
"CloudToDeviceMethods": {
"Start": {
"Type": "javascript",
"Path": "truck-start.js"
}
}
Functions are stored in Javascript files, which are loaded and executed at runtime, using Jint, a Javascript interpreter for .NET.
The Javascript files must have a main function, and accept two parameters:
- a context object which contains two properties:
-
currentTimeas a string with formatyyyy-MM-dd'T'HH:mm:sszzz -
deviceId, e.g. "Simulated.Elevator.123" -
deviceType, e.g. "Elevator"
-
- a
stateobject, which is the value returned by the function in the previous call. This is device state maintained by the simulation service, and used to generate telemetry messages.
The main function returns the new device state. Example:
function main(context, state) {
// Use context if the simulation depends on
// time or device details.
// Execute some logic, updating 'state'
return state;
}While it's not possible to attach a debugger to the Javascript interpreter,
it's possible to log information in the service log. For convenience, the
application provides a log() function which can be used to save information
useful to track and debug the function execution. In cases of syntax errors,
the interpreter will fail, and the service log will contain some information
about the Jint.Runtime.JavaScriptException exception occurred.
Logging example:
function main(context, state) {
log("This message will appear in the service logs.");
log(context.deviceId);
if (typeof(state) !== "undefined" && state !== null) {
log("Previous value: " + state.temperature);
}
// ...
return state;
}