This PR provides method support for chillers in the default simulation: reboot, firmware update, increase pressure, and decrease pressure.  It also keeps reported properties in sync with desired properties (by polling for value changes).

* DeviceMethod implmentation class

* First round of methods, added: 1) infra for registering methods for devices, 2) walking avail. methods for each device.  Main functionality missing is allowing specifation and then running the method script.

* remove todo from actor

* nits to code style

* Move methods ownership from devicebootstrap to deviceclient.

* error handling and todos.

* retry method registration, minor fixes.

* nits, logging, removed exception handling for registering methods (it's handled by the parent method)

* bumped version

* code style nits

* Methods use scripts.

Desired properties are polled in UpdateDeviceState and delta is copied into Reported property values.

Telemetry is not sent if the device is marked other than online == "True"

* code style/cleanup changes.

change get query for simulations to linq query rather than iterating the list.

* update version

* Exception handling for updatedevicestate

nit for rename method execution endpoint to include Async suffix.

* nit fix comment code style

* Inject scriptinterpreter. refactoring.

* nits

* state update bug

* no need for scriptengine null check

* Bootstrap individual methods, implement decreasepressure aka EMergencyValveRelease

* remove connection string :(

* fixed threading problem w/ methods, implemented & tested first four methods

* Implement switch to turn random telemetry off; e.g. when increasepressure is called, wait until decreasepressure is called to turn it back on.

* remove clearing of DeviceMethodStatus

* Code style changes + fix some jslint errors

* Fix js methods files

* DeviceMethods code style changes

* Devices.cs code style

* JavascriptInterpreter.cs code style

* script changes for messages, docs

* Cleanup up Javascript functions, create Issue to track implementing functions not yet done.

* Halve the amount of telemetry sent

* Address PR comments.

* issue for script interpreter todo

* todos, nits, pr feedback
This commit is contained in:
Tim Laverty 2017-09-07 15:08:26 -07:00 коммит произвёл GitHub
Родитель 6ca39dd56c
Коммит c33241a5cd
46 изменённых файлов: 895 добавлений и 276 удалений

Просмотреть файл

@ -8,6 +8,7 @@ using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
@ -24,25 +25,24 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
Task UpdateTwinAsync(Device device);
void RegisterMethodsForDevice(IDictionary<string, Script> methods);
void RegisterMethodsForDevice(IDictionary<string, Script> methods, Dictionary<string, object> deviceState);
}
public class DeviceClient : IDeviceClient
{
private const string DateFormat = "yyyy-MM-dd'T'HH:mm:sszzz";
private const string DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:sszzz";
// See also https://github.com/Azure/toketi-iothubreact/blob/master/src/main/scala/com/microsoft/azure/iot/iothubreact/MessageFromDevice.scala
private const string CreationTimeProperty = "$$CreationTimeUtc";
private const string CREATION_TIME_PROPERTY = "$$CreationTimeUtc";
private const string MessageSchemaProperty = "$$MessageSchema";
private const string ContentProperty = "$$ContentType";
private const string MESSAGE_SCHEMA_PROPERTY = "$$MessageSchema";
private const string CONTENT_PROPERTY = "$$ContentType";
private readonly Azure.Devices.Client.DeviceClient client;
private readonly ILogger log;
private readonly IoTHubProtocol protocol;
private readonly string deviceId;
private readonly IScriptInterpreter scriptInterpreter;
//used to create method pointers for the device for the IoTHub to callback to
private DeviceMethods deviceMethods;
@ -50,32 +50,41 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public DeviceClient(
Azure.Devices.Client.DeviceClient client,
IoTHubProtocol protocol,
ILogger logger,
string deviceId)
ILogger logger,
string deviceId,
IScriptInterpreter scriptInterpreter)
{
this.client = client;
this.protocol = protocol;
this.log = logger;
this.deviceId = deviceId;
this.scriptInterpreter = scriptInterpreter;
}
public IoTHubProtocol Protocol { get { return this.protocol; } }
public void RegisterMethodsForDevice(IDictionary<string, Script> methods)
public IoTHubProtocol Protocol
{
log.Debug("Attempting to setup methods for device", () => new
get { return this.protocol; }
}
public void RegisterMethodsForDevice(IDictionary<string, Script> methods,
Dictionary<string, object> deviceState)
{
this.log.Debug("Attempting to setup methods for device", () => new
{
this.deviceId
this.deviceId
});
deviceMethods = new DeviceMethods(this.client, log, methods, this.deviceId);
// TODO: Inject through the constructor instead
this.deviceMethods = new DeviceMethods(this.client, this.log, methods, deviceState, this.deviceId,
this.scriptInterpreter);
}
public async Task SendMessageAsync(string message, DeviceModel.DeviceModelMessageSchema schema)
{
var eventMessage = new Message(Encoding.UTF8.GetBytes(message));
eventMessage.Properties.Add(CreationTimeProperty, DateTimeOffset.UtcNow.ToString(DateFormat));
eventMessage.Properties.Add(MessageSchemaProperty, schema.Name);
eventMessage.Properties.Add(ContentProperty, "JSON");
eventMessage.Properties.Add(CREATION_TIME_PROPERTY, DateTimeOffset.UtcNow.ToString(DATE_FORMAT));
eventMessage.Properties.Add(MESSAGE_SCHEMA_PROPERTY, schema.Name);
eventMessage.Properties.Add(CONTENT_PROPERTY, "JSON");
await this.SendRawMessageAsync(eventMessage);
@ -117,7 +126,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
var props = azureTwin.Properties.Reported.GetEnumerator();
while (props.MoveNext())
{
var current = (KeyValuePair<string, object>)props.Current;
var current = (KeyValuePair<string, object>) props.Current;
if (!device.Twin.ReportedProperties.ContainsKey(current.Key))
{

Просмотреть файл

@ -2,100 +2,140 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
using System.Net;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
{
public interface IDeviceMethods
{
Task<MethodResponse> MethodExecution(MethodRequest methodRequest, object userContext);
}
public class DeviceMethods : IDeviceMethods
public class DeviceMethods
{
private static readonly TimeSpan retryMethodCallbackRegistration = TimeSpan.FromSeconds(10);
private readonly Azure.Devices.Client.DeviceClient client;
private readonly ILogger log;
private readonly IScriptInterpreter scriptInterpreter;
private IDictionary<string, Script> cloudToDeviceMethods;
private string deviceId;
private readonly IDictionary<string, Script> cloudToDeviceMethods;
private readonly Dictionary<string, object> deviceState;
private readonly string deviceId;
public DeviceMethods(
Azure.Devices.Client.DeviceClient client,
ILogger logger,
IDictionary<string, Script> methods,
string device)
IDictionary<string, Script> methods,
Dictionary<string, object> deviceState,
string device,
IScriptInterpreter scriptInterpreter)
{
this.client = client;
this.log = logger;
this.cloudToDeviceMethods = methods;
this.deviceId = device;
this.deviceState = deviceState;
this.scriptInterpreter = scriptInterpreter;
this.SetupMethodCallbacksForDevice();
}
public async Task<MethodResponse> MethodExecution(MethodRequest methodRequest, object userContext)
public async Task<MethodResponse> MethodHandlerAsync(MethodRequest methodRequest, object userContext)
{
this.log.Info("Creating task to execute method with json payload.", () => new
{
this.deviceId,
methodName = methodRequest.Name,
methodRequest.DataAsJson
});
// Kick the method off on a separate thread & immediately return
// Not immediately returning would block the client connection to the hub
var t = Task.Run(() => this.MethodExecution(methodRequest));
return new MethodResponse(
Encoding.UTF8.GetBytes("Executed Method:" + methodRequest.Name),
(int) HttpStatusCode.OK);
}
private void MethodExecution(MethodRequest methodRequest)
{
try
{
this.log.Info("Executing method with json payload.", () => new {methodRequest.Name,
methodRequest.DataAsJson, this.deviceId});
//TODO: Use JavaScript engine to execute methods.
//lock (actor.DeviceState)
//{
// actor.DeviceState = this.scriptInterpreter.Invoke(
// this.deviceModel.Simulation.Script,
// scriptContext,
// actor.DeviceState);
//}
string result = "'I am the simulator. Someone called " + methodRequest.Name + ".'";
this.log.Info("Executing method with json payload.", () => new
{
this.deviceId,
methodName = methodRequest.Name,
methodRequest.DataAsJson
});
this.log.Info("Executed method.", () => new {methodRequest.Name});
byte[] resultEncoded = Encoding.UTF8.GetBytes(result);
return new MethodResponse(resultEncoded, (int)HttpStatusCode.OK);
var scriptContext = new Dictionary<string, object>
{
["currentTime"] = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:sszzz"),
["deviceId"] = this.deviceId,
// TODO: add "deviceModel" so that the method scripts can use it like the "state" scripts
//["deviceModel"] = this.device.
};
if (methodRequest.DataAsJson != "null")
this.AddPayloadToContext(methodRequest.DataAsJson, scriptContext);
this.log.Debug("Executing method for device", () => new
{
this.deviceId,
methodName = methodRequest.Name,
this.deviceState
});
// ignore the return state - state updates are handled by callbacks from the script
this.scriptInterpreter.Invoke(
this.cloudToDeviceMethods[methodRequest.Name],
scriptContext,
this.deviceState);
this.log.Debug("Executed method for device", () => new { this.deviceId, methodRequest.Name });
}
catch (Exception e)
{
this.log.Error("Error while executing method for device",
() => new {
methodRequest.Name,
methodRequest.DataAsJson,
() => new
{
this.deviceId,
methodName = methodRequest.Name,
methodRequest.DataAsJson,
e
});
return new MethodResponse(Encoding.UTF8.GetBytes("Error while executing method for device"),
(int)HttpStatusCode.InternalServerError);
}
}
private void AddPayloadToContext(string dataAsJson, Dictionary<string, object> scriptContext)
{
var values = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataAsJson);
foreach (var item in values)
scriptContext.Add(item.Key, item.Value);
}
private void SetupMethodCallbacksForDevice()
{
this.log.Debug("Setting up methods for device.", () => new {
this.cloudToDeviceMethods.Count,
this.deviceId
this.log.Debug("Setting up methods for device.", () => new
{
this.deviceId,
methodsCount = this.cloudToDeviceMethods.Count
});
//walk this list and add a method handler for each method specified
// walk this list and add a method handler for each method specified
foreach (var item in this.cloudToDeviceMethods)
{
this.log.Debug("Setting up method for device.", () => new {item.Key,this.deviceId});
this.log.Debug("Setting up method for device.", () => new { item.Key, this.deviceId });
this.client.SetMethodHandlerAsync(item.Key, MethodExecution, null).
Wait(retryMethodCallbackRegistration);
this.client.SetMethodHandlerAsync(item.Key, this.MethodHandlerAsync, null)
.Wait(retryMethodCallbackRegistration);
this.log.Debug("Method for device setup successfully", () => new {
item.Key,
this.deviceId
this.log.Debug("Method for device setup successfully", () => new
{
this.deviceId,
methodName = item.Key
});
}
}

Просмотреть файл

@ -22,7 +22,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public class DeviceModels : IDeviceModels
{
private const string Ext = ".json";
private const string EXT = ".json";
private readonly IServicesConfig config;
private readonly ILogger log;
@ -69,10 +69,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public DeviceModel Get(string id)
{
var list = this.GetList();
foreach (var x in list)
{
if (x.Id == id) return x;
}
var item = list.FirstOrDefault(i => i.Id == id);
if (item != null)
return item;
this.log.Warn("Device model not found", () => new { id });
@ -87,7 +86,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
var fileEntries = Directory.GetFiles(this.config.DeviceModelsFolder);
this.deviceModelFiles = fileEntries.Where(fileName => fileName.EndsWith(Ext)).ToList();
this.deviceModelFiles = fileEntries.Where(fileName => fileName.EndsWith(EXT)).ToList();
this.log.Debug("Device model files", () => new { this.deviceModelFiles });

Просмотреть файл

@ -7,6 +7,7 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
using Device = Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.Device;
using TransportType = Microsoft.Azure.Devices.Client.TransportType;
@ -15,7 +16,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public interface IDevices
{
Task<Tuple<bool, string>> PingRegistryAsync();
IDeviceClient GetClient(Device device, IoTHubProtocol protocol);
IDeviceClient GetClient(Device device, IoTHubProtocol protocol, IScriptInterpreter scriptInterpreter);
Task<Device> GetOrCreateAsync(string deviceId);
Task<Device> GetAsync(string deviceId);
Task<Device> CreateAsync(string deviceId);
@ -51,42 +52,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
}
}
public IDeviceClient GetClient(Device device, IoTHubProtocol protocol)
public IDeviceClient GetClient(Device device, IoTHubProtocol protocol, IScriptInterpreter scriptInterpreter)
{
var connectionString = $"HostName={device.IoTHubHostName};DeviceId={device.Id};SharedAccessKey={device.AuthPrimaryKey}";
Azure.Devices.Client.DeviceClient sdkClient;
switch (protocol)
{
case IoTHubProtocol.AMQP:
this.log.Debug("Creating AMQP device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Amqp_Tcp_Only);
break;
case IoTHubProtocol.MQTT:
this.log.Debug("Creating MQTT device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Mqtt_Tcp_Only);
break;
case IoTHubProtocol.HTTP:
this.log.Debug("Creating HTTP device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Http1);
break;
default:
this.log.Error("Unable to create a client for the given protocol",
() => new { protocol });
throw new Exception($"Unable to create a client for the given protocol ({protocol})");
}
return new DeviceClient(sdkClient, protocol, this.log, device.Id);
Azure.Devices.Client.DeviceClient sdkClient = this.GetDeviceSDKClient(device, protocol);
return new DeviceClient(sdkClient, protocol, this.log, device.Id, scriptInterpreter);
}
public async Task<Device> GetOrCreateAsync(string deviceId)
@ -146,10 +115,48 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
var azureTwin = await this.registry.GetTwinAsync(azureDevice.Id);
this.log.Debug("Writing device twin", () => new { azureDevice.Id });
azureTwin.Tags[DeviceTwin.SimulatedTagKey] = DeviceTwin.SimulatedTagValue;
azureTwin.Tags[DeviceTwin.SIMULATED_TAG_KEY] = DeviceTwin.SIMULATED_TAG_VALUE;
azureTwin = await this.registry.UpdateTwinAsync(azureDevice.Id, azureTwin, "*");
return new Device(azureDevice, azureTwin, this.ioTHubHostName);
}
private Azure.Devices.Client.DeviceClient GetDeviceSDKClient(Device device, IoTHubProtocol protocol)
{
var connectionString = $"HostName={device.IoTHubHostName};DeviceId={device.Id};SharedAccessKey={device.AuthPrimaryKey}";
Azure.Devices.Client.DeviceClient sdkClient;
switch (protocol)
{
case IoTHubProtocol.AMQP:
this.log.Debug("Creating AMQP device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Amqp_Tcp_Only);
break;
case IoTHubProtocol.MQTT:
this.log.Debug("Creating MQTT device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Mqtt_Tcp_Only);
break;
case IoTHubProtocol.HTTP:
this.log.Debug("Creating HTTP device client",
() => new { device.Id, device.IoTHubHostName });
sdkClient = Azure.Devices.Client.DeviceClient.CreateFromConnectionString(connectionString, TransportType.Http1);
break;
default:
this.log.Error("Unable to create a client for the given protocol",
() => new { protocol });
throw new Exception($"Unable to create a client for the given protocol ({protocol})");
}
return sdkClient;
}
}
}

Просмотреть файл

@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json.Linq;

Просмотреть файл

@ -10,8 +10,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
public class DeviceTwin
{
// Simulated devices are marked with a tag "IsSimulated = Y"
public const string SimulatedTagKey = "IsSimulated";
public const string SimulatedTagValue = "Y";
public const string SIMULATED_TAG_KEY = "IsSimulated";
public const string SIMULATED_TAG_VALUE = "Y";
public string Etag { get; set; }
public string DeviceId { get; set; }
@ -29,29 +30,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
this.Tags = TwinCollectionToDictionary(twin.Tags);
this.DesiredProperties = TwinCollectionToDictionary(twin.Properties.Desired);
this.ReportedProperties = TwinCollectionToDictionary(twin.Properties.Reported);
this.IsSimulated = this.Tags.ContainsKey(SimulatedTagKey) && this.Tags[SimulatedTagKey].ToString() == SimulatedTagValue;
this.IsSimulated = this.Tags.ContainsKey(SIMULATED_TAG_KEY) && this.Tags[SIMULATED_TAG_KEY].ToString() == SIMULATED_TAG_VALUE;
}
}
/*
TODO: remove if not required
public Twin ToAzureModel()
{
var properties = new TwinProperties
{
Desired = DictionaryToTwinCollection(this.DesiredProperties),
Reported = DictionaryToTwinCollection(this.ReportedProperties),
};
return new Twin(this.DeviceId)
{
ETag = this.Etag,
Tags = DictionaryToTwinCollection(this.Tags),
Properties = properties
};
}
*/
/*
JValue: string, integer, float, boolean
JArray: list, array
@ -75,7 +57,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
{
if (twin.Value is JToken)
{
result.Add(twin.Key, (JToken)twin.Value);
result.Add(twin.Key, (JToken) twin.Value);
}
else
{

Просмотреть файл

@ -36,6 +36,48 @@
<None Update="data\devicemodels\prototype-02.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\EmptyTank-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\FillTank-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\RestartEngine-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\StartElevator-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\StartDevice-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\StopElevator-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\TempIncrease-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\TempDecrease-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\DecreasePressure-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\IncreasePressure-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\MoveDevice-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\StopDevice-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\FirmwareUpdate-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\scripts\reboot-method.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\devicemodels\truck-01.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Просмотреть файл

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Jint;
using Jint.Native;
using Jint.Runtime.Descriptors;
@ -24,6 +25,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
{
private readonly ILogger log;
private readonly string folder;
private Dictionary<string, object> deviceState;
public JavascriptInterpreter(
IServicesConfig config,
@ -43,18 +45,26 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
Dictionary<string, object> context,
Dictionary<string, object> state)
{
this.deviceState = state;
var engine = new Engine();
// Inject the logger in the JS context, to allow the JS function
// logging into the service logs
engine.SetValue("log", new Action<object>(this.JsLog));
//register callback for state updates
engine.SetValue("updateState", new Action<JsValue>(this.UpdateState));
//register sleep function for javascript use
engine.SetValue("sleep", new Action<int>(this.Sleep));
var sourceCode = this.LoadScript(filename);
this.log.Debug("Executing JS function", () => new { filename });
try
{
var output = engine.Execute(sourceCode).Invoke("main", context, state);
var output = engine.Execute(sourceCode).Invoke("main", context, this.deviceState);
var result = this.JsValueToDictionary(output);
this.log.Debug("JS function success", () => new { filename, result });
return result;
@ -134,5 +144,38 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
{
this.log.Debug("Log from JS", () => new { data });
}
private void Sleep(int timeInMs)
{
Task.Delay(timeInMs).Wait();
}
// TODO:Move this out of the scriptinterpreter class into DeviceClient to keep this class stateless
// https://github.com/Azure/device-simulation-dotnet/issues/45
private void UpdateState(JsValue data)
{
string key;
object value;
Dictionary<string, object> stateChanges;
this.log.Debug("Updating state from the script", () => new { data, this.deviceState });
stateChanges = this.JsValueToDictionary((JsValue) data);
//Update device state with the script data passed
lock (this.deviceState)
{
for (int i = 0; i < stateChanges.Count; i++)
{
key = stateChanges.Keys.ElementAt(i);
value = stateChanges.Values.ElementAt(i);
if (this.deviceState.ContainsKey(key))
{
this.log.Debug("state change", () => new { key, value });
this.deviceState[key] = value;
}
}
}
}
}
}

Просмотреть файл

@ -36,8 +36,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
switch (script.Type.ToLowerInvariant())
{
default:
this.log.Error("Unknowne script type", () => new { script.Type });
throw new NotSupportedException($"Unknowne script type `${script.Type}`.");
this.log.Error("Unknown script type", () => new { script.Type });
throw new NotSupportedException($"Unknown script type `${script.Type}`.");
case "javascript":
this.log.Debug("Invoking JS", () => new { script.Path, context, state });

Просмотреть файл

@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
@ -23,9 +22,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public class Simulations : ISimulations
{
private const string StorageCollection = "simulations";
private const string SimulationId = "1";
private const int DevicesPerModelInDefaultTemplate = 1;
private const string STORAGE_COLLECTION = "simulations";
private const string SIMULATION_ID = "1";
private const int DEVICES_PER_MODEL_IN_DEFAULT_TEMPLATE = 1;
private readonly IDeviceModels deviceModels;
private readonly IStorageAdapterClient storage;
@ -43,7 +42,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public async Task<IList<Models.Simulation>> GetListAsync()
{
var data = await this.storage.GetAllAsync(StorageCollection);
var data = await this.storage.GetAllAsync(STORAGE_COLLECTION);
var result = new List<Models.Simulation>();
foreach (var item in data.Items)
{
@ -57,7 +56,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public async Task<Models.Simulation> GetAsync(string id)
{
var item = await this.storage.GetAsync(StorageCollection, id);
var item = await this.storage.GetAsync(STORAGE_COLLECTION, id);
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
simulation.Etag = item.ETag;
return simulation;
@ -81,7 +80,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
}
// Note: forcing the ID because only one simulation can be created
simulation.Id = SimulationId;
simulation.Id = SIMULATION_ID;
simulation.Created = DateTimeOffset.UtcNow;
simulation.Modified = simulation.Created;
simulation.Version = 1;
@ -96,15 +95,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
simulation.DeviceModels.Add(new Models.Simulation.DeviceModelRef
{
Id = type.Id,
Count = DevicesPerModelInDefaultTemplate
Count = DEVICES_PER_MODEL_IN_DEFAULT_TEMPLATE
});
}
}
// Note: using UpdateAsync because the service generates the ID
var result = await this.storage.UpdateAsync(
StorageCollection,
SimulationId,
STORAGE_COLLECTION,
SIMULATION_ID,
JsonConvert.SerializeObject(simulation),
"*");
@ -119,10 +118,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
/// </summary>
public async Task<Models.Simulation> UpsertAsync(Models.Simulation simulation)
{
if (simulation.Id != SimulationId)
if (simulation.Id != SIMULATION_ID)
{
this.log.Warn("Invalid simulation ID. Only one simulation is allowed", () => { });
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SimulationId + "'.");
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SIMULATION_ID + "'.");
}
var simulations = await this.GetListAsync();
@ -135,14 +134,14 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
}
// Note: forcing the ID because only one simulation can be created
simulation.Id = SimulationId;
simulation.Id = SIMULATION_ID;
simulation.Created = DateTimeOffset.UtcNow;
simulation.Modified = simulation.Created;
simulation.Version = 1;
await this.storage.UpdateAsync(
StorageCollection,
SimulationId,
STORAGE_COLLECTION,
SIMULATION_ID,
JsonConvert.SerializeObject(simulation),
simulation.Etag);
@ -151,13 +150,13 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
public async Task<Models.Simulation> MergeAsync(SimulationPatch patch)
{
if (patch.Id != SimulationId)
if (patch.Id != SIMULATION_ID)
{
this.log.Warn("Invalid simulation ID.", () => new { patch.Id });
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SimulationId + "'.");
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SIMULATION_ID + "'.");
}
var item = await this.storage.GetAsync(StorageCollection, patch.Id);
var item = await this.storage.GetAsync(STORAGE_COLLECTION, patch.Id);
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
simulation.Etag = item.ETag;
@ -181,8 +180,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
simulation.Version += 1;
item = await this.storage.UpdateAsync(
StorageCollection,
SimulationId,
STORAGE_COLLECTION,
SIMULATION_ID,
JsonConvert.SerializeObject(simulation),
patch.Etag);
@ -191,10 +190,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
return simulation;
}
public async Task DeleteAsync(string id)
{
await this.storage.DeleteAsync(StorageCollection, id);
await this.storage.DeleteAsync(STORAGE_COLLECTION, id);
}
}
}

Просмотреть файл

@ -22,16 +22,16 @@
}
},
"Properties": {
"Type": "Chiller",
"Firmware": "1.0.0",
"FirmwareUpdateStatus": "",
"Location": "Building 43",
"Latitude": 47.638928,
"Longitude": -122.13476
"Type": "Chiller",
"Firmware": "1.0.0",
"DeviceMethodStatus": "",
"Location": "Building 43",
"Latitude": 47.638928,
"Longitude": -122.13476
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "chiller-temperature;v1",
@ -43,7 +43,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"humidity\":${humidity},\"humidity_unit\":\"${humidity_unit}\"}",
"MessageSchema": {
"Name": "chiller-humidity;v1",
@ -55,7 +55,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}",
"MessageSchema": {
"Name": "chiller-pressure;v1",
@ -70,19 +70,19 @@
"CloudToDeviceMethods": {
"Reboot": {
"Type": "javascript",
"Path": "TBD.js"
"Path": "Reboot-method.js"
},
"FirmwareUpdate": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "FirmwareUpdate-method.js"
},
"EmergencyValveRelease": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "DecreasePressure-method.js"
},
"IncreasePressure": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "IncreasePressure-method.js"
}
}
}

Просмотреть файл

@ -31,7 +31,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "chiller-temperature;v1",
@ -43,7 +43,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"humidity\":${humidity},\"humidity_unit\":\"${humidity_unit}\"}",
"MessageSchema": {
"Name": "chiller-humidity;v1",
@ -55,7 +55,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}",
"MessageSchema": {
"Name": "chiller-pressure;v1",
@ -70,19 +70,19 @@
"CloudToDeviceMethods": {
"Reboot": {
"Type": "javascript",
"Path": "TBD.js"
"Path": "Reboot-method.js"
},
"FirmwareUpdate": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "FirmwareUpdate-method.js"
},
"EmergencyValveRelease": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "DecreasePressure-method.js"
},
"IncreasePressure": {
"Type": "javascript",
"Path": "TBD.js"
"Type": "javascript",
"Path": "IncreasePressure-method.js"
}
}
}

Просмотреть файл

@ -28,7 +28,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"floor\":${floor}}",
"MessageSchema": {
"Name": "elevator-floor;v1",
@ -39,7 +39,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"vibration\":${vibration},\"vibration_unit\":\"${vibration_unit}\"}",
"MessageSchema": {
"Name": "elevator-vibration;v1",
@ -51,7 +51,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "elevator-temperature;v1",

Просмотреть файл

@ -28,7 +28,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"floor\":${floor}}",
"MessageSchema": {
"Name": "elevator-floor;v1",
@ -39,7 +39,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"vibration\":${vibration},\"vibration_unit\":\"${vibration_unit}\"}",
"MessageSchema": {
"Name": "elevator-vibration;v1",
@ -51,7 +51,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "elevator-temperature;v1",

Просмотреть файл

@ -29,7 +29,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"fuellevel\":${fuellevel},\"fuellevel_unit\":\"${fuellevel_unit}\"}",
"MessageSchema": {
"Name": "engine-fuellevel;v1",
@ -41,7 +41,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"coolant\":${coolant},\"coolant_unit\":\"${coolant_unit}\"}",
"MessageSchema": {
"Name": "engine-coolant;v1",
@ -53,7 +53,7 @@
}
},
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"vibration\":${vibration},\"vibration_unit\":\"${vibration_unit}\"}",
"MessageSchema": {
"Name": "engine-vibration;v1",

Просмотреть файл

@ -29,7 +29,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"fuellevel\":${fuellevel},\"fuellevel_unit\":\"${fuellevel_unit}\"}",
"MessageSchema": {
"Name": "engine-fuellevel;v1",
@ -41,7 +41,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"coolant\":${coolant},\"coolant_unit\":\"${coolant_unit}\"}",
"MessageSchema": {
"Name": "engine-coolant;v1",
@ -53,7 +53,7 @@
}
},
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"vibration\":${vibration},\"vibration_unit\":\"${vibration_unit}\"}",
"MessageSchema": {
"Name": "engine-vibration;v1",

Просмотреть файл

@ -32,7 +32,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "prototype-temperature;v1",
@ -44,7 +44,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}",
"MessageSchema": {
"Name": "prototype-pressure;v1",
@ -56,7 +56,7 @@
}
},
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"latitude\":${latitude},\"longitude\":${longitude}}",
"MessageSchema": {
"Name": "prototype-geolocation;v1",

Просмотреть файл

@ -31,7 +31,7 @@
},
"Telemetry": [
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"temperature\":${temperature},\"temperature_unit\":\"${temperature_unit}\"}",
"MessageSchema": {
"Name": "prototype-temperature;v1",
@ -43,7 +43,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"pressure\":${pressure},\"pressure_unit\":\"${pressure_unit}\"}",
"MessageSchema": {
"Name": "prototype-pressure;v1",
@ -55,7 +55,7 @@
}
},
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"latitude\":${latitude},\"longitude\":${longitude}}",
"MessageSchema": {
"Name": "prototype-geolocation;v1",

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
// Default state
var state = {
pressure: 250.0,
CalculateRandomizedTelemetry: false
};
/**
* Entry point function called by the simulation engine.
*
* @param context The context contains current time, device model and id
* @param previousState The device state since the last iteration
*/
/*jslint unparam: true*/
function main(context, previousState) {
// Reboot - devices goes offline and comes online after 20 seconds
log("Executing DecreasePressure simulation function.");
state.pressure = 150;
state.CalculateRandomizedTelemetry = true;
// update the state to 150
updateState(state);
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript EmptyTank function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript FillTank function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
// Default state
var state = {
online: true,
Firmware: "1.0.0",
DeviceMethodStatus: "Updating Firmware"
};
/**
* Entry point function called by the simulation engine.
*
* @param context The context contains current time, device model and id, not used
* @param previousState The device state since the last iteration, not used
*/
/*jslint unparam: true*/
function main(context, previousState) {
// Reboot - devices goes offline and comes online after 20 seconds
log("Executing firmware update simulation function, firmware version passed:" + context.Firmware);
// update the state to offline & firmware updating
state.online = false;
state.CalculateRandomizedTelemetry = false;
var status = "Command received, updating firmware version to ";
status = status.concat(context.Firmware);
state.DeviceMethodStatus = status;
updateState(state);
sleep(5000);
log("Image Downloading...");
state.DeviceMethodStatus = "Image Downloading...";
updateState(state);
sleep(10000);
log("Executing firmware update simulation function, firmware version passed:" + context.Firmware);
state.DeviceMethodStatus = "Downloaded, applying firmware...";
updateState(state);
sleep(5000);
state.DeviceMethodStatus = "Rebooting...";
updateState(state);
sleep(5000);
state.DeviceMethodStatus = "Firmware Updated.";
state.Firmware = context.Firmware;
updateState(state);
sleep(5000);
state.CalculateRandomizedTelemetry = true;
state.online = true;
updateState(state);
}

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
// Default state
var state = {
pressure: 250.0,
CalculateRandomizedTelemetry: true
};
/**
* Entry point function called by the simulation engine.
*
* @param context The context contains current time, device model and id, not used
* @param previousState The device state since the last iteration, not used
*/
/*jslint unparam: true*/
function main(context, previousState) {
// Reboot - devices goes offline and comes online after 20 seconds
log("Executing IncreasePressure simulation function.");
state.pressure = 250;
state.CalculateRandomizedTelemetry = false;
// update the state to 250
updateState(state);
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript MoveDevice function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript RestartEngine function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript StartDevice function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript StartElevator function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript StopDevice function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript StopElevator function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript TempDecrease function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
/*jslint unparam: true*/
function main(context, previousState) {
log("Executing JavaScript TempIncrease function.");
// TODO: This method is not implemented
// https://github.com/Azure/device-simulation-dotnet/issues/44
}

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";

Просмотреть файл

@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.
/*global log*/
/*global updateState*/
/*global sleep*/
/*jslint node: true*/
"use strict";
// Default state
var state = {
// reboot just changes whether the device is on or offline
online: true
};
/**
* Entry point function called by the method.
*
* @param context The context contains current time, device model and id
* @param previousState The device state since the last iteration
*/
/*jslint unparam: true*/
function main(context, previousState) {
// Reboot - devices goes offline and comes online after 20 seconds
log("Executing reboot simulation function.");
state.DeviceMethodStatus = "Rebooting device...";
state.CalculateRandomizedTelemetry = false;
state.online = false;
// update the state to offline
updateState(state);
// Sleep for 20 seconds
sleep(20000);
state.DeviceMethodStatus = "Successfully rebooted device.";
state.CalculateRandomizedTelemetry = true;
state.online = true;
// update the state back to online
updateState(state);
}

Просмотреть файл

@ -29,7 +29,7 @@
},
"Telemetry": [
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"latitude\":${latitude},\"longitude\":${longitude}}",
"MessageSchema": {
"Name": "truck-geolocation;v1",
@ -41,7 +41,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"speed\":${speed},\"speed_unit\":\"${speed_unit}\"}",
"MessageSchema": {
"Name": "truck-speed;v1",
@ -53,7 +53,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"cargotemperature\":${cargotemperature},\"cargotemperature_unit\":\"${cargotemperature_unit}\"}",
"MessageSchema": {
"Name": "truck-cargotemperature;v1",

Просмотреть файл

@ -29,7 +29,7 @@
},
"Telemetry": [
{
"Interval": "00:00:03",
"Interval": "00:00:06",
"MessageTemplate": "{\"latitude\":${latitude},\"longitude\":${longitude}}",
"MessageSchema": {
"Name": "truck-geolocation;v1",
@ -41,7 +41,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"speed\": ${speed},\"speed_unit\":\"${speed_unit}\"}",
"MessageSchema": {
"Name": "truck-speed;v1",
@ -53,7 +53,7 @@
}
},
{
"Interval": "00:00:05",
"Interval": "00:00:10",
"MessageTemplate": "{\"cargotemperature\":${cargotemperature},\"cargotemperature_unit\":\"${cargotemperature_unit}\"}",
"MessageSchema": {
"Name": "truck-cargotemperature;v1",

Просмотреть файл

@ -0,0 +1,11 @@
{
"profiles": {
"SimulationAgent": {
"commandName": "Project",
"environmentVariables": {
"PCS_STORAGEADAPTER_WEBSERVICE_URL": "http://localhost:9022/v1",
"PCS_IOTHUB_CONNSTRING": "EnterHubConnString"
}
}
}
}

Просмотреть файл

@ -81,7 +81,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
public class DeviceActor : IDeviceActor
{
private const string DeviceIdPrefix = "Simulated.";
private const string DEVICE_ID_PREFIX = "Simulated.";
// When the actor fails to connect to IoT Hub, it retries every 10 seconds
private static readonly TimeSpan retryConnectingFrequency = TimeSpan.FromSeconds(10);
@ -130,6 +130,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
// State machine logic, each of the following has a Run() method
private readonly Connect connectLogic;
private readonly UpdateDeviceState updateDeviceStateLogic;
private readonly DeviceBootstrap deviceBootstrapLogic;
private readonly SendTelemetry sendTelemetryLogic;
@ -208,11 +209,11 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
this.setupDone = true;
this.deviceId = DeviceIdPrefix + deviceModel.Id + "." + position;
this.deviceId = DEVICE_ID_PREFIX + deviceModel.Id + "." + position;
this.messages = deviceModel.Telemetry;
this.deviceStateInterval = deviceModel.Simulation.Script.Interval;
this.DeviceState = CloneObject(deviceModel.Simulation.InitialState);
this.DeviceState = this.SetupTelemetryAndProperties(deviceModel);
this.log.Debug("Initial device state", () => new { this.deviceId, this.DeviceState });
this.connectLogic.Setup(this.deviceId, deviceModel);
@ -226,6 +227,25 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
return this;
}
private Dictionary<string, object> SetupTelemetryAndProperties(DeviceModel deviceModel)
{
// put telemetry properties in state
Dictionary<string, object> state = CloneObject(deviceModel.Simulation.InitialState);
//TODO: think about whether these should be pulled from the hub instead of disk
//(the device model); i.e. what if someone has modified the hub twin directly
// put reported properties from device model into state
foreach (var property in deviceModel.Properties)
state.Add(property.Key, property.Value);
//TODO:This is used to control whether telemetry is calculated in UpdateDeviceState.
//methods can turn telemetry off/on; e.g. setting temp high- turnoff, set low, turn on
//it would be better to do this at the telemetry item level - we should add this in the future
state.Add("CalculateRandomizedTelemetry", true);
return state;
}
/// <summary>
/// Call this method to start the simulated device, e.g. sending
/// messages and responding to method calls.

Просмотреть файл

@ -6,6 +6,7 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulation.DeviceStatusLogic
{
@ -23,16 +24,19 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
private readonly ILogger log;
private string deviceId;
private IoTHubProtocol? protocol;
private readonly IScriptInterpreter scriptInterpreter;
// Ensure that setup is called once and only once (which helps also detecting thread safety issues)
private bool setupDone = false;
public Connect(
IDevices devices,
ILogger logger)
ILogger logger,
IScriptInterpreter scriptInterpreter)
{
this.log = logger;
this.devices = devices;
this.scriptInterpreter = scriptInterpreter;
}
public void Setup(string deviceId, DeviceModel deviceModel)
@ -76,7 +80,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
this.log.Debug("Device credentials retrieved", () => new { this.deviceId });
actor.Client = this.devices.GetClient(device, this.protocol.Value);
actor.Client = this.devices.GetClient(device, this.protocol.Value, this.scriptInterpreter);
// Device Twin properties can be set only over MQTT, so we need a dedicated client
// for the bootstrap
@ -86,7 +90,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
}
else
{
actor.BootstrapClient = this.devices.GetClient(device, IoTHubProtocol.MQTT);
// bootstrap client is used to call methods and must have a script interpreter associated w/ it.
actor.BootstrapClient = this.devices.GetClient(device, IoTHubProtocol.MQTT, this.scriptInterpreter);
}
this.log.Debug("Connection successful", () => new { this.deviceId });

Просмотреть файл

@ -40,8 +40,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
() => new { this.deviceId });
throw new DeviceActorAlreadyInitializedException();
}
this.setupDone = true;
this.deviceId = deviceId;
this.deviceModel = deviceModel;
}
@ -66,8 +66,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
}
// register methods for the device
actor.BootstrapClient.RegisterMethodsForDevice(deviceModel.CloudToDeviceMethods);
actor.BootstrapClient.RegisterMethodsForDevice(this.deviceModel.CloudToDeviceMethods, actor.DeviceState);
actor.MoveNext();
}
catch (Exception e)

Просмотреть файл

@ -56,29 +56,39 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
// Send the telemetry message
try
{
// Inject the device state into the message template
var msg = message.MessageTemplate;
lock (actor.DeviceState)
this.log.Debug("Checking to see if device is online", () => new { this.deviceId });
if ((bool) actor.DeviceState["online"])
{
foreach (var value in actor.DeviceState)
// Inject the device state into the message template
var msg = message.MessageTemplate;
lock (actor.DeviceState)
{
msg = msg.Replace("${" + value.Key + "}", value.Value.ToString());
foreach (var value in actor.DeviceState)
{
msg = msg.Replace("${" + value.Key + "}", value.Value.ToString());
}
}
}
this.log.Debug("SendTelemetry...",
() => new { this.deviceId, MessageSchema = message.MessageSchema.Name, msg });
actor.Client
.SendMessageAsync(msg, message.MessageSchema)
.Wait(connectionTimeout);
this.log.Debug("SendTelemetry...",
() => new { this.deviceId, MessageSchema = message.MessageSchema.Name, msg });
actor.Client
.SendMessageAsync(msg, message.MessageSchema)
.Wait(connectionTimeout);
this.log.Debug("SendTelemetry complete", () => new { this.deviceId });
}
else
{
// device could be rebooting, updating firmware, etc.
this.log.Debug("No telemetry will be sent as the device is not online...",
() => new { this.deviceId, actor.DeviceState });
}
}
catch (Exception e)
{
this.log.Error("SendTelemetry failed",
() => new { this.deviceId, e.Message, Error = e.GetType().FullName });
}
this.log.Debug("SendTelemetry complete", () => new { this.deviceId });
}
private void ValidateSetup()

Просмотреть файл

@ -2,6 +2,8 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
@ -15,20 +17,26 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
/// </summary>
public class UpdateDeviceState : IDeviceStatusLogic
{
// When connecting to IoT Hub, timeout after 10 seconds
private static readonly TimeSpan connectionTimeout = TimeSpan.FromSeconds(10);
private readonly IScriptInterpreter scriptInterpreter;
private readonly ILogger log;
private string deviceId;
private DeviceModel deviceModel;
private IDevices devices;
// Ensure that setup is called once and only once (which helps also detecting thread safety issues)
private bool setupDone = false;
public UpdateDeviceState(
IDevices devices,
IScriptInterpreter scriptInterpreter,
ILogger logger)
{
this.scriptInterpreter = scriptInterpreter;
this.log = logger;
this.devices = devices;
}
public void Setup(string deviceId, DeviceModel deviceModel)
@ -56,30 +64,125 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
return;
}
var scriptContext = new Dictionary<string, object>
// Compute new telemetry, find updated desired properties, push new reported property values.
try
{
["currentTime"] = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:sszzz"),
["deviceId"] = this.deviceId,
["deviceModel"] = this.deviceModel.Name
};
var scriptContext = new Dictionary<string, object>
{
["currentTime"] = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:sszzz"),
["deviceId"] = this.deviceId,
["deviceModel"] = this.deviceModel.Name
};
this.log.Debug("Updating device status", () => new { this.deviceId, deviceState = actor.DeviceState });
// until the correlating function has been called; e.g. when increasepressure is called, don't write
// telemetry until decreasepressure is called for that property.
// TODO: make this property a constant
// https://github.com/Azure/device-simulation-dotnet/issues/46
if ((bool) actor.DeviceState["CalculateRandomizedTelemetry"])
{
this.log.Debug("Updating device telemetry data", () => new { this.deviceId, deviceState = actor.DeviceState });
lock (actor.DeviceState)
{
actor.DeviceState = this.scriptInterpreter.Invoke(
this.deviceModel.Simulation.Script,
scriptContext,
actor.DeviceState);
}
this.log.Debug("New device telemetry data", () => new { this.deviceId, deviceState = actor.DeviceState });
}
else
{
this.log.Debug(
"Random telemetry generation is turned off for the actor",
() => new { this.deviceId, deviceState = actor.DeviceState });
}
lock (actor.DeviceState)
this.log.Debug(
"Checking for desired property updates & updated reported properties",
() => new { this.deviceId, deviceState = actor.DeviceState });
// Get device
var device = this.GetDevice(actor.CancellationToken);
lock (actor.DeviceState)
{
// TODO: the device state update should be an in-memory task without network access, so we should move this
// logic out to a separate task/thread - https://github.com/Azure/device-simulation-dotnet/issues/47
// check for differences between reported/desired properties,
// update reported properties with any state changes (either from desired prop changes, methods, etc.)
if (this.ChangeTwinPropertiesToMatchDesired(device, actor.DeviceState)
|| this.ChangeTwinPropertiesToMatchActorState(device, actor.DeviceState))
actor.BootstrapClient.UpdateTwinAsync(device).Wait((int) connectionTimeout.TotalMilliseconds);
}
// Start sending telemetry messages
if (actor.ActorStatus == Status.UpdatingDeviceState)
{
actor.MoveNext();
}
else
{
this.log.Debug(
"Already sending telemetry, running local simulation and watching desired property changes",
() => new { this.deviceId });
}
}
catch (Exception e)
{
actor.DeviceState = this.scriptInterpreter.Invoke(
this.deviceModel.Simulation.Script,
scriptContext,
actor.DeviceState);
this.log.Error("UpdateDeviceState failed",
() => new { this.deviceId, e.Message, Error = e.GetType().FullName });
}
}
private bool ChangeTwinPropertiesToMatchActorState(Device device, Dictionary<string, object> actorState)
{
bool differences = false;
foreach (var item in actorState)
{
if (device.Twin.ReportedProperties.ContainsKey(item.Key))
{
if (device.Twin.ReportedProperties[item.Key].ToString() != actorState[item.Key].ToString())
{
// Update the Hub twin to match the actor state
device.Twin.ReportedProperties[item.Key] = actorState[item.Key].ToString();
differences = true;
}
}
}
this.log.Debug("New device status", () => new { this.deviceId, deviceState = actor.DeviceState });
return differences;
}
// Start sending telemetry messages
if (actor.ActorStatus == Status.UpdatingDeviceState)
private bool ChangeTwinPropertiesToMatchDesired(Device device, Dictionary<string, object> actorState)
{
bool differences = false;
foreach (var item in device.Twin.DesiredProperties)
{
actor.MoveNext();
if (device.Twin.ReportedProperties.ContainsKey(item.Key))
{
if (device.Twin.ReportedProperties[item.Key].ToString() != device.Twin.DesiredProperties[item.Key].ToString())
{
// update the hub reported property to match match hub desired property
device.Twin.ReportedProperties[item.Key] = device.Twin.DesiredProperties[item.Key];
// update actor state property to match hub desired changes
if (actorState.ContainsKey(item.Key))
actorState[item.Key] = device.Twin.DesiredProperties[item.Key];
differences = true;
}
}
}
return differences;
}
private Device GetDevice(CancellationToken token)
{
var task = this.devices.GetAsync(this.deviceId);
task.Wait((int) connectionTimeout.TotalMilliseconds, token);
return task.Result;
}
private void ValidateSetup()

Просмотреть файл

@ -16,7 +16,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
public class Simulation : ISimulation
{
private const int CheckInterval = 3000;
private const int CHECK_INTERVAL = 3000;
private readonly ILogger log;
private readonly ISimulations simulations;
@ -59,7 +59,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
this.runner.Stop();
}
Thread.Sleep(CheckInterval);
Thread.Sleep(CHECK_INTERVAL);
}
}
}

Просмотреть файл

@ -1,29 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9003/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "http://localhost:9003/v1/status",
"environmentVariables": {
"PCS_IOTHUB_CONNSTRING": "HostName={Your Azure IoT Hub hostname};SharedAccessKeyName=iothubowner;SharedAccessKey={SECRET KEY}"
}
},
"WebService": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:9003/v1/status",
"environmentVariables": {
"PCS_IOTHUB_CONNSTRING": "HostName={Your Azure IoT Hub hostname};SharedAccessKeyName=iothubowner;SharedAccessKey={SECRET KEY}"
},
"applicationUrl": "http://localhost:9003/v1/status"
}
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9003/",
"sslPort": 0
}
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "http://localhost:9003/v1/status",
"environmentVariables": {
"PCS_STORAGEADAPTER_WEBSERVICE_URL": "http://localhost:9022/v1",
"PCS_IOTHUB_CONNSTRING": "EnterHubConnString"
}
},
"WebService": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:9003/v1/status",
"environmentVariables": {
"PCS_IOTHUB_CONNSTRING": "HostName={Your Azure IoT Hub hostname};SharedAccessKeyName=iothubowner;SharedAccessKey={SECRET KEY}"
},
"applicationUrl": "http://localhost:9003/v1/status"
}
}
}

Просмотреть файл

@ -19,7 +19,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{71
.gitignore = .gitignore
.travis.yml = .travis.yml
CONTRIBUTING.md = CONTRIBUTING.md
DEVELOPMENT.md = DEVELOPMENT.md
device-simulation.sln.DotSettings = device-simulation.sln.DotSettings
LICENSE = LICENSE
README.md = README.md

Просмотреть файл

@ -19,6 +19,8 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MQTT/@EntryIndexedValue">MQTT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSL/@EntryIndexedValue">SSL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UID/@EntryIndexedValue">UID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>

Просмотреть файл

@ -1 +1 @@
0.1.7
0.1.8