diff --git a/DigitalTwins-CodeFirst-dotnet.sln b/DigitalTwins-CodeFirst-dotnet.sln index 1eb76ec..f7a43c4 100644 --- a/DigitalTwins-CodeFirst-dotnet.sln +++ b/DigitalTwins-CodeFirst-dotnet.sln @@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D8068994 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telstra.Twins.Test", "Tests\Telstra.Twins.Test\Telstra.Twins.Test.csproj", "{4E5BF74D-C0EF-4D13-872C-F40FC347AD67}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5BA14B5E-B883-4B92-802D-0BA49C93842B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FactoryExample", "Examples\FactoryExample\FactoryExample.csproj", "{B1000F84-515B-4406-8B11-CC80051C531A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2249F79B-DD17-4FAF-91B7-3E69A5E9546C}" ProjectSection(SolutionItems) = preProject README.md = README.md @@ -67,8 +71,21 @@ Global {4E5BF74D-C0EF-4D13-872C-F40FC347AD67}.Release|x64.Build.0 = Release|Any CPU {4E5BF74D-C0EF-4D13-872C-F40FC347AD67}.Release|x86.ActiveCfg = Release|Any CPU {4E5BF74D-C0EF-4D13-872C-F40FC347AD67}.Release|x86.Build.0 = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|x64.Build.0 = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Debug|x86.Build.0 = Debug|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|Any CPU.Build.0 = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|x64.ActiveCfg = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|x64.Build.0 = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|x86.ActiveCfg = Release|Any CPU + {B1000F84-515B-4406-8B11-CC80051C531A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4E5BF74D-C0EF-4D13-872C-F40FC347AD67} = {D8068994-FDA6-41F1-9196-59AADA892F11} + {B1000F84-515B-4406-8B11-CC80051C531A} = {5BA14B5E-B883-4B92-802D-0BA49C93842B} EndGlobalSection EndGlobal diff --git a/DigitalTwins-CodeFirst-dotnet.sln.DotSettings b/DigitalTwins-CodeFirst-dotnet.sln.DotSettings index c69b61e..6881f4a 100644 --- a/DigitalTwins-CodeFirst-dotnet.sln.DotSettings +++ b/DigitalTwins-CodeFirst-dotnet.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file diff --git a/Examples/FactoryExample/CreateExample.cs b/Examples/FactoryExample/CreateExample.cs new file mode 100644 index 0000000..ce9cdb8 --- /dev/null +++ b/Examples/FactoryExample/CreateExample.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.DigitalTwins.Core; +using Azure.Identity; +using Telstra.Twins.Core; +using Telstra.Twins.Services; + +namespace FactoryExample +{ + public static class CreateExample + { + //public static async Task CreateModels(string tenantId, string clientId, string clientSecret, string adtEndpoint) + public static async Task CreateModelsAsync(string adtEndpoint, CancellationToken cancellationToken) + { + var modelLibrary = new ModelLibrary(); + var serializer = new DigitalTwinSerializer(modelLibrary); + + var models = Program.ModelTypes.Select(x => serializer.SerializeModel(x)); + + try + { + //var client = GetDigitalTwinsClient(tenantId, clientId, clientSecret, adtEndpoint); + var client = GetDigitalTwinsClient(adtEndpoint); + var response = await client.CreateModelsAsync(models, cancellationToken); + Console.WriteLine("CREATE MODELS SUCCESS"); + foreach (var modelData in response.Value) + { + Console.WriteLine( + $"{modelData.Id}: {modelData.LanguageDisplayNames.FirstOrDefault().Value}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"CREATE FAILED: {ex.Message}"); + } + } + + public static async Task CreateTwinsAsync(string adtEndpoint, CancellationToken cancellationToken) + { + var modelLibrary = new ModelLibrary(); + var serializer = new DigitalTwinSerializer(modelLibrary); + + var factory = Program.CreateFactoryTwin(); + + try + { + //var client = GetDigitalTwinsClient(tenantId, clientId, clientSecret, adtEndpoint); + var client = GetDigitalTwinsClient(adtEndpoint); + + await CreateTwinInstanceAsync(client, factory.FactoryId, serializer.SerializeTwin(factory), + cancellationToken); + + foreach (var floor in factory.Floors) + { + await CreateTwinInstanceAsync(client, floor.FloorId, serializer.SerializeTwin(floor), + cancellationToken); + await CreateTwinRelationshipAsync(client, "floors", factory.FactoryId, + floor.FloorId, cancellationToken); + + foreach (var line in floor.RunsLines) + { + await CreateTwinInstanceAsync(client, line.LineId, serializer.SerializeTwin(line), + cancellationToken); + await CreateTwinRelationshipAsync(client, "runsLines", floor.FloorId, + line.LineId, cancellationToken); + + foreach (var step in line.RunsSteps) + { + await CreateTwinInstanceAsync(client, step.StepId, serializer.SerializeTwin(step), + cancellationToken); + await CreateTwinRelationshipAsync(client, "runsSteps", line.LineId, + step.StepId, cancellationToken); + } + + foreach (var step in line.RunsSteps) + { + if (step.StepLink != null) + { + await CreateTwinRelationshipAsync(client, "stepLink", step.StepId, + step.StepLink.StepId, cancellationToken); + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"CREATE FAILED: {ex.Message}"); + } + } + + private static async Task CreateTwinInstanceAsync(DigitalTwinsClient client, string? id, string dtdl, + CancellationToken cancellationToken) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + var basicDigitalTwin = JsonSerializer.Deserialize(dtdl); + var response = + await client.CreateOrReplaceDigitalTwinAsync(id, basicDigitalTwin, null, cancellationToken); + if (response?.Value != null) + { + Console.WriteLine("CREATE TWIN SUCCESS: Id={0}, ETag={1}", response.Value.Id, + response.Value.ETag); + } + } + + private static async Task CreateTwinRelationshipAsync(DigitalTwinsClient client, + string relationshipName, string? sourceId, string? targetId, CancellationToken cancellationToken) + { + if (sourceId == null) + { + throw new ArgumentNullException(nameof(sourceId)); + } + + if (targetId == null) + { + throw new ArgumentNullException(nameof(targetId)); + } + + var relationship = new BasicRelationship + { + Id = $"{sourceId}_{targetId}", + Name = relationshipName, + SourceId = sourceId, + TargetId = targetId + }; + var response = await client.CreateOrReplaceRelationshipAsync(sourceId, relationship.Id, + relationship, null, cancellationToken); + Console.WriteLine("CREATE RELATIONSHIP SUCCESS: Id={0}, ETag={1}", response.Value.Id, + response.Value.ETag); + } + + private static DigitalTwinsClient GetDigitalTwinsClient(string adtEndpoint) + //private static DigitalTwinsClient GetDigitalTwinsClient(string tenantId, string clientId, string clientSecret, string adtEndpoint) + { + // These environment variables are necessary for DefaultAzureCredential to use application Id and client secret to login. + //Environment.SetEnvironmentVariable("AZURE_CLIENT_SECRET", clientSecret); + //Environment.SetEnvironmentVariable("AZURE_CLIENT_ID", clientId); + //Environment.SetEnvironmentVariable("AZURE_TENANT_ID", tenantId); + + // DefaultAzureCredential supports different authentication mechanisms and determines the appropriate credential type based of the environment it is executing in. + // It attempts to use multiple credential types in an order until it finds a working credential. + var tokenCredential = new DefaultAzureCredential(true); + + var client = new DigitalTwinsClient( + new Uri(adtEndpoint), + tokenCredential); + + return client; + } + } +} diff --git a/Examples/FactoryExample/Devices/ProductionStep.cs b/Examples/FactoryExample/Devices/ProductionStep.cs new file mode 100644 index 0000000..abdff26 --- /dev/null +++ b/Examples/FactoryExample/Devices/ProductionStep.cs @@ -0,0 +1,27 @@ +using System; +using Telstra.Twins; +using Telstra.Twins.Attributes; + +namespace FactoryExample.Devices +{ + [DigitalTwin(Version = 1, DisplayName = "Factory Production Steps - Interface Model")] + public class ProductionStep : TwinBase + { + // ContainsEquipment + + [TwinProperty] public bool FinalStep { get; set; } + + // HasConnectedDevices + + //[TwinProperty] public ProductionStepStatus OperationStatus { get; set; } + + [TwinProperty] public DateTimeOffset? StartTime { get; set; } + + [TwinProperty] public string? StepId { get; set; } + + [TwinRelationship(DisplayName = "Step Link")] + public ProductionStep? StepLink { get; set; } + + [TwinProperty] public string? StepName { get; set; } + } +} diff --git a/Examples/FactoryExample/Devices/ProductionStepFanning.cs b/Examples/FactoryExample/Devices/ProductionStepFanning.cs new file mode 100644 index 0000000..18a67ae --- /dev/null +++ b/Examples/FactoryExample/Devices/ProductionStepFanning.cs @@ -0,0 +1,23 @@ +using Telstra.Twins.Attributes; +using Telstra.Twins.Semantics; + +namespace FactoryExample.Devices +{ + [DigitalTwin(Version = 1, DisplayName = "Factory Production Step: Fanning/Roasting - Interface Model", + ExtendsModelId = "dtmi:factoryexample:devices:productionstep;1")] + public class ProductionStepFanning : ProductionStep + { + [TwinProperty(SemanticType = SemanticType.Temperature, Unit = TemperatureUnit.DegreeCelsius, + Writable = true)] + public double? ChassisTemperature { get; set; } + + [TwinProperty] + public double? FanSpeed { get; set; } + + [TwinProperty(SemanticType = SemanticType.TimeSpan, Unit = TimeUnit.Minute)] + public int? RoastingTime { get; set; } + + [TwinProperty(SemanticType = SemanticType.Power, Unit = PowerUnit.Kilowatt)] + public double? PowerUsage { get; set; } + } +} diff --git a/Examples/FactoryExample/Devices/ProductionStepGrinding.cs b/Examples/FactoryExample/Devices/ProductionStepGrinding.cs new file mode 100644 index 0000000..b036e27 --- /dev/null +++ b/Examples/FactoryExample/Devices/ProductionStepGrinding.cs @@ -0,0 +1,26 @@ +using Telstra.Twins.Attributes; +using Telstra.Twins.Semantics; + +namespace FactoryExample.Devices +{ + [DigitalTwin(Version = 1, DisplayName = "Factory Production Step: Grinding/Crushing - Interface Model", + ExtendsModelId = "dtmi:factoryexample:devices:productionstep;1")] + public class ProductionStepGrinding : ProductionStep + { + [TwinProperty(SemanticType = SemanticType.Temperature, Unit = TemperatureUnit.DegreeCelsius, + Writable = true)] + public double? ChassisTemperature { get; set; } + + [TwinProperty(SemanticType = SemanticType.Force, Unit = ForceUnit.Newton)] + public double? Force { get; set; } + + [TwinProperty(SemanticType = SemanticType.TimeSpan, Unit = TimeUnit.Minute)] + public int? GrindingTime { get; set; } + + [TwinProperty(SemanticType = SemanticType.Power, Unit = PowerUnit.Kilowatt)] + public double? PowerUsage { get; set; } + + [TwinProperty(SemanticType = SemanticType.Frequency, Unit = FrequencyUnit.Hertz)] + public double? Vibration { get; set; } + } +} diff --git a/Examples/FactoryExample/Devices/ProductionStepMoulding.cs b/Examples/FactoryExample/Devices/ProductionStepMoulding.cs new file mode 100644 index 0000000..7a0f2e1 --- /dev/null +++ b/Examples/FactoryExample/Devices/ProductionStepMoulding.cs @@ -0,0 +1,17 @@ +using Telstra.Twins.Attributes; +using Telstra.Twins.Semantics; + +namespace FactoryExample.Devices +{ + [DigitalTwin(Version = 1, DisplayName = "Factory Production Step: Moulding - Interface Model", + ExtendsModelId = "dtmi:factoryexample:devices:productionstep;1")] + public class ProductionStepMoulding : ProductionStep + { + [TwinProperty(SemanticType = SemanticType.Temperature, Unit = TemperatureUnit.DegreeCelsius, + Writable = true)] + public double? ChassisTemperature { get; set; } + + [TwinProperty] + public double? PowerUsage { get; set; } + } +} diff --git a/Examples/FactoryExample/Devices/ProductionStepStatus.cs b/Examples/FactoryExample/Devices/ProductionStepStatus.cs new file mode 100644 index 0000000..ca629e8 --- /dev/null +++ b/Examples/FactoryExample/Devices/ProductionStepStatus.cs @@ -0,0 +1,9 @@ +namespace FactoryExample.Devices +{ + public enum ProductionStepStatus + { + Unknown = 0, + Offline = 1, + Online = 2 + } +} diff --git a/Examples/FactoryExample/FactoryExample.csproj b/Examples/FactoryExample/FactoryExample.csproj new file mode 100644 index 0000000..864e46f --- /dev/null +++ b/Examples/FactoryExample/FactoryExample.csproj @@ -0,0 +1,17 @@ + + + + Exe + netcoreapp3.1 + enable + + + + + + + + + + + diff --git a/Examples/FactoryExample/Models/Factory.cs b/Examples/FactoryExample/Models/Factory.cs new file mode 100644 index 0000000..870dab7 --- /dev/null +++ b/Examples/FactoryExample/Models/Factory.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using FactoryExample.Schema; +using Telstra.Twins; +using Telstra.Twins.Attributes; + +namespace FactoryExample.Models +{ + [DigitalTwin(Version = 1, DisplayName = "Digital Factory - Interface Model")] + public class Factory : TwinBase + { + [TwinProperty] public string? Country { get; set; } + + [TwinProperty] public string? FactoryId { get; set; } + + [TwinProperty(Writable = true)] public string? FactoryName { get; set; } + + [TwinRelationship(DisplayName = "Has Floors")] + public IList Floors { get; } = new List(); + + [TwinProperty] public GeoCord? GeoLocation { get; set; } + + [TwinProperty] public DateTimeOffset LastSupplyDate { get; set; } + + // ServesRetailer + // SuppliedBy + // TransportationBy + + [TwinProperty(Writable = true)] public string? Tags { get; set; } + + [TwinProperty(Writable = true)] public string? ZipCode { get; set; } + } +} diff --git a/Examples/FactoryExample/Models/FactoryFloor.cs b/Examples/FactoryExample/Models/FactoryFloor.cs new file mode 100644 index 0000000..fd99142 --- /dev/null +++ b/Examples/FactoryExample/Models/FactoryFloor.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Telstra.Twins; +using Telstra.Twins.Attributes; +using Telstra.Twins.Semantics; + +namespace FactoryExample.Models +{ + [DigitalTwin(Version = 1, DisplayName = "Digital Factory - Interface Model")] + public class FactoryFloor : TwinBase + { + [TwinProperty] public double? ComfortIndex { get; set; } + + [TwinProperty(Writable = true)] public string? FloorId { get; set; } + + // FloorHasRooms + // FloorHasZones + + [TwinProperty(Writable = true)] public string? FloorName { get; set; } + + [TwinRelationship(DisplayName = "Runs Production Lines")] + public IList RunsLines { get; } = new List(); + + [TwinProperty(SemanticType = SemanticType.Temperature, Unit = TemperatureUnit.DegreeCelsius, + Writable = true)] + public double? Temperature { get; set; } + } +} diff --git a/Examples/FactoryExample/Models/ProductionLine.cs b/Examples/FactoryExample/Models/ProductionLine.cs new file mode 100644 index 0000000..cb47cb5 --- /dev/null +++ b/Examples/FactoryExample/Models/ProductionLine.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using FactoryExample.Devices; +using Telstra.Twins; +using Telstra.Twins.Attributes; + +namespace FactoryExample.Models +{ + [DigitalTwin(Version = 1, DisplayName = "Factory Production Line - Interface Model")] + public class ProductionLine : TwinBase + { + // ContainsEquipment + + [TwinProperty(Writable = true)] public string? CurrentProductId { get; set; } + + [TwinProperty(Writable = true)] public string? LineId { get; set; } + + [TwinProperty(Writable = true)] public string? LineName { get; set; } + + //[TwinProperty] public ProductionLineStatus LineOperationStatus { get; set; } + + [TwinProperty(Writable = true)] public int? ProductBatchNumber { get; set; } + + [TwinRelationship(DisplayName = "Runs Steps")] + public IList RunsSteps { get; } = new List(); + } +} diff --git a/Examples/FactoryExample/Models/ProductionLineStatus.cs b/Examples/FactoryExample/Models/ProductionLineStatus.cs new file mode 100644 index 0000000..b1bc2ce --- /dev/null +++ b/Examples/FactoryExample/Models/ProductionLineStatus.cs @@ -0,0 +1,9 @@ +namespace FactoryExample.Models +{ + public enum ProductionLineStatus + { + Unknown = 0, + Offline = 1, + Online = 2 + } +} diff --git a/Examples/FactoryExample/ParseExample.cs b/Examples/FactoryExample/ParseExample.cs new file mode 100644 index 0000000..3c5dedc --- /dev/null +++ b/Examples/FactoryExample/ParseExample.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Microsoft.Azure.DigitalTwins.Parser; +using Telstra.Twins.Core; +using Telstra.Twins.Services; + +namespace FactoryExample +{ + public static class ParseExample + { + public static async Task ParseModelsAsync(CancellationToken cancellationToken) + { + var modelLibrary = new ModelLibrary(); + var serializer = new DigitalTwinSerializer(modelLibrary); + + var models = Program.ModelTypes.Select(x => serializer.SerializeModel(x)); + + try + { + var parser = new ModelParser(); + var entityInfos = await parser.ParseAsync(models); + Console.WriteLine("PARSE SUCCESS:"); + foreach (var kvp in entityInfos) + { + Console.WriteLine( + $"[{kvp.Key}] = {kvp.Value.EntityKind} {kvp.Value.Id} ({kvp.Value.DisplayName.FirstOrDefault().Value})"); + } + } + catch (ParsingException ex) + { + Console.WriteLine($"PARSE FAILED: {ex.Message}"); + Console.WriteLine("ERRORS:"); + var count = 0; + foreach (var error in ex.Errors) + { + Console.WriteLine($"{++count}. {error}"); + } + } + catch (RequestFailedException ex) + { + Console.WriteLine($"REQUEST FAILED: {ex.Message}"); + } + } + } +} diff --git a/Examples/FactoryExample/Program.cs b/Examples/FactoryExample/Program.cs new file mode 100644 index 0000000..a9bffa2 --- /dev/null +++ b/Examples/FactoryExample/Program.cs @@ -0,0 +1,153 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FactoryExample.Devices; +using FactoryExample.Models; +using FactoryExample.Schema; +using Microsoft.Extensions.Configuration; + +namespace FactoryExample +{ + public static class Program + { + public static readonly Type[] ModelTypes = + { + typeof(Factory), typeof(FactoryFloor), typeof(ProductionLine), typeof(ProductionStep), + typeof(ProductionStepGrinding), typeof(ProductionStepFanning), typeof(ProductionStepMoulding) + }; + + public static Factory CreateFactoryTwin() + { + var production1Step3Moulding = new ProductionStepMoulding + { + ChassisTemperature = 50, + FinalStep = true, + //OperationStatus = ProductionStepStatus.Online, + PowerUsage = 100, + StartTime = DateTimeOffset.UnixEpoch, + StepId = "line1.step3", + StepName = "Moulding Step" + }; + + var production1Step2Grinding = new ProductionStepGrinding + { + ChassisTemperature = 50, + FinalStep = false, + Force = 8.0, + GrindingTime = 30, + //OperationStatus = ProductionStepStatus.Online, + PowerUsage = 100, + StartTime = DateTimeOffset.UnixEpoch, + StepId = "line1.step2", + StepName = "Grinding Step", + StepLink = production1Step3Moulding + }; + + var production1Step1Fanning = new ProductionStepFanning() + { + ChassisTemperature = 50, + FinalStep = false, + FanSpeed = 0.5, + //OperationStatus = ProductionStepStatus.Online, + PowerUsage = 100, + StartTime = DateTimeOffset.UnixEpoch, + StepId = "line1.step1", + StepName = "Fanning Step", + StepLink = production1Step2Grinding + }; + + var productionLine1 = new ProductionLine + { + CurrentProductId = "product5", + LineId = "line1", + LineName = "Production Line 1", + //LineOperationStatus = ProductionLineStatus.Online, + ProductBatchNumber = 6 + }; + productionLine1.RunsSteps.Add(production1Step1Fanning); + productionLine1.RunsSteps.Add(production1Step2Grinding); + productionLine1.RunsSteps.Add(production1Step3Moulding); + + var factory1Floor1 = new FactoryFloor + { + ComfortIndex = 0.8, FloorId = "factory1.floor1", FloorName = "Factory 1 Floor 1", Temperature = 23 + }; + factory1Floor1.RunsLines.Add(productionLine1); + + var factory1 = new Factory + { + Country = "AU", + FactoryId = "factory1", + FactoryName = "Chocolate Factory", + GeoLocation = new GeoCord { Latitude = -27.4705, Longitude = 153.026 }, + LastSupplyDate = new DateTimeOffset(2021, 11, 17, 18, 37, 0, TimeSpan.FromHours(10)), + Tags = String.Empty, + ZipCode = "4000" + }; + factory1.Floors.Add(factory1Floor1); + return factory1; + } + + public static async Task Main(string[] args) + { + Console.WriteLine("Digital Twin Code First Factory Example"); + + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", true) + .AddEnvironmentVariables() + .AddCommandLine(args) + .Build(); + + var show = configuration.GetValue("serialize"); + if (!string.IsNullOrWhiteSpace(show)) + { + switch (show.ToLowerInvariant()) + { + case "model": + SerializeExample.SerializeModels(); + return; + case "twin": + SerializeExample.SerializeTwins(); + return; + } + } + + var check = configuration.GetValue("parse"); + if (!string.IsNullOrWhiteSpace(check)) + { + switch (check.ToLowerInvariant()) + { + case "model": + await ParseExample.ParseModelsAsync(CancellationToken.None); + return; + } + } + + var create = configuration.GetValue("create"); + if (!string.IsNullOrWhiteSpace(create)) + { + var adtEndpoint = configuration.GetValue("endpoint"); + switch (create.ToLowerInvariant()) + { + case "model": + await ParseExample.ParseModelsAsync(CancellationToken.None); + await CreateExample.CreateModelsAsync(adtEndpoint, CancellationToken.None); + return; + case "twin": + await CreateExample.CreateTwinsAsync(adtEndpoint, CancellationToken.None); + return; + } + } + + ShowHelp(); + } + + private static void ShowHelp() + { + Console.WriteLine(" --serialize model : shows serialized model examples"); + Console.WriteLine(" --serialize twin : shows serialized twin examples"); + Console.WriteLine(" --parse model : parse and validate the example model"); + Console.WriteLine(" --create model --endpoint : parse and upload the example model"); + } + } +} diff --git a/Examples/FactoryExample/ReadMe.md b/Examples/FactoryExample/ReadMe.md new file mode 100644 index 0000000..38b717a --- /dev/null +++ b/Examples/FactoryExample/ReadMe.md @@ -0,0 +1,103 @@ +Code First Azure Digital Twins -- End to end example +==================================================== + +Requirements: + +* dotnet (3.1 LTS or higher) +* PowerShell (7.0 LTS or higher) + +Based on https://github.com/Azure-Samples/digital-twins-samples/tree/master/HandsOnLab + +Create infrastructure +--------------------- + +You need to create a digital twins instance in Azure to deploy the model to. There is a +script `deploy-infrastructure.ps1` that will created the needed resources. + +To run the script you need to load the required PowerShell modules, then connect to your +Azure account and set the context for the subscription you want to use, then run the +script. + +``` pwsh + Install-Module -Name Az -Scope CurrentUser -Force + Install-Module -Name Az.DigitalTwins -Scope CurrentUser -Force + Register-AzResourceProvider -ProviderNamespace Microsoft.DigitalTwins + + Connect-AzAccount + Set-AzContext -SubscriptionId $SubscriptionId + + $VerbosePreference = 'Continue' + ./deploy-infrastructure.ps1 +``` + +Digital Twins Explorer +---------------------- + +After you have created the infrastructure, open `https://portal.azure.com/` and go to the resource group. + +Open the Azure Digital Twins resource. + +Copy the Host name, and then open the Azure Digital Twins Explorer (preview). + +For the Azure Digital Twins URL, use `https://`, with the Host name copied from above. + +The explorer should start out empty (see below). + +Running basic checks +-------------------- + +To see the example serialized model: + +``` +dotnet run -- --serialize model +``` + +To see the example serialized twin instances: + +``` +dotnet run -- --serialize twin +``` + +To run parse and validate the model with `Microsoft.Azure.DigitalTwins.Parser`: + +``` +dotnet run -- --parse model +``` + +Running the example to create models and twins +---------------------------------------------- + +To upload the models to Azure: + +``` pwsh +$rgName = "rg-codefirsttwins-dev-001" +$dtName = "dt-codefirsttwins-0x$((Get-AzContext).Subscription.Id.Substring(0,4))-dev" +$hostName = (Get-AzDigitalTwinsInstance -ResourceGroupName $rgName -ResourceName $dtName).HostName +dotnet run -- --create model --endpoint "https://$hostName" +``` + +After this, if you refresh the Models in Digital Twins Explorer, you will see the uploaded models. + +![Explorer model graph](images/model-graph-example.png) + +You then need to upload the Twin instances: + +``` pwsh +dotnet run -- --create twin --endpoint "https://$hostName" +``` + +To see the Twin instances, ensure the query explorer has the default query `SELECT * FROM digitaltwins`, +and then click Run Query. The twin instances should appear in the Twins list and Twin Graph. + +![Explorer twins graph](images/twins-graph-example.png) + +Cleanup +------- + +After running the example, you can clean up the Azure resources (to save money). + +``` pwsh +./remove-infrastructure.ps1 +``` + + diff --git a/Examples/FactoryExample/Schema/GeoCord.cs b/Examples/FactoryExample/Schema/GeoCord.cs new file mode 100644 index 0000000..8fb250a --- /dev/null +++ b/Examples/FactoryExample/Schema/GeoCord.cs @@ -0,0 +1,11 @@ +using Telstra.Twins.Attributes; + +namespace FactoryExample.Schema +{ + [DigitalTwin(Version = 1)] + public class GeoCord + { + [TwinProperty("lat")] public double Latitude { get; set; } + [TwinProperty("lon")] public double Longitude { get; set; } + } +} diff --git a/Examples/FactoryExample/SerializeExample.cs b/Examples/FactoryExample/SerializeExample.cs new file mode 100644 index 0000000..b9d6b72 --- /dev/null +++ b/Examples/FactoryExample/SerializeExample.cs @@ -0,0 +1,39 @@ +using System; +using FactoryExample.Devices; +using Telstra.Twins.Core; +using Telstra.Twins.Services; + +namespace FactoryExample +{ + public static class SerializeExample + { + public static void SerializeModels() + { + var modelLibrary = new ModelLibrary(); + var serializer = new DigitalTwinSerializer(modelLibrary); + + foreach (var modelType in Program.ModelTypes) + { + var modelDtdl = serializer.SerializeModel(modelType); + Console.WriteLine(modelDtdl); + Console.WriteLine(); + } + } + + public static void SerializeTwins() + { + var modelLibrary = new ModelLibrary(); + var serializer = new DigitalTwinSerializer(modelLibrary); + + var factory = Program.CreateFactoryTwin(); + + var twin1Dtdl = serializer.SerializeTwin(factory); + Console.WriteLine(twin1Dtdl); + Console.WriteLine(); + + var twin2Dtdl = serializer.SerializeTwin(factory.Floors[0].RunsLines[0].RunsSteps[0] as ProductionStepFanning); + Console.WriteLine(twin2Dtdl); + Console.WriteLine(); + } + } +} diff --git a/Examples/FactoryExample/deploy-infrastructure.ps1 b/Examples/FactoryExample/deploy-infrastructure.ps1 new file mode 100644 index 0000000..9796c78 --- /dev/null +++ b/Examples/FactoryExample/deploy-infrastructure.ps1 @@ -0,0 +1,86 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Deploy the Azure infrastructure for the project. + +.NOTES + + Running these scripts requires the following to be installed: + * PowerShell, https://github.com/PowerShell/PowerShell + * Azure PowerShell module, https://docs.microsoft.com/en-us/powershell/azure/install-az-ps + * Azure Digital Twins module (preview installed separately) + + You also need to connect to Azure (log in), and set the desired subscripition context. + + Follow standard naming conventions from Azure Cloud Adoption Framework, + with an additional organisation or subscription identifier (after app name) in global names + to make them unique. + https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming + + Follow standard tagging conventions from Azure Cloud Adoption Framework. + https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging + +.EXAMPLE + + Install-Module -Name Az -Scope CurrentUser -Force + Install-Module -Name Az.DigitalTwins -Scope CurrentUser -Force + Register-AzResourceProvider -ProviderNamespace Microsoft.DigitalTwins + Connect-AzAccount + Set-AzContext -SubscriptionId $SubscriptionId + $VerbosePreference = 'Continue' + ./deploy-infrastructure.ps1 +#> +[CmdletBinding()] +param ( + ## Number of initial scripts to skip (if they have already been run) + [int]$Skip = 0, + ## Deployment environment, e.g. Prod, Dev, QA, Stage, Test. + [string]$Environment = $ENV:DEPLOY_ENVIRONMENT ?? 'Dev', + ## The Azure region where the resource is deployed. + [string]$Location = $ENV:DEPLOY_LOCATION ?? 'australiaeast', + ## Identifier for the organisation (or subscription) to make global names unique. + [string]$OrgId = $ENV:DEPLOY_ORGID ?? "0x$((Get-AzContext).Subscription.Id.Substring(0,4))" +) + +# Following standard naming conventions from Azure Cloud Adoption Framework +# https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming +# With an additional organisation or subscription identifier (after app name) in global names to make them unique + +# Following standard tagging conventions from Azure Cloud Adoption Framework +# https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging + + +# Pre-requisites: +# +# Running these scripts requires the following to be installed: +# * PowerShell, https://github.com/PowerShell/PowerShell +# * Azure PowerShell module, https://docs.microsoft.com/en-us/powershell/azure/install-az-ps +# Install-Module -Name Az -Scope CurrentUser -Force +# * Azure Digital Twins module (preview installed separately) +# Install-Module -Name Az.DigitalTwins -Scope CurrentUser -Force +# Register-AzResourceProvider -ProviderNamespace Microsoft.DigitalTwins +# +# You also need to authenticate and set subscription you are using: +# Connect-AzAccount +# Set-AzContext -SubscriptionId $SubscriptionId +# +# To see messages, set verbose preference before running: +# $VerbosePreference = 'Continue' +# ./deploy-infrastructure.ps1 + +$ErrorActionPreference="Stop" + +$SubscriptionId = (Get-AzContext).Subscription.Id +Write-Verbose "Using context subscription ID $SubscriptionId" + +$scriptItems = Get-ChildItem "$PSScriptRoot/infrastructure" -Filter '*.ps1' ` + | Sort-Object -Property Name ` + | Select-Object -Skip $Skip + +$scriptItems | ForEach-Object { + Write-Verbose "Running $($_.Name)" + & $_.FullName -Environment $Environment -Location $Location -OrgId $OrgId +} + +Write-Verbose "Deployment Complete" diff --git a/Examples/FactoryExample/images/model-graph-example.png b/Examples/FactoryExample/images/model-graph-example.png new file mode 100644 index 0000000..a331fac Binary files /dev/null and b/Examples/FactoryExample/images/model-graph-example.png differ diff --git a/Examples/FactoryExample/images/twins-graph-example.png b/Examples/FactoryExample/images/twins-graph-example.png new file mode 100644 index 0000000..1ddae21 Binary files /dev/null and b/Examples/FactoryExample/images/twins-graph-example.png differ diff --git a/Examples/FactoryExample/infrastructure/0001 - Create resource group.ps1 b/Examples/FactoryExample/infrastructure/0001 - Create resource group.ps1 new file mode 100644 index 0000000..177a111 --- /dev/null +++ b/Examples/FactoryExample/infrastructure/0001 - Create resource group.ps1 @@ -0,0 +1,18 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + [string]$Environment, + [string]$Location, + [string]$OrgId +) + +$appName = 'codefirsttwins' + +$rgName = "rg-$appName-$Environment-001".ToLowerInvariant() +$tags = @{ WorkloadName = 'codefirsttwins'; DataClassification = 'Non-business'; Criticality = 'Low'; ` + BusinessUnit = 'Demo'; ApplicationName = $appName; Env = $Environment } + +Write-Verbose "Creating resource group $rgName in location $Location" + +New-AzResourceGroup -Name $rgName -Location $Location -Tag $tags -Force diff --git a/Examples/FactoryExample/infrastructure/0002 - Create Digital Twins service.ps1 b/Examples/FactoryExample/infrastructure/0002 - Create Digital Twins service.ps1 new file mode 100644 index 0000000..1c3302a --- /dev/null +++ b/Examples/FactoryExample/infrastructure/0002 - Create Digital Twins service.ps1 @@ -0,0 +1,31 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + [string]$Environment, + [string]$Location, + [string]$OrgId +) + +$appName = 'codefirsttwins' +$roleName = 'Azure Digital Twins Data Owner' + +$rgName = "rg-$appName-$Environment-001".ToLowerInvariant() +$rg = Get-AzResourceGroup -Name $rgName + +$dtName = "dt-$appName-$OrgId-$Environment".ToLowerInvariant() + +$contextAccount = (Get-AzContext).Account + +Write-Verbose "Creating digital twins service $dtName in resource group $rgName" + +New-AzDigitalTwinsInstance -ResourceName $dtName ` + -ResourceGroupName $rgName -Location $rg.Location -Tag $rg.Tags + +Write-Verbose "Assigning data permissions to digital twins service $dtName to $contextAccount" + +$user = Get-AzADUser -UserPrincipalName $contextAccount +$dti = Get-AzDigitalTwinsInstance -ResourceGroupName $rgName -ResourceName $dtName + +New-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $roleName -Scope $dti.Id + diff --git a/Examples/FactoryExample/infrastructure/0003 - Create IOT hub.ps1 b/Examples/FactoryExample/infrastructure/0003 - Create IOT hub.ps1 new file mode 100644 index 0000000..64b8e29 --- /dev/null +++ b/Examples/FactoryExample/infrastructure/0003 - Create IOT hub.ps1 @@ -0,0 +1,22 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + [string]$Environment, + [string]$Location, + [string]$OrgId +) + +$appName = 'codefirsttwins' +$iotSku = 'S1' +$iotUnits = 1 + +$rgName = "rg-$appName-$Environment-001".ToLowerInvariant() +$rg = Get-AzResourceGroup -Name $rgName + +$iotName = "iot-$appName-$OrgId-$Environment".ToLowerInvariant() + +Write-Verbose "Creating IOT hub $iotName in resource group $rgName" + +New-AzIotHub -Name $iotName -SkuName $iotSku -Units $iotUnits ` + -ResourceGroupName $rgName -Location $rg.Location -Tag $rg.Tags diff --git a/Examples/FactoryExample/infrastructure/9001 - Output settings.ps1 b/Examples/FactoryExample/infrastructure/9001 - Output settings.ps1 new file mode 100644 index 0000000..e694aae --- /dev/null +++ b/Examples/FactoryExample/infrastructure/9001 - Output settings.ps1 @@ -0,0 +1,18 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + [string]$Environment, + [string]$Location, + [string]$OrgId +) + +$appName = 'codefirsttwins' +$rgName = "rg-$appName-$Environment-001".ToLowerInvariant() +$dtName = "dt-$appName-$OrgId-$Environment".ToLowerInvariant() + +Write-Verbose 'Digital Twins host name:' +(Get-AzDigitalTwinsInstance -ResourceGroupName $rgName -ResourceName $dtName).HostName + +Write-Verbose 'IOT Hub host name:' +(Get-AzIotHub $rgName).Properties.HostName diff --git a/Examples/FactoryExample/remove-infrastructure.ps1 b/Examples/FactoryExample/remove-infrastructure.ps1 new file mode 100644 index 0000000..594af16 --- /dev/null +++ b/Examples/FactoryExample/remove-infrastructure.ps1 @@ -0,0 +1,24 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + ## Number of initial scripts to skip (if they have already been run) + [int]$Skip = 0, + ## Deployment environment, e.g. Prod, Dev, QA, Stage, Test. + [string]$Environment = $ENV:DEPLOY_ENVIRONMENT ?? 'Dev', + ## The Azure region where the resource is deployed. + [string]$Location = $ENV:DEPLOY_LOCATION ?? 'australiaeast', + ## Identifier for the organisation (or subscription) to make global names unique. + [string]$OrgId = $ENV:DEPLOY_ORGID ?? "0x$((Get-AzContext).Subscription.Id.Substring(0,4))" +) + +$ErrorActionPreference="Stop" + +$SubscriptionId = (Get-AzContext).Subscription.Id +Write-Verbose "Removing from context subscription ID $SubscriptionId" + +$appName = 'codefirsttwins' + +$rgName = "rg-$appName-$Environment-001".ToLowerInvariant() + +Remove-AzResourceGroup -Name $rgName