├── .env.temp ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── IIoTEdgeApp.sln ├── LICENSE ├── ModuleWrapper ├── .gitignore ├── IModuleClient.cs ├── MockMessageInjector.cs ├── MockModuleClientProxy.cs ├── ModuleClientProxy.cs ├── ModuleHost.cs ├── ModuleWrapper.csproj ├── SerilogSettings.json └── TaskTimer.cs ├── ModuleWrapperTest ├── .gitignore ├── ModuleWrapperTest.csproj ├── Program.cs └── TestModule.cs ├── README.md ├── SECURITY.md ├── deployment.debug.template.json ├── deployment.template.json ├── documentation ├── grafana_README.md └── influxdb_README.md ├── images └── arch_diagram.PNG └── modules ├── grafana ├── .dockerignore ├── .gitignore ├── Dockerfile.amd64 ├── Dockerfile.amd64.debug ├── dashboards │ └── machineDataDashboard.json ├── datasources │ └── influxdb.yaml ├── module.json └── provisioning_dashboards │ └── docker-dashboard.yml ├── influxdb ├── Dockerfile.amd64 ├── Dockerfile.amd64.debug └── module.json ├── orchestrator ├── .dockerignore ├── .gitignore ├── Abstraction │ ├── IHttpHandler.cs │ └── ITimeSeriesRecorder.cs ├── Dockerfile.amd64 ├── Dockerfile.amd64.debug ├── Mock │ ├── MockHttpClientHandler.cs │ ├── MockTimeSeriesRecorder.cs │ └── TelemetryMessageSample.json ├── Orchestrator.csproj ├── OrchestratorModule.cs ├── Program.cs ├── Service │ ├── HttpClientHandler.cs │ ├── InfluxDBRecorder.cs │ ├── ORM.cs │ └── TelemetrySelection.json └── module.json └── simulator ├── .dockerignore ├── .gitignore ├── Dockerfile.amd64 ├── Dockerfile.amd64.debug ├── Program.cs ├── Service ├── Measurement.cs └── MessageEmitter.cs ├── Simulator.csproj ├── SimulatorModule.cs ├── module.json └── sampleData.csv /.env.temp: -------------------------------------------------------------------------------- 1 | CONTAINER_REGISTRY_ADDRESS= 2 | CONTAINER_REGISTRY_USERNAME= 3 | CONTAINER_REGISTRY_PASSWORD= 4 | 5 | INFLUX_BIND=/srv/influx:/var/lib/influxdb 6 | INFLUX_URL=http://influxdb:8086 7 | INFLUX_USERNAME="unauthenticated for now" 8 | INFLUX_PASSWORD="unauthenticated for now" 9 | INFLUX_DATABASE=inference 10 | INLFUX_ADMIN_USER=grafana 11 | INFLUX_ADMIN_PASS=grafana 12 | INFLUX_RETENTION_IN_DAYS=30 13 | GRAFANA_ADMIN_PASSWORD=admin 14 | GRAFANA_LOG_CONSOLE_LEVEL=warn 15 | 16 | BUILD_BUILDID=1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignore config and .env 2 | config/ 3 | .env 4 | .vs/ 5 | 6 | project.lock.json 7 | project.fragment.lock.json 8 | artifacts/ 9 | **/Properties/launchSettings.json 10 | 11 | *_i.c 12 | *_p.c 13 | *_i.h 14 | *.ilk 15 | *.meta 16 | *.obj 17 | *.pch 18 | *.pdb 19 | *.pgc 20 | *.pgd 21 | *.rsp 22 | *.sbr 23 | *.tlb 24 | *.tli 25 | *.tlh 26 | *.tmp 27 | *.tmp_proj 28 | *.log 29 | *.vspscc 30 | *.vssscc 31 | .builds 32 | *.pidb 33 | *.svclog 34 | *.scc 35 | .vs 36 | 37 | **/[Bb]in/ 38 | **/[Oo]bj/ 39 | 40 | **/*.cache 41 | 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "simulator Remote Debug (.NET Core)", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:pickRemoteProcess}", 9 | "pipeTransport": { 10 | "pipeProgram": "docker", 11 | "pipeArgs": [ 12 | "exec", 13 | "-i", 14 | "simulator", 15 | "sh", 16 | "-c" 17 | ], 18 | "debuggerPath": "~/vsdbg/vsdbg", 19 | "pipeCwd": "${workspaceFolder}", 20 | "quoteArgs": true 21 | }, 22 | "sourceFileMap": { 23 | "/app": "${workspaceFolder}/modules/simulator" 24 | }, 25 | "justMyCode": true 26 | }, 27 | { 28 | "name": "simulator Local Debug (.NET Core)", 29 | "type": "coreclr", 30 | "preLaunchTask": "build simulator", 31 | "request": "launch", 32 | "program": "${workspaceRoot}/modules/simulator/bin/Debug/netcoreapp2.1/simulator.dll", 33 | "args": [ 34 | "Environment=Debug" 35 | ], 36 | "cwd": "${workspaceRoot}/modules/simulator", 37 | "internalConsoleOptions": "openOnSessionStart", 38 | "stopAtEntry": false, 39 | "console": "internalConsole", 40 | "env": { 41 | "EdgeHubConnectionString": "${config:azure-iot-edge.EdgeHubConnectionString}", 42 | "EdgeModuleCACertificateFile": "${config:azure-iot-edge.EdgeModuleCACertificateFile}" 43 | } 44 | }, 45 | { 46 | "name": "orchestrator Remote Debug (.NET Core)", 47 | "type": "coreclr", 48 | "request": "attach", 49 | "processId": "${command:pickRemoteProcess}", 50 | "pipeTransport": { 51 | "pipeProgram": "docker", 52 | "pipeArgs": [ 53 | "exec", 54 | "-i", 55 | "orchestrator", 56 | "sh", 57 | "-c" 58 | ], 59 | "debuggerPath": "~/vsdbg/vsdbg", 60 | "pipeCwd": "${workspaceFolder}", 61 | "quoteArgs": true 62 | }, 63 | "sourceFileMap": { 64 | "/app": "${workspaceFolder}/modules/orchestrator" 65 | }, 66 | "justMyCode": true 67 | }, 68 | { 69 | "name": "orchestrator Local Debug (.NET Core)", 70 | "type": "coreclr", 71 | "preLaunchTask": "build orchestrator", 72 | "request": "launch", 73 | "program": "${workspaceRoot}/modules/orchestrator/bin/Debug/netcoreapp2.1/orchestrator.dll", 74 | "args": [ 75 | "Environment=Debug" 76 | ], 77 | "cwd": "${workspaceRoot}/modules/orchestrator", 78 | "internalConsoleOptions": "openOnSessionStart", 79 | "stopAtEntry": false, 80 | "console": "internalConsole", 81 | "env": { 82 | "EdgeHubConnectionString": "${config:azure-iot-edge.EdgeHubConnectionString}", 83 | "EdgeModuleCACertificateFile": "${config:azure-iot-edge.EdgeModuleCACertificateFile}" 84 | } 85 | } 86 | ] 87 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build simulator", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "options": { 18 | "cwd": "${workspaceFolder}/modules/simulator" 19 | }, 20 | "group": "build", 21 | "presentation": { 22 | "reveal": "silent" 23 | }, 24 | "problemMatcher": "$msCompile" 25 | }, 26 | { 27 | "label": "build orchestrator", 28 | "command": "dotnet", 29 | "type": "shell", 30 | "args": [ 31 | "build", 32 | // Ask dotnet build to generate full paths for file names. 33 | "/property:GenerateFullPaths=true", 34 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 35 | "/consoleloggerparameters:NoSummary" 36 | ], 37 | "options": { 38 | "cwd": "${workspaceFolder}/modules/orchestrator" 39 | }, 40 | "group": "build", 41 | "presentation": { 42 | "reveal": "silent" 43 | }, 44 | "problemMatcher": "$msCompile" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | First of all, thank you for taking the time to contribute to this project. We've tried to make a stable project and try to fix bugs and add new features continuously. You can help us do more. 3 | 4 | ## Getting started 5 | 6 | ### Check out the roadmap 7 | 8 | We have some functionalities in mind and we have issued them and there is a *milestone* label available on the issue. If there is a bug or a feature that is not listed in the **issues** page or there is no one assigned to the issue, feel free to fix/add it! Although it's better to discuss it in the issue or create a new issue for it so there is no confilcting code. 9 | 10 | ### Writing some code! 11 | 12 | Contributing to a project on AzureDevOps/Github is pretty straight forward. If this is you're first time, these are the steps you should take. 13 | 14 | - Fork this repo. 15 | 16 | And that's it! Read the code available and change the part you don't like! You're change should not break the existing code and should pass the tests. 17 | 18 | If you're adding a new functionality, start from the branch **master**. It would be a better practice to create a new branch and work in there. 19 | 20 | When you're done, submit a pull request and for one of the maintainers to check it out. We would let you know if there is any problem or any changes that should be considered. 21 | 22 | ### Tests 23 | 24 | We've written tests and you can run them to assure the stability of the code. If you're adding a new functionality please write a test for it. 25 | 26 | ### Documentation 27 | 28 | Every chunk of code that may be hard to understand has some comments above it. If you write some new code or change some part of the existing code in a way that it would not be functional without changing it's usages, it needs to be documented. -------------------------------------------------------------------------------- /IIoTEdgeApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Simulator", "modules\simulator\Simulator.csproj", "{9AB141DA-7EFD-4A29-AC4C-5419382D0D5A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{3D5C2213-2050-49C5-AB46-8F03EAC1465E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orchestrator", "modules\orchestrator\Orchestrator.csproj", "{DFFD8EAE-A22C-4131-8F30-BB2026D23437}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModuleWrapper", "ModuleWrapper\ModuleWrapper.csproj", "{79883DDF-B2D5-4443-A056-7A58947F7C35}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModuleWrapperTest", "ModuleWrapperTest\ModuleWrapperTest.csproj", "{DB80F8D0-F350-4500-9182-E8F38D32D450}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {9AB141DA-7EFD-4A29-AC4C-5419382D0D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {9AB141DA-7EFD-4A29-AC4C-5419382D0D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {9AB141DA-7EFD-4A29-AC4C-5419382D0D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {9AB141DA-7EFD-4A29-AC4C-5419382D0D5A}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DFFD8EAE-A22C-4131-8F30-BB2026D23437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DFFD8EAE-A22C-4131-8F30-BB2026D23437}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DFFD8EAE-A22C-4131-8F30-BB2026D23437}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DFFD8EAE-A22C-4131-8F30-BB2026D23437}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {79883DDF-B2D5-4443-A056-7A58947F7C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {79883DDF-B2D5-4443-A056-7A58947F7C35}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {79883DDF-B2D5-4443-A056-7A58947F7C35}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {79883DDF-B2D5-4443-A056-7A58947F7C35}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {DB80F8D0-F350-4500-9182-E8F38D32D450}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {DB80F8D0-F350-4500-9182-E8F38D32D450}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {DB80F8D0-F350-4500-9182-E8F38D32D450}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {DB80F8D0-F350-4500-9182-E8F38D32D450}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {9AB141DA-7EFD-4A29-AC4C-5419382D0D5A} = {3D5C2213-2050-49C5-AB46-8F03EAC1465E} 44 | {DFFD8EAE-A22C-4131-8F30-BB2026D23437} = {3D5C2213-2050-49C5-AB46-8F03EAC1465E} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {7A78B0AA-0C42-4D34-8C88-4BEB238FA102} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /ModuleWrapper/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /ModuleWrapper/IModuleClient.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ModuleWrapper 3 | { 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Azure.Devices.Client; 7 | using Microsoft.Azure.Devices.Shared; 8 | 9 | public interface IModuleClient 10 | { 11 | Task OpenAsync(); 12 | Task CloseAsync(); 13 | Task SendEventAsync(string outputName, Message message); 14 | Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext); 15 | Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext); 16 | Task GetTwinAsync(CancellationToken cancellationToken); 17 | Task GetTwinAsync(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ModuleWrapper/MockMessageInjector.cs: -------------------------------------------------------------------------------- 1 | namespace ModuleWrapper 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Hosting; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | public delegate Task InjectMessageAsyncDelegate(IConfiguration configuration, IModuleClient moduleClient); 10 | public class MockMessageInjector : IHostedService 11 | { 12 | public IConfiguration Configuration { get; } 13 | public IModuleClient ModuleClient { get; } 14 | public InjectMessageAsyncDelegate InjectMessage { get; } 15 | public MockMessageInjector(IConfiguration configuration, 16 | IModuleClient moduleClient, 17 | InjectMessageAsyncDelegate injectMessage) 18 | { 19 | Configuration = configuration; 20 | ModuleClient = moduleClient; 21 | InjectMessage = injectMessage; 22 | } 23 | public async Task StartAsync(CancellationToken cancellationToken) 24 | { 25 | // TODO: Make this repeating 26 | await InjectMessage(Configuration, ModuleClient); 27 | } 28 | 29 | public async Task StopAsync(CancellationToken cancellationToken) 30 | { 31 | await Task.FromResult(0); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ModuleWrapper/MockModuleClientProxy.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ModuleWrapper 3 | { 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Azure.Amqp.Framing; 9 | using Microsoft.Azure.Devices.Client; 10 | using Microsoft.Azure.Devices.Shared; 11 | using Serilog; 12 | 13 | public class MockModuleClientProxy : IModuleClient 14 | { 15 | public Dictionary> MessageQueues { get; private set; } 16 | public Dictionary> InputMessageHandlers { get; private set; } 17 | public Dictionary MethodMessageHandlers { get; private set; } 18 | public TaskTimer TaskTimer { get; private set; } 19 | public CancellationTokenSource CancellationTokenSource { get; private set; } 20 | 21 | public MockModuleClientProxy(CancellationTokenSource cancellationTokenSource) 22 | { 23 | CancellationTokenSource = cancellationTokenSource; 24 | 25 | MessageQueues = new Dictionary>(); 26 | InputMessageHandlers = new Dictionary(); 27 | MethodMessageHandlers = new Dictionary(); 28 | TaskTimer = new TaskTimer(OnTimer, 29 | TimeSpan.FromSeconds(1), 30 | CancellationTokenSource); 31 | } 32 | 33 | private void OnTimer() 34 | { 35 | lock (MessageQueues) 36 | foreach (var queue in MessageQueues) 37 | { 38 | if (InputMessageHandlers.ContainsKey(queue.Key)) 39 | foreach (var message in queue.Value) 40 | InputMessageHandlers[queue.Key].Item1(message, InputMessageHandlers[queue.Key].Item2); 41 | MessageQueues[queue.Key].Clear(); 42 | } 43 | // TODO: Process method messsages too 44 | } 45 | 46 | public async Task SendEventAsync(string outputName, Message message) 47 | { 48 | lock (MessageQueues) 49 | { 50 | if (!MessageQueues.ContainsKey(outputName)) 51 | MessageQueues[outputName] = new List(); 52 | MessageQueues[outputName].Add(message); 53 | } 54 | Log.Information($"Message Sent to {outputName}"); 55 | await Task.FromResult(0); 56 | } 57 | 58 | public async Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext) 59 | { 60 | InputMessageHandlers[inputName] = (messageHandler, userContext); 61 | 62 | Log.Information($"Message Handler Set for {inputName}"); 63 | await Task.FromResult(0); 64 | } 65 | 66 | public async Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext) 67 | { 68 | MethodMessageHandlers[methodName] = methodHandler; 69 | 70 | Log.Information($"Method Handler Set for {methodName}"); 71 | await Task.FromResult(0); 72 | } 73 | 74 | public async Task OpenAsync() 75 | { 76 | Log.Information("Opened ModuleClient"); 77 | TaskTimer.Start(); 78 | 79 | await Task.FromResult(0); 80 | } 81 | 82 | public async Task CloseAsync() 83 | { 84 | Log.Information("Closed ModuleClient"); 85 | CancellationTokenSource.Cancel(); 86 | 87 | await Task.FromResult(0); 88 | } 89 | 90 | public async Task GetTwinAsync(CancellationToken cancellationToken) 91 | { 92 | Log.Information("GetTwinAsync"); 93 | return await Task.FromResult(null); 94 | } 95 | 96 | public async Task GetTwinAsync() 97 | { 98 | Log.Information("GetTwinAsync"); 99 | return await Task.FromResult(null); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ModuleWrapper/ModuleClientProxy.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ModuleWrapper 3 | { 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Azure.Devices.Client; 7 | using Microsoft.Azure.Devices.Client.Transport.Mqtt; 8 | using Microsoft.Azure.Devices.Shared; 9 | using Serilog; 10 | 11 | public class ModuleClientProxy : IModuleClient 12 | { 13 | private ModuleClient ModuleClient { get; } 14 | public CancellationTokenSource CancellationTokenSource { get; private set; } 15 | 16 | public ModuleClientProxy( 17 | CancellationTokenSource cancellationTokenSource) 18 | { 19 | CancellationTokenSource = cancellationTokenSource; 20 | 21 | MqttTransportSettings mqttSetting = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only); 22 | ITransportSettings[] settings = { mqttSetting }; 23 | 24 | ModuleClient = ModuleClient.CreateFromEnvironmentAsync(settings).Result; ; 25 | Log.Information("Created ModuleClient From Environment"); 26 | } 27 | 28 | public async Task SendEventAsync(string outputName, Message message) 29 | { 30 | await ModuleClient.SendEventAsync(outputName, message); 31 | Log.Information($"Message Sent to {outputName}"); 32 | } 33 | 34 | public async Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext) 35 | { 36 | await ModuleClient.SetInputMessageHandlerAsync(inputName, messageHandler, userContext); 37 | Log.Information($"Message Handler Set for {inputName}"); 38 | } 39 | 40 | public async Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext) 41 | { 42 | await ModuleClient.SetMethodHandlerAsync(methodName, methodHandler, userContext); 43 | Log.Information($"Method Handler Set for {methodName}"); 44 | } 45 | 46 | 47 | public async Task OpenAsync() 48 | { 49 | await ModuleClient.OpenAsync(); 50 | Log.Information("Opened ModuleClient"); 51 | } 52 | 53 | public async Task CloseAsync() 54 | { 55 | await ModuleClient.CloseAsync(); 56 | Log.Information("Closed ModuleClient"); 57 | } 58 | 59 | public async Task GetTwinAsync(CancellationToken cancellationToken) 60 | { 61 | return await ModuleClient.GetTwinAsync(cancellationToken); 62 | } 63 | 64 | public async Task GetTwinAsync() 65 | { 66 | return await ModuleClient.GetTwinAsync(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ModuleWrapper/ModuleHost.cs: -------------------------------------------------------------------------------- 1 | namespace ModuleWrapper 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Serilog; 9 | using Microsoft.Extensions.Configuration; 10 | using Serilog.Context; 11 | using System.Reflection; 12 | using System.Threading; 13 | using System.Runtime.Loader; 14 | 15 | public static class ModuleHost 16 | { 17 | /// 18 | /// Handles cleanup operations when app is cancelled or unloads 19 | /// 20 | public static Task WhenCancelled(CancellationToken cancellationToken) 21 | { 22 | var taskCompletionSource = new TaskCompletionSource(); 23 | cancellationToken.Register(s => ((TaskCompletionSource)s).SetResult(true), taskCompletionSource); 24 | return taskCompletionSource.Task; 25 | } 26 | 27 | public delegate void ConfigureServices(IServiceCollection collection, IConfiguration configuration); 28 | 29 | public static async Task Run(string[] args, ConfigureServices configureServices = null) 30 | where THostedService : class, IHostedService 31 | { 32 | Serilog.Debugging.SelfLog.Enable(Console.Error); 33 | 34 | var cancellationTokenSource = new CancellationTokenSource(); 35 | AssemblyLoadContext.Default.Unloading += (cts) => cancellationTokenSource.Cancel(); 36 | Console.CancelKeyPress += (sender, cts) => cancellationTokenSource.Cancel(); 37 | 38 | var assembly = Assembly.GetExecutingAssembly(); 39 | IConfiguration configuration; 40 | using (var resourceStream = assembly.GetManifestResourceStream("ModuleWrapper.SerilogSettings.json")) 41 | { 42 | configuration = new ConfigurationBuilder() 43 | .AddJsonStream(resourceStream) 44 | .AddJsonFile("SerilogSettings.json", true) 45 | .AddJsonFile("appsettings.json", optional: true) 46 | .AddEnvironmentVariables() 47 | .AddCommandLine(args) 48 | .Build(); 49 | } 50 | Log.Logger = new LoggerConfiguration() 51 | .ReadFrom.Configuration(configuration) 52 | .CreateLogger(); 53 | 54 | LogContext.PushProperty("ModuleId", typeof(THostedService).Name); 55 | LogContext.PushProperty("FunctionId", nameof(Run)); 56 | 57 | var host = new HostBuilder() 58 | .UseDefaultServiceProvider((context, options) => { 59 | options.ValidateOnBuild = true; 60 | }) 61 | .ConfigureHostConfiguration(configHost => 62 | { 63 | configHost.SetBasePath(Directory.GetCurrentDirectory()); 64 | configHost.AddJsonFile("hostsettings.json", optional: true); 65 | configHost.AddEnvironmentVariables(); 66 | configHost.AddCommandLine(args); 67 | }) 68 | .ConfigureAppConfiguration((hostContext, configApp) => 69 | { 70 | configApp.AddJsonFile("appsettings.json", optional: true); 71 | configApp.AddJsonFile( 72 | $"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", 73 | optional: true); 74 | configApp.AddEnvironmentVariables(); 75 | configApp.AddCommandLine(args); 76 | }) 77 | .ConfigureServices((hostContext, services) => 78 | { 79 | services.AddHostedService(); 80 | services.AddSingleton(cancellationTokenSource); 81 | 82 | configureServices?.Invoke(services, hostContext.Configuration); 83 | }) 84 | .UseSerilog() 85 | .UseConsoleLifetime() 86 | .Build(); 87 | 88 | await Task.WhenAny(host.RunAsync(), WhenCancelled(cancellationTokenSource.Token)); 89 | 90 | Log.Information("Exiting.."); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /ModuleWrapper/ModuleWrapper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ModuleWrapper/SerilogSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ 4 | "Serilog.Sinks.Console" 5 | ], 6 | "MinimumLevel": { 7 | "Default": "Debug", 8 | "Override": { 9 | "Microsoft": "Information", 10 | "Microsoft.AspNetCore": "Warning" 11 | } 12 | }, 13 | "WriteTo": [ 14 | { 15 | "Name": "Console" 16 | } 17 | ], 18 | "Enrich": [ 19 | "FromLogContext", 20 | "WithMachineName", 21 | "WithThreadId" 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /ModuleWrapper/TaskTimer.cs: -------------------------------------------------------------------------------- 1 | namespace ModuleWrapper 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Serilog; 7 | 8 | public class TaskTimer 9 | { 10 | CancellationTokenSource cancellationTokenSource; 11 | TimeSpan timerPeriod; 12 | Action onElapsedCallback; 13 | bool continueOnError; 14 | 15 | public TaskTimer(Action onElapsedCallback, 16 | TimeSpan timerPeriod, 17 | CancellationTokenSource cancellationTokenSource, 18 | bool continueOnError = true) 19 | { 20 | this.cancellationTokenSource = cancellationTokenSource; 21 | this.timerPeriod = timerPeriod; 22 | this.onElapsedCallback = onElapsedCallback; 23 | this.continueOnError = continueOnError; 24 | } 25 | 26 | public void Start() 27 | { 28 | Task elapsedTask = null; 29 | elapsedTask = new Task((x) => 30 | { 31 | Elapsed(elapsedTask, cancellationTokenSource); 32 | }, cancellationTokenSource.Token); 33 | 34 | HandleError(elapsedTask); 35 | 36 | elapsedTask.Start(); 37 | } 38 | 39 | private void Elapsed(Task task, object objParam) 40 | { 41 | var start = DateTime.Now; 42 | var cancellationTokenSource = (CancellationTokenSource)objParam; 43 | if (cancellationTokenSource.Token.IsCancellationRequested) 44 | { 45 | Log.Information("A cancellation has been requested."); 46 | return; 47 | } 48 | 49 | onElapsedCallback(); 50 | 51 | var delay = timerPeriod - (DateTime.Now - start); 52 | if (delay.Ticks > 0) 53 | { 54 | Log.Verbose($"Waiting for {delay}"); 55 | task = Task.Delay(delay); 56 | } 57 | HandleError(task.ContinueWith(Elapsed, cancellationTokenSource)); 58 | } 59 | 60 | private void HandleError(Task task) 61 | { 62 | task.ContinueWith((e) => 63 | { 64 | Log.Error($"Exception when running timer callback: {e.Exception}"); 65 | if (!continueOnError) 66 | cancellationTokenSource.Cancel(); 67 | else 68 | task.ContinueWith(Elapsed, cancellationTokenSource); 69 | }, TaskContinuationOptions.OnlyOnFaulted); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /ModuleWrapperTest/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /ModuleWrapperTest/ModuleWrapperTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ModuleWrapperTest/Program.cs: -------------------------------------------------------------------------------- 1 | namespace ModuleWrapperTest 2 | { 3 | using ModuleWrapper; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Azure.Devices.Client; 8 | using System.Text; 9 | 10 | class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | await ModuleHost.Run(args, 15 | (services, configuration) => 16 | { 17 | services.AddSingleton(); 18 | 19 | services.AddHostedService(x => 20 | new MockMessageInjector(x.GetRequiredService(), 21 | x.GetRequiredService(), 22 | async (config, client) => 23 | await client.SendEventAsync("input", new Message(Encoding.UTF8.GetBytes("Sample Message"))))); 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ModuleWrapperTest/TestModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Devices.Client; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using ModuleWrapper; 5 | using Serilog; 6 | using Serilog.Context; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModuleWrapperTest 11 | { 12 | public class TestModule : IHostedService 13 | { 14 | public IConfiguration Configuration { get; } 15 | public IModuleClient ModuleClient { get; } 16 | public CancellationTokenSource CancellationTokenSource { get; } 17 | 18 | public TestModule(IConfiguration configuration, 19 | IModuleClient moduleClient, 20 | CancellationTokenSource cancellationTokenSource) 21 | { 22 | Configuration = configuration; 23 | ModuleClient = moduleClient; 24 | CancellationTokenSource = cancellationTokenSource; 25 | } 26 | 27 | public async Task StartAsync(CancellationToken cancellationToken) 28 | { 29 | using (LogContext.PushProperty("FunctionId", nameof(StartAsync))) 30 | { 31 | Log.Information("Opening Edge Module Connection"); 32 | await ModuleClient.OpenAsync(); 33 | 34 | Log.Information("Beginning to Process Messages"); 35 | 36 | await ModuleClient.SetInputMessageHandlerAsync("input", 37 | new MessageHandler(async (message, context) => 38 | { 39 | Log.Information("Received message in input route."); 40 | return await Task.FromResult(MessageResponse.Completed); 41 | }), ModuleClient); 42 | } 43 | } 44 | 45 | public Task StopAsync(CancellationToken cancellationToken) 46 | { 47 | using (LogContext.PushProperty("FunctionId", nameof(StopAsync))) 48 | { 49 | Log.Information("Shutting Down"); 50 | return Task.FromResult(0); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - azure-iot-edge 7 | - vs-code 8 | --- 9 | 10 | # Introduction 11 | This project two development patterns: 12 | 13 | 1) A new pattern for IoT Edge module development using dependency injection. This allows for unit tests and debugging modules without needing to run the simulator. 14 | 15 | 2) InfluxDB and Grafana working on IoT Edge (see `documentation` folder for more specifics on each) 16 | 17 | ## Contents 18 | 19 | Outline the file contents of the repository. It helps users navigate the codebase, build configuration and any related assets. 20 | 21 | | File/folder | Description | 22 | |-------------------|--------------------------------------------| 23 | | `documentation` | Details of InfluxDB and Grafana config | 24 | | `modules` | IoT Edge module code | 25 | | `images` | Architecture diagram | 26 | | `ModuleWrapper` | Dependency Injection and configuration | 27 | | `ModuleWrapperTest` | For ModuleWrapper testing | 28 | | `deployment.template.json` | IoT Edge deployment configuration | 29 | | `.gitignore` | Define what to ignore at commit time. | 30 | | `CONTRIBUTING.md` | Guidelines for contributing to the sample. | 31 | | `README.md` | This README file. | 32 | | `LICENSE` | The license for the sample. | 33 | 34 | ## Simplified Architecture Diagram 35 | 36 | ![Architecture Diagram](./images/arch_diagram.PNG) 37 | 38 | The simulator module uses Edge Hub routing to send a telemetry message to the orchestrator module. 39 | 40 | The orchestrator module uses Edge Hub routing to forward messages to the $upstream Azure IoT Hub. 41 | 42 | InfluxDB and Grafana are deployed within the IoT Edge but use REST APIs to communicate. 43 | 44 | ### Development machine prerequisites 45 | 46 | The edge and cloud samples are designed to be built and run together on a development machine in Visual Studio Code using Docker CE, the Azure IoT EdgeHub Dev Tool and the Azure Functions Core Tools. Below are the prerequisites to build and run the sample on a local development machine: 47 | 48 | 1. Language SDK 49 | [.NET Core SDK (2.1)](https://www.microsoft.com/net/download) 50 | 51 | 2. Docker 52 | 53 | [Docker Community Edition](https://docs.docker.com/install/) - required for Azure IoT Edge module development, deployment and debugging. Docker CE is free, but may require registration with Docker account to download. Docker on Windows requires Hyper-V support. Please make sure your Windows version supports Hyper-V. For Windows 10, Hyper-V is available with the Pro or Enterprise versions. 54 | 55 | 3. Visual Studio Code and extensions 56 | 57 | > **Note**: Extensions can be installed either via links to the Visual Studio Code Marketplace below or by searching extensions by name in the Marketplace from the Extensions tab in Visual Studio Code. 58 | 59 | Install [Visual Studio Code](https://code.visualstudio.com/) first and then add the following extensions: 60 | 61 | - [C# extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) (only required for C# version of sample) - provides C# syntax checking, build and debug support 62 | - [Azure IoT Tools](https://marketplace.visualstudio.com/items?itemName=vsciot-vscode.azure-iot-tools) - provides Azure IoT Edge development tooling 63 | 64 | > **Note**: Azure IoT Tools is an extension pack that installs 3 extensions that will show up in the Extensions pane in Visual Studio Code - *Azure IoT Hub Toolkit*, *Azure IoT Edge* and *Azure IoT Workbench*. 65 | 66 | ### Azure Prerequisites 67 | 68 | 1. Azure IoT Hub Service 69 | 70 | To run the samples, you will need an Azure subscription and a provisioned Azure IoT Hub service. Every Azure subscription allows one free F1 tier Azure IoT Hub. The F1 tier Azure IoT is sufficient for this sample. 71 | 72 | [Create an IoT hub using the Azure portal](https://docs.microsoft.com/en-us/azure/iot-hub/quickstart-send-telemetry-dotnet#create-an-iot-hub) 73 | 74 | 2. Azure Container Registry (optional) 75 | 76 | This sample can be built and run in the local Azure IoT Edge Simulator without pushing Azure IoT Edge modules to a container registry. A container registry is only needed when deploying to an actual Azure IoT Edge device. Any Docker container registry can be used, including DockerHub and Azure Container Registry. 77 | 78 | [Create an Azure Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-portal) 79 | 80 | ## Setup 81 | 82 | ### Configure Azure IoT Edge development environment 83 | 84 | 1. Connect your Azure account to Visual Studio Code 85 | 86 | The *Azure IoT Tools* Visual Studio Code extension pack installs a prerequisite *Azure Account* extension if its not already present. This extension allows Visual Studio Code to connect to your Azure subscription. For this sample, Visual Studio Code needs to connect to you Azure IoT Hub service. 87 | 88 | Open the command palette and search for *Azure: Sign In* 89 | 90 | Select this command and you will be prompted to sign into your Azure account in a separate browser window. After sign-in, you should see *Azure:* followed by your login account in the status bar at the bottom of Visual Studio Code. 91 | 92 | 2. Connect to your Azure IoT Hub 93 | 94 | There are 2 ways to connect to your Azure IoT Hub from within Visual Studio Code: 95 | 96 | Open the command palette and search for *Azure IoT Hub: Select IoT Hub* 97 | 98 | ​ **or** 99 | 100 | With the Explorer icon in the Visual Studio Code Activity Bar selected, go to the *AZURE IOT HUB* section in the Explorer pane of Visual Studio Code. Select the "..." to open the Azure IoT Hub context menu. From the Context Menu, choose *Select IoT Hub*. 101 | 102 | Both options will open a selection list of available subscriptions at the top of the Visual Studio window. After selecting your subscription, all available Azure IoT Hubs in your subscription will be presented in another selection list. After selecting your Azure IoT Hub, the *AZURE IOT HUB** section in the Explorer pane of Visual Studio Code will be populated with configured Devices and Endpoints. The Devices list will initially be empty for a new Azure IoT Hub. 103 | 104 | 3. Create an Azure IoT Edge device 105 | 106 | This sample is designed to run in the Azure IoT Edge Simulator on a local development machine. However, the Simulator still connects to your Azure IoT Hub service, and therefore needs an Azure IoT Edge device definition in Azure IoT Hub. You can create an Azure IoT Edge device in the Azure portal, but its easier from Visual Studio Code with the Azure IoT Edge extension installed. 107 | 108 | There are 2 ways to create an Azure IoT Edge device from Visual Studio Code: 109 | 110 | Open the command palette and search for *Azure IoT Hub: Create IoT Edge Device*. 111 | 112 | ​ **or** 113 | 114 | With the Explorer icon in the Visual Studio Code Activity Bar selected, go to the *AZURE IOT HUB* section in the Explorer pane of Visual Studio Code. Select the "..." to open the Azure IoT Hub context menu. From the Context Menu, choose *Create IoT Edge Device*. 115 | 116 | Both options will open a prompt for you to enter the name of the device. 117 | 118 | > **Note:** There is also a *Azure IoT Hub: Create Device* command. This creates a basic IoT device definition, which does not support the Azure IoT Edge Runtime and does not work with the Azure IoT Edge Simulator. 119 | 120 | 4. Configure Azure IoT Edge Simulator to use your Edge Device identity 121 | 122 | Again, there are 2 ways to create setup the Azure IoT Edge Simulator from within Visual Studio Code 123 | 124 | Open the palette and search for *Azure IoT Edge: Setup IoT Edge Simulator*. After selecting the command, a list of devices is displayed. Select the device you created in the previous step. 125 | 126 | **or** 127 | 128 | With the Explorer icon in the Visual Studio Code Activity Bar selected, go to the *AZURE IOT HUB* section in the Explorer pane of Visual Studio Code. Expand the Devices list, and right click on the device you created in the previous step to open the Context Menu. Select *Setup IoT Edge Simulator* from the Context Menu. 129 | 130 | This command will pass your Edge device credentials to the Azure IoT Edge Simulator via a command in the Terminal Window. 131 | 132 | > **Note:** If you try to use the *Setup IoT Edge Simulator* command without first connecting to your Azure IoT Hub, you will instead be prompted to enter the connection string for an Azure IoT Hub device. 133 | 134 | 5. Set environment variables 135 | 136 | The Azure IoT Edge solution deployment manifests (*deployment.template.json* and *deployment.debug.template.json*) and module metadata files (*module.json*) support environment variable substitution. There are 3 environment variable placeholders used in this sample - *$CONTAINER_REGISTRY_USERNAME*, *$CONTAINER_REGISTRY_PASSWORD* and *$CONTAINER_REGISTRY_ADDRESS*. These are used to specify your container registry address and login credentials. To run the code in the Azure IoT Edge Simulator, the *$CONTAINER_REGISTRY_ADDRESS* can be set to the Docker local registry container value of *localhost:5000*. When using the local registry container value, the $CONTAINER_REGISTRY_USERNAME and $CONTAINER_REGISTRY_PASSWORD are not used. However, since they are defined in the deployment manifests, they must be defined in order to avoid the "Please set registry credential to .env file." warning message on initial load. 137 | To protect secrets, *.env* files should not be included in source control. Therefore, this sample includes a *.env.temp* template file that can be renamed to *.env* or the values can be copied to your .env file. To build and run the sample in the Azure IoT Edge Simulator, the following values can be used: 138 | 139 | ``` 140 | CONTAINER_REGISTRY_ADDRESS=localhost:5000 141 | CONTAINER_REGISTRY_USERNAME= 142 | CONTAINER_REGISTRY_PASSWORD= 143 | ``` 144 | > **Note:** *CONTAINER_REGISTRY_USERNAME* and *CONTAINER_REGISTRY_PASSWORD* are not used with the local registry container (*localhost:5000*), but these variables must be defined with any non-empty value. 145 | 146 | If you wish to deploy the solution to a real Edge device, make sure to set the values to your container registry. 147 | 148 | 6. Verify Docker runtime mode (**Windows only**) 149 | 150 | This sample is built to run in an Ubuntu container and requires a Linux Container runtime. If running on Windows, make sure that that Docker CE is running in the default Linux container mode, not Windows container mode. 151 | 152 | You can do this by right clicking the Docker icon in the system tray. If the context menu shows "Switch to Windows containers...", Docker is running in Linux container mode. 153 | 154 | ## Running the sample 155 | 156 | ### Environment Variables 157 | 158 | Change the name of the `.env.temp` file to `.env`. For the CONTAINER_* environment names you can use the below for local development. 159 | 160 | ``` 161 | CONTAINER_REGISTRY_ADDRESS=localhost:5000 162 | CONTAINER_REGISTRY_USERNAME= 163 | CONTAINER_REGISTRY_PASSWORD= 164 | ``` 165 | 166 | ### Debug mode 167 | 168 | To test individual modules that you are developing (eg. not the influx and grafana modules since they are from Docker hub), set the **Environment** variable to **Debug**. In this sample this allows you to run the `simulator` and `orchestrator` modules. In VSCode, in the `launch.json` file, this is set for you for each module individually in `Local Debug` mode. 169 | 170 | ### InfluxDB and Grafana 171 | 172 | For instructions on setup for these two modules and how they work, see the `documentation` folder. 173 | 174 | ### IoT Edge simulator 175 | 176 | To test the full solution, right click on the `deployment.template.json` or `deployment.debug.template.json` and select "Build and Run IoT Edge Solution in Simulator." 177 | 178 | To access the grafana dashboard, go to `localhost:3000` and use `username:admin` and `password:admin`. () 179 | 180 | # Contributing 181 | 182 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 183 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 184 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 185 | 186 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 187 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 188 | provided by the bot. You will only need to do this once across all repos using our CLA. 189 | 190 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 191 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 192 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center at [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://technet.microsoft.com/en-us/security/dn606155). 12 | 13 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 14 | 15 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 16 | 17 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 18 | * Full paths of source file(s) related to the manifestation of the issue 19 | * The location of the affected source code (tag/branch/commit or direct URL) 20 | * Any special configuration required to reproduce the issue 21 | * Step-by-step instructions to reproduce the issue 22 | * Proof-of-concept or exploit code (if possible) 23 | * Impact of the issue, including how an attacker might exploit the issue 24 | 25 | This information will help us triage your report more quickly. 26 | 27 | ## Preferred Languages 28 | 29 | We prefer all communications to be in English. 30 | 31 | ## Policy 32 | 33 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 34 | 35 | -------------------------------------------------------------------------------- /deployment.debug.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-template": "2.0.0", 3 | "modulesContent": { 4 | "$edgeAgent": { 5 | "properties.desired": { 6 | "schemaVersion": "1.0", 7 | "runtime": { 8 | "type": "docker", 9 | "settings": { 10 | "minDockerVersion": "v1.25", 11 | "loggingOptions": "", 12 | "registryCredentials": { 13 | "${CONTAINER_REGISTRY_GROUP}": { 14 | "username": "${CONTAINER_REGISTRY_USERNAME}", 15 | "password": "${CONTAINER_REGISTRY_PASSWORD}", 16 | "address": "${CONTAINER_REGISTRY_ADDRESS}" 17 | } 18 | } 19 | } 20 | }, 21 | "systemModules": { 22 | "edgeAgent": { 23 | "type": "docker", 24 | "settings": { 25 | "image": "mcr.microsoft.com/azureiotedge-agent:1.0", 26 | "createOptions": {} 27 | } 28 | }, 29 | "edgeHub": { 30 | "type": "docker", 31 | "status": "running", 32 | "restartPolicy": "always", 33 | "settings": { 34 | "image": "mcr.microsoft.com/azureiotedge-hub:1.0", 35 | "createOptions": { 36 | "HostConfig": { 37 | "PortBindings": { 38 | "5671/tcp": [ 39 | { 40 | "HostPort": "5671" 41 | } 42 | ], 43 | "8883/tcp": [ 44 | { 45 | "HostPort": "8883" 46 | } 47 | ], 48 | "443/tcp": [ 49 | { 50 | "HostPort": "443" 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "modules": { 60 | "simulator": { 61 | "version": "1.0", 62 | "type": "docker", 63 | "status": "running", 64 | "restartPolicy": "always", 65 | "settings": { 66 | "image": "${MODULES.simulator.debug}", 67 | "createOptions": {} 68 | } 69 | }, 70 | "orchestrator": { 71 | "version": "1.0", 72 | "type": "docker", 73 | "status": "running", 74 | "restartPolicy": "always", 75 | "settings": { 76 | "image": "${MODULES.orchestrator.debug}", 77 | "createOptions": { 78 | "Env": [ 79 | "INFLUX_URL=${INFLUX_URL}", 80 | "INFLUX_USERNAME=${INFLUX_USERNAME}", 81 | "INFLUX_PASSWORD=${INFLUX_PASSWORD}", 82 | "INFLUX_RETENTION_IN_DAYS=${INFLUX_RETENTION_IN_DAYS}" 83 | ] 84 | } 85 | } 86 | }, 87 | "influxdb": { 88 | "version": "1.0", 89 | "type": "docker", 90 | "status": "running", 91 | "restartPolicy": "always", 92 | "settings": { 93 | "image": "${MODULES.influxdb.debug}", 94 | "createOptions": { 95 | "Hostname": "influxdb", 96 | "Env": [ 97 | "INFLUX_DATABASE=${INFLUX_DATABASE}", 98 | "INLFUX_ADMIN_USER=${INFLUX_ADMIN_USER}", 99 | "INFLUX_ADMIN_PASS=${INFLUX_ADMIN_PASS}" 100 | ], 101 | "HostConfig": { 102 | "Binds": [ 103 | "${INFLUX_BIND}" 104 | ], 105 | "PortBindings": { 106 | "8086/tcp": [ 107 | { 108 | "HostPort": "8086" 109 | } 110 | ] 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "grafana": { 117 | "version": "1.0", 118 | "type": "docker", 119 | "status": "running", 120 | "restartPolicy": "always", 121 | "settings": { 122 | "image": "${MODULES.grafana.debug}", 123 | "createOptions": { 124 | "Hostname": "grafana", 125 | "Env": [ 126 | "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}", 127 | "GF_LOG_CONSOLE_LEVEL=${GRAFANA_LOG_CONSOLE_LEVEL}" 128 | ], 129 | "HostConfig": { 130 | "PortBindings": { 131 | "3000/tcp": [ 132 | { 133 | "HostPort": "3000" 134 | } 135 | ] 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "$edgeHub": { 145 | "properties.desired": { 146 | "schemaVersion": "1.0", 147 | "routes": { 148 | "simulatorToOrchestrator": "FROM /messages/modules/simulator/outputs/* INTO BrokeredEndpoint(\"/modules/orchestrator/inputs/telemetry\")", 149 | "orchestratorToIoTHub": "FROM /messages/modules/orchestrator/outputs/* INTO $upstream" 150 | }, 151 | "storeAndForwardConfiguration": { 152 | "timeToLiveSecs": 7200 153 | } 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /deployment.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-template": "2.0.0", 3 | "modulesContent": { 4 | "$edgeAgent": { 5 | "properties.desired": { 6 | "schemaVersion": "1.0", 7 | "runtime": { 8 | "type": "docker", 9 | "settings": { 10 | "minDockerVersion": "v1.25", 11 | "loggingOptions": "", 12 | "registryCredentials": { 13 | "${CONTAINER_REGISTRY_GROUP}": { 14 | "username": "${CONTAINER_REGISTRY_USERNAME}", 15 | "password": "${CONTAINER_REGISTRY_PASSWORD}", 16 | "address": "${CONTAINER_REGISTRY_ADDRESS}" 17 | } 18 | } 19 | } 20 | }, 21 | "systemModules": { 22 | "edgeAgent": { 23 | "type": "docker", 24 | "settings": { 25 | "image": "mcr.microsoft.com/azureiotedge-agent:1.0", 26 | "createOptions": {} 27 | } 28 | }, 29 | "edgeHub": { 30 | "type": "docker", 31 | "status": "running", 32 | "restartPolicy": "always", 33 | "settings": { 34 | "image": "mcr.microsoft.com/azureiotedge-hub:1.0", 35 | "createOptions": { 36 | "HostConfig": { 37 | "PortBindings": { 38 | "5671/tcp": [ 39 | { 40 | "HostPort": "5671" 41 | } 42 | ], 43 | "8883/tcp": [ 44 | { 45 | "HostPort": "8883" 46 | } 47 | ], 48 | "443/tcp": [ 49 | { 50 | "HostPort": "443" 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "modules": { 60 | "simulator": { 61 | "version": "1.0", 62 | "type": "docker", 63 | "status": "running", 64 | "restartPolicy": "always", 65 | "settings": { 66 | "image": "${MODULES.simulator}", 67 | "createOptions": {} 68 | } 69 | }, 70 | "orchestrator": { 71 | "version": "1.0", 72 | "type": "docker", 73 | "status": "running", 74 | "restartPolicy": "always", 75 | "settings": { 76 | "image": "${MODULES.orchestrator}", 77 | "createOptions": { 78 | "Env": [ 79 | "INFLUX_URL=${INFLUX_URL}", 80 | "INFLUX_USERNAME=${INFLUX_USERNAME}", 81 | "INFLUX_PASSWORD=${INFLUX_PASSWORD}", 82 | "INFLUX_RETENTION_IN_DAYS=${INFLUX_RETENTION_IN_DAYS}" 83 | ] 84 | } 85 | } 86 | }, 87 | "influxdb": { 88 | "version": "1.0", 89 | "type": "docker", 90 | "status": "running", 91 | "restartPolicy": "always", 92 | "settings": { 93 | "image": "${MODULES.influxdb}", 94 | "createOptions": { 95 | "Hostname": "influxdb", 96 | "Env": [ 97 | "INFLUX_DATABASE=${INFLUX_DATABASE}", 98 | "INLFUX_ADMIN_USER=${INFLUX_ADMIN_USER}", 99 | "INFLUX_ADMIN_PASS=${INFLUX_ADMIN_PASS}" 100 | ], 101 | "HostConfig": { 102 | "Binds": [ 103 | "${INFLUX_BIND}" 104 | ], 105 | "PortBindings": { 106 | "8086/tcp": [ 107 | { 108 | "HostPort": "8086" 109 | } 110 | ] 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "grafana": { 117 | "version": "1.0", 118 | "type": "docker", 119 | "status": "running", 120 | "restartPolicy": "always", 121 | "settings": { 122 | "image": "${MODULES.grafana}", 123 | "createOptions": { 124 | "Hostname": "grafana", 125 | "Env": [ 126 | "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}", 127 | "GF_LOG_CONSOLE_LEVEL=${GRAFANA_LOG_CONSOLE_LEVEL}" 128 | ], 129 | "HostConfig": { 130 | "PortBindings": { 131 | "3000/tcp": [ 132 | { 133 | "HostPort": "3000" 134 | } 135 | ] 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "$edgeHub": { 145 | "properties.desired": { 146 | "schemaVersion": "1.0", 147 | "routes": { 148 | "simulatorToOrchestrator": "FROM /messages/modules/simulator/outputs/* INTO BrokeredEndpoint(\"/modules/orchestrator/inputs/telemetry\")", 149 | "orchestratorToIoTHub": "FROM /messages/modules/orchestrator/outputs/* INTO $upstream" 150 | }, 151 | "storeAndForwardConfiguration": { 152 | "timeToLiveSecs": 7200 153 | } 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /documentation/grafana_README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Grafana 2 | 3 | _"[Grafana](https://grafana.com/grafana/) allows you to query, visualize, alert on and understand your metrics no matter where they are stored. Create, explore, and share dashboards with your team and foster a data driven culture."_ - Grafana landing page 4 | 5 | Grafana comes with a built-in plugin to InfluxDB which allows for easy configuration and querying. Further, the included panels (Grafana's term for a chart / graph / other visualization) work very well to have immediate results in displaying data and iterating on different possibilities. There are many additional panels and data sources which can be added to extend out-of-the box functionality. 6 | 7 | ## Grafana on Azure IoT Edge 8 | 9 | A requirement for using Grafana on Azure IoT Edge is to be able to load Grafana with a saved connection to the on-device InfluxDB and pre-configured dashboards. Further, if IoT Edge restarts, Grafana should reload without any changes. 10 | 11 | # Dockerfiles 12 | 13 | For Grafana to load configurations when it starts, the Grafana image available at DockerHub https://hub.docker.com/u/grafana/ is not sufficient (unlike the InfluxDB image). We have extended the Dockerfile for the module to work on IoT Edge which make the necessary updates. Lines 21-23 in `Dockefile.amd64` show where the provisioning files, dashboards and datasources are copied into the image in case you want to change or update that at a later time. `ENV GF_PANELS_DISABLE_SANITIZE_HTML` is set to `false` by default in the Dockerfiles. If you need to use Javascript in your dashboards, set this to `true`. The folder locations and that environment variable should be all you would need to configure although you have the option of looking into adding user groups as well. 14 | 15 | [Grafana Provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/) 16 | 17 | In this solution the provisioning dashboard configuration is in `provisioning_dashboards/docker-dashboard.yml` and the datasources are in `datasources/`. In particular with the datasources, note the `name` since it is used in the preconfigured dashboards, the type (influxdb in this solution) and the database name which is where we have written the data previously. The datasource enables the preconfigured dashboards to automatically connect and query the InfluxDB databases. 18 | 19 | A preconfigured dashboard with panels is in `dashboards\`. This is a sample to demonstrate how to use the `uid` field to specify a repeatable entry point to Grafana, automatically connecting to a datasource with a query and displaying that result with a preset refresh rate. 20 | 21 | # Adding dashboards and panels 22 | 23 | The [Grafana getting started guide](https://grafana.com/docs/grafana/latest/guides/getting_started/) provides guidance on panels, dashboards and datasources which are likely the first things you will want to do when working with this solution. 24 | 25 | Two primary differences to note between the documentation and using Grafana on Edge with this solution: 26 | 27 | 1) Any new panels or dashboards that you add in Grafana **must** be exported and added in code. If you add a dashboard or panel locally it will persist as long as the module doesn't restart. 28 | NB. I've tried adding a bind mount and this does not seem to work. 29 | 30 | 2) You will notice a difference in the JSON for exporting dashboards if you select "Export for sharing externally". Thus far, it seems better _not_ to select this. If you do select it you will see two additional sections `inputs` and `requires`. Importantly, the `inputs` section will add `DS_` in front of your data sources (even though you have already preprended DS_). You can remove the `inputs` section **and** you must change all of the inputs through the JSON from, for example, `${DS_DS_INFLUX}` to your already configured `DS_INFLUX`. 31 | 32 | 3) The first time you open grafana on a device if you do not go to the uid directly, you will likely have to navigate to find the dashboard as it will not show on the homepage. 33 | 34 | __UID__: At the very bottom of a dashboard file there is a `uid` field. You can use this to set the URL to go directly to your dashboard: http://localhost:3000/d/ 35 | 36 | -------------------------------------------------------------------------------- /documentation/influxdb_README.md: -------------------------------------------------------------------------------- 1 | # Introduction to InfluxDB 2 | 3 | [InfluxDB](https://docs.influxdata.com/influxdb/v1.7/) is an open source schemaless time series database. 4 | 5 | It is important to understand [the key concepts of InfluxDB](https://docs.influxdata.com/influxdb/v1.7/concepts/key_concepts/) and 6 | [the difference between InfluxDB and SQL](https://docs.influxdata.com/influxdb/v1.7/concepts/crosswalk/). 7 | In this solution we use InfluxDB 1.7 which is the current version recommended for production systems with InfluxDB 2.0 in beta. 8 | 9 | ## Terminology 10 | 11 | From the above documentation: 12 | 13 | - **Point** : A single data record which has four parts: a measurement, tag set, field set and a timestamp. 14 | - **Fields**: Non-indexed data which is recorded, ex. temperature, pressure, weight. Required. Must be either string, bool, float or int. 15 | - **Tag** : A tag is made up of an indexed key-vaue pair which can be queried to group points. Often multiple tags, referred to as a tag set, are used ex. location and machine_type and these are called tag sets. Optional but recommended. Always stored as strings. 16 | - **Series** : A collection of points that share a measurement, tag set, and field key. 17 | - **Measurement** : A container for tags, fields, and time. Conceptually similar to a table. 18 | 19 | Queries on data should be made against the time and the tag since fields are not indexed. 20 | 21 | # IoT Edge InfluxDB Solution 22 | 23 | ## Mapping File 24 | 25 | To use InfluxDB 1.7 on Azure IoT Edge in this solution, you need to understand and know how to change the mapping file (`TelemetrySelection.json` in this solution) and possibly a few options for configuring InfluxDB. The mapping file defines the JSON schema of incoming data and adds two options for data handling - `IgnoreIfMissing` and `AbandonIfMissing`. 26 | 27 | The mapping file structure is: 28 | 29 | ``` 30 | { 31 | "Databases": [ 32 | { 33 | "Name": "", 34 | "Measurements": [ 35 | { 36 | "Name": "", 37 | "Tags": [ 38 | { 39 | "Name": "", 40 | "PayloadMapping": "", 41 | "IgnoreIfMissing": "", 42 | "ValueIfMissing": , 43 | "AbandonIfEmpty": 44 | } 45 | ], 46 | "Fields": [ 47 | { 48 | "Name": "", 49 | "PayloadMapping": "", 50 | "Type": "", 51 | "IgnoreIfMissing": "", 52 | "ValueIfMissing": , 53 | "AbandonIfEmpty": , 54 | } 55 | ] 56 | } 57 | ], 58 | "TimestampPayloadMapping": " - Note that this currently must be milliseconds" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | The mapping file allows for an array of databases, an array of tags, and an array of fields to allow for multiple definitions depending on the incoming JSON. 65 | 66 | * **Name** - This is the name of the key that will be written to InfluxDB for this value. 67 | * **PayloadMapping** - This is a dot notation path to the data value in the JSON Schema. 68 | * **IgnoreIfMissing** - If this is `true`, then when the data is written to InfluxDB this value will not be included (unlike SQL, InfluxDB does not require that all data be included and allows for more tags and fields to be added over time). If it is false, then it will use the `ValueIfMissing` field to write data. 69 | * **ValueIfMissing** - If data is missing and you still wish to write something to InfluxDB, enter it into this field. For example, instead of having no value when a sensor is not working, you wish to write 0 or -1 to later indicate a failure on a grafana dashboard it would go here. 70 | * **AbandonIfMissing** - If this is `true`, none of the data will be written to InfluxDB. One use case would be if there is some critical data that must be included for any of it to make sense in InfluxDB. Ex, if there are no tags and querying by time alone would not make sense. 71 | 72 | ### Mapping File Example 73 | 74 | If you have a sample JSON like what is in `TelemetryMessageSample.json`: 75 | 76 | ``` 77 | { 78 | "Timestamp": "1583451789832", 79 | "System": { 80 | "Core": { 81 | "MachineSampleWeight": { 82 | "TestWeight": 40.0 83 | } 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | then the mapping file could look like: 90 | 91 | ``` 92 | { 93 | "Databases": [ 94 | { 95 | "Name": "telemetry", 96 | "Measurements": [ 97 | { 98 | "Name": "influx_measurements", 99 | "Tags": [ 100 | { 101 | "Name": "MachineId", 102 | "PayloadMapping": "MachineId", 103 | "AbandonIfEmpty": false 104 | } 105 | ], 106 | "Fields": [ 107 | { 108 | "Name": "TestWeight", 109 | "PayloadMapping": "System.Core.MachineSampleWeight.TestWeight", 110 | "Type": "float", 111 | "AbandonIfEmpty": false, 112 | "IgnoreIfEmpty": true 113 | } 114 | ] 115 | } 116 | ], 117 | "TimestampPayloadMapping": "Timestamp" 118 | } 119 | ] 120 | } 121 | ``` 122 | 123 | `PayloadMapping` above demonstrates the dot notation of how to map between the JSON schema and an InfluxDB key. 124 | 125 | You can see that `MachineId` has `AbandonIfEmpty` set to `false`. This is because currently we do not include `MachineId` in the incoming JSON but we know that we will likely include it later. Further, as an example you can see that if the weight value is empty we `Ignore` that value. Since this is the only data value we write, we could also have used `AbandonIfEmpty` set to true. 126 | 127 | ### InfluxDB Options 128 | 129 | In the `InfluxDBRecorder.cs` file, we currently configure a retention policy for data of 30 days for all databases. You may want to configure this in the `InitializeAsync` method depending on your use case. See [InfluxDB Retention Policy](https://docs.influxdata.com/influxdb/v1.7/query_language/database_management/#create-retention-policies-with-create-retention-policy) for more information on available options. 130 | 131 | Also note that this currently only expects the timestamp in milliseconds while [InfluxDB supports precision](https://www.docs.influxdata.com/influxdb/v1.7/tools/shell/) from nanoseconds up to hour and above. 132 | 133 | ### Useful links for understanding InfluxDB 134 | 135 | [InfluxDB Key Concepts](https://docs.influxdata.com/influxdb/v1.7/concepts/key_concepts/) 136 | [InfluxDB CLI](https://docs.influxdata.com/influxdb/v1.7/tools/shell/) -- You can run the command `docker exec -it influx` to use the CLI and see/verify what is being written to InfluxDB from the Orchestrator Module and check settings (ex. is retention policy what I expect?) 137 | [InfluxDB Query Language](https://docs.influxdata.com/influxdb/v1.7/query_language/) 138 | [InfluxDB Compared to SQL](https://docs.influxdata.com/influxdb/v1.7/concepts/crosswalk/) 139 | [AdysTech.InfluxDB.Client.Net](https://github.com/AdysTech/InfluxDB.Client.Net) - This is covered more below but the README explains how this dotnet package works with InfluxDB. 140 | 141 | ## Orchestrator Module and How It Works with the Mapping File and InfluxDB 142 | 143 | In the Orchestrator module we've added `InfluxDBRecorder.cs` and `ORM.cs`. `InfluxDBRecorder` is an abstraction of `ITimeSeriesRecorder` and is called within the `OrchestratorModule`. It acts as the interface to InfluxDB using the dotnet package `AdysTech.InfluxDB.Client.Net`, the InfluxDB structure in `ORM`, and the mapping file. 144 | 145 | `InfluxDBRecorder` translates between the mapping file and InfluxDB using Newtonsoft to parse the incoming JSON and converts it into the ORM class and [AdysTech.InfluxDB.Client.Net](https://github.com/AdysTech/InfluxDB.Client.Net) to write to InfluxDB. 146 | 147 | [AdysTech.InfluxDB.Client.Net](https://github.com/AdysTech/InfluxDB.Client.Net) is an open source Dotnet client to connect to InfluxDB 1.7 since there is no official dotnet client. This is referenced as (one of the dotnet packages to use on the InfluxDB website)[https://docs.influxdata.com/influxdb/v1.7/tools/api_client_libraries/]. It has very good documentation that shows how it interfaces with InfluxDB. In this solution, we use it to create databases with data retention polices and to write data in `InfluxDBRecorder`. 148 | 149 | `ORM.cs` takes the InfluxDB database and data structure and translates that into a class for use with `InfluxDBRecorder` and the mapping file. Within the class, you will find definitions that align with InfluxDB's data structure including database, measurement, timestamp, tags, fields and field types (the four datatypes which InfluxDB allows for fields). Some items within that file, `PayloadMapping`, `AbandonIfMissing`, `ValueIfMissing` will be explained in the section on the mapping file below. 150 | 151 | ## Dockerfile 152 | 153 | The InfluxDB docker image referenced in the folder `modules\influxdb`. This folder is not strictly necessary, you could add the image to the `deployment.template.json` file. However, by adding a `module.json` file and Dockerfiles for this public container, you can upload the container to an Azure Container Registry, as is done in this repo. While this is currently redundant since 1.7 is the latest image on Dockerhub and in each build we are using the same environment variables, it highlights how to put different builds of a public docker image in a private container. Additionally, by storing an image in a separate private repository, you can guarantee reusing the same image in each build regardless of changes on Dockerhub. If you later decide to use a specific build, you can modify the `deployment.json` file or modify the Dockerfiles and `module.json` in the `influxdb` folder. The way to undo adding new images to the container registry would be to change the `${BUILD_BUILDID}` field in `module.json`. 154 | 155 | ## INFLUX_BIND Environment Variable 156 | 157 | This module requires an INFLUX_BIND environment variable to map a folder inside of the container to the device. Without this environment variable set, data is **not** persistent. Therefore, this environment variable is included for when the module restarts or if for some reason you need to export the data from the device filesystem directly. -------------------------------------------------------------------------------- /images/arch_diagram.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/iot-edge-influxdb-grafana/3a38dad76b3f42e463755301d8e95918b8d0b6b6/images/arch_diagram.PNG -------------------------------------------------------------------------------- /modules/grafana/.dockerignore: -------------------------------------------------------------------------------- 1 | [b|B]in 2 | [O|o]bj -------------------------------------------------------------------------------- /modules/grafana/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /modules/grafana/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | ARG GRAFANA_VERSION="6.5.0" 2 | 3 | FROM grafana/grafana:${GRAFANA_VERSION} 4 | 5 | USER root 6 | 7 | ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false" 8 | 9 | RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \ 10 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ 11 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \ 12 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ 13 | apk --no-cache upgrade && \ 14 | apk add --no-cache udev ttf-opensans chromium && \ 15 | rm -rf /tmp/* && \ 16 | rm -rf /usr/share/grafana/tools/phantomjs; \ 17 | fi 18 | 19 | USER grafana 20 | 21 | COPY /provisioning_dashboards/* /etc/grafana/provisioning/dashboards/ 22 | COPY /datasources/* /etc/grafana/provisioning/datasources/ 23 | COPY /dashboards/* /var/lib/grafana/dashboards/ 24 | 25 | ENV GF_RENDERER_PLUGIN_CHROME_BIN="/usr/bin/chromium-browser" 26 | ENV GF_PANELS_DISABLE_SANITIZE_HTML=false 27 | 28 | RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \ 29 | grafana-cli \ 30 | --pluginsDir "$GF_PATHS_PLUGINS" \ 31 | --pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \ 32 | plugins install grafana-image-renderer; \ 33 | fi 34 | 35 | ARG GF_INSTALL_PLUGINS="" 36 | 37 | RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \ 38 | OLDIFS=$IFS; \ 39 | IFS=','; \ 40 | for plugin in ${GF_INSTALL_PLUGINS}; do \ 41 | IFS=$OLDIFS; \ 42 | grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \ 43 | done; \ 44 | fi 45 | 46 | -------------------------------------------------------------------------------- /modules/grafana/Dockerfile.amd64.debug: -------------------------------------------------------------------------------- 1 | ARG GRAFANA_VERSION="6.5.0" 2 | 3 | FROM grafana/grafana:${GRAFANA_VERSION} 4 | 5 | USER root 6 | 7 | ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false" 8 | 9 | RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \ 10 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ 11 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \ 12 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ 13 | apk --no-cache upgrade && \ 14 | apk add --no-cache udev ttf-opensans chromium && \ 15 | rm -rf /tmp/* && \ 16 | rm -rf /usr/share/grafana/tools/phantomjs; \ 17 | fi 18 | 19 | USER grafana 20 | 21 | COPY /provisioning_dashboards/* /etc/grafana/provisioning/dashboards/ 22 | COPY /datasources/* /etc/grafana/provisioning/datasources/ 23 | COPY /dashboards/* /var/lib/grafana/dashboards/ 24 | 25 | ENV GF_RENDERER_PLUGIN_CHROME_BIN="/usr/bin/chromium-browser" 26 | ENV GF_PANELS_DISABLE_SANITIZE_HTML=false 27 | 28 | RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \ 29 | grafana-cli \ 30 | --pluginsDir "$GF_PATHS_PLUGINS" \ 31 | --pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \ 32 | plugins install grafana-image-renderer; \ 33 | fi 34 | 35 | ARG GF_INSTALL_PLUGINS="" 36 | 37 | RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \ 38 | OLDIFS=$IFS; \ 39 | IFS=','; \ 40 | for plugin in ${GF_INSTALL_PLUGINS}; do \ 41 | IFS=$OLDIFS; \ 42 | grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \ 43 | done; \ 44 | fi 45 | 46 | -------------------------------------------------------------------------------- /modules/grafana/dashboards/machineDataDashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_INFLUXDB", 5 | "label": "InfluxDB", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "influxdb", 9 | "pluginName": "InfluxDB" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "panel", 15 | "id": "bargauge", 16 | "name": "Bar Gauge", 17 | "version": "" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "gauge", 22 | "name": "Gauge", 23 | "version": "" 24 | }, 25 | { 26 | "type": "grafana", 27 | "id": "grafana", 28 | "name": "Grafana", 29 | "version": "6.5.0" 30 | }, 31 | { 32 | "type": "panel", 33 | "id": "graph", 34 | "name": "Graph", 35 | "version": "" 36 | }, 37 | { 38 | "type": "datasource", 39 | "id": "influxdb", 40 | "name": "InfluxDB", 41 | "version": "1.0.0" 42 | }, 43 | { 44 | "type": "panel", 45 | "id": "singlestat", 46 | "name": "Singlestat", 47 | "version": "" 48 | } 49 | ], 50 | "annotations": { 51 | "list": [ 52 | { 53 | "builtIn": 1, 54 | "datasource": "-- Grafana --", 55 | "enable": true, 56 | "hide": true, 57 | "iconColor": "rgba(0, 211, 255, 1)", 58 | "name": "Annotations & Alerts", 59 | "type": "dashboard" 60 | } 61 | ] 62 | }, 63 | "editable": true, 64 | "gnetId": null, 65 | "graphTooltip": 0, 66 | "id": null, 67 | "links": [], 68 | "panels": [ 69 | { 70 | "aliasColors": {}, 71 | "bars": false, 72 | "dashLength": 10, 73 | "dashes": false, 74 | "datasource": "DS_INFLUXDB", 75 | "fill": 1, 76 | "fillGradient": 0, 77 | "gridPos": { 78 | "h": 9, 79 | "w": 12, 80 | "x": 0, 81 | "y": 0 82 | }, 83 | "hiddenSeries": false, 84 | "id": 2, 85 | "legend": { 86 | "avg": false, 87 | "current": false, 88 | "max": false, 89 | "min": false, 90 | "show": true, 91 | "total": false, 92 | "values": false 93 | }, 94 | "lines": true, 95 | "linewidth": 1, 96 | "nullPointMode": "null", 97 | "options": { 98 | "dataLinks": [] 99 | }, 100 | "percentage": false, 101 | "pointradius": 2, 102 | "points": false, 103 | "renderer": "flot", 104 | "seriesOverrides": [], 105 | "spaceLength": 10, 106 | "stack": false, 107 | "steppedLine": false, 108 | "targets": [ 109 | { 110 | "alias": "Ambient Temperature", 111 | "groupBy": [ 112 | { 113 | "params": [ 114 | "$__interval" 115 | ], 116 | "type": "time" 117 | }, 118 | { 119 | "params": [ 120 | "null" 121 | ], 122 | "type": "fill" 123 | } 124 | ], 125 | "measurement": "telemetry", 126 | "orderByTime": "ASC", 127 | "policy": "default", 128 | "refId": "A", 129 | "resultFormat": "time_series", 130 | "select": [ 131 | [ 132 | { 133 | "params": [ 134 | "AmbientTemperature" 135 | ], 136 | "type": "field" 137 | }, 138 | { 139 | "params": [], 140 | "type": "last" 141 | } 142 | ] 143 | ], 144 | "tags": [] 145 | }, 146 | { 147 | "alias": "Machine Temperature", 148 | "groupBy": [ 149 | { 150 | "params": [ 151 | "$__interval" 152 | ], 153 | "type": "time" 154 | }, 155 | { 156 | "params": [ 157 | "null" 158 | ], 159 | "type": "fill" 160 | } 161 | ], 162 | "measurement": "telemetry", 163 | "orderByTime": "ASC", 164 | "policy": "default", 165 | "refId": "B", 166 | "resultFormat": "time_series", 167 | "select": [ 168 | [ 169 | { 170 | "params": [ 171 | "MachineTemperature" 172 | ], 173 | "type": "field" 174 | }, 175 | { 176 | "params": [], 177 | "type": "last" 178 | } 179 | ] 180 | ], 181 | "tags": [] 182 | } 183 | ], 184 | "thresholds": [], 185 | "timeFrom": null, 186 | "timeRegions": [], 187 | "timeShift": null, 188 | "title": "Factory Floor Temperatures", 189 | "tooltip": { 190 | "shared": true, 191 | "sort": 0, 192 | "value_type": "individual" 193 | }, 194 | "type": "graph", 195 | "xaxis": { 196 | "buckets": null, 197 | "mode": "time", 198 | "name": null, 199 | "show": true, 200 | "values": [] 201 | }, 202 | "yaxes": [ 203 | { 204 | "format": "short", 205 | "label": null, 206 | "logBase": 1, 207 | "max": null, 208 | "min": null, 209 | "show": true 210 | }, 211 | { 212 | "format": "short", 213 | "label": null, 214 | "logBase": 1, 215 | "max": null, 216 | "min": null, 217 | "show": true 218 | } 219 | ], 220 | "yaxis": { 221 | "align": false, 222 | "alignLevel": null 223 | } 224 | }, 225 | { 226 | "datasource": "DS_INFLUXDB", 227 | "gridPos": { 228 | "h": 9, 229 | "w": 5, 230 | "x": 12, 231 | "y": 0 232 | }, 233 | "id": 4, 234 | "options": { 235 | "fieldOptions": { 236 | "calcs": [ 237 | "lastNotNull" 238 | ], 239 | "defaults": { 240 | "mappings": [], 241 | "max": 100, 242 | "min": 0, 243 | "thresholds": [ 244 | { 245 | "color": "green", 246 | "value": null 247 | }, 248 | { 249 | "color": "red", 250 | "value": 100 251 | } 252 | ] 253 | }, 254 | "override": {}, 255 | "values": false 256 | }, 257 | "orientation": "auto", 258 | "showThresholdLabels": false, 259 | "showThresholdMarkers": true 260 | }, 261 | "pluginVersion": "6.5.0", 262 | "targets": [ 263 | { 264 | "alias": "Conveyor Belt Speed", 265 | "groupBy": [ 266 | { 267 | "params": [ 268 | "$__interval" 269 | ], 270 | "type": "time" 271 | }, 272 | { 273 | "params": [ 274 | "null" 275 | ], 276 | "type": "fill" 277 | } 278 | ], 279 | "measurement": "telemetry", 280 | "orderByTime": "ASC", 281 | "policy": "default", 282 | "refId": "A", 283 | "resultFormat": "time_series", 284 | "select": [ 285 | [ 286 | { 287 | "params": [ 288 | "ConveyorBeltSpeed" 289 | ], 290 | "type": "field" 291 | }, 292 | { 293 | "params": [], 294 | "type": "mean" 295 | } 296 | ] 297 | ], 298 | "tags": [] 299 | } 300 | ], 301 | "timeFrom": null, 302 | "timeShift": null, 303 | "title": "Conveyor Belt Speed", 304 | "type": "gauge" 305 | }, 306 | { 307 | "datasource": "DS_INFLUXDB", 308 | "gridPos": { 309 | "h": 9, 310 | "w": 5, 311 | "x": 17, 312 | "y": 0 313 | }, 314 | "id": 6, 315 | "options": { 316 | "displayMode": "lcd", 317 | "fieldOptions": { 318 | "calcs": [ 319 | "last" 320 | ], 321 | "defaults": { 322 | "mappings": [], 323 | "max": 100, 324 | "min": 0, 325 | "thresholds": [ 326 | { 327 | "color": "green", 328 | "value": null 329 | }, 330 | { 331 | "color": "red", 332 | "value": 80 333 | } 334 | ] 335 | }, 336 | "override": {}, 337 | "values": false 338 | }, 339 | "orientation": "vertical" 340 | }, 341 | "pluginVersion": "6.5.0", 342 | "targets": [ 343 | { 344 | "groupBy": [ 345 | { 346 | "params": [ 347 | "$__interval" 348 | ], 349 | "type": "time" 350 | }, 351 | { 352 | "params": [ 353 | "null" 354 | ], 355 | "type": "fill" 356 | } 357 | ], 358 | "measurement": "telemetry", 359 | "orderByTime": "ASC", 360 | "policy": "default", 361 | "refId": "A", 362 | "resultFormat": "time_series", 363 | "select": [ 364 | [ 365 | { 366 | "params": [ 367 | "GearTension" 368 | ], 369 | "type": "field" 370 | }, 371 | { 372 | "params": [], 373 | "type": "last" 374 | } 375 | ] 376 | ], 377 | "tags": [] 378 | } 379 | ], 380 | "timeFrom": null, 381 | "timeShift": null, 382 | "title": "Gear Tension", 383 | "type": "bargauge" 384 | }, 385 | { 386 | "cacheTimeout": null, 387 | "colorBackground": false, 388 | "colorValue": false, 389 | "colors": [ 390 | "#299c46", 391 | "rgba(237, 129, 40, 0.89)", 392 | "#d44a3a" 393 | ], 394 | "datasource": "DS_INFLUXDB", 395 | "format": "none", 396 | "gauge": { 397 | "maxValue": 100, 398 | "minValue": 0, 399 | "show": false, 400 | "thresholdLabels": false, 401 | "thresholdMarkers": true 402 | }, 403 | "gridPos": { 404 | "h": 6, 405 | "w": 5, 406 | "x": 0, 407 | "y": 9 408 | }, 409 | "id": 8, 410 | "interval": null, 411 | "links": [], 412 | "mappingType": 1, 413 | "mappingTypes": [ 414 | { 415 | "name": "value to text", 416 | "value": 1 417 | }, 418 | { 419 | "name": "range to text", 420 | "value": 2 421 | } 422 | ], 423 | "maxDataPoints": 100, 424 | "nullPointMode": "connected", 425 | "nullText": null, 426 | "options": {}, 427 | "postfix": "", 428 | "postfixFontSize": "50%", 429 | "prefix": "", 430 | "prefixFontSize": "50%", 431 | "rangeMaps": [ 432 | { 433 | "from": "null", 434 | "text": "N/A", 435 | "to": "null" 436 | } 437 | ], 438 | "sparkline": { 439 | "fillColor": "rgba(31, 118, 189, 0.18)", 440 | "full": false, 441 | "lineColor": "rgb(31, 120, 193)", 442 | "show": false, 443 | "ymax": null, 444 | "ymin": null 445 | }, 446 | "tableColumn": "", 447 | "targets": [ 448 | { 449 | "groupBy": [ 450 | { 451 | "params": [ 452 | "$__interval" 453 | ], 454 | "type": "time" 455 | }, 456 | { 457 | "params": [ 458 | "null" 459 | ], 460 | "type": "fill" 461 | } 462 | ], 463 | "measurement": "telemetry", 464 | "orderByTime": "ASC", 465 | "policy": "default", 466 | "refId": "A", 467 | "resultFormat": "time_series", 468 | "select": [ 469 | [ 470 | { 471 | "params": [ 472 | "WorkerDetected" 473 | ], 474 | "type": "field" 475 | }, 476 | { 477 | "params": [], 478 | "type": "last" 479 | } 480 | ] 481 | ], 482 | "tags": [] 483 | } 484 | ], 485 | "thresholds": "", 486 | "timeFrom": null, 487 | "timeShift": null, 488 | "title": "Workers Detected", 489 | "type": "singlestat", 490 | "valueFontSize": "80%", 491 | "valueMaps": [ 492 | { 493 | "op": "=", 494 | "text": "N/A", 495 | "value": "null" 496 | } 497 | ], 498 | "valueName": "current" 499 | } 500 | ], 501 | "refresh": "5s", 502 | "schemaVersion": 21, 503 | "style": "dark", 504 | "tags": [], 505 | "templating": { 506 | "list": [] 507 | }, 508 | "time": { 509 | "from": "now-5m", 510 | "to": "now" 511 | }, 512 | "timepicker": { 513 | "refresh_intervals": [ 514 | "5s", 515 | "10s", 516 | "30s", 517 | "1m", 518 | "5m", 519 | "15m", 520 | "30m", 521 | "1h", 522 | "2h", 523 | "1d" 524 | ] 525 | }, 526 | "timezone": "", 527 | "title": "US East Factory Floor 1 Telemetry", 528 | "uid": "telemetry", 529 | "version": 1 530 | } -------------------------------------------------------------------------------- /modules/grafana/datasources/influxdb.yaml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | # list of datasources that should be deleted from the database 5 | #deleteDatasources: 6 | # - name: InfluxDB 7 | # orgId: 1 8 | 9 | # list of datasources to insert/update depending 10 | # what's available in the database 11 | datasources: 12 | # name of the datasource. Required 13 | - name: DS_INFLUXDB 14 | # datasource type. Required 15 | type: influxdb 16 | # access mode. proxy or direct (Server or Browser in the UI). Required 17 | access: proxy 18 | # url 19 | url: http://influxdb:8086 20 | # database name, if used 21 | database: machineInfo 22 | # mark as default datasource. Max one per org 23 | isDefault: true 24 | # allow users to edit datasources from the UI. 25 | editable: false 26 | 27 | -------------------------------------------------------------------------------- /modules/grafana/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-version": "0.0.1", 3 | "description": "", 4 | "image": { 5 | "repository": "${CONTAINER_REGISTRY_ADDRESS}/grafana", 6 | "tag": { 7 | "version": "6.5.0.${BUILD_BUILDID}", 8 | "platforms": { 9 | "amd64": "./Dockerfile.amd64", 10 | "amd64.debug": "./Dockerfile.amd64.debug" 11 | } 12 | }, 13 | "buildOptions": [], 14 | "contextPath": "./" 15 | }, 16 | "language": "other" 17 | } 18 | -------------------------------------------------------------------------------- /modules/grafana/provisioning_dashboards/docker-dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards 10 | options: 11 | path: /var/lib/grafana/dashboards -------------------------------------------------------------------------------- /modules/influxdb/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM registry.hub.docker.com/library/influxdb:1.7 -------------------------------------------------------------------------------- /modules/influxdb/Dockerfile.amd64.debug: -------------------------------------------------------------------------------- 1 | FROM registry.hub.docker.com/library/influxdb:1.7 -------------------------------------------------------------------------------- /modules/influxdb/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-version": "0.0.1", 3 | "description": "", 4 | "image": { 5 | "repository": "${CONTAINER_REGISTRY_ADDRESS}/influxdb", 6 | "tag": { 7 | "version": "1.7.${BUILD_BUILDID}", 8 | "platforms": { 9 | "amd64": "./Dockerfile.amd64", 10 | "amd64.debug": "./Dockerfile.amd64.debug" 11 | } 12 | }, 13 | "buildOptions": [], 14 | "contextPath": "./" 15 | }, 16 | "language": "csharp" 17 | } 18 | -------------------------------------------------------------------------------- /modules/orchestrator/.dockerignore: -------------------------------------------------------------------------------- 1 | [b|B]in 2 | [O|o]bj -------------------------------------------------------------------------------- /modules/orchestrator/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /modules/orchestrator/Abstraction/IHttpHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator.Abstraction 2 | { 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | public interface IHttpHandler 6 | { 7 | HttpResponseMessage Get(string url); 8 | HttpResponseMessage Post(string url, HttpContent content); 9 | Task GetAsync(string url); 10 | Task PostAsync(string url, HttpContent content); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/orchestrator/Abstraction/ITimeSeriesRecorder.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator.Abstraction 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public interface ITimeSeriesRecorder 6 | { 7 | Task InitializeAsync(); 8 | Task RecordMessageAsync(string telemetryJson); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/orchestrator/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-env 2 | WORKDIR /app 3 | COPY ./modules/orchestrator/*.csproj ./modules/orchestrator/ 4 | COPY ./ModuleWrapper/*.csproj ./ModuleWrapper/ 5 | WORKDIR /app/modules/orchestrator 6 | RUN dotnet restore 7 | 8 | WORKDIR /app 9 | COPY . ./ 10 | RUN dotnet publish -c Debug -o /app/out 11 | 12 | FROM mcr.microsoft.com/dotnet/core/runtime:2.1-stretch-slim 13 | WORKDIR /app 14 | COPY --from=build-env /app/out ./ 15 | 16 | RUN useradd -ms /bin/bash moduleuser 17 | USER moduleuser 18 | 19 | ENTRYPOINT ["dotnet", "Orchestrator.dll"] -------------------------------------------------------------------------------- /modules/orchestrator/Dockerfile.amd64.debug: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/runtime:2.1-stretch-slim AS base 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends unzip procps && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | RUN useradd -ms /bin/bash moduleuser 8 | USER moduleuser 9 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg 10 | 11 | FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-env 12 | WORKDIR /app 13 | COPY ./modules/orchestrator/*.csproj ./modules/orchestrator/ 14 | COPY ./ModuleWrapper/*.csproj ./ModuleWrapper/ 15 | WORKDIR /app/modules/orchestrator 16 | RUN dotnet restore 17 | 18 | WORKDIR /app 19 | COPY . ./ 20 | RUN dotnet publish -c Debug -o /app/out 21 | 22 | FROM base 23 | WORKDIR /app 24 | COPY --from=build-env /app/out ./ 25 | 26 | ENTRYPOINT ["dotnet", "Orchestrator.dll"] -------------------------------------------------------------------------------- /modules/orchestrator/Mock/MockHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator.Mock 2 | { 3 | using Orchestrator.Abstraction; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | public class MockHttpClientHandler : IHttpHandler 9 | { 10 | public HttpResponseMessage Get(string url) 11 | { 12 | return new HttpResponseMessage(HttpStatusCode.OK); 13 | } 14 | 15 | public HttpResponseMessage Post(string url, HttpContent content) 16 | { 17 | return new HttpResponseMessage(HttpStatusCode.OK); 18 | } 19 | 20 | public async Task GetAsync(string url) 21 | { 22 | return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); 23 | } 24 | 25 | public async Task PostAsync(string url, HttpContent content) 26 | { 27 | return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/orchestrator/Mock/MockTimeSeriesRecorder.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Orchestrator.Mock 3 | { 4 | using Orchestrator.Abstraction; 5 | using Serilog; 6 | using System.Threading.Tasks; 7 | 8 | public class MockTimeSeriesRecorder : ITimeSeriesRecorder 9 | { 10 | public Task InitializeAsync() 11 | { 12 | return Task.FromResult(0); 13 | } 14 | 15 | public Task RecordMessageAsync(string telemetryJson) 16 | { 17 | Log.Information("Recording telemetry.."); 18 | return Task.FromResult(0); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /modules/orchestrator/Mock/TelemetryMessageSample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Timestamp": "1582940332052", 3 | "MachineId": "XYZ", 4 | "MachineTemperature": "80.0", 5 | "AmbientTemperature": "-40.0", 6 | "ConveyorBeltSpeed": "10", 7 | "GearTension": "-10", 8 | "WorkerDetected": "1" 9 | } -------------------------------------------------------------------------------- /modules/orchestrator/Orchestrator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp2.1 5 | 6 | 7 | 8 | True 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /modules/orchestrator/OrchestratorModule.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Hosting; 6 | using ModuleWrapper; 7 | using Newtonsoft.Json.Linq; 8 | using Orchestrator.Abstraction; 9 | using Serilog; 10 | using Serilog.Context; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Net.Http; 14 | using System.Text; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | 18 | public class OrchestratorModule : IHostedService 19 | { 20 | public IConfiguration Configuration { get; } 21 | public IModuleClient ModuleClient { get; } 22 | public IHttpHandler HttpClient { get; } 23 | public ITimeSeriesRecorder TimeSeriesRecorder { get; } 24 | public CancellationTokenSource CancellationTokenSource { get; } 25 | 26 | 27 | public OrchestratorModule(IConfiguration configuration, 28 | IModuleClient moduleClient, 29 | IHttpHandler httpClient, 30 | ITimeSeriesRecorder timeSeriesRecorder, 31 | CancellationTokenSource cancellationTokenSource) 32 | { 33 | Configuration = configuration; 34 | ModuleClient = moduleClient; 35 | CancellationTokenSource = cancellationTokenSource; 36 | HttpClient = httpClient; 37 | TimeSeriesRecorder = timeSeriesRecorder; 38 | } 39 | 40 | public async Task StartAsync(CancellationToken cancellationToken) 41 | { 42 | using (LogContext.PushProperty("FunctionId", nameof(StartAsync))) 43 | { 44 | Log.Information("Opening Edge Module Connection"); 45 | await ModuleClient.OpenAsync(); 46 | 47 | Log.Information("Initializing InfluxDBRecorder"); 48 | await TimeSeriesRecorder.InitializeAsync(); 49 | 50 | Log.Information("Beginning to Process Messages"); 51 | await ModuleClient.SetInputMessageHandlerAsync("telemetry", new MessageHandler( 52 | async (message, e) => 53 | { 54 | Log.Information("Processing message.."); 55 | var telemetryJson = Encoding.UTF8.GetString(message.GetBytes()); 56 | try 57 | { 58 | await TimeSeriesRecorder.RecordMessageAsync(telemetryJson); 59 | } 60 | catch (Exception ex) 61 | { 62 | Log.Error(ex, $"Error for message {telemetryJson}"); 63 | } 64 | return await Task.FromResult(MessageResponse.Completed); 65 | } 66 | ), ModuleClient); 67 | } 68 | } 69 | 70 | public Task StopAsync(CancellationToken cancellationToken) 71 | { 72 | using (LogContext.PushProperty("FunctionId", nameof(StopAsync))) 73 | { 74 | Log.Information("Shutting Down"); 75 | return Task.FromResult(0); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /modules/orchestrator/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Net.Http; 6 | 7 | using ModuleWrapper; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Configuration; 10 | using AdysTech.InfluxDB.Client.Net; 11 | using Microsoft.Azure.Devices.Client; 12 | using System.IO; 13 | using Orchestrator.Mock; 14 | using Orchestrator.Abstraction; 15 | using Orchestrator.Service; 16 | 17 | class Program 18 | { 19 | static async Task Main(string[] args) 20 | { 21 | await ModuleHost.Run(args, 22 | (services, configuration) => 23 | { 24 | if (configuration["Environment"] == "Debug") 25 | { 26 | services.AddSingleton(); 27 | services.AddHostedService(x => 28 | new MockMessageInjector(x.GetRequiredService(), 29 | x.GetRequiredService(), 30 | async (config, client) => 31 | await client.SendEventAsync("telemetry", new Message(File.ReadAllBytes("Mock/TelemetryMessageSample.json"))))); 32 | 33 | services.AddSingleton(); 34 | services.AddSingleton(); 35 | services.AddSingleton((e) => 36 | new InfluxDBClient( 37 | configuration.GetValue("INFLUX_URL", "http://localhost:8897"), 38 | configuration.GetValue("INFLUX_USERNAME", ""), 39 | configuration.GetValue("INFLUX_PASSWORD", ""))); 40 | 41 | services.AddSingleton((e) => new Service.HttpClientHandler( "http://localhost:8000/")); 42 | } 43 | else 44 | { 45 | services.AddSingleton(); 46 | // services.AddSingleton((e) => new Service.HttpClientHandler(configuration.GetValue("ML_BASE_URI", "http://ml:5001/"))); 47 | services.AddSingleton(); 48 | services.AddSingleton((e) => 49 | new InfluxDBClient( 50 | configuration.GetValue("INFLUX_URL", "http://influxdb:8086"), 51 | configuration.GetValue("INFLUX_USERNAME", ""), 52 | configuration.GetValue("INFLUX_PASSWORD", ""))); 53 | 54 | services.AddSingleton(); 55 | } 56 | 57 | }); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /modules/orchestrator/Service/HttpClientHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Orchestrator.Service 3 | { 4 | using Orchestrator.Abstraction; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | public class HttpClientHandler : IHttpHandler 9 | { 10 | public HttpClient Client { get; set; } 11 | public HttpClientHandler(string baseAddress) 12 | { 13 | Client = new HttpClient() { BaseAddress = new System.Uri(baseAddress) }; 14 | } 15 | 16 | public HttpResponseMessage Get(string url) 17 | { 18 | return GetAsync(url).Result; 19 | } 20 | 21 | public HttpResponseMessage Post(string url, HttpContent content) 22 | { 23 | return PostAsync(url, content).Result; 24 | } 25 | 26 | public async Task GetAsync(string url) 27 | { 28 | return await Client.GetAsync(url); 29 | } 30 | 31 | public async Task PostAsync(string url, HttpContent content) 32 | { 33 | return await Client.PostAsync(url, content); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/orchestrator/Service/InfluxDBRecorder.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator.Service 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using Serilog; 11 | using AdysTech.InfluxDB.Client.Net; 12 | using Microsoft.Extensions.Configuration; 13 | using ModuleWrapper; 14 | using System.Reflection; 15 | using Orchestrator.Abstraction; 16 | 17 | public class InfluxDBRecorder : ITimeSeriesRecorder 18 | { 19 | private enum ValidationStatus 20 | { 21 | success, 22 | fail, 23 | useDefaultValue 24 | } 25 | 26 | private enum InsertStatus 27 | { 28 | ok, 29 | abandon, 30 | noneAdded 31 | } 32 | 33 | public IConfiguration Configuration { get; } 34 | public IModuleClient ModuleClient { get; } 35 | public CancellationTokenSource CancellationTokenSource { get; } 36 | private IInfluxDBClient InfluxDBClient { get; } 37 | 38 | private List databases = null; 39 | 40 | public InfluxDBRecorder(IConfiguration configuration, 41 | IModuleClient moduleClient, 42 | CancellationTokenSource cancellationTokenSource, 43 | IInfluxDBClient influxDBClient) 44 | { 45 | Configuration = configuration; 46 | ModuleClient = moduleClient; 47 | CancellationTokenSource = cancellationTokenSource; 48 | InfluxDBClient = influxDBClient; 49 | } 50 | 51 | public async Task InitializeAsync() 52 | { 53 | ORM orm = null; 54 | 55 | using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Orchestrator.Service.TelemetrySelection.json")) 56 | using (StreamReader reader = new StreamReader(stream)) 57 | { 58 | string result = reader.ReadToEnd(); 59 | orm = JsonConvert.DeserializeObject(result, new JsonSerializerSettings 60 | { 61 | NullValueHandling = NullValueHandling.Ignore, 62 | MissingMemberHandling = MissingMemberHandling.Error 63 | }); 64 | } 65 | while (true) 66 | { 67 | try 68 | { 69 | await InfluxDBClient.GetInfluxDBNamesAsync(); 70 | Log.Information("Connected to Influxdb"); 71 | break; 72 | } 73 | catch (Exception ex) 74 | { 75 | Log.Information("Could not connect to Influx: " + ex.Message); 76 | Log.Information("Retying in 5 seconds..."); 77 | } 78 | await Task.Delay(5000); 79 | } 80 | 81 | foreach (var db in orm.Databases) 82 | { 83 | if (string.IsNullOrWhiteSpace(db.Name)) 84 | { 85 | throw new Exception("Error: Empty or null database name in TelemetrySelection: " + orm); 86 | } 87 | //can check if DB exists first so success true/false has meaning 88 | //can add check for success = false if DB doesn't already exist 89 | bool success = await InfluxDBClient.CreateDatabaseAsync(db.Name); 90 | var retentionPolicy = new InfluxRetentionPolicy() 91 | { 92 | Name = "ConfiguredRetentionPolicyFromEnv", 93 | DBName = db.Name, 94 | Duration = TimeSpan.FromDays(Configuration.GetValue("INFLUX_RETENTION_IN_DAYS", 30)), 95 | IsDefault = false 96 | }; 97 | if (!await InfluxDBClient.CreateRetentionPolicyAsync(retentionPolicy)) 98 | { 99 | throw new Exception("Error: Could not create retention policy for influxdb: " + db.Name); 100 | } 101 | } 102 | 103 | databases = orm.Databases; 104 | } 105 | 106 | public async Task RecordMessageAsync(string telemetryJson) 107 | { 108 | JToken events = JToken.Parse(telemetryJson); 109 | 110 | JArray eventsArray = events as JArray; 111 | if (eventsArray == null) 112 | { 113 | if (events is JObject) 114 | { 115 | eventsArray = new JArray(); 116 | eventsArray.Add(events); 117 | } 118 | else 119 | { 120 | throw new Exception("Error: entries is not JArray or JObject"); 121 | } 122 | } 123 | await InsertJsonIntoInfluxAsync(eventsArray); 124 | } 125 | 126 | private async Task InsertJsonIntoInfluxAsync(JArray entries) 127 | { 128 | foreach (JObject entry in entries) 129 | { 130 | foreach (var db in databases) 131 | { 132 | DateTime utcTimestamp; 133 | if (!TryGetTimestampFromJson(out utcTimestamp, entry, db)) 134 | { 135 | continue; 136 | } 137 | 138 | foreach (var measurement in db.Measurements) 139 | { 140 | var dataPoint = new InfluxDatapoint(); 141 | if (string.IsNullOrWhiteSpace(measurement.Name)) 142 | { 143 | Log.Warning("Error: Null or empty measurement name"); 144 | continue; 145 | } 146 | dataPoint.MeasurementName = measurement.Name; 147 | dataPoint.UtcTimestamp = utcTimestamp; 148 | dataPoint.Precision = TimePrecision.Milliseconds; 149 | 150 | //validate 151 | var tagValidation = TryValidateTags(measurement.Tags, entry); 152 | var fieldValidation = TryValidateFields(measurement.Fields, entry); 153 | 154 | if (tagValidation.insertStatus == InsertStatus.abandon 155 | || fieldValidation.insertStatus == InsertStatus.abandon) 156 | { 157 | Log.Warning("Could not validate tags/fields or insert was abandoned for measurement: {measurementName}", measurement.Name); 158 | continue; 159 | } 160 | 161 | //make sure at least 1 field was added successfully 162 | if (fieldValidation.insertStatus == InsertStatus.noneAdded) 163 | { 164 | Log.Warning("Error: No fields successfully added for {measurementName}", measurement.Name); 165 | continue; 166 | } 167 | 168 | //insert valid data 169 | var tagPoints = GetTagInserts(measurement.Tags, entry, tagValidation.validationStatuses); 170 | foreach (var point in tagPoints) 171 | { 172 | dataPoint.Tags.Add(point.name, point.data); 173 | } 174 | 175 | var fieldPoints = GetFieldInserts(measurement.Fields, entry, fieldValidation.validationStatuses); 176 | foreach (var point in fieldPoints) 177 | { 178 | var fieldValue = new InfluxValueField(point.data); 179 | dataPoint.Fields.Add(point.name, fieldValue); 180 | } 181 | 182 | var success = await InfluxDBClient.PostPointAsync(db.Name, dataPoint); 183 | if (!success) 184 | { 185 | Log.Warning("Could not add data point {measurementName} to influx", measurement.Name); 186 | continue; 187 | } 188 | Log.Information("Adding {measurementName} was successful", measurement.Name); 189 | } 190 | } 191 | } 192 | } 193 | 194 | private static bool TryGetTimestampFromJson(out DateTime utcTimestamp, JObject entry, Database db) 195 | { 196 | try 197 | { 198 | long unixTimeStamp = (long)entry.SelectToken(db.TimestampPayloadMapping); 199 | utcTimestamp = DateTimeOffset.FromUnixTimeMilliseconds(unixTimeStamp).UtcDateTime; 200 | return true; 201 | } 202 | catch (Exception ex) 203 | { 204 | Log.Error("Error: Could not get/convert event timestamp in Json: {entry} \n {exceptionMessage}", entry, ex.Message); 205 | utcTimestamp = new DateTime(); 206 | return false; 207 | } 208 | } 209 | 210 | private static (InsertStatus insertStatus, List validationStatuses) TryValidateTags(List tags, JObject entry) 211 | { 212 | var statuses = new List(); 213 | foreach (var tag in tags) 214 | { 215 | var data = (string)entry.SelectToken(tag.PayloadMapping); 216 | 217 | if (string.IsNullOrWhiteSpace(data) || string.IsNullOrWhiteSpace(tag.Name)) 218 | { 219 | if (tag.AbandonIfEmpty) 220 | { 221 | Log.Information("Abandoning Insert: Tag data for {tagName} is missing and AbandonIfEmpty is true", tag.Name); 222 | return (InsertStatus.abandon, null); 223 | } 224 | else 225 | { 226 | Log.Warning("Error: Null or empty tag data/name, skipping..."); 227 | statuses.Add(ValidationStatus.fail); 228 | continue; 229 | } 230 | } 231 | 232 | statuses.Add(ValidationStatus.success); 233 | } 234 | return (InsertStatus.ok, statuses); 235 | } 236 | 237 | private static (InsertStatus insertStatus, List validationStatuses) TryValidateFields(List fields, JObject entry) 238 | { 239 | var fieldsValidated = 0; 240 | var statuses = new List(); 241 | foreach (var field in fields) 242 | { 243 | if (string.IsNullOrWhiteSpace(field.Name)) 244 | { 245 | Log.Warning("Error: Null or empty field name, skipping..."); 246 | statuses.Add(ValidationStatus.fail); 247 | continue; 248 | } 249 | 250 | var fieldAndFieldType = GetFieldAsType(field, entry); 251 | 252 | //If field data is null, use default values. 253 | bool stringIsEmpty = (fieldAndFieldType.FieldType == typeof(string)) && 254 | (string.IsNullOrWhiteSpace(fieldAndFieldType.fieldAsType)); 255 | if (fieldAndFieldType.fieldAsType is null || stringIsEmpty) 256 | { 257 | if (field.AbandonIfEmpty) 258 | { 259 | Log.Information("Abandoning Insert: Field data for {fieldName} is missing and AbandonIfEmpty is true", field.Name); 260 | return (InsertStatus.abandon, null); 261 | } 262 | 263 | if (field.IgnoreIfEmpty) 264 | { 265 | //Ignore this field, process the next 266 | statuses.Add(ValidationStatus.fail); 267 | continue; 268 | } 269 | 270 | statuses.Add(ValidationStatus.useDefaultValue); 271 | fieldsValidated++; 272 | continue; 273 | } 274 | statuses.Add(ValidationStatus.success); 275 | fieldsValidated++; 276 | } 277 | if (fieldsValidated < 1) 278 | { 279 | return (InsertStatus.noneAdded, null); 280 | } 281 | 282 | return (InsertStatus.ok, statuses); 283 | } 284 | 285 | private static (dynamic fieldAsType, Type FieldType) GetFieldAsType(Field field, JObject entry) 286 | { 287 | //Determine the field type based on what was suplied in the json 288 | Type ResFieldType = null; 289 | switch (field.Type) 290 | { 291 | case FieldType.@float: 292 | ResFieldType = typeof(double); 293 | break; 294 | case FieldType.@string: 295 | ResFieldType = typeof(string); 296 | break; 297 | case FieldType.integer: 298 | ResFieldType = typeof(int); 299 | break; 300 | case FieldType.Boolean: 301 | ResFieldType = typeof(bool); 302 | break; 303 | } 304 | var data = entry.SelectToken(field.PayloadMapping); 305 | var dataString = data?.ToString(); 306 | 307 | if(string.IsNullOrEmpty(dataString)) 308 | return(null, ResFieldType); 309 | 310 | try 311 | { 312 | return (Convert.ChangeType(data, ResFieldType), ResFieldType); 313 | } 314 | catch (Exception ex) 315 | { 316 | Log.Error("Error: Could not convert data to type, returning null", entry, ex.Message); 317 | } 318 | 319 | return(null, ResFieldType); 320 | } 321 | 322 | private static List<(string name, string data)> GetTagInserts(List tags, JObject entry, List validationStatuses) 323 | { 324 | var points = new List<(string name, string data)>(); 325 | for (var i = 0; i < tags.Count; i++) 326 | { 327 | var tag = tags[i]; 328 | var data = (string)entry.SelectToken(tag.PayloadMapping); 329 | 330 | if (validationStatuses[i] == ValidationStatus.success) 331 | { 332 | points.Add((tag.Name, data)); 333 | } 334 | } 335 | return points; 336 | } 337 | 338 | private static List<(string name, dynamic data)> GetFieldInserts(List fields, JObject entry, List validationStatuses) 339 | { 340 | var points = new List<(string name, dynamic data)>(); 341 | 342 | for (var i = 0; i < fields.Count; i++) 343 | { 344 | if(validationStatuses[i] != ValidationStatus.fail) 345 | { 346 | var field = fields[i]; 347 | var data = (string)entry.SelectToken(field.PayloadMapping); 348 | dynamic fieldAsType = null; 349 | 350 | if (validationStatuses[i] == ValidationStatus.success) 351 | { 352 | var fType = GetFieldAsType(field, entry); 353 | fieldAsType = fType.fieldAsType; 354 | } 355 | else if (validationStatuses[i] == ValidationStatus.useDefaultValue) 356 | { 357 | fieldAsType = field.ValueIfMissing; 358 | } 359 | points.Add((field.Name, fieldAsType)); 360 | } 361 | } 362 | return points; 363 | } 364 | 365 | public async Task DisconnectAsync() 366 | { 367 | Log.Information("Disconnecting Edge Hub Client Connection"); 368 | 369 | await ModuleClient.CloseAsync(); 370 | } 371 | 372 | 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /modules/orchestrator/Service/ORM.cs: -------------------------------------------------------------------------------- 1 | namespace Orchestrator.Service 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | 7 | [JsonObject(ItemRequired = Required.Always)] 8 | internal class ORM 9 | { 10 | public List Databases { get; set; } 11 | } 12 | 13 | [JsonObject(ItemRequired = Required.Always)] 14 | internal class Database 15 | { 16 | public string Name { get; set; } 17 | public List Measurements { get; set; } 18 | public string TimestampPayloadMapping { get; set; } 19 | } 20 | 21 | [JsonObject(ItemRequired = Required.Always)] 22 | internal class Measurement 23 | { 24 | public string Name { get; set; } 25 | public List Tags { get; set; } 26 | public List Fields { get; set; } 27 | } 28 | 29 | [JsonObject(ItemRequired = Required.Always)] 30 | internal class Tag 31 | { 32 | public string Name { get; set; } 33 | public string PayloadMapping { get; set; } 34 | public bool AbandonIfEmpty { get; set; } 35 | } 36 | 37 | internal class Field 38 | { 39 | [JsonProperty(Required = Required.Always)] 40 | public string Name { get; set; } 41 | 42 | [JsonProperty(Required = Required.Always)] 43 | public string PayloadMapping { get; set; } 44 | 45 | [JsonProperty(Required = Required.Always)] 46 | public FieldType Type { get; set; } 47 | 48 | [JsonProperty(Required = Required.Always)] 49 | public bool AbandonIfEmpty { get; set; } 50 | public bool IgnoreIfEmpty { get; set; } 51 | public object ValueIfMissing { get; set; } 52 | } 53 | 54 | internal enum FieldType 55 | { 56 | //prefix with @ since string and float are reserve words 57 | [Description("string")] 58 | @string, 59 | [Description("integer")] 60 | integer, 61 | [Description("float")] 62 | @float, 63 | [Description("Boolean")] 64 | Boolean 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /modules/orchestrator/Service/TelemetrySelection.json: -------------------------------------------------------------------------------- 1 | { 2 | "Databases": [ 3 | { 4 | "Name": "machineInfo", 5 | "Measurements": [ 6 | { 7 | "Name": "telemetry", 8 | "Tags": [ 9 | { 10 | "Name": "MachineId", 11 | "PayloadMapping": "MachineId", 12 | "AbandonIfEmpty": false 13 | } 14 | ], 15 | "Fields": [ 16 | { 17 | "Name": "MachineTemperature", 18 | "PayloadMapping": "MachineTemperature", 19 | "Type": "float", 20 | "AbandonIfEmpty": false, 21 | "IgnoreIfEmpty": true 22 | }, 23 | { 24 | "Name": "AmbientTemperature", 25 | "PayloadMapping": "AmbientTemperature", 26 | "Type": "float", 27 | "AbandonIfEmpty": false, 28 | "IgnoreIfEmpty": true 29 | }, 30 | { 31 | "Name": "ConveyorBeltSpeed", 32 | "PayloadMapping": "ConveyorBeltSpeed", 33 | "Type": "integer", 34 | "AbandonIfEmpty": false, 35 | "IgnoreIfEmpty": true 36 | }, 37 | { 38 | "Name": "GearTension", 39 | "PayloadMapping": "GearTension", 40 | "Type": "integer", 41 | "AbandonIfEmpty": false, 42 | "IgnoreIfEmpty": true 43 | }, 44 | { 45 | "Name": "WorkerDetected", 46 | "PayloadMapping": "WorkerDetected", 47 | "Type": "integer", 48 | "AbandonIfEmpty": false, 49 | "IgnoreIfEmpty": true 50 | } 51 | ] 52 | } 53 | ], 54 | "TimestampPayloadMapping": "Timestamp" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /modules/orchestrator/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-version": "0.0.1", 3 | "description": "", 4 | "image": { 5 | "repository": "${CONTAINER_REGISTRY_ADDRESS}/orchestrator", 6 | "tag": { 7 | "version": "0.0.${BUILD_BUILDID}", 8 | "platforms": { 9 | "amd64": "./Dockerfile.amd64", 10 | "amd64.debug": "./Dockerfile.amd64.debug" 11 | } 12 | }, 13 | "buildOptions": [], 14 | "contextPath": "../../" 15 | }, 16 | "language": "csharp" 17 | } 18 | -------------------------------------------------------------------------------- /modules/simulator/.dockerignore: -------------------------------------------------------------------------------- 1 | [b|B]in 2 | [O|o]bj -------------------------------------------------------------------------------- /modules/simulator/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /modules/simulator/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-env 2 | WORKDIR /app 3 | COPY ./modules/simulator/*.csproj ./modules/simulator/ 4 | COPY ./ModuleWrapper/*.csproj ./ModuleWrapper/ 5 | WORKDIR /app/modules/simulator 6 | RUN dotnet restore 7 | 8 | WORKDIR /app 9 | COPY . ./ 10 | RUN dotnet publish -c Release -o /app/out 11 | 12 | FROM mcr.microsoft.com/dotnet/core/runtime:2.1-stretch-slim 13 | WORKDIR /app 14 | COPY --from=build-env /app/out ./ 15 | COPY /modules/simulator/sampleData.csv ./ 16 | 17 | RUN useradd -ms /bin/bash moduleuser 18 | USER moduleuser 19 | 20 | ENTRYPOINT ["dotnet", "Simulator.dll"] -------------------------------------------------------------------------------- /modules/simulator/Dockerfile.amd64.debug: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/runtime:2.1-stretch-slim AS base 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends unzip procps && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | RUN useradd -ms /bin/bash moduleuser 8 | USER moduleuser 9 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg 10 | 11 | FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-env 12 | 13 | WORKDIR /app 14 | COPY ./modules/simulator/*.csproj ./modules/simulator/ 15 | COPY ./ModuleWrapper/*.csproj ./ModuleWrapper/ 16 | WORKDIR /app/modules/simulator 17 | RUN dotnet restore 18 | 19 | WORKDIR /app 20 | COPY . ./ 21 | RUN dotnet publish -c Debug -o /app/out 22 | 23 | FROM base 24 | WORKDIR /app 25 | COPY --from=build-env /app/out ./ 26 | COPY /modules/simulator/sampleData.csv ./ 27 | 28 | ENTRYPOINT ["dotnet", "Simulator.dll"] -------------------------------------------------------------------------------- /modules/simulator/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Simulator 2 | { 3 | using System.Threading.Tasks; 4 | using ModuleWrapper; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | class Program 8 | { 9 | static async Task Main(string[] args) 10 | { 11 | await ModuleHost.Run(args, 12 | (services, configuration) => 13 | { 14 | if (configuration["Environment"] == "Debug") 15 | services.AddSingleton(); 16 | else 17 | services.AddSingleton(); 18 | }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/simulator/Service/Measurement.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Simulator.Service 3 | { 4 | using Newtonsoft.Json; 5 | public class Measurement 6 | { 7 | 8 | [JsonProperty(PropertyName = "Timestamp")] 9 | public string Timestamp { get; set; } 10 | 11 | [JsonProperty(PropertyName = "MachineId")] 12 | public string MachineId { get; set; } 13 | 14 | [JsonProperty(PropertyName = "MachineTemperature")] 15 | public string MachineTemperature { get; set; } 16 | 17 | [JsonProperty(PropertyName = "AmbientTemperature")] 18 | public string AmbientTemperature { get; set; } 19 | 20 | [JsonProperty(PropertyName = "ConveyorBeltSpeed")] 21 | public string ConveyorBeltSpeed { get; set; } 22 | 23 | 24 | [JsonProperty(PropertyName = "GearTension")] 25 | public string GearTension { get; set; } 26 | 27 | [JsonProperty(PropertyName = "WorkerDetected")] 28 | public string WorkerDetected { get; set; } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /modules/simulator/Service/MessageEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace Simulator.Service 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using ModuleWrapper; 5 | using Newtonsoft.Json; 6 | using Serilog; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | public class MessageEmitter 17 | { 18 | int MessageIndex; 19 | int IntervalInMilliseconds; 20 | 21 | IModuleClient ModuleClient { get; } 22 | List Messages { get; set; } 23 | CancellationTokenSource CancellationTokenSource { get; } 24 | 25 | TaskTimer TaskTimer { get; set; } 26 | 27 | public MessageEmitter(IModuleClient moduleClient, CancellationTokenSource cancellationTokenSource) 28 | { 29 | MessageIndex = 0; 30 | IntervalInMilliseconds = 1000; 31 | ModuleClient = moduleClient; 32 | CancellationTokenSource = cancellationTokenSource; 33 | } 34 | 35 | public async Task Init() 36 | { 37 | var twin = await ModuleClient.GetTwinAsync(); 38 | if (twin!= null && twin.Properties.Desired.Contains("IntervalInMilliseconds")) 39 | int.TryParse(twin.Properties.Desired["IntervalInMilliseconds"], out IntervalInMilliseconds); 40 | 41 | Messages = File.ReadAllLines("sampleData.csv") 42 | .Skip(1) 43 | .Select(v => CreateMeasurementFromCsvLine(v)) 44 | .Where(v => v != null) 45 | .ToList(); 46 | 47 | Debug.Assert(Messages.Count > 0); 48 | 49 | Log.Information($"Loaded {Messages.Count} messages."); 50 | 51 | TaskTimer = new TaskTimer(async () => await EmitMessage(), 52 | TimeSpan.FromMilliseconds(IntervalInMilliseconds), 53 | CancellationTokenSource); 54 | } 55 | 56 | /// 57 | /// This method is the main message pump 58 | /// It starts a Task based timer that periodically emits a message 59 | /// 60 | public void Start() 61 | { 62 | // Start the timer 63 | TaskTimer.Start(); 64 | } 65 | 66 | public async Task EmitMessage() 67 | { 68 | if (MessageIndex >= Messages.Count) 69 | MessageIndex = 0; 70 | 71 | Log.Information($"Emitting telemetry message {MessageIndex}"); 72 | 73 | var message = Messages[MessageIndex++]; 74 | message.Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(); 75 | 76 | var json = JsonConvert.SerializeObject(message, Formatting.Indented); 77 | using (var iotMessage = new Message(Encoding.UTF8.GetBytes(json.Replace("NaN", "")))) 78 | { 79 | await ModuleClient.SendEventAsync("telemetry", iotMessage); 80 | } 81 | } 82 | 83 | private Measurement CreateMeasurementFromCsvLine(string line) 84 | { 85 | try 86 | { 87 | var data = line.Split(','); 88 | var sampleMeasurement = new Measurement() 89 | { 90 | MachineId = data[1], 91 | MachineTemperature = data[2], 92 | AmbientTemperature = data[3], 93 | ConveyorBeltSpeed = data[4], 94 | GearTension = data[5], 95 | WorkerDetected = data[6] 96 | }; 97 | return sampleMeasurement; 98 | } 99 | catch (Exception ex) 100 | { 101 | Log.Error(ex, $"Error converting csv line {line} to object"); 102 | } 103 | return null; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /modules/simulator/Simulator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp2.1 5 | 6 | 7 | True 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | -------------------------------------------------------------------------------- /modules/simulator/SimulatorModule.cs: -------------------------------------------------------------------------------- 1 | namespace Simulator 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Hosting; 5 | using ModuleWrapper; 6 | using Serilog; 7 | using Serilog.Context; 8 | using Simulator.Service; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | public class SimulatorModule : IHostedService 12 | { 13 | public IConfiguration Configuration { get; } 14 | public IModuleClient ModuleClient{ get; } 15 | public CancellationTokenSource CancellationTokenSource { get; } 16 | 17 | public SimulatorModule(IConfiguration configuration, 18 | IModuleClient moduleClient, 19 | CancellationTokenSource cancellationTokenSource) 20 | { 21 | Configuration = configuration; 22 | ModuleClient = moduleClient; 23 | CancellationTokenSource = cancellationTokenSource; 24 | } 25 | 26 | public async Task StartAsync(CancellationToken cancellationToken) 27 | { 28 | using (LogContext.PushProperty("FunctionId", nameof(StartAsync))) 29 | { 30 | Log.Information("Opening Edge Module Connection"); 31 | await ModuleClient.OpenAsync(); 32 | 33 | Log.Information("Beginning to Process Messages"); 34 | MessageEmitter messageEmitter = new MessageEmitter(ModuleClient, CancellationTokenSource); 35 | await messageEmitter.Init(); 36 | messageEmitter.Start(); 37 | } 38 | } 39 | 40 | public Task StopAsync(CancellationToken cancellationToken) 41 | { 42 | using (LogContext.PushProperty("FunctionId", nameof(StopAsync))) 43 | { 44 | Log.Information("Shutting Down"); 45 | return Task.FromResult(0); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/simulator/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-version": "0.0.1", 3 | "description": "", 4 | "image": { 5 | "repository": "${CONTAINER_REGISTRY_ADDRESS}/simulator", 6 | "tag": { 7 | "version": "0.0.${BUILD_BUILDID}", 8 | "platforms": { 9 | "amd64": "./Dockerfile.amd64", 10 | "amd64.debug": "./Dockerfile.amd64.debug" 11 | } 12 | }, 13 | "buildOptions": [], 14 | "contextPath": "../../" 15 | }, 16 | "language": "csharp" 17 | } 18 | -------------------------------------------------------------------------------- /modules/simulator/sampleData.csv: -------------------------------------------------------------------------------- 1 | Timestamp,MachineId,MachineTemperature,AmbientTemperature,ConveyorBeltSpeed,GearTension,WorkerDetected 2 | 0,XY1,152,19,75,94,0 3 | 0,XY1,160,25,55,111,0 4 | 0,XY1,165,30,43,123,0 5 | 0,XY1,172,34,29,133,0 6 | 0,XY1,179,38,17,144,0 7 | 0,XY1,6,43,10,155,0 8 | 0,XY1,12,49,3,165,2 9 | 0,XY1,20,54,2,171,2 10 | 0,XY1,26,58,2,176,2 11 | 0,XY1,32,66,5,179,2 12 | 0,XY1,38,71,10,180,2 13 | 0,XY1,44,76,19,180,2 14 | 0,XY1,50,83,31,179,2 15 | 0,XY1,56,87,41,174,2 16 | 0,XY1,63,3,52,169,2 17 | 0,XY1,68,8,66,161,2 18 | 0,XY1,74,12,78,152,2 19 | 0,XY1,80,16,87,143,2 20 | 0,XY1,91,20,95,133,2 21 | 0,XY1,96,25,104,122,2 22 | 0,XY1,107,29,111,108,0 23 | 0,XY1,113,34,116,95,0 24 | 0,XY1,119,42,120,80,0 25 | 0,XY1,124,46,120,65,0 26 | 0,XY1,130,51,119,49,0 27 | 0,XY1,136,59,114,40,0 28 | 0,XY1,140,64,107,27,0 29 | 0,XY1,145,68,98,17,0 30 | 0,XY1,151,72,89,10,0 31 | 0,XY1,156,78,75,6,0 32 | 0,XY1,161,82,64,2,0 33 | 0,XY1,167,85,52,2,0 34 | 0,XY1,176,89,43,3,0 35 | 0,XY1,2,3,35,6,0 36 | 0,XY1,8,7,26,13,2 37 | 0,XY1,12,13,19,20,2 38 | 0,XY1,17,18,13,30,2 39 | 0,XY1,21,22,8,39,2 40 | 0,XY1,25,27,5,50,2 41 | 0,XY1,30,31,3,63,2 42 | 0,XY1,35,35,2,74,2 43 | 0,XY1,41,39,2,90,2 44 | 0,XY1,45,44,4,103,2 45 | 0,XY1,49,47,8,114,2 46 | 0,XY1,55,51,12,128,2 47 | 0,XY1,61,55,17,140,2 48 | 0,XY1,70,61,26,155,2 49 | 0,XY1,77,65,34,162,2 50 | 0,XY1,82,70,42,169,2 51 | 0,XY1,88,77,50,174,0 52 | 0,XY1,93,81,58,179,0 53 | 0,XY1,98,86,67,180,0 54 | 0,XY1,104,90,78,180,0 55 | 0,XY1,110,6,85,177,0 56 | 0,XY1,117,11,93,173,0 57 | 0,XY1,122,19,100,161,0 58 | 0,XY1,128,23,107,148,0 59 | 0,XY1,133,29,112,133,0 60 | 0,XY1,140,33,116,117,0 61 | 0,XY1,146,37,119,100,0 62 | 0,XY1,150,43,120,88,0 63 | 0,XY1,154,48,120,71,0 64 | 0,XY1,159,52,118,60,0 65 | 0,XY1,167,57,114,49,2 66 | 0,XY1,173,61,107,27,2 67 | 0,XY1,179,65,99,15,2 68 | 0,XY1,7,70,89,3,2 69 | 0,XY1,15,74,77,1,2 70 | 0,XY1,21,78,65,3,2 71 | 0,XY1,26,83,52,7,2 72 | 0,XY1,32,87,39,15,2 73 | 0,XY1,38,6,28,23,2 74 | 0,XY1,43,10,19,36,2 75 | 0,XY1,49,14,11,51,2 76 | 0,XY1,54,19,6,67,2 77 | 0,XY1,62,24,2,85,2 78 | 0,XY1,68,29,2,102,2 79 | 0,XY1,75,33,3,125,2 80 | 0,XY1,82,37,7,140,2 81 | 0,XY1,87,42,12,154,0 82 | 0,XY1,92,46,20,165,0 83 | 0,XY1,98,49,31,173,0 84 | 0,XY1,103,54,41,179,0 85 | 0,XY1,109,59,53,180,0 86 | 0,XY1,115,66,66,179,0 87 | 0,XY1,120,71,74,174,0 88 | 0,XY1,126,76,83,166,0 89 | 0,XY1,132,80,89,155,0 90 | 0,XY1,139,84,98,142,0 91 | 0,XY1,145,89,105,127,0 92 | 0,XY1,150,4,110,110,0 93 | 0,XY1,156,9,114,92,0 94 | 0,XY1,161,13,118,76,0 95 | 0,XY1,165,17,120,58,0 96 | 0,XY1,169,21,120,46,0 97 | 0,XY1,178,25,120,35,0 98 | 0,XY1,5,30,117,25,2 99 | 0,XY1,10,34,113,17,2 100 | 0,XY1,16,38,108,9,2 101 | 0,XY1,22,42,98,3,2 102 | 0,XY1,28,46,88,2,2 103 | 0,XY1,34,50,78,3,2 104 | 0,XY1,39,54,67,6,2 105 | 0,XY1,45,59,55,11,2 106 | 0,XY1,50,63,44,17,2 107 | 0,XY1,55,67,32,25,2 108 | 0,XY1,59,71,22,47,2 109 | 0,XY1,64,78,14,65,2 110 | 0,XY1,68,82,8,81,2 111 | 0,XY1,76,90,4,93,2 112 | 0,XY1,82,8,2,108,2 113 | 0,XY1,88,12,2,123,0 114 | 0,XY1,93,16,5,134,0 115 | 0,XY1,98,24,10,152,0 116 | 0,XY1,104,30,16,165,0 117 | 0,XY1,110,36,22,174,0 118 | 0,XY1,115,41,29,179,0 119 | 0,XY1,119,46,46,180,0 120 | 0,XY1,123,49,60,178,0 121 | 0,XY1,128,54,72,171,0 122 | 0,XY1,132,59,87,160,0 123 | 0,XY1,138,67,96,144,0 124 | 0,XY1,146,75,102,132,0 125 | 0,XY1,154,79,108,118,0 126 | 0,XY1,159,84,113,102,0 127 | 0,XY1,163,3,117,91,0 128 | 0,XY1,168,7,120,76,0 129 | 0,XY1,172,11,120,64,0 130 | 0,XY1,176,16,117,52,2 131 | 0,XY1,3,20,112,39,2 132 | 0,XY1,10,24,106,29,2 133 | 0,XY1,17,28,99,20,2 134 | 0,XY1,23,33,90,11,2 135 | 0,XY1,30,37,80,5,2 136 | 0,XY1,36,41,72,2,2 137 | 0,XY1,45,47,64,2,2 138 | 0,XY1,52,54,55,3,2 139 | 0,XY1,57,58,47,6,2 140 | 0,XY1,62,62,37,19,2 141 | 0,XY1,65,67,30,33,2 142 | 0,XY1,70,71,23,48,2 143 | 0,XY1,73,75,15,64,0 144 | 0,XY1,79,80,8,84,0 145 | 0,XY1,84,84,4,101,0 146 | 0,XY1,90,88,2,123,0 147 | 0,XY1,95,4,2,138,0 148 | 0,XY1,101,9,3,157,0 149 | 0,XY1,106,13,6,168,0 150 | 0,XY1,112,17,10,177,0 151 | 0,XY1,118,25,17,180,0 152 | 0,XY1,124,30,23,180,0 153 | 0,XY1,131,35,31,173,0 154 | 0,XY1,137,39,38,163,0 155 | 0,XY1,145,44,46,144,0 156 | 0,XY1,152,49,54,126,0 157 | 0,XY1,162,56,61,108,0 158 | 0,XY1,168,60,73,92,2 159 | 0,XY1,175,64,84,71,2 160 | 0,XY1,6,68,91,54,2 161 | 0,XY1,9,72,98,36,2 162 | 0,XY1,13,79,106,18,2 163 | 0,XY1,17,83,113,8,2 164 | 0,XY1,22,89,117,4,2 165 | 0,XY1,29,3,120,2,2 166 | 0,XY1,35,8,120,2,2 167 | 0,XY1,41,13,120,5,2 168 | 0,XY1,48,19,117,17,2 169 | 0,XY1,53,27,114,28,2 170 | 0,XY1,59,31,109,44,2 171 | 0,XY1,64,39,103,59,2 172 | 0,XY1,75,44,96,77,0 173 | 0,XY1,81,49,89,94,0 174 | 0,XY1,87,53,81,111,0 175 | 0,XY1,91,59,72,129,0 176 | 0,XY1,97,63,64,143,0 177 | 0,XY1,103,67,56,160,0 178 | 0,XY1,109,73,47,170,0 179 | 0,XY1,115,78,39,176,0 180 | 0,XY1,125,85,30,180,0 181 | 0,XY1,132,90,24,180,0 182 | 0,XY1,137,6,18,177,0 183 | 0,XY1,146,11,13,170,0 184 | 0,XY1,152,15,8,160,0 185 | 0,XY1,159,19,3,148,0 186 | 0,XY1,165,23,2,134,0 187 | 0,XY1,172,27,2,117,0 188 | 0,XY1,178,32,3,100,2 189 | 0,XY1,4,37,6,82,2 190 | 0,XY1,10,41,11,66,2 191 | 0,XY1,16,45,17,49,2 192 | 0,XY1,19,48,23,35,2 193 | 0,XY1,23,52,31,21,2 194 | 0,XY1,30,57,42,12,2 195 | 0,XY1,36,61,50,5,2 196 | 0,XY1,41,66,59,2,2 197 | 0,XY1,47,72,67,2,2 198 | 0,XY1,53,76,75,4,2 199 | 0,XY1,59,80,84,7,2 200 | 0,XY1,64,84,94,12,2 201 | 0,XY1,71,88,102,26,2 202 | 0,XY1,76,4,107,50,2 203 | 0,XY1,82,8,112,67,2 204 | 0,XY1,92,12,116,84,0 205 | 0,XY1,98,16,119,102,0 206 | 0,XY1,104,20,120,127,0 207 | 0,XY1,110,25,120,155,0 208 | 0,XY1,115,29,119,169,0 209 | 0,XY1,121,34,117,176,0 210 | 0,XY1,127,39,113,180,0 211 | 0,XY1,132,43,109,178,0 212 | 0,XY1,139,48,102,172,0 213 | 0,XY1,145,55,95,163,0 214 | 0,XY1,150,60,79,150,0 215 | 0,XY1,156,64,67,136,0 216 | 0,XY1,161,68,56,112,0 217 | 0,XY1,167,74,45,95,0 218 | 0,XY1,174,79,34,78,2 219 | 0,XY1,1,83,24,62,2 220 | 0,XY1,7,87,16,45,2 221 | 0,XY1,13,2,9,29,2 222 | 0,XY1,18,8,4,16,2 223 | 0,XY1,24,14,2,8,2 224 | 0,XY1,28,22,2,3,2 225 | 0,XY1,32,26,4,2,2 226 | 0,XY1,39,31,7,3,2 227 | 0,XY1,48,36,11,8,2 228 | 0,XY1,53,41,16,15,2 229 | 0,XY1,59,46,23,22,2 230 | 0,XY1,64,49,29,30,2 231 | 0,XY1,73,53,39,47,2 232 | 0,XY1,80,58,49,64,2 233 | 0,XY1,85,62,58,92,0 234 | 0,XY1,91,66,65,110,0 235 | 0,XY1,96,71,79,127,0 236 | 0,XY1,101,75,90,142,0 237 | 0,XY1,107,79,99,155,0 238 | 0,XY1,112,84,108,167,0 239 | 0,XY1,120,88,118,175,0 240 | 0,XY1,125,4,120,180,0 241 | 0,XY1,137,9,120,180,0 242 | 0,XY1,143,13,116,177,0 243 | 0,XY1,149,17,111,171,0 244 | 0,XY1,155,21,102,161,0 245 | 0,XY1,161,26,96,149,2 246 | 0,XY1,168,30,89,135,2 247 | 0,XY1,174,37,83,114,2 248 | 0,XY1,179,40,64,89,2 249 | 0,XY1,6,43,50,66,2 250 | 0,XY1,12,46,39,44,2 251 | 0,XY1,17,48,20,27,2 252 | 0,XY1,23,51,12,16,2 253 | 0,XY1,30,58,7,8,2 254 | 0,XY1,36,64,3,3,2 255 | 0,XY1,40,69,2,2,2 256 | 0,XY1,44,74,2,4,2 257 | 0,XY1,50,78,6,8,2 258 | 0,XY1,58,83,11,14,2 259 | 0,XY1,65,88,19,21,2 260 | 0,XY1,71,4,27,29,0 261 | 0,XY1,77,10,38,38,0 262 | 0,XY1,83,14,52,53,0 263 | 0,XY1,88,18,65,72,0 264 | 0,XY1,93,22,77,90,0 265 | 0,XY1,99,28,91,106,0 266 | 0,XY1,106,32,100,127,0 267 | 0,XY1,112,36,109,142,0 268 | 0,XY1,123,41,116,155,0 269 | 0,XY1,130,46,119,168,0 270 | 0,XY1,137,49,120,176,0 271 | 0,XY1,144,53,117,180,0 272 | 0,XY1,151,57,108,180,0 273 | 0,XY1,157,60,99,177,2 274 | 0,XY1,163,64,88,169,2 275 | 0,XY1,168,67,78,159,2 276 | 0,XY1,175,70,71,138,2 277 | 0,XY1,2,74,61,120,2 278 | 0,XY1,9,77,42,103,2 279 | 0,XY1,14,80,31,87,2 280 | 0,XY1,20,84,20,69,2 281 | 0,XY1,26,88,12,52,2 282 | 0,XY1,31,2,6,37,2 283 | 0,XY1,37,6,3,24,2 284 | 0,XY1,43,10,1,13,2 285 | 0,XY1,46,13,3,6,2 286 | 0,XY1,51,16,8,2,2 287 | 0,XY1,59,20,13,2,2 288 | 0,XY1,64,24,24,3,2 289 | 0,XY1,70,29,34,6,0 290 | 0,XY1,76,34,44,12,0 291 | 0,XY1,81,38,59,21,0 292 | 0,XY1,90,42,73,38,0 293 | 0,XY1,95,46,85,48,0 294 | 0,XY1,101,47,104,58,0 295 | 0,XY1,106,51,111,75,0 296 | 0,XY1,112,54,118,92,0 297 | 0,XY1,118,57,120,108,0 298 | 0,XY1,124,61,120,126,0 299 | 0,XY1,130,64,116,143,0 300 | 0,XY1,136,67,111,157,0 301 | 0,XY1,145,70,104,167,0 302 | 0,XY1,151,74,95,176,0 303 | 0,XY1,157,77,82,180,2 304 | 0,XY1,164,80,70,177,2 305 | 0,XY1,170,83,59,165,2 306 | 0,XY1,175,87,47,153,2 307 | 0,XY1,2,90,29,124,2 308 | 0,XY1,9,4,20,107,2 309 | 0,XY1,15,8,12,90,2 310 | 0,XY1,21,11,3,70,2 311 | 0,XY1,26,14,2,54,2 312 | 0,XY1,32,17,5,39,2 313 | 0,XY1,42,20,11,24,2 314 | 0,XY1,48,23,19,14,2 315 | 0,XY1,53,27,28,7,2 316 | 0,XY1,58,30,44,2,2 317 | 0,XY1,64,33,57,2,2 318 | 0,XY1,68,36,70,3,0 319 | 0,XY1,77,39,89,8,0 320 | 0,XY1,83,43,99,22,0 321 | 0,XY1,89,46,109,35,0 322 | 0,XY1,93,48,116,52,0 323 | 0,XY1,99,51,120,68,0 324 | 0,XY1,106,54,120,80,0 325 | 0,XY1,113,58,118,96,0 326 | 0,XY1,119,61,114,112,0 327 | 0,XY1,130,64,109,125,0 328 | 0,XY1,135,67,102,141,0 329 | 0,XY1,142,71,96,155,0 330 | 0,XY1,148,74,87,165,0 331 | 0,XY1,154,77,78,178,0 332 | 0,XY1,159,80,69,180,0 333 | 0,XY1,170,83,57,178,2 334 | 0,XY1,176,87,47,172,2 335 | 0,XY1,4,90,37,163,2 336 | 0,XY1,9,5,29,148,2 337 | 0,XY1,15,8,23,134,2 338 | 0,XY1,21,11,17,116,2 339 | 0,XY1,26,14,11,94,2 340 | 0,XY1,32,18,7,68,2 341 | 0,XY1,38,21,3,51,2 342 | 0,XY1,45,24,2,37,2 343 | 0,XY1,50,28,4,24,2 344 | 0,XY1,56,30,8,13,2 345 | 0,XY1,62,34,16,5,2 346 | 0,XY1,67,37,25,2,2 347 | 0,XY1,74,40,35,2,2 348 | 0,XY1,80,44,46,4,0 349 | 0,XY1,90,46,64,10,0 350 | 0,XY1,101,49,77,32,0 351 | 0,XY1,106,54,87,48,0 352 | 0,XY1,117,57,99,65,0 353 | 0,XY1,123,60,108,82,0 354 | 0,XY1,129,63,116,98,0 355 | 0,XY1,134,66,119,111,0 356 | 0,XY1,140,70,120,126,0 357 | 0,XY1,145,73,120,144,0 358 | 0,XY1,151,77,119,159,0 359 | 0,XY1,157,80,117,169,0 360 | 0,XY1,164,83,113,177,0 361 | 0,XY1,169,87,107,180,0 362 | 0,XY1,175,89,102,180,0 363 | 0,XY1,180,4,80,176,2 364 | 0,XY1,7,8,69,169,2 365 | 0,XY1,13,11,56,156,2 366 | 0,XY1,18,14,44,142,2 367 | 0,XY1,25,17,33,128,2 368 | 0,XY1,31,21,22,111,2 369 | 0,XY1,36,24,14,89,2 370 | 0,XY1,42,27,8,72,2 371 | 0,XY1,48,32,4,55,2 372 | 0,XY1,53,36,2,40,2 373 | 0,XY1,59,39,3,27,2 374 | 0,XY1,66,42,6,16,2 375 | 0,XY1,72,46,11,8,2 376 | 0,XY1,78,48,16,3,2 377 | 0,XY1,82,52,24,2,0 378 | 0,XY1,88,55,35,3,0 379 | 0,XY1,91,58,42,6,0 380 | 0,XY1,101,61,51,11,0 381 | 0,XY1,107,64,61,21,0 382 | 0,XY1,112,67,71,33,0 383 | 0,XY1,118,70,81,48,0 384 | 0,XY1,124,73,89,64,0 385 | 0,XY1,135,77,96,84,0 386 | 0,XY1,142,82,103,103,0 387 | 0,XY1,148,86,109,124,0 388 | 0,XY1,153,90,114,139,0 389 | 0,XY1,159,7,118,149,0 390 | 0,XY1,165,12,120,161,0 391 | 0,XY1,170,18,118,168,2 392 | 0,XY1,176,23,114,174,2 393 | 0,XY1,4,28,108,179,2 394 | 0,XY1,9,33,100,180,2 395 | 0,XY1,17,38,90,178,2 396 | 0,XY1,24,43,80,172,2 397 | 0,XY1,30,46,67,161,2 398 | 0,XY1,35,48,50,149,2 399 | 0,XY1,41,52,35,134,2 400 | 0,XY1,46,55,25,117,2 401 | 0,XY1,52,59,16,100,2 402 | 0,XY1,58,63,10,71,2 403 | 0,XY1,63,66,6,51,2 404 | 0,XY1,69,73,3,36,2 405 | 0,XY1,76,77,2,13,2 406 | 0,XY1,83,81,2,6,0 407 | 0,XY1,89,86,4,2,0 408 | 0,XY1,92,4,9,2,0 409 | 0,XY1,98,8,19,3,0 410 | 0,XY1,103,12,28,6,0 411 | 0,XY1,112,15,42,19,0 412 | 0,XY1,118,18,52,31,0 413 | 0,XY1,125,23,66,45,0 414 | 0,XY1,131,27,79,64,0 415 | 0,XY1,137,32,90,80,0 416 | 0,XY1,144,34,104,97,0 417 | 0,XY1,149,38,111,115,0 418 | 0,XY1,161,41,117,132,0 419 | 0,XY1,168,44,120,147,0 420 | 0,XY1,173,49,120,159,0 421 | 0,XY1,179,57,117,166,2 422 | 0,XY1,5,61,112,173,2 423 | 0,XY1,12,66,105,177,2 424 | 0,XY1,23,71,99,180,2 425 | 0,XY1,30,75,91,180,2 426 | 0,XY1,36,79,83,176,2 427 | 0,XY1,47,83,74,169,2 428 | 0,XY1,53,88,65,159,2 429 | 0,XY1,58,3,55,144,2 430 | 0,XY1,64,7,47,129,2 431 | 0,XY1,70,12,39,112,2 432 | 0,XY1,79,17,31,94,2 433 | 0,XY1,86,22,24,77,2 434 | 0,XY1,91,26,17,60,2 435 | 0,XY1,97,30,13,44,0 436 | 0,XY1,101,34,8,30,0 437 | 0,XY1,106,39,5,16,0 438 | 0,XY1,110,44,2,8,0 439 | 0,XY1,118,48,2,3,0 440 | 0,XY1,124,52,2,2,0 441 | 0,XY1,129,57,4,3,0 442 | 0,XY1,135,60,9,8,0 443 | 0,XY1,142,64,14,21,0 444 | 0,XY1,148,67,21,32,0 445 | 0,XY1,154,70,27,47,0 446 | 0,XY1,160,75,35,64,0 447 | 0,XY1,165,80,42,80,0 448 | 0,XY1,171,84,52,97,0 449 | 0,XY1,177,88,62,115,0 450 | 0,XY1,3,3,73,134,1 451 | 0,XY1,10,6,81,149,2 452 | 0,XY1,16,9,90,163,2 453 | 0,XY1,22,13,98,172,2 454 | 0,XY1,29,17,105,178,2 455 | 0,XY1,34,21,111,180,2 456 | 0,XY1,40,26,114,180,2 457 | 0,XY1,47,30,117,179,2 458 | 0,XY1,53,34,120,176,2 459 | 0,XY1,58,39,121,166,2 460 | 0,XY1,66,44,120,155,2 461 | 0,XY1,71,47,117,134,2 462 | 0,XY1,77,52,112,115,2 463 | 0,XY1,82,56,107,96,2 464 | 0,XY1,90,61,99,79,2 465 | 0,XY1,96,66,90,51,0 466 | 0,XY1,102,70,81,35,0 467 | 0,XY1,108,76,74,20,0 468 | 0,XY1,112,80,64,9,0 469 | 0,XY1,117,88,55,3,0 470 | 0,XY1,121,8,45,2,0 471 | 0,XY1,126,11,35,3,0 472 | 0,XY1,132,15,28,5,0 473 | 0,XY1,137,18,21,17,0 474 | 0,XY1,143,21,15,28,0 475 | 0,XY1,149,26,11,41,0 476 | 0,XY1,155,31,6,59,0 477 | 0,XY1,163,35,3,76,0 478 | 0,XY1,169,40,2,93,0 479 | 0,XY1,175,44,2,111,0 480 | 0,XY1,2,47,4,127,2 481 | 0,XY1,8,51,7,142,2 482 | 0,XY1,13,54,12,156,2 483 | 0,XY1,19,57,17,169,2 484 | 0,XY1,25,60,24,176,2 485 | 0,XY1,31,64,30,180,2 486 | 0,XY1,36,69,38,180,2 487 | 0,XY1,42,74,45,177,2 488 | 0,XY1,48,78,55,172,2 489 | 0,XY1,53,82,62,164,2 490 | 0,XY1,59,86,74,146,2 491 | 0,XY1,64,90,83,132,2 492 | 0,XY1,71,6,91,115,2 493 | 0,XY1,76,10,97,98,2 494 | 0,XY1,82,15,103,81,0 495 | 0,XY1,88,20,108,64,0 496 | 0,XY1,92,25,113,45,0 497 | 0,XY1,99,29,119,32,0 498 | 0,XY1,105,34,120,19,0 499 | 0,XY1,111,38,120,10,0 500 | 0,XY1,116,43,117,4,0 501 | 0,XY1,120,47,112,2,0 502 | 0,XY1,124,51,105,2,0 503 | 0,XY1,129,55,95,4,0 504 | 0,XY1,136,58,85,8,0 505 | 0,XY1,143,61,74,13,0 506 | 0,XY1,149,64,62,24,0 507 | 0,XY1,154,67,52,37,0 508 | 0,XY1,160,73,40,52,1 509 | 0,XY1,165,77,30,68,2 510 | 0,XY1,171,82,20,86,2 511 | 0,XY1,177,86,13,105,2 512 | 0,XY1,3,90,7,122,2 513 | 0,XY1,9,5,3,141,2 514 | 0,XY1,15,10,2,155,2 515 | 0,XY1,21,13,2,166,2 516 | 0,XY1,26,18,5,174,2 517 | 0,XY1,32,21,10,179,2 518 | 0,XY1,39,24,17,180,2 519 | 0,XY1,44,28,29,178,2 520 | 0,XY1,52,33,39,171,2 521 | 0,XY1,57,37,50,166,2 522 | 0,XY1,63,42,61,158,0 523 | 0,XY1,70,46,73,150,0 524 | 0,XY1,75,49,87,137,0 525 | 0,XY1,81,54,97,120,0 526 | 0,XY1,91,62,107,101,0 527 | 0,XY1,98,67,114,80,0 528 | 0,XY1,105,72,118,63,0 529 | 0,XY1,110,76,120,47,0 530 | 0,XY1,116,82,120,32,0 531 | 0,XY1,121,86,117,18,0 532 | 0,XY1,127,3,112,9,0 533 | 0,XY1,133,7,103,3,0 534 | 0,XY1,143,11,93,1,0 535 | 0,XY1,148,14,82,3,0 536 | 0,XY1,154,18,70,5,0 537 | 0,XY1,160,23,59,11,0 538 | 0,XY1,168,27,47,17,2 539 | 0,XY1,174,32,35,29,2 540 | 0,XY1,180,36,24,45,2 541 | 0,XY1,8,41,15,62,2 542 | 0,XY1,14,46,9,78,2 543 | 0,XY1,19,50,4,95,2 544 | 0,XY1,25,56,2,112,2 545 | 0,XY1,31,62,2,132,2 546 | 0,XY1,37,66,5,146,2 547 | 0,XY1,43,69,11,160,2 548 | 0,XY1,49,74,19,169,2 549 | 0,XY1,56,78,29,178,2 550 | 0,XY1,62,83,40,181,2 551 | 0,XY1,67,87,52,179,2 552 | 0,XY1,74,2,63,174,2 553 | 0,XY1,80,6,76,163,0 554 | 0,XY1,85,11,87,147,0 555 | 0,XY1,91,16,99,136,0 556 | 0,XY1,95,21,107,124,0 557 | 0,XY1,101,27,114,112,0 558 | 0,XY1,107,31,118,100,0 559 | 0,XY1,114,36,120,81,0 560 | 0,XY1,119,41,120,65,0 561 | 0,XY1,125,45,116,48,0 562 | 0,XY1,130,48,111,32,0 563 | 0,XY1,136,53,103,20,0 564 | 0,XY1,143,57,95,7,0 565 | 0,XY1,147,61,85,2,0 566 | 0,XY1,153,66,73,1,0 567 | 0,XY1,157,70,58,2,0 568 | 0,XY1,167,74,46,5,2 569 | 0,XY1,172,79,35,19,2 570 | 0,XY1,178,84,23,31,2 571 | 0,XY1,5,88,15,44,2 572 | 0,XY1,10,4,8,65,2 573 | 0,XY1,17,8,4,81,2 574 | 0,XY1,23,13,2,101,2 575 | 0,XY1,28,17,2,118,2 576 | 0,XY1,34,21,5,137,2 577 | 0,XY1,40,26,11,151,2 578 | 0,XY1,45,29,18,163,2 579 | 0,XY1,51,33,27,173,2 580 | 0,XY1,58,36,40,179,2 581 | 0,XY1,64,39,52,179,2 582 | 0,XY1,70,44,63,175,2 583 | 0,XY1,76,47,76,158,2 584 | 0,XY1,82,52,88,145,0 585 | 0,XY1,88,56,98,128,0 586 | 0,XY1,95,61,107,108,0 587 | 0,XY1,102,65,114,93,0 588 | 0,XY1,108,71,118,82,0 589 | 0,XY1,115,76,120,57,0 590 | 0,XY1,122,80,120,42,0 591 | 0,XY1,128,84,117,28,0 592 | 0,XY1,134,88,112,16,0 593 | 0,XY1,141,4,103,8,0 594 | 0,XY1,146,8,93,3,0 595 | 0,XY1,152,11,83,2,0 596 | 0,XY1,157,15,70,3,0 597 | 0,XY1,163,18,58,7,0 598 | 0,XY1,172,27,47,12,1 599 | 0,XY1,178,31,34,21,2 600 | 0,XY1,6,35,23,37,2 601 | 0,XY1,12,40,15,51,2 602 | 0,XY1,17,44,8,68,2 603 | 0,XY1,23,47,4,86,2 604 | 0,XY1,28,52,2,102,2 605 | 0,XY1,34,60,2,119,2 606 | 0,XY1,40,64,5,138,2 607 | 0,XY1,45,69,10,151,2 608 | 0,XY1,52,73,19,163,2 609 | 0,XY1,62,77,29,172,2 610 | 0,XY1,68,81,40,178,2 611 | 0,XY1,73,85,51,180,2 612 | 0,XY1,79,89,64,179,2 613 | 0,XY1,86,5,75,173,0 614 | 0,XY1,91,9,86,164,0 615 | 0,XY1,96,14,98,152,0 616 | 0,XY1,101,18,107,139,0 617 | 0,XY1,109,23,114,123,0 618 | 0,XY1,114,27,118,105,0 619 | 0,XY1,120,31,120,89,0 620 | 0,XY1,127,36,120,77,0 621 | 0,XY1,133,40,117,65,0 622 | 0,XY1,139,45,112,50,0 623 | 0,XY1,147,48,103,40,0 624 | 0,XY1,157,52,93,24,0 625 | 0,XY1,164,56,82,13,0 626 | 0,XY1,168,59,71,6,0 627 | 0,XY1,172,63,58,2,0 628 | 0,XY1,177,70,46,2,2 629 | 0,XY1,5,74,34,3,2 630 | 0,XY1,10,78,24,8,2 631 | 0,XY1,16,83,15,13,2 632 | 0,XY1,23,87,8,24,2 633 | 0,XY1,29,2,4,39,2 634 | 0,XY1,35,7,2,54,2 635 | 0,XY1,40,11,2,74,2 636 | 0,XY1,46,16,5,93,2 637 | 0,XY1,52,20,10,113,2 638 | 0,XY1,59,24,19,132,2 639 | 0,XY1,65,28,29,147,2 640 | 0,XY1,71,33,40,159,2 641 | 0,XY1,76,37,52,169,2 642 | 0,XY1,82,41,64,177,2 643 | 0,XY1,88,44,75,180,0 644 | 0,XY1,92,47,86,180,0 645 | 0,XY1,98,53,98,176,0 646 | 0,XY1,105,57,106,163,0 647 | 0,XY1,110,62,114,150,0 648 | 0,XY1,115,66,118,131,0 649 | 0,XY1,121,71,120,114,0 650 | 0,XY1,127,75,120,90,0 651 | 0,XY1,135,79,117,72,0 652 | 0,XY1,142,84,111,56,0 653 | 0,XY1,147,89,103,45,0 654 | 0,XY1,153,4,95,30,0 655 | 0,XY1,159,9,82,21,0 656 | 0,XY1,164,13,70,7,0 657 | 0,XY1,170,16,60,3,2 658 | 0,XY1,175,20,48,2,2 659 | 0,XY1,180,23,37,3,2 660 | 0,XY1,5,27,27,7,2 661 | 0,XY1,12,31,15,12,2 662 | 0,XY1,17,35,8,23,2 663 | 0,XY1,24,40,4,36,2 664 | 0,XY1,30,44,2,51,2 665 | 0,XY1,35,47,2,67,2 666 | 0,XY1,42,52,5,87,2 667 | 0,XY1,48,57,11,104,2 668 | 0,XY1,53,61,18,125,2 669 | 0,XY1,64,66,29,140,2 670 | 0,XY1,70,71,40,154,2 671 | 0,XY1,76,76,52,165,0 672 | 0,XY1,83,80,64,173,0 673 | 0,XY1,89,83,76,179,0 674 | 0,XY1,93,87,88,180,0 675 | 0,XY1,99,90,98,177,0 676 | 0,XY1,105,4,107,171,0 677 | 0,XY1,110,7,113,161,0 678 | 0,XY1,116,9,118,149,0 679 | 0,XY1,121,12,120,134,0 680 | 0,XY1,127,15,120,118,0 681 | 0,XY1,134,18,117,101,0 682 | 0,XY1,141,24,111,81,0 683 | 0,XY1,147,29,103,64,0 684 | 0,XY1,156,33,94,48,0 685 | 0,XY1,163,39,83,33,2 686 | 0,XY1,170,44,71,24,2 687 | 0,XY1,175,47,59,16,2 688 | 0,XY1,3,52,47,10,2 689 | 0,XY1,8,55,35,6,2 690 | 0,XY1,12,58,24,2,2 691 | 0,XY1,16,61,15,2,2 692 | 0,XY1,21,67,8,4,2 693 | 0,XY1,25,72,4,10,2 694 | 0,XY1,29,76,2,30,2 695 | 0,XY1,35,80,2,44,2 696 | 0,XY1,40,84,6,59,2 697 | 0,XY1,44,87,11,77,2 698 | 0,XY1,48,3,19,94,2 699 | 0,XY1,52,7,29,111,2 700 | 0,XY1,56,11,39,128,2 701 | 0,XY1,60,16,51,143,0 702 | 0,XY1,64,21,63,157,0 703 | 0,XY1,69,26,76,170,0 704 | 0,XY1,73,30,87,176,0 705 | 0,XY1,78,35,98,180,0 706 | 0,XY1,82,43,106,180,0 707 | 0,XY1,87,47,113,177,0 708 | 0,XY1,91,52,118,170,0 709 | 0,XY1,96,56,120,161,0 710 | 0,XY1,101,60,120,144,0 711 | 0,XY1,105,63,117,129,0 712 | 0,XY1,110,66,111,113,0 713 | 0,XY1,114,73,104,95,0 714 | 0,XY1,118,77,94,78,0 715 | 0,XY1,123,81,82,62,0 716 | 0,XY1,127,85,71,45,0 717 | 0,XY1,132,90,60,27,2 718 | 0,XY1,136,5,48,17,2 719 | 0,XY1,141,8,37,7,2 720 | 0,XY1,146,11,24,3,2 721 | 0,XY1,151,14,16,2,2 722 | 0,XY1,155,17,8,2,2 723 | 0,XY1,159,20,4,5,2 724 | 0,XY1,164,24,2,17,2 725 | 0,XY1,168,28,2,27,2 726 | 0,XY1,173,33,5,41,2 727 | 0,XY1,178,39,11,56,2 728 | 0,XY1,6,43,19,74,2 729 | 0,XY1,15,46,29,106,2 730 | 0,XY1,21,50,39,126,2 731 | 0,XY1,26,55,52,142,2 732 | 0,XY1,35,60,64,155,2 733 | 0,XY1,41,64,76,166,0 734 | 0,XY1,48,69,87,174,0 735 | 0,XY1,53,73,98,179,0 736 | 0,XY1,59,77,106,180,0 737 | 0,XY1,64,83,113,176,0 738 | 0,XY1,71,87,118,170,0 739 | 0,XY1,78,3,120,148,0 740 | 0,XY1,85,7,120,134,0 741 | 0,XY1,91,11,116,108,0 742 | 0,XY1,98,17,111,89,0 743 | 0,XY1,105,22,103,72,0 744 | 0,XY1,112,25,95,54,0 745 | 0,XY1,118,28,83,40,0 746 | 0,XY1,121,34,72,26,0 747 | 0,XY1,126,39,58,15,2 748 | 0,XY1,131,43,47,7,2 749 | 0,XY1,139,47,35,2,2 750 | 0,XY1,146,53,24,2,2 751 | 0,XY1,154,57,15,4,2 752 | 0,XY1,164,60,8,7,2 753 | 0,XY1,169,63,4,20,2 754 | 0,XY1,173,66,2,32,2 755 | 0,XY1,178,70,2,46,2 756 | 0,XY1,4,74,5,68,2 757 | 0,XY1,10,79,11,86,2 758 | 0,XY1,21,83,18,103,2 759 | 0,XY1,27,89,27,119,2 760 | 0,XY1,31,4,38,137,2 761 | 0,XY1,35,9,49,150,2 762 | 0,XY1,39,13,61,163,0 763 | 0,XY1,43,18,76,173,0 764 | 0,XY1,50,26,87,179,0 765 | 0,XY1,56,33,98,180,0 766 | 0,XY1,62,37,107,178,0 767 | 0,XY1,71,42,114,172,0 768 | 0,XY1,78,46,119,160,0 769 | 0,XY1,85,50,120,143,0 770 | 0,XY1,92,54,120,119,0 771 | 0,XY1,96,59,117,103,0 772 | 0,XY1,101,63,111,86,0 773 | 0,XY1,106,68,103,69,0 774 | 0,XY1,111,72,93,52,0 775 | 0,XY1,117,77,82,37,0 776 | 0,XY1,123,81,71,24,0 777 | 0,XY1,127,84,60,12,2 778 | 0,XY1,131,87,49,5,2 779 | 0,XY1,135,2,37,2,2 780 | 0,XY1,140,6,27,2,2 781 | 0,XY1,144,10,18,5,2 782 | 0,XY1,148,13,11,10,2 783 | 0,XY1,152,17,5,16,2 784 | 0,XY1,157,24,2,23,2 785 | 0,XY1,162,28,2,31,2 786 | 0,XY1,168,33,3,42,2 787 | 0,XY1,172,37,8,58,2 788 | 0,XY1,176,41,13,87,2 789 | 0,XY1,180,45,22,103,2 790 | 0,XY1,4,49,31,120,2 791 | 0,XY1,9,53,42,137,2 792 | 0,XY1,13,58,54,150,0 793 | 0,XY1,18,63,64,163,0 794 | 0,XY1,24,67,75,172,0 795 | 0,XY1,27,71,87,178,0 796 | 0,XY1,32,75,98,180,0 797 | 0,XY1,37,79,107,179,0 798 | 0,XY1,42,84,113,173,0 799 | 0,XY1,47,89,118,165,0 800 | 0,XY1,53,4,120,154,0 801 | 0,XY1,63,9,120,140,0 802 | 0,XY1,68,18,117,118,0 803 | 0,XY1,75,22,111,101,0 804 | 0,XY1,81,27,103,85,0 805 | 0,XY1,86,31,94,66,0 806 | 0,XY1,92,37,84,49,0 807 | 0,XY1,98,39,72,32,2 808 | 0,XY1,104,43,58,11,2 809 | 0,XY1,110,46,47,5,2 810 | 0,XY1,117,48,35,2,2 811 | 0,XY1,123,53,23,2,2 812 | 0,XY1,129,56,15,4,2 813 | 0,XY1,135,59,9,9,2 814 | 0,XY1,142,62,4,19,2 815 | 0,XY1,147,65,2,30,2 816 | 0,XY1,153,70,2,43,2 817 | 0,XY1,159,75,5,53,2 818 | 0,XY1,164,83,10,68,2 819 | 0,XY1,172,88,17,94,2 820 | 0,XY1,177,4,29,113,2 821 | 0,XY1,5,8,39,130,2 822 | 0,XY1,10,12,52,144,0 823 | 0,XY1,20,17,64,158,0 824 | 0,XY1,26,21,76,168,0 825 | 0,XY1,32,25,87,175,0 826 | 0,XY1,38,30,98,180,0 827 | 0,XY1,44,34,107,180,0 828 | 0,XY1,50,38,114,175,0 829 | 0,XY1,54,43,118,168,0 830 | 0,XY1,59,46,120,158,0 831 | 0,XY1,64,54,120,144,0 832 | 0,XY1,70,59,117,130,0 833 | 0,XY1,76,64,112,107,0 834 | 0,XY1,81,68,103,91,0 835 | 0,XY1,91,72,94,74,0 836 | 0,XY1,96,76,83,42,2 837 | 0,XY1,102,80,72,27,2 838 | 0,XY1,108,85,61,17,2 839 | 0,XY1,114,90,50,7,2 840 | 0,XY1,121,4,38,3,2 841 | 0,XY1,127,7,27,1,2 842 | 0,XY1,134,11,19,3,2 843 | 0,XY1,140,14,11,6,2 844 | 0,XY1,148,18,5,19,2 845 | 0,XY1,154,23,2,32,2 846 | 0,XY1,160,27,2,46,2 847 | 0,XY1,167,32,3,57,2 848 | 0,XY1,173,37,7,72,2 849 | 0,XY1,177,41,13,100,2 850 | 0,XY1,3,46,20,117,2 851 | 0,XY1,7,49,29,133,0 852 | 0,XY1,11,53,40,148,0 853 | 0,XY1,16,58,52,161,0 854 | 0,XY1,20,63,64,171,0 855 | 0,XY1,25,67,76,177,0 856 | 0,XY1,29,72,88,180,0 857 | 0,XY1,34,76,98,180,0 858 | 0,XY1,38,80,107,176,0 859 | 0,XY1,42,84,114,169,0 860 | 0,XY1,46,89,118,158,0 861 | 0,XY1,52,4,120,144,0 862 | 0,XY1,56,9,120,130,0 863 | 0,XY1,61,13,117,113,0 864 | 0,XY1,65,18,111,94,2 865 | 0,XY1,70,22,103,77,2 866 | 0,XY1,74,28,95,60,2 867 | 0,XY1,78,33,84,44,2 868 | 0,XY1,82,38,73,30,2 869 | 0,XY1,86,43,61,17,2 870 | 0,XY1,90,46,48,9,2 871 | 0,XY1,93,50,37,3,2 872 | 0,XY1,97,53,26,2,2 873 | 0,XY1,101,57,18,2,2 874 | 0,XY1,107,60,11,6,2 875 | 0,XY1,111,64,5,12,2 876 | 0,XY1,117,68,2,27,2 877 | 0,XY1,121,72,2,41,2 878 | 0,XY1,127,76,3,57,2 879 | 0,XY1,130,81,7,74,2 880 | 0,XY1,135,85,13,91,0 881 | 0,XY1,139,89,21,107,0 882 | 0,XY1,143,5,31,122,0 883 | 0,XY1,147,9,42,146,0 884 | 0,XY1,151,14,57,159,0 885 | 0,XY1,155,18,67,170,0 886 | 0,XY1,159,22,83,178,0 887 | 0,XY1,163,26,93,180,0 888 | 0,XY1,168,31,105,179,0 889 | 0,XY1,172,36,112,172,0 890 | 0,XY1,177,40,118,163,0 891 | 0,XY1,2,44,120,151,0 892 | 0,XY1,6,47,120,138,0 893 | 0,XY1,10,52,117,122,0 894 | 0,XY1,14,56,112,101,0 895 | 0,XY1,19,61,105,84,0 896 | 0,XY1,23,67,94,67,2 897 | 0,XY1,27,71,83,50,2 898 | 0,XY1,33,75,72,36,2 899 | 0,XY1,38,79,59,23,2 900 | 0,XY1,43,85,47,11,2 901 | 0,XY1,47,90,36,4,2 902 | 0,XY1,50,7,26,2,2 903 | 0,XY1,55,10,15,2,2 904 | 0,XY1,59,12,9,4,2 905 | 0,XY1,63,16,4,8,2 906 | 0,XY1,69,19,2,14,2 907 | 0,XY1,74,23,2,30,2 908 | 0,XY1,78,27,5,43,2 909 | 0,XY1,83,31,13,62,2 910 | 0,XY1,88,34,20,80,2 911 | 0,XY1,91,38,36,98,0 912 | 0,XY1,96,43,52,116,0 913 | 0,XY1,100,46,64,128,0 914 | 0,XY1,105,50,78,139,0 915 | 0,XY1,110,57,88,151,0 916 | 0,XY1,114,62,102,160,0 917 | 0,XY1,119,66,114,170,0 918 | 0,XY1,124,71,119,177,0 919 | 0,XY1,129,75,120,180,0 920 | 0,XY1,133,79,120,180,0 921 | 0,XY1,139,85,117,176,0 922 | 0,XY1,146,3,110,169,0 923 | 0,XY1,151,9,103,156,0 924 | 0,XY1,157,15,85,143,0 925 | 0,XY1,163,20,72,128,2 926 | 0,XY1,169,24,61,111,2 927 | 0,XY1,175,28,47,92,2 928 | 0,XY1,2,33,36,75,2 929 | 0,XY1,8,38,18,55,2 930 | 0,XY1,14,42,11,38,2 931 | 0,XY1,20,46,6,24,2 932 | 0,XY1,26,49,2,14,2 933 | 0,XY1,32,58,2,7,2 934 | 0,XY1,38,61,5,2,2 935 | 0,XY1,44,64,13,2,2 936 | 0,XY1,50,66,23,4,2 937 | 0,XY1,56,70,35,9,2 938 | 0,XY1,62,73,49,25,2 939 | 0,XY1,68,76,61,39,0 940 | 0,XY1,74,80,73,63,0 941 | 0,XY1,80,83,85,87,0 942 | 0,XY1,86,88,99,106,0 943 | 0,XY1,91,3,108,123,0 944 | 0,XY1,100,8,114,139,0 945 | 0,XY1,110,12,119,153,0 946 | 0,XY1,115,16,120,161,0 947 | 0,XY1,121,21,120,170,0 948 | 0,XY1,126,25,117,177,0 949 | 0,XY1,133,29,112,180,0 950 | 0,XY1,139,33,104,178,0 951 | 0,XY1,145,39,95,173,0 952 | 0,XY1,151,43,85,163,0 953 | 0,XY1,157,46,74,152,0 954 | 0,XY1,163,51,62,138,0 955 | 0,XY1,169,55,52,122,2 956 | 0,XY1,175,60,37,105,2 957 | 0,XY1,2,65,27,88,2 958 | 0,XY1,8,69,18,70,2 959 | 0,XY1,14,74,10,53,2 960 | 0,XY1,20,78,5,39,2 961 | 0,XY1,26,82,2,25,2 962 | 0,XY1,32,87,2,14,2 963 | 0,XY1,38,2,3,5,2 964 | 0,XY1,44,7,7,2,2 965 | 0,XY1,50,11,13,2,2 966 | 0,XY1,55,15,20,5,2 967 | 0,XY1,61,18,29,10,2 968 | 0,XY1,66,20,39,26,2 969 | 0,XY1,72,25,51,48,2 970 | 0,XY1,77,29,61,65,0 971 | 0,XY1,85,33,73,81,0 972 | 0,XY1,90,36,84,98,0 973 | 0,XY1,95,40,95,116,0 974 | 0,XY1,101,43,104,133,0 975 | 0,XY1,109,46,112,158,0 976 | 0,XY1,115,51,118,170,0 977 | 0,XY1,120,56,120,176,0 978 | 0,XY1,126,60,120,179,0 979 | 0,XY1,133,65,117,180,0 980 | 0,XY1,139,69,113,177,0 981 | 0,XY1,145,73,105,170,0 982 | 0,XY1,151,77,96,160,0 983 | 0,XY1,157,82,84,145,0 984 | 0,XY1,163,86,72,131,0 985 | 0,XY1,168,90,61,114,2 986 | 0,XY1,175,8,50,94,2 987 | 0,XY1,2,12,38,68,2 988 | 0,XY1,8,17,26,44,2 989 | 0,XY1,14,21,17,29,2 990 | 0,XY1,20,26,10,17,2 991 | 0,XY1,26,31,5,5,2 992 | 0,XY1,32,35,2,2,2 993 | 0,XY1,38,40,2,2,2 994 | 0,XY1,44,44,3,5,2 995 | 0,XY1,50,47,8,8,2 996 | 0,XY1,57,51,13,13,2 997 | 0,XY1,63,55,21,32,2 998 | 0,XY1,69,58,33,56,2 999 | 0,XY1,75,62,44,72,0 1000 | 0,XY1,80,65,55,93,0 1001 | 0,XY1,86,69,67,112,0 1002 | 0,XY1,92,74,78,130,0 1003 | 0,XY1,98,78,89,145,0 1004 | 0,XY1,103,83,99,159,0 1005 | 0,XY1,110,87,107,170,0 1006 | 0,XY1,115,90,114,177,0 1007 | 0,XY1,121,5,118,180,0 1008 | 0,XY1,127,9,120,180,0 1009 | 0,XY1,133,16,120,178,0 1010 | 0,XY1,139,21,118,167,0 1011 | 0,XY1,145,25,113,155,0 1012 | 0,XY1,151,30,106,139,0 1013 | 0,XY1,156,35,98,124,0 1014 | 0,XY1,162,39,87,106,2 1015 | 0,XY1,172,43,77,87,2 1016 | 0,XY1,177,48,65,70,2 1017 | 0,XY1,4,52,55,53,2 1018 | 0,XY1,10,56,44,38,2 1019 | 0,XY1,16,61,33,25,2 1020 | 0,XY1,22,66,23,14,2 1021 | 0,XY1,27,70,15,7,2 1022 | 0,XY1,33,74,8,2,2 1023 | 0,XY1,40,78,3,2,2 1024 | 0,XY1,46,83,2,4,2 1025 | 0,XY1,52,87,2,10,2 1026 | 0,XY1,57,4,5,24,2 1027 | 0,XY1,63,9,10,41,2 1028 | 0,XY1,70,12,17,59,2 1029 | 0,XY1,76,15,26,77,2 1030 | 0,XY1,82,18,36,93,0 1031 | 0,XY1,88,25,47,111,0 1032 | 0,XY1,93,30,58,127,0 1033 | 0,XY1,99,34,73,142,0 1034 | 0,XY1,105,38,84,156,0 1035 | 0,XY1,111,43,96,168,0 1036 | 0,XY1,118,46,106,175,0 1037 | 0,XY1,124,50,113,180,0 1038 | 0,XY1,132,54,117,180,0 1039 | 0,XY1,138,56,120,177,0 1040 | 0,XY1,145,63,120,172,0 1041 | 0,XY1,151,67,117,165,0 1042 | 0,XY1,157,71,112,153,0 1043 | 0,XY1,163,76,105,130,0 1044 | 0,XY1,169,80,95,114,0 1045 | 0,XY1,175,84,85,96,2 1046 | 0,XY1,5,89,72,80,2 1047 | 0,XY1,12,4,61,63,2 1048 | 0,XY1,18,8,49,44,2 1049 | 0,XY1,25,12,37,30,2 1050 | 0,XY1,31,17,26,16,2 1051 | 0,XY1,36,21,17,7,2 1052 | 0,XY1,42,26,9,3,2 1053 | 0,XY1,48,31,5,1,2 1054 | 0,XY1,55,37,2,2,2 1055 | 0,XY1,62,41,2,6,2 1056 | 0,XY1,72,46,4,12,2 1057 | 0,XY1,79,49,9,23,2 1058 | 0,XY1,89,54,17,35,2 1059 | 0,XY1,93,58,26,50,0 1060 | 0,XY1,100,62,36,66,0 1061 | 0,XY1,106,67,47,84,0 1062 | 0,XY1,112,72,58,103,0 1063 | 0,XY1,118,77,69,120,0 1064 | 0,XY1,124,81,80,137,0 1065 | 0,XY1,130,85,91,151,0 1066 | 0,XY1,136,90,100,163,0 1067 | 0,XY1,142,5,108,172,0 1068 | 0,XY1,148,10,114,179,0 1069 | 0,XY1,154,14,119,180,0 1070 | 0,XY1,160,17,120,178,0 1071 | 0,XY1,167,21,119,172,0 1072 | 0,XY1,173,24,116,163,0 1073 | 0,XY1,178,31,110,153,0 1074 | 0,XY1,5,35,102,143,2 1075 | 0,XY1,11,40,93,130,2 1076 | 0,XY1,17,44,83,118,2 1077 | 0,XY1,22,47,72,98,2 1078 | 0,XY1,28,51,61,82,2 1079 | 0,XY1,34,56,49,65,2 1080 | 0,XY1,39,61,37,48,2 1081 | 0,XY1,45,65,26,34,2 1082 | 0,XY1,53,70,17,21,2 1083 | 0,XY1,59,75,10,12,2 1084 | 0,XY1,65,80,5,4,2 1085 | 0,XY1,71,84,2,2,2 1086 | 0,XY1,75,89,2,2,2 1087 | 0,XY1,80,6,3,4,2 1088 | 0,XY1,84,10,8,7,2 1089 | 0,XY1,89,13,14,16,0 1090 | 0,XY1,92,17,23,30,0 1091 | 0,XY1,97,20,36,54,0 1092 | 0,XY1,101,28,47,71,0 1093 | 0,XY1,106,33,58,100,0 1094 | 0,XY1,111,37,73,116,0 1095 | 0,XY1,115,41,85,134,0 1096 | 0,XY1,119,45,96,149,0 1097 | 0,XY1,123,48,105,170,0 1098 | 0,XY1,127,53,112,177,0 1099 | 0,XY1,132,58,117,180,0 1100 | 0,XY1,136,62,120,180,0 1101 | 0,XY1,139,67,120,176,0 1102 | 0,XY1,145,70,118,169,0 1103 | 0,XY1,150,73,112,157,0 1104 | 0,XY1,154,77,105,137,0 1105 | 0,XY1,160,82,95,125,2 1106 | 0,XY1,165,86,85,111,2 1107 | 0,XY1,169,90,72,98,2 1108 | 0,XY1,177,5,61,85,2 1109 | 0,XY1,4,9,49,67,2 1110 | 0,XY1,10,13,37,48,2 1111 | 0,XY1,17,18,26,33,2 1112 | 0,XY1,22,22,17,21,2 1113 | 0,XY1,28,27,10,10,2 1114 | 0,XY1,34,32,4,4,2 1115 | 0,XY1,40,36,2,2,2 1116 | 0,XY1,45,40,2,2,2 1117 | 0,XY1,51,45,5,4,2 1118 | 0,XY1,58,48,9,10,2 1119 | 0,XY1,64,53,17,17,2 1120 | 0,XY1,70,57,26,28,0 1121 | 0,XY1,80,60,37,41,0 1122 | 0,XY1,88,64,49,57,0 1123 | 0,XY1,92,71,61,75,0 1124 | 0,XY1,97,75,73,93,0 1125 | 0,XY1,103,80,85,111,0 1126 | 0,XY1,112,84,96,128,0 1127 | 0,XY1,118,89,105,145,0 1128 | 0,XY1,124,4,113,158,0 1129 | 0,XY1,129,10,118,169,0 1130 | 0,XY1,136,15,120,176,0 1131 | 0,XY1,143,19,120,180,0 1132 | 0,XY1,149,23,117,180,0 1133 | 0,XY1,156,28,112,176,0 1134 | 0,XY1,161,32,105,169,0 1135 | 0,XY1,168,34,95,158,2 1136 | 0,XY1,175,38,84,131,2 1137 | 0,XY1,2,45,72,111,2 1138 | 0,XY1,8,49,61,95,2 1139 | 0,XY1,13,53,49,82,2 1140 | 0,XY1,19,58,37,69,2 1141 | 0,XY1,30,63,26,43,2 1142 | 0,XY1,36,67,16,29,2 1143 | 0,XY1,42,72,9,18,2 1144 | 0,XY1,48,76,4,8,2 1145 | 0,XY1,53,81,2,3,2 1146 | 0,XY1,59,86,2,2,2 1147 | 0,XY1,65,90,5,3,2 1148 | 0,XY1,70,6,10,7,2 1149 | 0,XY1,76,10,17,11,2 1150 | 0,XY1,80,13,26,17,0 1151 | 0,XY1,87,17,37,36,0 1152 | 0,XY1,91,20,49,53,0 1153 | 0,XY1,97,23,61,70,0 1154 | 0,XY1,102,27,74,87,0 1155 | 0,XY1,106,32,85,115,0 1156 | 0,XY1,112,36,96,132,0 1157 | 0,XY1,118,40,106,149,0 1158 | 0,XY1,124,44,113,163,0 1159 | 0,XY1,130,48,118,172,0 1160 | 0,XY1,136,52,120,178,0 1161 | 0,XY1,142,57,120,180,0 1162 | 0,XY1,147,61,117,179,0 1163 | 0,XY1,153,66,112,173,0 1164 | 0,XY1,159,70,105,164,2 1165 | 0,XY1,165,74,95,149,2 1166 | 0,XY1,171,78,84,118,2 1167 | 0,XY1,177,83,72,101,2 1168 | 0,XY1,3,86,61,85,2 1169 | 0,XY1,9,90,48,70,2 1170 | 0,XY1,15,3,37,57,2 1171 | 0,XY1,21,10,26,36,2 1172 | 0,XY1,26,15,16,23,2 1173 | 0,XY1,32,19,9,13,2 1174 | 0,XY1,37,24,4,6,2 1175 | 0,XY1,43,28,2,2,2 1176 | 0,XY1,49,33,2,2,2 1177 | 0,XY1,55,38,5,4,2 1178 | 0,XY1,61,43,10,9,2 1179 | 0,XY1,66,46,17,18,0 1180 | 0,XY1,72,51,27,29,0 1181 | 0,XY1,77,54,37,43,0 1182 | 0,XY1,84,57,50,59,0 1183 | 0,XY1,90,60,61,77,0 1184 | 0,XY1,94,65,74,95,0 1185 | 0,XY1,98,69,85,115,0 1186 | 0,XY1,102,74,96,133,0 1187 | 0,XY1,107,78,106,151,0 1188 | 0,XY1,111,82,112,163,0 1189 | 0,XY1,117,87,117,172,0 1190 | 0,XY1,121,2,120,178,0 1191 | 0,XY1,126,9,120,180,0 1192 | 0,XY1,130,13,118,179,0 1193 | 0,XY1,135,18,114,173,0 1194 | 0,XY1,140,22,106,165,2 1195 | 0,XY1,144,26,95,154,2 1196 | 0,XY1,148,31,84,140,2 1197 | 0,XY1,153,36,72,125,2 1198 | 0,XY1,160,40,61,103,2 1199 | 0,XY1,166,44,49,82,2 1200 | 0,XY1,172,47,37,65,2 1201 | 0,XY1,178,50,26,48,2 1202 | 0,XY1,5,54,17,37,2 1203 | 0,XY1,10,60,9,28,2 1204 | 0,XY1,17,65,4,18,2 1205 | 0,XY1,22,69,2,11,2 1206 | 0,XY1,28,73,2,5,2 1207 | 0,XY1,37,79,4,2,2 1208 | 0,XY1,44,83,10,2,2 1209 | 0,XY1,52,88,18,4,0 1210 | 0,XY1,58,3,27,10,0 1211 | 0,XY1,64,7,37,17,0 1212 | 0,XY1,69,11,48,29,0 1213 | 0,XY1,75,14,60,46,0 1214 | 0,XY1,81,17,71,62,0 1215 | 0,XY1,85,22,89,80,0 1216 | 0,XY1,90,27,100,97,0 1217 | 0,XY1,94,31,110,114,0 1218 | 0,XY1,98,35,115,138,0 1219 | 0,XY1,106,39,120,156,0 1220 | 0,XY1,114,44,120,167,0 1221 | 0,XY1,120,47,119,175,0 1222 | 0,XY1,124,51,114,180,0 1223 | 0,XY1,129,56,107,180,0 1224 | 0,XY1,133,60,99,178,1 1225 | 0,XY1,142,65,89,171,2 1226 | 0,XY1,148,69,75,159,2 1227 | 0,XY1,159,73,64,147,2 1228 | 0,XY1,165,79,50,132,2 1229 | 0,XY1,170,84,38,115,2 1230 | 0,XY1,177,89,26,98,2 1231 | 0,XY1,4,7,15,81,2 1232 | 0,XY1,8,11,8,65,2 1233 | 0,XY1,13,14,4,44,2 1234 | 0,XY1,18,18,2,30,2 1235 | 0,XY1,22,21,3,19,2 1236 | 0,XY1,31,25,7,12,2 1237 | 0,XY1,36,30,13,7,2 1238 | 0,XY1,42,35,21,3,2 1239 | 0,XY1,47,40,31,1,2 1240 | 0,XY1,54,44,44,2,0 1241 | 0,XY1,61,47,58,5,0 1242 | 0,XY1,67,52,69,19,0 1243 | 0,XY1,73,56,80,30,0 1244 | 0,XY1,79,59,91,44,0 1245 | 0,XY1,85,62,100,60,0 1246 | 0,XY1,90,69,110,77,0 1247 | 0,XY1,96,73,115,94,0 1248 | 0,XY1,101,77,119,114,0 1249 | 0,XY1,107,81,120,131,0 1250 | 0,XY1,113,86,120,145,0 1251 | 0,XY1,120,1,116,161,0 1252 | 0,XY1,128,6,111,172,0 1253 | 0,XY1,137,10,104,180,0 1254 | 0,XY1,143,15,95,179,0 1255 | 0,XY1,146,19,85,175,2 1256 | 0,XY1,151,23,73,168,2 1257 | 0,XY1,163,27,61,157,2 1258 | 0,XY1,169,32,50,144,2 1259 | 0,XY1,175,37,38,129,2 1260 | 0,XY1,3,41,27,113,2 1261 | 0,XY1,9,46,17,95,2 1262 | 0,XY1,16,52,10,78,2 1263 | 0,XY1,22,56,4,58,2 1264 | 0,XY1,27,61,2,42,2 1265 | 0,XY1,33,64,2,28,2 1266 | 0,XY1,39,68,5,17,2 1267 | 0,XY1,45,74,16,9,2 1268 | 0,XY1,51,78,25,3,2 1269 | 0,XY1,57,83,35,2,2 1270 | 0,XY1,63,87,49,3,2 1271 | 0,XY1,68,3,61,5,0 1272 | 0,XY1,74,7,83,18,0 1273 | 0,XY1,80,10,93,29,0 1274 | 0,XY1,85,13,104,43,0 1275 | 0,XY1,89,17,111,59,0 1276 | 0,XY1,94,20,116,76,0 1277 | 0,XY1,98,26,120,93,0 1278 | 0,XY1,106,30,120,110,0 1279 | 0,XY1,112,35,119,127,0 1280 | 0,XY1,118,39,115,142,0 1281 | 0,XY1,124,43,108,155,0 1282 | 0,XY1,131,47,99,167,0 1283 | 0,XY1,137,51,90,176,0 1284 | 0,XY1,143,56,79,180,0 1285 | 0,XY1,147,60,68,180,0 1286 | 0,XY1,152,64,57,177,2 1287 | 0,XY1,157,69,42,171,2 1288 | 0,XY1,166,73,31,161,2 1289 | 0,XY1,172,77,22,148,2 1290 | 0,XY1,178,81,14,133,2 1291 | 0,XY1,3,86,8,116,2 1292 | 0,XY1,8,90,4,100,2 1293 | 0,XY1,13,5,2,82,2 1294 | 0,XY1,17,12,2,65,2 1295 | 0,XY1,23,17,5,49,2 1296 | 0,XY1,29,21,10,32,2 1297 | 0,XY1,35,25,17,19,2 1298 | 0,XY1,41,29,26,11,2 1299 | 0,XY1,46,32,38,4,2 1300 | 0,XY1,52,36,49,2,0 1301 | 0,XY1,59,42,61,2,0 1302 | 0,XY1,64,46,72,5,0 1303 | 0,XY1,72,50,83,12,0 1304 | 0,XY1,77,53,93,19,0 1305 | 0,XY1,89,57,105,27,0 1306 | 0,XY1,93,60,112,45,0 1307 | 0,XY1,99,64,117,60,0 1308 | 0,XY1,108,69,120,78,0 1309 | 0,XY1,115,75,120,95,0 1310 | 0,XY1,121,82,118,115,0 1311 | 0,XY1,127,86,113,131,0 1312 | 0,XY1,134,2,105,149,0 1313 | 0,XY1,141,9,97,163,0 1314 | 0,XY1,147,14,87,172,0 1315 | 0,XY1,154,21,75,179,2 1316 | 0,XY1,158,26,63,180,2 1317 | 0,XY1,163,30,50,177,2 1318 | 0,XY1,169,35,38,171,2 1319 | 0,XY1,178,39,26,161,2 1320 | 0,XY1,4,47,18,150,2 1321 | 0,XY1,15,51,10,131,2 1322 | 0,XY1,21,55,5,110,2 1323 | 0,XY1,27,62,2,93,2 1324 | 0,XY1,33,66,2,76,2 1325 | 0,XY1,39,70,6,58,2 1326 | 0,XY1,44,75,12,43,2 1327 | 0,XY1,50,78,19,29,2 1328 | 0,XY1,57,82,28,18,2 1329 | 0,XY1,63,85,41,7,0 1330 | 0,XY1,70,2,52,3,0 1331 | 0,XY1,77,6,64,2,0 1332 | 0,XY1,82,9,77,3,0 1333 | 0,XY1,88,13,88,6,0 1334 | 0,XY1,93,17,98,16,0 1335 | 0,XY1,100,21,108,22,0 1336 | 0,XY1,107,26,114,32,0 1337 | 0,XY1,112,30,118,48,0 1338 | 0,XY1,118,34,120,69,0 1339 | 0,XY1,124,39,120,87,0 1340 | 0,XY1,129,43,117,105,0 1341 | 0,XY1,136,46,112,126,0 1342 | 0,XY1,143,50,106,142,0 1343 | 0,XY1,150,57,89,158,0 1344 | 0,XY1,156,62,79,170,0 1345 | 0,XY1,161,66,67,178,2 1346 | 0,XY1,166,71,53,180,2 1347 | 0,XY1,170,75,42,178,2 1348 | 0,XY1,177,79,31,170,2 1349 | 0,XY1,4,83,22,159,2 1350 | 0,XY1,12,87,14,144,2 1351 | 0,XY1,17,4,8,119,2 1352 | 0,XY1,28,9,3,90,2 1353 | 0,XY1,34,16,2,69,2 1354 | 0,XY1,41,20,2,49,2 1355 | 0,XY1,48,24,5,34,2 1356 | 0,XY1,54,29,10,22,2 1357 | 0,XY1,65,34,18,10,2 1358 | 0,XY1,76,39,29,3,2 1359 | 0,XY1,82,43,43,1,2 1360 | 0,XY1,89,46,55,3,2 1361 | 0,XY1,93,49,65,5,0 1362 | 0,XY1,100,52,77,10,0 1363 | 0,XY1,106,55,88,16,0 1364 | 0,XY1,112,59,98,31,0 1365 | 0,XY1,118,62,108,41,0 1366 | 0,XY1,126,65,114,56,0 1367 | 0,XY1,132,72,118,69,0 1368 | 0,XY1,139,76,120,96,0 1369 | 0,XY1,144,80,120,117,0 1370 | 0,XY1,150,84,117,134,0 1371 | 0,XY1,156,89,111,148,0 1372 | 0,XY1,163,4,103,165,0 1373 | 0,XY1,168,9,95,177,0 1374 | 0,XY1,175,13,81,180,0 1375 | 0,XY1,1,17,70,179,0 1376 | 0,XY1,11,21,58,172,2 1377 | 0,XY1,16,26,44,161,2 1378 | 0,XY1,22,30,33,144,2 1379 | 0,XY1,29,36,24,130,2 1380 | 0,XY1,35,40,15,113,2 1381 | 0,XY1,41,44,9,82,2 1382 | 0,XY1,46,47,4,60,2 1383 | 0,XY1,52,51,1,40,2 1384 | 0,XY1,57,56,4,24,2 1385 | 0,XY1,64,61,9,13,2 1386 | 0,XY1,70,66,15,3,2 1387 | 0,XY1,77,70,33,2,2 1388 | 0,XY1,84,75,49,5,2 1389 | 0,XY1,90,79,64,9,2 1390 | 0,XY1,94,83,75,17,0 1391 | 0,XY1,101,87,86,35,0 1392 | 0,XY1,109,3,96,50,0 1393 | 0,XY1,114,7,106,69,0 1394 | 0,XY1,120,11,113,81,0 1395 | 0,XY1,125,14,119,96,0 1396 | 0,XY1,131,18,121,114,0 1397 | 0,XY1,137,25,119,129,0 1398 | 0,XY1,144,29,116,148,0 1399 | 0,XY1,151,33,108,165,0 1400 | 0,XY1,156,38,97,173,0 1401 | 0,XY1,162,42,87,178,0 1402 | 0,XY1,169,46,74,180,0 1403 | 0,XY1,174,50,62,178,0 1404 | 0,XY1,2,55,50,171,0 1405 | 0,XY1,8,59,37,163,2 1406 | 0,XY1,12,63,27,150,2 1407 | 0,XY1,17,67,17,126,2 1408 | 0,XY1,22,72,8,108,2 1409 | 0,XY1,29,76,3,92,2 1410 | 0,XY1,35,80,2,71,2 1411 | 0,XY1,42,84,3,55,2 1412 | 0,XY1,49,89,12,29,2 1413 | 0,XY1,55,4,20,16,2 1414 | 0,XY1,61,9,29,8,2 1415 | 0,XY1,68,13,42,3,2 1416 | 0,XY1,73,20,52,2,2 1417 | 0,XY1,81,25,64,3,2 1418 | 0,XY1,86,29,75,6,2 1419 | 0,XY1,91,33,86,16,2 1420 | 0,XY1,97,37,97,31,2 1421 | 0,XY1,102,41,108,48,0 1422 | 0,XY1,109,46,114,69,0 1423 | 0,XY1,116,49,118,94,0 1424 | 0,XY1,124,53,120,106,0 1425 | 0,XY1,130,57,120,123,0 1426 | 0,XY1,141,60,118,135,0 1427 | 0,XY1,148,64,111,152,0 1428 | 0,XY1,154,68,104,163,0 1429 | 0,XY1,159,72,85,176,0 1430 | 0,XY1,166,77,73,180,0 1431 | 0,XY1,172,81,62,179,0 1432 | 0,XY1,178,85,48,175,0 1433 | 0,XY1,4,90,37,166,2 1434 | 0,XY1,10,5,20,155,2 1435 | 0,XY1,16,10,12,142,2 1436 | 0,XY1,19,14,5,126,2 1437 | 0,XY1,24,18,2,108,2 1438 | 0,XY1,28,23,2,87,2 1439 | 0,XY1,36,27,7,69,2 1440 | 0,XY1,41,31,20,52,2 1441 | 0,XY1,47,35,32,37,2 1442 | 0,XY1,53,41,46,16,2 1443 | 0,XY1,58,45,58,6,2 1444 | 0,XY1,65,48,69,2,2 1445 | 0,XY1,71,52,80,2,2 1446 | 0,XY1,77,56,91,5,2 1447 | 0,XY1,84,62,100,10,2 1448 | 0,XY1,89,66,110,26,0 1449 | 0,XY1,96,71,116,43,0 1450 | 0,XY1,103,76,119,65,0 1451 | 0,XY1,109,80,120,88,0 1452 | 0,XY1,114,84,119,111,0 1453 | 0,XY1,120,89,116,136,0 1454 | 0,XY1,125,4,111,147,0 1455 | 0,XY1,131,9,103,157,0 1456 | 0,XY1,137,12,95,172,0 1457 | 0,XY1,143,15,84,178,0 1458 | 0,XY1,148,19,73,180,0 1459 | 0,XY1,156,22,61,179,0 1460 | 0,XY1,163,26,50,175,0 1461 | 0,XY1,172,29,39,167,0 1462 | 0,XY1,179,35,26,156,0 1463 | 0,XY1,6,40,16,143,2 1464 | 0,XY1,12,44,9,129,2 1465 | 0,XY1,18,47,4,112,2 1466 | 0,XY1,23,51,2,85,2 1467 | 0,XY1,34,55,2,58,2 1468 | 0,XY1,39,60,10,42,2 1469 | 0,XY1,43,64,18,29,2 1470 | 0,XY1,49,69,27,17,2 1471 | 0,XY1,53,74,40,8,2 1472 | 0,XY1,58,78,51,2,2 1473 | 0,XY1,64,83,65,2,2 1474 | 0,XY1,70,87,79,3,2 1475 | 0,XY1,76,3,89,8,2 1476 | 0,XY1,81,8,102,21,2 1477 | 0,XY1,89,12,112,33,0 1478 | 0,XY1,93,17,118,52,0 1479 | 0,XY1,99,22,121,71,0 1480 | 0,XY1,104,27,119,93,0 1481 | 0,XY1,110,31,116,114,0 1482 | 0,XY1,116,37,109,136,0 1483 | 0,XY1,121,42,100,154,0 1484 | 0,XY1,128,46,91,162,0 1485 | 0,XY1,134,50,80,169,0 1486 | 0,XY1,139,54,69,174,0 1487 | 0,XY1,151,57,55,179,0 1488 | 0,XY1,158,61,40,180,0 1489 | 0,XY1,165,64,30,176,0 1490 | 0,XY1,170,71,20,168,2 1491 | 0,XY1,177,74,13,155,2 1492 | 0,XY1,4,78,7,138,2 1493 | 0,XY1,9,85,3,117,2 1494 | 0,XY1,17,89,2,87,2 1495 | 0,XY1,24,4,3,65,2 1496 | 0,XY1,29,9,7,37,2 1497 | 0,XY1,35,13,12,20,2 1498 | 0,XY1,41,17,24,11,2 1499 | 0,XY1,45,22,36,3,2 1500 | 0,XY1,50,26,47,2,2 1501 | 0,XY1,55,30,58,3,2 1502 | 0,XY1,60,34,69,11,2 1503 | 0,XY1,66,39,80,22,2 1504 | 0,XY1,72,44,91,34,2 1505 | 0,XY1,83,47,102,63,0 1506 | 0,XY1,89,53,109,91,0 1507 | 0,XY1,93,57,116,112,0 1508 | 0,XY1,100,62,119,129,0 1509 | 0,XY1,107,66,120,144,0 1510 | 0,XY1,114,71,118,159,0 1511 | 0,XY1,120,76,113,169,0 1512 | 0,XY1,125,80,107,178,0 1513 | 0,XY1,131,84,99,180,0 1514 | 0,XY1,137,88,87,180,0 1515 | 0,XY1,144,4,75,178,0 1516 | 0,XY1,151,8,63,175,0 1517 | 0,XY1,157,12,51,168,0 1518 | 0,XY1,163,15,39,149,0 1519 | 0,XY1,169,18,29,130,0 1520 | 0,XY1,175,23,19,114,2 1521 | 0,XY1,180,27,11,94,2 1522 | 0,XY1,8,32,5,77,2 1523 | 0,XY1,13,37,2,60,2 1524 | 0,XY1,19,40,2,33,2 1525 | 0,XY1,25,43,4,21,2 1526 | 0,XY1,30,46,9,9,2 1527 | 0,XY1,36,52,15,3,2 1528 | 0,XY1,42,56,24,2,2 1529 | 0,XY1,49,60,35,3,2 1530 | 0,XY1,53,64,47,9,2 1531 | 0,XY1,57,69,59,21,2 1532 | 0,XY1,62,73,71,39,2 1533 | 0,XY1,71,78,83,59,2 1534 | 0,XY1,77,82,94,81,2 1535 | 0,XY1,82,86,104,101,0 1536 | 0,XY1,89,90,111,127,0 1537 | 0,XY1,94,6,117,147,0 1538 | 0,XY1,100,10,120,163,0 1539 | 0,XY1,108,15,120,172,0 1540 | 0,XY1,113,20,118,179,0 1541 | 0,XY1,119,25,113,181,0 1542 | 0,XY1,126,32,106,179,0 1543 | 0,XY1,133,36,98,176,0 1544 | 0,XY1,139,41,88,169,0 1545 | 0,XY1,145,46,77,152,0 1546 | 0,XY1,155,49,60,135,0 1547 | 0,XY1,161,53,37,118,0 1548 | 0,XY1,168,57,27,102,0 1549 | 0,XY1,173,60,18,80,0 1550 | 0,XY1,180,63,10,63,0 1551 | 0,XY1,7,65,4,46,2 1552 | 0,XY1,12,70,2,28,2 1553 | 0,XY1,18,74,2,17,2 1554 | 0,XY1,25,78,6,7,2 1555 | 0,XY1,31,82,12,3,2 1556 | 0,XY1,36,87,19,2,2 1557 | 0,XY1,42,2,29,3,2 1558 | 0,XY1,53,6,41,5,2 1559 | 0,XY1,59,10,52,21,2 1560 | 0,XY1,64,15,64,47,2 1561 | 0,XY1,68,19,77,67,2 1562 | 0,XY1,74,23,88,90,2 1563 | 0,XY1,84,28,98,112,2 1564 | 0,XY1,89,32,107,135,2 1565 | 0,XY1,94,37,114,155,2 1566 | 0,XY1,101,41,118,169,0 1567 | 0,XY1,106,46,120,178,0 1568 | 0,XY1,112,49,120,180,0 1569 | 0,XY1,120,53,117,176,0 1570 | 0,XY1,125,59,111,172,0 1571 | 0,XY1,131,64,103,165,0 1572 | 0,XY1,137,69,93,155,0 1573 | 0,XY1,145,73,83,136,0 1574 | 0,XY1,151,82,70,108,0 1575 | 0,XY1,156,86,58,91,0 1576 | 0,XY1,162,90,46,66,0 1577 | 0,XY1,168,6,34,46,0 1578 | 0,XY1,173,10,23,30,0 1579 | 0,XY1,180,13,14,15,0 1580 | 0,XY1,7,17,7,7,1 1581 | 0,XY1,13,20,3,2,2 1582 | 0,XY1,19,23,2,2,2 1583 | 0,XY1,25,28,2,3,2 1584 | 0,XY1,30,33,6,16,2 1585 | 0,XY1,36,40,12,27,2 1586 | 0,XY1,43,45,19,40,2 1587 | 0,XY1,49,48,29,56,2 1588 | 0,XY1,56,52,39,74,2 1589 | 0,XY1,62,56,52,90,2 1590 | 0,XY1,69,60,64,110,2 1591 | 0,XY1,73,64,77,127,2 1592 | 0,XY1,78,69,88,142,2 1593 | 0,XY1,82,74,99,156,2 1594 | 0,XY1,89,78,108,169,2 1595 | 0,XY1,93,84,114,179,2 1596 | 0,XY1,99,89,119,180,0 1597 | 0,XY1,105,4,120,178,0 1598 | 0,XY1,110,8,120,172,0 1599 | 0,XY1,116,15,116,163,0 1600 | 0,XY1,123,20,111,150,0 1601 | 0,XY1,129,24,103,139,0 1602 | 0,XY1,134,28,93,124,0 1603 | 0,XY1,141,33,82,106,0 1604 | 0,XY1,149,38,71,85,0 1605 | 0,XY1,159,43,60,66,0 1606 | 0,XY1,166,46,48,49,0 1607 | 0,XY1,171,51,34,32,0 1608 | 0,XY1,177,54,23,19,0 1609 | 0,XY1,5,57,15,10,0 1610 | 0,XY1,10,61,8,4,0 1611 | 0,XY1,16,64,3,2,2 1612 | 0,XY1,21,69,2,3,2 1613 | 0,XY1,28,75,2,5,2 1614 | 0,XY1,36,79,6,9,2 1615 | 0,XY1,41,84,11,17,2 1616 | 0,XY1,47,88,19,28,2 1617 | 0,XY1,53,4,29,56,2 1618 | 0,XY1,58,9,39,72,2 1619 | 0,XY1,65,15,50,90,2 1620 | 0,XY1,71,19,61,112,2 1621 | 0,XY1,77,22,73,131,2 1622 | 0,XY1,82,26,86,148,2 1623 | 0,XY1,87,32,96,161,2 1624 | 0,XY1,90,36,107,170,2 1625 | 0,XY1,94,40,114,178,0 1626 | 0,XY1,98,45,118,180,0 1627 | 0,XY1,109,48,120,179,0 1628 | 0,XY1,115,53,120,174,0 1629 | 0,XY1,120,58,116,165,0 1630 | 0,XY1,126,63,111,155,0 1631 | 0,XY1,132,69,103,141,0 1632 | 0,XY1,137,74,92,119,0 1633 | 0,XY1,144,78,81,105,0 1634 | 0,XY1,150,82,70,93,0 1635 | 0,XY1,156,87,58,78,0 1636 | 0,XY1,161,3,45,54,0 1637 | 0,XY1,167,7,34,40,0 1638 | 0,XY1,173,11,23,26,0 1639 | 0,XY1,178,14,14,14,0 1640 | 0,XY1,6,17,8,6,2 1641 | 0,XY1,12,21,4,2,2 1642 | 0,XY1,17,25,2,2,2 1643 | 0,XY1,23,30,2,4,2 1644 | 0,XY1,28,34,5,7,2 1645 | 0,XY1,34,38,11,13,2 1646 | 0,XY1,40,43,19,26,2 1647 | 0,XY1,47,46,29,40,2 1648 | 0,XY1,53,50,40,54,2 1649 | 0,XY1,58,55,52,71,2 1650 | 0,XY1,64,59,64,89,2 1651 | 0,XY1,70,63,75,105,2 1652 | 0,XY1,75,67,87,123,2 1653 | 0,XY1,81,70,99,138,2 1654 | 0,XY1,88,75,107,152,2 1655 | 0,XY1,92,78,114,165,2 1656 | 0,XY1,96,82,119,174,0 1657 | 0,XY1,101,86,120,179,0 1658 | 0,XY1,105,2,120,180,0 1659 | 0,XY1,114,6,116,178,0 1660 | 0,XY1,119,10,111,172,0 1661 | 0,XY1,126,15,103,163,0 1662 | 0,XY1,132,20,92,149,0 1663 | 0,XY1,137,24,81,134,0 1664 | 0,XY1,143,29,70,118,0 1665 | 0,XY1,150,35,58,93,0 1666 | 0,XY1,156,40,47,81,0 1667 | 0,XY1,162,45,34,65,0 1668 | 0,XY1,168,48,24,50,0 1669 | 0,XY1,174,53,15,31,0 1670 | 0,XY1,179,56,8,19,2 1671 | 0,XY1,6,59,4,10,2 1672 | 0,XY1,12,66,2,4,2 1673 | 0,XY1,17,72,2,2,2 1674 | 0,XY1,24,76,5,2,2 1675 | 0,XY1,30,80,11,4,2 1676 | 0,XY1,35,89,19,8,2 1677 | 0,XY1,41,4,27,13,2 1678 | 0,XY1,46,9,38,24,2 1679 | 0,XY1,54,13,49,36,2 1680 | 0,XY1,61,17,61,51,2 1681 | 0,XY1,67,23,71,68,2 1682 | 0,XY1,73,29,83,85,2 1683 | 0,XY1,79,35,93,104,2 1684 | 0,XY1,85,38,105,122,2 1685 | 0,XY1,91,43,114,138,2 1686 | 0,XY1,96,46,118,151,0 1687 | 0,XY1,101,52,120,163,0 1688 | 0,XY1,105,57,120,172,0 1689 | 0,XY1,109,62,117,178,0 1690 | 0,XY1,114,67,111,181,0 1691 | 0,XY1,119,72,103,179,0 1692 | 0,XY1,125,77,93,174,0 1693 | 0,XY1,130,81,81,155,0 1694 | 0,XY1,137,85,69,125,0 1695 | 0,XY1,145,89,58,103,0 1696 | 0,XY1,152,4,44,85,0 1697 | 0,XY1,157,9,33,66,0 1698 | 0,XY1,163,12,22,52,0 1699 | 0,XY1,169,16,14,37,0 1700 | 0,XY1,174,19,8,27,0 1701 | 0,XY1,180,22,4,16,2 1702 | 0,XY1,6,28,2,7,2 1703 | 0,XY1,13,32,2,2,2 1704 | 0,XY1,21,36,5,2,2 1705 | 0,XY1,27,40,10,3,2 1706 | 0,XY1,35,44,18,7,2 1707 | 0,XY1,40,48,27,15,2 1708 | 0,XY1,46,53,39,25,2 1709 | 0,XY1,53,57,50,40,2 1710 | 0,XY1,59,62,61,58,2 1711 | 0,XY1,64,66,72,76,2 1712 | 0,XY1,70,70,83,92,2 1713 | 0,XY1,77,74,94,112,2 1714 | 0,XY1,82,79,105,129,2 1715 | 0,XY1,88,84,112,144,0 1716 | 0,XY1,94,88,117,160,0 1717 | 0,XY1,100,2,120,169,0 1718 | 0,XY1,106,6,120,176,0 1719 | 0,XY1,112,9,119,180,0 1720 | 0,XY1,116,13,113,180,0 1721 | 0,XY1,121,18,107,175,0 1722 | 0,XY1,125,22,99,168,0 1723 | 0,XY1,129,27,89,158,0 1724 | 0,XY1,137,32,78,144,0 1725 | 0,XY1,145,36,67,129,0 1726 | 0,XY1,150,41,56,113,0 1727 | 0,XY1,157,47,42,95,0 1728 | 0,XY1,163,51,31,79,0 1729 | 0,XY1,169,55,22,57,2 1730 | 0,XY1,175,58,13,41,2 1731 | 0,XY1,2,62,7,32,2 1732 | 0,XY1,9,64,3,24,2 1733 | 0,XY1,14,69,1,14,2 1734 | 0,XY1,22,74,3,9,2 1735 | 0,XY1,27,78,6,3,2 1736 | 0,XY1,33,83,12,1,2 1737 | 0,XY1,39,87,19,3,2 1738 | 0,XY1,46,2,28,6,2 1739 | 0,XY1,51,6,42,12,2 1740 | 0,XY1,57,11,53,18,2 1741 | 0,XY1,63,16,75,34,2 1742 | 0,XY1,70,21,95,49,2 1743 | 0,XY1,75,25,104,68,2 1744 | 0,XY1,84,29,112,88,2 1745 | 0,XY1,89,33,117,107,0 1746 | 0,XY1,94,38,120,125,0 1747 | 0,XY1,100,42,120,142,0 1748 | 0,XY1,107,46,118,155,0 1749 | 0,XY1,118,49,113,166,0 1750 | 0,XY1,126,53,106,174,0 1751 | 0,XY1,130,57,98,179,0 1752 | 0,XY1,134,64,88,180,0 1753 | 0,XY1,139,68,77,178,0 1754 | 0,XY1,148,73,62,172,0 1755 | 0,XY1,154,77,52,164,0 1756 | 0,XY1,159,81,38,152,0 1757 | 0,XY1,165,86,27,138,0 1758 | 0,XY1,170,1,17,119,0 1759 | 0,XY1,176,6,10,102,0 1760 | 0,XY1,3,11,5,86,2 1761 | 0,XY1,10,14,2,68,2 1762 | 0,XY1,15,17,2,37,2 1763 | 0,XY1,21,21,4,24,2 1764 | 0,XY1,27,26,9,11,2 1765 | 0,XY1,32,30,16,6,2 1766 | 0,XY1,38,34,25,3,2 1767 | 0,XY1,43,38,36,2,2 1768 | 0,XY1,50,43,50,3,2 1769 | 0,XY1,56,47,61,6,2 1770 | 0,XY1,67,51,73,11,2 1771 | 0,XY1,73,56,84,20,2 1772 | 0,XY1,84,61,95,39,2 1773 | 0,XY1,91,65,104,56,2 1774 | 0,XY1,96,70,112,72,0 1775 | 0,XY1,103,74,118,90,0 1776 | 0,XY1,109,78,120,107,0 1777 | 0,XY1,119,83,120,124,0 1778 | 0,XY1,125,88,118,142,0 1779 | 0,XY1,132,3,114,156,0 1780 | 0,XY1,137,7,108,166,0 1781 | 0,XY1,142,11,99,176,0 1782 | 0,XY1,147,14,88,180,0 1783 | 0,XY1,154,17,76,180,0 1784 | 0,XY1,160,21,64,175,0 1785 | 0,XY1,166,25,52,166,0 1786 | 0,XY1,172,30,39,155,0 1787 | 0,XY1,2,34,29,142,2 1788 | 0,XY1,8,38,19,127,2 1789 | 0,XY1,14,42,10,111,2 1790 | 0,XY1,20,47,5,93,2 1791 | 0,XY1,26,55,2,71,2 1792 | 0,XY1,31,58,2,49,2 1793 | 0,XY1,37,61,4,34,2 1794 | 0,XY1,43,65,9,16,2 1795 | 0,XY1,49,72,16,8,2 1796 | 0,XY1,54,77,25,3,2 1797 | 0,XY1,60,81,37,2,2 1798 | 0,XY1,66,89,48,2,2 1799 | 0,XY1,72,4,61,4,2 1800 | 0,XY1,77,9,72,10,2 1801 | 0,XY1,83,13,84,19,0 1802 | 0,XY1,89,18,96,32,0 1803 | 0,XY1,93,22,105,46,0 1804 | 0,XY1,99,27,112,62,0 1805 | 0,XY1,105,31,117,81,0 1806 | 0,XY1,111,35,120,98,0 1807 | 0,XY1,117,40,120,119,0 1808 | 0,XY1,123,45,117,137,0 1809 | 0,XY1,129,49,110,155,0 1810 | 0,XY1,135,53,101,166,0 1811 | 0,XY1,141,58,92,175,0 1812 | 0,XY1,147,63,81,180,0 1813 | 0,XY1,152,66,70,179,0 1814 | 0,XY1,156,69,59,173,0 1815 | 0,XY1,161,72,44,165,2 1816 | 0,XY1,166,76,33,150,2 1817 | 0,XY1,172,80,24,134,2 1818 | 0,XY1,177,85,13,118,2 1819 | 0,XY1,4,90,7,100,2 1820 | 0,XY1,10,5,3,82,2 1821 | 0,XY1,16,9,2,66,2 1822 | 0,XY1,21,13,3,49,2 1823 | 0,XY1,27,16,9,22,2 1824 | 0,XY1,33,20,16,10,2 1825 | 0,XY1,39,23,25,4,2 1826 | 0,XY1,44,28,35,2,2 1827 | 0,XY1,51,32,46,4,2 1828 | 0,XY1,57,36,57,7,2 1829 | 0,XY1,62,40,68,14,2 1830 | 0,XY1,68,44,81,20,2 1831 | 0,XY1,74,48,92,28,0 1832 | 0,XY1,80,52,104,52,0 1833 | 0,XY1,91,57,111,69,0 1834 | 0,XY1,96,62,116,87,0 1835 | 0,XY1,101,71,120,103,0 1836 | 0,XY1,107,75,120,120,0 1837 | 0,XY1,112,80,118,139,0 1838 | 0,XY1,119,84,111,157,0 1839 | 0,XY1,125,88,102,167,0 1840 | 0,XY1,131,6,93,176,0 1841 | 0,XY1,137,11,82,180,0 1842 | 0,XY1,142,16,71,180,0 1843 | 0,XY1,148,22,60,177,0 1844 | 0,XY1,154,26,48,168,2 1845 | 0,XY1,158,29,37,158,2 1846 | 0,XY1,163,32,27,145,2 1847 | 0,XY1,168,35,18,131,2 1848 | 0,XY1,173,38,11,114,2 1849 | 0,XY1,2,43,5,96,2 1850 | 0,XY1,9,47,2,80,2 1851 | 0,XY1,15,51,2,59,2 1852 | 0,XY1,21,55,4,40,2 1853 | 0,XY1,27,58,8,26,2 1854 | 0,XY1,36,62,14,16,2 1855 | 0,XY1,45,65,23,6,2 1856 | 0,XY1,52,70,35,2,2 1857 | 0,XY1,58,75,46,2,2 1858 | 0,XY1,64,79,61,3,2 1859 | 0,XY1,71,84,72,7,0 1860 | 0,XY1,77,88,83,21,0 1861 | 0,XY1,83,3,93,28,0 1862 | 0,XY1,90,8,103,38,0 1863 | 0,XY1,98,13,111,48,0 1864 | 0,XY1,104,17,117,74,0 1865 | 0,XY1,115,22,120,93,0 1866 | 0,XY1,121,26,120,111,0 1867 | 0,XY1,128,31,116,129,0 1868 | 0,XY1,135,35,109,144,0 1869 | 0,XY1,143,39,102,158,0 1870 | 0,XY1,149,43,90,168,0 1871 | 0,XY1,157,47,79,176,0 1872 | 0,XY1,165,52,68,180,0 1873 | 0,XY1,170,56,56,180,1 1874 | 0,XY1,175,61,42,175,2 1875 | 0,XY1,5,65,31,168,2 1876 | 0,XY1,12,71,20,158,2 1877 | 0,XY1,19,75,10,141,2 1878 | 0,XY1,25,80,2,126,2 1879 | 0,XY1,30,83,2,92,2 1880 | 0,XY1,36,86,4,76,2 1881 | 0,XY1,41,89,8,58,2 1882 | 0,XY1,47,7,15,41,2 1883 | 0,XY1,54,10,23,26,2 1884 | 0,XY1,62,13,35,9,2 1885 | 0,XY1,67,17,52,4,2 1886 | 0,XY1,73,23,64,2,2 1887 | 0,XY1,79,30,75,2,2 1888 | 0,XY1,84,34,86,5,2 1889 | 0,XY1,91,38,96,8,0 1890 | 0,XY1,95,43,105,22,0 1891 | 0,XY1,101,46,112,35,0 1892 | 0,XY1,107,50,117,49,0 1893 | 0,XY1,112,55,120,63,0 1894 | 0,XY1,118,59,120,75,0 1895 | 0,XY1,124,63,118,86,0 1896 | 0,XY1,130,67,112,97,0 1897 | 0,XY1,136,71,105,116,0 1898 | 0,XY1,143,76,96,134,0 1899 | 0,XY1,150,84,86,149,0 1900 | 0,XY1,161,89,71,163,0 1901 | 0,XY1,168,4,59,171,0 1902 | 0,XY1,174,8,47,178,0 1903 | 0,XY1,180,13,35,180,2 1904 | 0,XY1,5,17,24,179,2 1905 | 0,XY1,9,22,16,173,2 1906 | 0,XY1,13,26,9,164,2 1907 | 0,XY1,17,30,4,153,2 1908 | 0,XY1,23,34,2,137,2 1909 | 0,XY1,31,39,2,118,2 1910 | 0,XY1,37,43,4,101,2 1911 | 0,XY1,43,46,9,85,2 1912 | 0,XY1,49,48,16,65,2 1913 | 0,XY1,54,51,27,48,2 1914 | 0,XY1,60,54,39,33,2 1915 | 0,XY1,66,57,51,21,2 1916 | 0,XY1,73,61,62,12,2 1917 | 0,XY1,84,64,75,5,0 1918 | 0,XY1,92,67,87,2,0 1919 | 0,XY1,97,73,97,2,0 1920 | 0,XY1,103,77,106,5,0 1921 | 0,XY1,109,83,113,10,0 1922 | 0,XY1,115,86,118,26,0 1923 | 0,XY1,120,90,120,39,0 1924 | 0,XY1,127,4,120,54,0 1925 | 0,XY1,134,7,117,71,0 1926 | 0,XY1,141,11,111,86,0 1927 | 0,XY1,148,14,97,101,0 1928 | 0,XY1,156,17,83,117,0 1929 | 0,XY1,161,20,71,130,0 1930 | 0,XY1,167,23,58,146,0 1931 | 0,XY1,173,27,47,159,2 1932 | 0,XY1,178,30,35,169,2 1933 | 0,XY1,6,35,26,176,2 1934 | 0,XY1,12,38,17,180,2 1935 | 0,XY1,16,41,9,180,2 1936 | 0,XY1,21,44,4,177,2 1937 | 0,XY1,27,46,2,169,2 1938 | 0,XY1,36,50,2,159,2 1939 | 0,XY1,41,53,5,146,2 1940 | 0,XY1,48,56,11,127,2 1941 | 0,XY1,54,59,18,105,2 1942 | 0,XY1,61,62,28,72,2 1943 | 0,XY1,66,66,39,55,2 1944 | 0,XY1,73,69,51,40,2 1945 | 0,XY1,79,73,62,26,2 1946 | 0,XY1,84,77,75,16,0 1947 | 0,XY1,92,80,86,7,0 1948 | 0,XY1,98,83,98,2,0 1949 | 0,XY1,104,87,106,2,0 1950 | 0,XY1,110,1,113,3,0 1951 | 0,XY1,116,6,118,8,0 1952 | 0,XY1,122,9,120,21,0 1953 | 0,XY1,128,12,120,34,0 1954 | 0,XY1,134,15,117,51,0 1955 | 0,XY1,140,18,112,67,0 1956 | 0,XY1,146,21,104,85,0 1957 | 0,XY1,152,24,95,102,0 1958 | 0,XY1,159,27,83,114,0 1959 | 0,XY1,165,30,71,126,0 1960 | 0,XY1,170,33,60,136,0 1961 | 0,XY1,176,36,47,146,2 1962 | 0,XY1,3,39,35,165,2 1963 | 0,XY1,8,42,25,174,2 1964 | 0,XY1,14,45,16,179,2 1965 | 0,XY1,21,48,9,181,2 1966 | 0,XY1,26,51,4,179,2 1967 | 0,XY1,31,54,2,174,2 1968 | 0,XY1,35,57,2,164,2 1969 | 0,XY1,41,60,8,152,2 1970 | 0,XY1,47,64,14,139,2 1971 | 0,XY1,52,67,22,123,2 1972 | 0,XY1,58,70,33,105,2 1973 | 0,XY1,63,74,44,80,2 1974 | 0,XY1,69,77,56,58,2 1975 | 0,XY1,75,80,67,43,2 1976 | 0,XY1,80,84,79,29,0 1977 | 0,XY1,86,87,93,17,0 1978 | 0,XY1,91,90,104,9,0 1979 | 0,XY1,96,6,117,3,0 1980 | 0,XY1,103,10,120,2,0 1981 | 0,XY1,115,14,120,3,0 1982 | 0,XY1,120,16,118,6,0 1983 | 0,XY1,126,19,112,11,0 1984 | 0,XY1,133,22,105,22,0 1985 | 0,XY1,141,25,87,35,0 1986 | 0,XY1,146,30,76,49,0 1987 | 0,XY1,152,34,63,66,0 1988 | 0,XY1,157,37,50,84,0 1989 | 0,XY1,166,42,39,103,0 1990 | 0,XY1,172,45,28,122,0 1991 | 0,XY1,177,47,19,143,0 1992 | 0,XY1,5,50,10,155,2 1993 | 0,XY1,10,53,5,163,2 1994 | 0,XY1,16,57,2,170,2 1995 | 0,XY1,22,60,2,177,2 1996 | 0,XY1,30,64,5,180,2 1997 | 0,XY1,33,67,10,179,2 1998 | 0,XY1,37,69,16,175,2 1999 | 0,XY1,43,72,25,167,2 2000 | 0,XY1,46,76,38,156,2 2001 | 0,XY1,53,79,49,143,2 2002 | 0,XY1,59,83,61,126,2 2003 | 0,XY1,64,86,71,103,2 2004 | 0,XY1,70,90,83,86,2 2005 | 0,XY1,76,4,93,69,2 2006 | 0,XY1,82,7,105,52,0 2007 | 0,XY1,88,10,112,37,0 2008 | 0,XY1,94,14,117,24,0 2009 | 0,XY1,100,17,120,12,0 2010 | 0,XY1,105,20,120,5,0 2011 | 0,XY1,111,23,117,2,0 2012 | 0,XY1,118,27,112,4,0 2013 | 0,XY1,124,33,105,8,0 2014 | 0,XY1,129,38,96,16,0 2015 | 0,XY1,135,42,86,36,0 2016 | 0,XY1,142,46,71,50,0 2017 | 0,XY1,148,50,61,67,0 2018 | 0,XY1,153,55,47,84,0 2019 | 0,XY1,159,59,36,103,0 2020 | 0,XY1,165,63,26,119,2 2021 | 0,XY1,172,68,17,138,2 2022 | 0,XY1,177,73,9,152,2 2023 | 0,XY1,5,77,4,164,2 2024 | 0,XY1,11,81,2,170,2 2025 | 0,XY1,18,86,2,176,2 2026 | 0,XY1,23,1,5,180,2 2027 | 0,XY1,29,6,11,179,2 2028 | 0,XY1,35,10,27,174,2 2029 | 0,XY1,41,14,41,163,2 2030 | 0,XY1,45,19,52,152,2 2031 | 0,XY1,50,24,63,138,2 2032 | 0,XY1,56,28,85,122,2 2033 | 0,XY1,60,33,97,105,0 2034 | 0,XY1,67,38,106,84,0 2035 | 0,XY1,72,43,112,62,0 2036 | 0,XY1,78,47,117,46,0 2037 | 0,XY1,86,51,120,32,0 2038 | 0,XY1,96,56,120,19,0 2039 | 0,XY1,102,60,117,11,0 2040 | 0,XY1,109,67,111,4,0 2041 | 0,XY1,117,73,103,2,0 2042 | 0,XY1,123,78,93,6,0 2043 | 0,XY1,128,82,83,12,0 2044 | 0,XY1,134,86,70,21,0 2045 | 0,XY1,140,90,59,28,0 2046 | 0,XY1,149,5,47,44,0 2047 | 0,XY1,157,10,35,59,0 2048 | 0,XY1,164,15,24,77,0 2049 | 0,XY1,170,20,16,93,2 2050 | 0,XY1,176,24,9,111,2 2051 | 0,XY1,4,33,4,128,2 2052 | 0,XY1,9,38,2,142,2 2053 | 0,XY1,15,42,2,157,2 2054 | 0,XY1,21,46,5,171,2 2055 | 0,XY1,26,50,11,178,2 2056 | 0,XY1,32,54,18,180,2 2057 | 0,XY1,37,60,28,180,2 2058 | 0,XY1,45,65,39,178,2 2059 | 0,XY1,50,69,52,168,2 2060 | 0,XY1,55,73,63,158,2 2061 | 0,XY1,60,80,75,144,2 2062 | 0,XY1,64,87,87,130,2 2063 | 0,XY1,72,3,98,111,2 2064 | 0,XY1,77,8,108,91,0 2065 | 0,XY1,85,12,114,74,0 2066 | 0,XY1,90,17,118,57,0 2067 | 0,XY1,95,21,120,31,0 2068 | 0,XY1,101,25,120,19,0 2069 | 0,XY1,106,30,118,10,0 2070 | 0,XY1,112,36,112,2,0 2071 | 0,XY1,118,41,105,2,0 2072 | 0,XY1,125,46,96,5,0 2073 | 0,XY1,130,50,86,9,0 2074 | 0,XY1,136,56,75,24,0 2075 | 0,XY1,143,61,63,39,0 2076 | 0,XY1,148,65,50,54,0 2077 | 0,XY1,154,70,38,70,0 2078 | 0,XY1,160,75,27,88,0 2079 | 0,XY1,167,80,19,105,0 2080 | 0,XY1,173,85,11,122,2 2081 | 0,XY1,178,90,6,139,2 2082 | 0,XY1,5,5,2,156,2 2083 | 0,XY1,10,9,2,169,2 2084 | 0,XY1,16,16,3,176,2 2085 | 0,XY1,23,21,8,180,2 2086 | 0,XY1,30,26,14,180,2 2087 | 0,XY1,36,30,22,179,2 2088 | 0,XY1,42,35,31,174,2 2089 | 0,XY1,48,40,42,165,2 2090 | 0,XY1,54,44,53,147,2 2091 | 0,XY1,59,49,64,132,2 2092 | 0,XY1,65,53,75,115,2 2093 | 0,XY1,69,57,87,98,2 2094 | 0,XY1,75,61,97,81,0 2095 | 0,XY1,80,66,108,62,0 2096 | 0,XY1,89,70,114,42,0 2097 | 0,XY1,93,74,120,29,0 2098 | 0,XY1,99,79,120,17,0 2099 | 0,XY1,105,84,117,9,0 2100 | 0,XY1,112,88,108,4,0 2101 | 0,XY1,118,3,100,2,0 2102 | 0,XY1,124,7,90,3,0 2103 | 0,XY1,129,13,77,6,0 2104 | 0,XY1,135,18,64,10,0 2105 | 0,XY1,142,22,53,17,0 2106 | 0,XY1,149,26,41,32,0 2107 | 0,XY1,160,30,30,46,0 2108 | 0,XY1,166,37,20,62,2 2109 | 0,XY1,172,43,12,79,2 2110 | 0,XY1,180,47,6,96,2 2111 | 0,XY1,8,52,3,119,2 2112 | 0,XY1,13,57,2,138,2 2113 | 0,XY1,19,63,3,152,2 2114 | 0,XY1,25,67,7,164,2 2115 | 0,XY1,30,71,13,177,2 2116 | 0,XY1,36,77,23,180,2 2117 | 0,XY1,42,81,33,178,2 2118 | 0,XY1,48,85,44,170,2 2119 | 0,XY1,55,4,56,163,2 2120 | 0,XY1,60,9,69,153,2 2121 | 0,XY1,66,14,81,144,2 2122 | 0,XY1,72,18,92,134,2 2123 | 0,XY1,75,23,102,117,0 2124 | 0,XY1,80,28,110,100,0 2125 | 0,XY1,86,32,116,84,0 2126 | 0,XY1,94,36,120,62,0 2127 | 0,XY1,100,40,120,42,0 2128 | 0,XY1,106,46,119,27,0 2129 | 0,XY1,111,50,114,16,0 2130 | 0,XY1,117,56,109,6,0 2131 | 0,XY1,124,61,100,2,0 2132 | 0,XY1,129,66,91,2,0 2133 | 0,XY1,135,70,78,5,0 2134 | 0,XY1,141,74,65,12,0 2135 | 0,XY1,146,80,53,28,0 2136 | 0,XY1,152,85,41,49,0 2137 | 0,XY1,157,89,30,70,0 2138 | 0,XY1,165,4,20,87,2 2139 | 0,XY1,170,9,12,104,2 2140 | 0,XY1,3,13,6,123,2 2141 | 0,XY1,8,18,2,139,2 2142 | 0,XY1,14,24,2,153,2 2143 | 0,XY1,21,29,3,166,2 2144 | 0,XY1,28,33,7,178,2 2145 | 0,XY1,34,37,14,180,2 2146 | 0,XY1,39,41,23,178,2 2147 | 0,XY1,45,46,33,163,2 2148 | 0,XY1,56,50,44,151,2 2149 | 0,XY1,62,54,56,142,2 2150 | 0,XY1,69,59,67,132,2 2151 | 0,XY1,74,63,80,118,2 2152 | 0,XY1,83,67,92,103,2 2153 | 0,XY1,87,72,102,86,2 2154 | 0,XY1,92,77,110,69,0 2155 | 0,XY1,97,82,116,52,0 2156 | 0,XY1,102,86,119,37,0 2157 | 0,XY1,109,90,120,24,0 2158 | 0,XY1,114,5,119,14,0 2159 | 0,XY1,120,10,115,7,0 2160 | 0,XY1,125,14,108,2,0 2161 | 0,XY1,132,19,100,2,0 2162 | 0,XY1,138,24,89,3,0 2163 | 0,XY1,144,29,78,6,0 2164 | 0,XY1,151,35,64,12,0 2165 | 0,XY1,156,40,52,22,0 2166 | 0,XY1,162,44,42,35,0 2167 | 0,XY1,168,48,31,49,0 2168 | 0,XY1,173,53,21,67,2 2169 | 0,XY1,179,57,13,85,2 2170 | 0,XY1,5,63,7,102,2 2171 | 0,XY1,12,67,3,118,2 2172 | 0,XY1,18,71,1,135,2 2173 | 0,XY1,23,75,3,150,2 2174 | 0,XY1,29,80,7,163,2 2175 | 0,XY1,35,84,12,173,2 2176 | 0,XY1,40,3,20,178,2 2177 | 0,XY1,46,10,29,180,2 2178 | 0,XY1,53,14,41,178,2 2179 | 0,XY1,58,20,52,173,2 2180 | 0,XY1,64,25,63,165,2 2181 | 0,XY1,70,30,75,153,2 2182 | 0,XY1,75,35,87,139,2 2183 | 0,XY1,81,39,96,124,0 2184 | 0,XY1,86,45,105,111,0 2185 | 0,XY1,93,50,112,100,0 2186 | 0,XY1,97,55,117,88,0 2187 | 0,XY1,103,60,120,70,0 2188 | 0,XY1,112,64,120,51,0 2189 | 0,XY1,118,69,119,37,0 2190 | 0,XY1,123,73,113,22,0 2191 | 0,XY1,129,78,107,13,0 2192 | 0,XY1,136,83,99,6,0 2193 | 0,XY1,143,3,89,2,0 2194 | 0,XY1,149,10,78,2,0 2195 | 0,XY1,154,14,67,3,0 2196 | 0,XY1,160,19,56,7,0 2197 | 0,XY1,166,23,42,13,0 2198 | 0,XY1,172,27,31,35,2 2199 | 0,XY1,178,32,22,50,2 2200 | 0,XY1,4,36,13,66,2 2201 | 0,XY1,10,40,7,84,2 2202 | 0,XY1,16,45,3,101,2 2203 | 0,XY1,21,48,1,120,2 2204 | 0,XY1,28,54,3,136,2 2205 | 0,XY1,34,58,6,150,2 2206 | 0,XY1,40,63,12,165,2 2207 | 0,XY1,46,67,19,175,2 2208 | 0,XY1,53,72,28,179,2 2209 | 0,XY1,64,76,42,180,2 2210 | 0,XY1,70,82,53,177,2 2211 | 0,XY1,77,86,64,166,2 2212 | 0,XY1,82,2,75,154,2 2213 | 0,XY1,88,6,87,141,0 2214 | 0,XY1,93,11,96,125,0 2215 | 0,XY1,98,16,105,108,0 2216 | 0,XY1,105,21,112,89,0 2217 | 0,XY1,110,26,117,76,0 2218 | 0,XY1,114,30,120,60,0 2219 | 0,XY1,121,34,119,49,0 2220 | 0,XY1,125,40,113,37,0 2221 | 0,XY1,131,46,107,24,0 2222 | 0,XY1,137,49,98,13,0 2223 | 0,XY1,143,54,88,6,0 2224 | 0,XY1,149,59,71,2,0 2225 | 0,XY1,156,64,60,2,0 2226 | 0,XY1,164,68,45,4,0 2227 | 0,XY1,174,76,35,9,0 2228 | 0,XY1,1,80,24,15,0 2229 | 0,XY1,7,84,16,26,2 2230 | 0,XY1,14,90,9,40,2 2231 | 0,XY1,20,5,4,57,2 2232 | 0,XY1,26,12,2,74,2 2233 | 0,XY1,31,17,2,91,2 2234 | 0,XY1,37,21,5,108,2 2235 | 0,XY1,43,27,10,125,2 2236 | 0,XY1,49,31,17,142,2 2237 | 0,XY1,56,35,26,155,2 2238 | 0,XY1,63,40,39,168,2 2239 | 0,XY1,69,45,52,177,2 2240 | 0,XY1,76,49,64,180,2 2241 | 0,XY1,84,53,75,180,2 2242 | 0,XY1,90,57,86,176,2 2243 | 0,XY1,96,62,96,166,0 2244 | 0,XY1,101,67,107,154,0 2245 | 0,XY1,107,72,115,140,0 2246 | 0,XY1,114,76,119,124,0 2247 | 0,XY1,118,80,120,107,0 2248 | 0,XY1,122,84,120,90,0 2249 | 0,XY1,127,2,117,72,0 2250 | 0,XY1,132,7,112,54,0 2251 | 0,XY1,139,13,105,43,0 2252 | 0,XY1,147,18,96,34,0 2253 | 0,XY1,152,23,86,25,0 2254 | 0,XY1,158,27,75,18,0 2255 | 0,XY1,163,33,63,7,0 2256 | 0,XY1,169,37,50,2,0 2257 | 0,XY1,176,42,38,2,0 2258 | 0,XY1,3,46,27,4,2 2259 | 0,XY1,10,51,19,7,2 2260 | 0,XY1,16,58,11,13,2 2261 | 0,XY1,21,66,6,23,2 2262 | 0,XY1,27,70,2,35,2 2263 | 0,XY1,32,74,2,50,2 2264 | 0,XY1,40,78,3,67,2 2265 | 0,XY1,46,83,10,85,2 2266 | 0,XY1,52,87,20,103,2 2267 | 0,XY1,58,2,30,120,2 2268 | 0,XY1,67,6,44,137,2 2269 | 0,XY1,76,11,55,151,2 2270 | 0,XY1,82,16,77,163,2 2271 | 0,XY1,88,21,88,174,2 2272 | 0,XY1,93,26,98,179,0 2273 | 0,XY1,100,30,109,180,0 2274 | 0,XY1,105,35,116,178,0 2275 | 0,XY1,111,40,119,172,0 2276 | 0,XY1,118,44,120,163,0 2277 | 0,XY1,124,48,119,150,0 2278 | 0,XY1,129,53,116,137,0 2279 | 0,XY1,133,57,111,120,0 2280 | 0,XY1,137,62,103,98,0 2281 | 0,XY1,142,66,94,75,0 2282 | 0,XY1,148,71,83,57,0 2283 | 0,XY1,156,75,72,38,0 2284 | 0,XY1,163,80,61,25,0 2285 | 0,XY1,169,85,48,16,0 2286 | 0,XY1,174,89,31,9,0 2287 | 0,XY1,180,5,22,4,0 2288 | 0,XY1,7,11,14,2,2 2289 | 0,XY1,13,18,8,2,2 2290 | 0,XY1,19,25,2,3,2 2291 | 0,XY1,26,30,2,7,2 2292 | 0,XY1,32,34,8,11,2 2293 | 0,XY1,37,38,14,24,2 2294 | 0,XY1,43,43,23,39,2 2295 | 0,XY1,49,46,35,54,2 2296 | 0,XY1,54,51,46,71,2 2297 | 0,XY1,60,56,57,88,2 2298 | 0,XY1,67,60,68,105,2 2299 | 0,XY1,72,64,80,122,2 2300 | 0,XY1,78,69,90,139,2 2301 | 0,XY1,84,76,101,156,2 2302 | 0,XY1,89,81,112,166,2 2303 | 0,XY1,94,88,117,175,0 2304 | 0,XY1,100,3,120,179,0 2305 | 0,XY1,107,7,120,180,0 2306 | 0,XY1,114,12,116,178,0 2307 | 0,XY1,120,18,109,172,0 2308 | 0,XY1,125,23,102,161,0 2309 | 0,XY1,131,29,92,149,0 2310 | 0,XY1,137,34,80,135,0 2311 | 0,XY1,141,38,68,118,0 2312 | 0,XY1,146,42,57,101,0 2313 | 0,XY1,151,46,42,85,0 2314 | 0,XY1,160,50,32,67,0 2315 | 0,XY1,166,55,22,46,0 2316 | 0,XY1,172,59,14,32,0 2317 | 0,XY1,177,63,8,19,0 2318 | 0,XY1,4,67,4,9,2 2319 | 0,XY1,11,71,2,3,2 2320 | 0,XY1,16,76,2,2,2 2321 | 0,XY1,22,81,7,2,2 2322 | 0,XY1,29,85,13,4,2 2323 | 0,XY1,35,90,20,8,2 2324 | 0,XY1,41,5,30,15,2 2325 | 0,XY1,46,9,40,29,2 2326 | 0,XY1,53,17,55,43,2 2327 | 0,XY1,59,21,66,72,2 2328 | 0,XY1,64,29,78,92,2 2329 | 0,XY1,70,34,91,108,2 2330 | 0,XY1,81,40,101,126,2 2331 | 0,XY1,87,44,109,142,2 2332 | 0,XY1,93,48,115,155,2 2333 | 0,XY1,99,52,119,166,0 2334 | 0,XY1,105,56,120,174,0 2335 | 0,XY1,110,61,119,180,0 2336 | 0,XY1,116,66,115,180,0 2337 | 0,XY1,123,70,109,177,0 2338 | 0,XY1,130,78,101,171,0 2339 | 0,XY1,137,82,91,161,0 2340 | 0,XY1,143,87,80,150,0 2341 | 0,XY1,148,2,67,135,0 2342 | 0,XY1,152,9,55,114,0 2343 | 0,XY1,156,13,43,97,0 2344 | 0,XY1,162,18,31,80,0 2345 | 0,XY1,166,22,21,63,0 2346 | 0,XY1,172,26,13,45,0 2347 | 0,XY1,178,34,7,31,0 2348 | 0,XY1,10,38,3,19,2 2349 | 0,XY1,16,44,1,9,2 2350 | 0,XY1,21,47,3,4,2 2351 | 0,XY1,27,52,7,2,2 2352 | 0,XY1,34,57,13,3,2 2353 | 0,XY1,39,63,22,6,2 2354 | 0,XY1,45,71,32,10,2 2355 | 0,XY1,51,75,51,22,2 2356 | 0,XY1,56,80,62,35,2 2357 | 0,XY1,62,86,79,52,2 2358 | 0,XY1,68,1,90,69,2 2359 | 0,XY1,75,6,101,86,2 2360 | 0,XY1,80,14,109,103,2 2361 | 0,XY1,86,19,116,120,2 2362 | 0,XY1,92,23,119,136,2 2363 | 0,XY1,98,27,120,161,0 2364 | 0,XY1,103,31,119,172,0 2365 | 0,XY1,109,36,115,178,0 2366 | 0,XY1,116,40,109,180,0 2367 | 0,XY1,121,45,102,178,0 2368 | 0,XY1,127,49,91,172,0 2369 | 0,XY1,133,53,80,163,0 2370 | 0,XY1,140,59,68,151,0 2371 | 0,XY1,146,64,55,135,0 2372 | 0,XY1,152,68,43,119,0 2373 | 0,XY1,160,73,31,100,0 2374 | 0,XY1,163,79,21,84,0 2375 | 0,XY1,167,83,13,56,0 2376 | 0,XY1,172,87,7,41,0 2377 | 0,XY1,176,2,3,21,2 2378 | 0,XY1,6,6,2,11,2 2379 | 0,XY1,12,11,3,5,2 2380 | 0,XY1,18,16,7,2,2 2381 | 0,XY1,24,21,13,2,2 2382 | 0,XY1,30,26,26,4,2 2383 | 0,XY1,35,30,38,8,2 2384 | 0,XY1,41,34,49,17,2 2385 | 0,XY1,46,39,67,24,2 2386 | 0,XY1,53,43,78,43,2 2387 | 0,XY1,61,47,90,59,2 2388 | 0,XY1,66,51,99,88,2 2389 | 0,XY1,72,59,109,105,2 2390 | 0,XY1,77,64,116,125,2 2391 | 0,XY1,89,68,119,141,0 2392 | 0,XY1,95,72,120,154,0 2393 | 0,XY1,101,77,119,167,0 2394 | 0,XY1,106,85,115,176,0 2395 | 0,XY1,112,4,109,180,0 2396 | 0,XY1,118,8,100,178,0 2397 | 0,XY1,123,13,90,172,0 2398 | 0,XY1,129,17,79,163,0 2399 | 0,XY1,136,21,67,151,0 2400 | 0,XY1,143,26,55,137,0 2401 | 0,XY1,148,32,43,120,0 2402 | 0,XY1,154,37,32,103,0 2403 | 0,XY1,160,45,22,82,0 2404 | 0,XY1,165,49,13,65,0 2405 | 0,XY1,171,53,7,48,2 2406 | 0,XY1,175,57,3,32,2 2407 | 0,XY1,2,64,1,19,2 2408 | 0,XY1,6,68,3,10,2 2409 | 0,XY1,12,72,7,3,2 2410 | 0,XY1,17,76,13,1,2 2411 | 0,XY1,23,81,22,3,2 2412 | 0,XY1,29,89,32,8,2 2413 | 0,XY1,35,7,43,13,2 2414 | 0,XY1,40,11,55,26,2 2415 | 0,XY1,46,15,67,35,2 2416 | 0,XY1,52,19,80,44,2 2417 | 0,XY1,60,24,92,55,2 2418 | 0,XY1,66,28,103,66,2 2419 | 0,XY1,76,37,110,84,2 2420 | 0,XY1,82,42,116,101,0 2421 | 0,XY1,88,48,119,118,0 2422 | 0,XY1,93,55,120,134,0 2423 | 0,XY1,98,61,119,149,0 2424 | 0,XY1,104,69,116,161,0 2425 | 0,XY1,110,73,109,170,0 2426 | 0,XY1,115,79,102,178,0 2427 | 0,XY1,121,84,92,180,0 2428 | 0,XY1,127,2,82,179,0 2429 | 0,XY1,133,10,70,174,0 2430 | 0,XY1,139,14,60,166,0 2431 | 0,XY1,145,19,45,152,0 2432 | 0,XY1,150,25,34,138,0 2433 | 0,XY1,156,29,24,122,0 2434 | 0,XY1,162,34,16,105,2 2435 | 0,XY1,168,39,9,85,2 2436 | 0,XY1,174,43,4,67,2 2437 | 0,XY1,179,47,2,51,2 2438 | 0,XY1,5,51,2,32,2 2439 | 0,XY1,10,58,5,20,2 2440 | 0,XY1,14,66,16,11,2 2441 | 0,XY1,24,73,25,4,2 2442 | 0,XY1,30,77,35,2,2 2443 | 0,XY1,36,81,47,2,2 2444 | 0,XY1,41,85,59,5,2 2445 | 0,XY1,47,4,72,12,2 2446 | 0,XY1,53,10,84,27,2 2447 | 0,XY1,58,14,95,42,2 2448 | 0,XY1,64,20,104,57,2 2449 | 0,XY1,70,24,111,69,0 2450 | 0,XY1,76,28,116,85,0 2451 | 0,XY1,81,33,120,100,0 2452 | 0,XY1,89,38,120,111,0 2453 | 0,XY1,97,42,118,133,0 2454 | 0,XY1,103,47,113,148,0 2455 | 0,XY1,109,52,106,160,0 2456 | 0,XY1,115,57,97,170,0 2457 | 0,XY1,120,62,86,177,0 2458 | 0,XY1,126,66,75,180,0 2459 | 0,XY1,132,73,62,179,0 2460 | 0,XY1,140,78,50,175,0 2461 | 0,XY1,147,82,39,167,0 2462 | 0,XY1,153,87,27,155,0 2463 | 0,XY1,158,3,18,142,0 2464 | 0,XY1,164,7,10,127,2 2465 | 0,XY1,170,11,4,106,2 2466 | 0,XY1,175,16,2,89,2 2467 | 0,XY1,2,23,2,69,2 2468 | 0,XY1,9,27,9,52,2 2469 | 0,XY1,20,31,16,38,2 2470 | 0,XY1,24,35,25,24,2 2471 | 0,XY1,28,39,37,14,2 2472 | 0,XY1,33,44,47,6,2 2473 | 0,XY1,42,47,60,2,2 2474 | 0,XY1,49,52,72,2,2 2475 | 0,XY1,54,56,84,4,2 2476 | 0,XY1,60,61,95,9,2 2477 | 0,XY1,66,65,104,14,2 2478 | 0,XY1,72,70,112,28,2 2479 | 0,XY1,77,74,117,44,0 2480 | 0,XY1,83,80,120,60,0 2481 | 0,XY1,90,84,120,77,0 2482 | 0,XY1,95,90,118,92,0 2483 | 0,XY1,101,5,113,103,0 2484 | 0,XY1,106,12,107,114,0 2485 | 0,XY1,113,18,98,126,0 2486 | 0,XY1,119,22,86,146,0 2487 | 0,XY1,126,26,75,161,0 2488 | 0,XY1,132,30,63,170,0 2489 | 0,XY1,137,35,51,177,0 2490 | 0,XY1,144,40,39,180,0 2491 | 0,XY1,150,47,28,180,0 2492 | 0,XY1,156,51,18,176,0 2493 | 0,XY1,163,56,10,169,2 2494 | 0,XY1,168,60,5,156,2 2495 | 0,XY1,174,64,2,142,2 2496 | 0,XY1,180,69,2,127,2 2497 | 0,XY1,6,73,4,111,2 2498 | 0,XY1,12,77,9,93,2 2499 | 0,XY1,17,82,17,76,2 2500 | 0,XY1,24,86,26,51,2 2501 | 0,XY1,28,2,43,33,2 2502 | 0,XY1,33,7,55,13,2 2503 | 0,XY1,38,11,69,6,2 2504 | 0,XY1,42,18,80,2,2 2505 | 0,XY1,49,26,91,2,2 2506 | 0,XY1,54,31,101,4,2 2507 | 0,XY1,61,35,111,8,2 2508 | 0,XY1,68,41,119,13,0 2509 | 0,XY1,73,46,120,24,0 2510 | 0,XY1,81,52,119,37,0 2511 | 0,XY1,86,57,110,52,0 2512 | 0,XY1,91,61,100,68,0 2513 | 0,XY1,97,69,91,86,0 2514 | 0,XY1,103,74,80,105,0 2515 | 0,XY1,110,80,69,123,0 2516 | 0,XY1,117,85,58,135,0 2517 | 0,XY1,123,89,47,149,0 2518 | 0,XY1,129,6,35,159,0 2519 | 0,XY1,134,11,25,173,0 2520 | 0,XY1,141,16,17,178,0 2521 | 0,XY1,148,20,10,180,0 2522 | 0,XY1,154,24,5,179,2 2523 | 0,XY1,160,30,2,173,2 2524 | 0,XY1,167,34,2,165,2 2525 | 0,XY1,173,41,5,154,2 2526 | 0,XY1,178,45,10,139,2 2527 | 0,XY1,7,48,18,124,2 2528 | 0,XY1,14,53,26,102,2 2529 | 0,XY1,19,58,39,86,2 2530 | 0,XY1,25,66,52,68,2 2531 | 0,XY1,31,71,62,51,2 2532 | 0,XY1,36,75,80,37,2 2533 | 0,XY1,40,80,92,24,2 2534 | 0,XY1,46,84,102,13,2 2535 | 0,XY1,50,89,110,5,2 2536 | 0,XY1,59,4,116,2,2 2537 | 0,XY1,65,10,120,2,0 2538 | 0,XY1,70,14,120,5,0 2539 | 0,XY1,76,19,119,10,0 2540 | 0,XY1,81,24,115,15,0 2541 | 0,XY1,89,29,109,26,0 2542 | 0,XY1,93,36,99,41,0 2543 | 0,XY1,102,41,89,57,0 2544 | 0,XY1,108,46,78,74,0 2545 | 0,XY1,113,49,65,93,0 2546 | 0,XY1,120,54,53,111,0 2547 | 0,XY1,128,58,41,127,0 2548 | 0,XY1,133,66,30,146,0 2549 | 0,XY1,140,71,20,155,0 2550 | 0,XY1,146,75,12,163,0 2551 | 0,XY1,151,79,6,171,1 2552 | 0,XY1,157,87,2,176,2 2553 | 0,XY1,164,2,2,180,2 2554 | 0,XY1,170,7,3,180,2 2555 | 0,XY1,175,11,8,176,2 2556 | 0,XY1,2,16,14,170,2 2557 | 0,XY1,8,20,23,158,2 2558 | 0,XY1,15,27,34,145,2 2559 | 0,XY1,22,32,44,131,2 2560 | 0,XY1,29,38,58,113,2 2561 | 0,XY1,36,42,69,96,2 2562 | 0,XY1,42,46,81,79,2 2563 | 0,XY1,53,51,92,62,2 2564 | 0,XY1,57,55,102,41,2 2565 | 0,XY1,62,60,110,28,2 2566 | 0,XY1,68,64,116,17,2 2567 | 0,XY1,77,69,120,8,0 2568 | 0,XY1,83,73,120,3,0 2569 | 0,XY1,88,78,119,3,0 2570 | 0,XY1,93,82,114,7,0 2571 | 0,XY1,98,88,108,13,0 2572 | 0,XY1,104,4,99,19,0 2573 | 0,XY1,114,9,89,40,0 2574 | 0,XY1,121,13,78,54,0 2575 | 0,XY1,127,17,65,76,0 2576 | 0,XY1,132,25,53,93,0 2577 | 0,XY1,139,29,41,112,0 2578 | 0,XY1,145,33,30,129,0 2579 | 0,XY1,152,38,20,144,0 2580 | 0,XY1,159,42,12,162,0 2581 | 0,XY1,165,46,5,173,0 2582 | 0,XY1,170,52,2,179,2 2583 | 0,XY1,176,57,2,180,2 2584 | 0,XY1,4,61,4,179,2 2585 | 0,XY1,10,66,9,175,2 2586 | 0,XY1,16,70,15,165,2 2587 | 0,XY1,22,74,23,154,2 2588 | 0,XY1,31,78,35,141,2 2589 | 0,XY1,37,83,46,123,2 2590 | 0,XY1,43,87,61,103,2 2591 | 0,XY1,49,2,72,87,2 2592 | 0,XY1,56,6,83,60,2 2593 | 0,XY1,60,11,95,40,2 2594 | 0,XY1,66,15,105,27,2 2595 | 0,XY1,70,20,112,16,2 2596 | 0,XY1,77,24,117,6,0 2597 | 0,XY1,83,28,120,2,0 2598 | 0,XY1,93,33,120,2,0 2599 | 0,XY1,101,37,119,4,0 2600 | 0,XY1,106,41,113,8,0 2601 | 0,XY1,113,45,106,13,0 2602 | 0,XY1,120,49,98,24,0 2603 | 0,XY1,127,53,88,40,0 2604 | 0,XY1,137,57,78,55,0 2605 | 0,XY1,144,63,66,72,0 2606 | 0,XY1,150,68,52,89,0 2607 | 0,XY1,155,76,40,106,0 2608 | 0,XY1,161,80,30,124,0 2609 | 0,XY1,166,85,20,140,0 2610 | 0,XY1,172,89,13,154,0 2611 | 0,XY1,178,6,7,165,0 2612 | 0,XY1,6,10,3,173,0 2613 | 0,XY1,12,15,2,179,2 2614 | 0,XY1,18,19,3,180,2 2615 | 0,XY1,24,24,7,180,2 2616 | 0,XY1,30,28,12,177,2 2617 | 0,XY1,35,33,22,172,2 2618 | 0,XY1,42,37,35,162,2 2619 | 0,XY1,47,45,46,150,2 2620 | 0,XY1,53,48,57,135,2 2621 | 0,XY1,58,53,68,117,2 2622 | 0,XY1,64,57,80,100,2 2623 | 0,XY1,70,63,90,82,2 2624 | 0,XY1,74,68,100,65,2 2625 | 0,XY1,80,76,110,44,2 2626 | 0,XY1,89,80,115,30,2 2627 | 0,XY1,94,85,119,19,2 2628 | 0,XY1,99,89,119,10,2 2629 | 0,XY1,105,4,116,4,0 2630 | 0,XY1,111,9,109,2,0 2631 | 0,XY1,121,14,100,3,0 2632 | 0,XY1,126,19,91,5,0 2633 | 0,XY1,132,23,80,11,0 2634 | 0,XY1,137,28,69,28,0 2635 | 0,XY1,145,32,58,47,0 2636 | 0,XY1,150,36,44,64,0 2637 | 0,XY1,157,40,30,81,0 2638 | 0,XY1,165,44,20,100,0 2639 | 0,XY1,172,48,13,117,0 2640 | 0,XY1,178,54,7,135,0 2641 | 0,XY1,4,59,3,149,0 2642 | 0,XY1,10,63,2,161,0 2643 | 0,XY1,16,67,3,170,2 2644 | 0,XY1,23,75,7,180,2 2645 | 0,XY1,30,82,12,179,2 2646 | 0,XY1,35,87,20,176,2 2647 | 0,XY1,41,2,29,170,2 2648 | 0,XY1,46,8,42,162,2 2649 | 0,XY1,52,15,54,143,2 2650 | 0,XY1,59,19,67,128,2 2651 | 0,XY1,65,23,78,112,2 2652 | 0,XY1,71,28,89,92,2 2653 | 0,XY1,76,33,99,75,2 2654 | 0,XY1,80,37,108,57,2 2655 | 0,XY1,86,41,115,42,2 2656 | 0,XY1,90,46,119,27,2 2657 | 0,XY1,95,49,120,14,0 2658 | 0,XY1,102,53,120,7,0 2659 | 0,XY1,108,57,117,2,0 2660 | 0,XY1,114,62,110,2,0 2661 | 0,XY1,119,66,103,3,0 2662 | 0,XY1,125,70,94,9,0 2663 | 0,XY1,130,74,83,15,0 2664 | 0,XY1,136,79,72,26,0 2665 | 0,XY1,143,83,61,40,0 2666 | 0,XY1,148,88,50,57,0 2667 | 0,XY1,157,3,38,85,0 2668 | 0,XY1,163,7,28,102,0 2669 | 0,XY1,169,12,19,122,0 2670 | 0,XY1,174,16,12,140,0 2671 | 0,XY1,1,20,6,154,0 2672 | 0,XY1,7,24,2,168,2 2673 | 0,XY1,14,28,2,176,2 2674 | 0,XY1,19,33,3,180,2 2675 | 0,XY1,25,37,8,180,2 2676 | 0,XY1,32,41,13,177,2 2677 | 0,XY1,39,46,22,170,2 2678 | 0,XY1,45,49,31,161,2 2679 | 0,XY1,50,53,47,153,2 2680 | 0,XY1,56,57,62,143,2 2681 | 0,XY1,63,62,74,134,2 2682 | 0,XY1,69,69,85,118,2 2683 | 0,XY1,75,76,95,96,2 2684 | 0,XY1,82,81,104,79,2 2685 | 0,XY1,87,85,113,63,2 2686 | 0,XY1,91,89,118,46,2 2687 | 0,XY1,97,4,120,30,0 2688 | 0,XY1,106,9,119,19,0 2689 | 0,XY1,114,15,115,10,0 2690 | 0,XY1,121,19,109,4,0 2691 | 0,XY1,131,24,100,2,0 2692 | 0,XY1,137,28,87,2,0 2693 | 0,XY1,142,32,76,6,0 2694 | 0,XY1,148,36,64,10,0 2695 | 0,XY1,154,40,52,16,0 2696 | 0,XY1,160,46,40,31,0 2697 | 0,XY1,166,51,29,47,0 2698 | 0,XY1,172,55,20,63,0 2699 | 0,XY1,179,62,12,80,0 2700 | 0,XY1,5,66,7,96,0 2701 | 0,XY1,11,70,2,122,0 2702 | 0,XY1,18,74,2,140,2 2703 | 0,XY1,24,79,7,154,2 2704 | 0,XY1,30,83,12,165,2 2705 | 0,XY1,35,87,20,173,2 2706 | 0,XY1,46,2,33,179,2 2707 | 0,XY1,53,6,47,180,2 2708 | 0,XY1,60,13,70,175,2 2709 | 0,XY1,66,17,81,168,2 2710 | 0,XY1,71,21,91,156,2 2711 | 0,XY1,77,27,103,143,2 2712 | 0,XY1,82,31,111,132,2 2713 | 0,XY1,88,39,118,118,2 2714 | 0,XY1,93,43,120,104,2 2715 | 0,XY1,100,46,120,81,2 2716 | 0,XY1,104,52,118,65,2 2717 | 0,XY1,109,56,113,48,0 2718 | 0,XY1,115,60,105,32,0 2719 | 0,XY1,124,66,91,18,0 2720 | 0,XY1,130,71,80,9,0 2721 | 0,XY1,137,75,69,3,0 2722 | 0,XY1,144,79,58,2,0 2723 | 0,XY1,150,83,44,3,0 2724 | 0,XY1,155,87,33,6,0 2725 | 0,XY1,161,3,23,11,0 2726 | 0,XY1,166,8,15,21,0 2727 | 0,XY1,172,12,8,33,0 2728 | 0,XY1,179,17,3,50,0 2729 | 0,XY1,6,21,2,67,0 2730 | 0,XY1,12,26,3,87,0 2731 | 0,XY1,17,30,7,103,0 2732 | 0,XY1,23,34,12,120,2 2733 | 0,XY1,28,39,26,137,2 2734 | 0,XY1,34,43,36,151,2 2735 | 0,XY1,41,46,48,166,2 2736 | 0,XY1,52,50,69,174,2 2737 | 0,XY1,58,55,90,179,2 2738 | 0,XY1,63,59,107,180,2 2739 | 0,XY1,69,63,117,178,2 2740 | 0,XY1,75,68,120,172,2 2741 | 0,XY1,82,74,120,163,2 2742 | 0,XY1,88,79,116,151,2 2743 | 0,XY1,92,86,106,137,2 2744 | 0,XY1,98,90,98,117,2 2745 | 0,XY1,103,5,87,100,2 2746 | 0,XY1,111,10,66,88,0 2747 | 0,XY1,115,14,55,71,0 2748 | 0,XY1,121,19,44,60,0 2749 | 0,XY1,130,23,32,46,0 2750 | 0,XY1,137,28,16,31,0 2751 | 0,XY1,143,33,9,18,0 2752 | 0,XY1,148,37,4,7,0 2753 | 0,XY1,155,41,2,3,0 2754 | 0,XY1,161,46,2,2,0 2755 | 0,XY1,167,49,5,2,0 2756 | 0,XY1,173,53,10,5,0 2757 | 0,XY1,178,57,18,17,0 2758 | 0,XY1,5,62,27,30,0 2759 | 0,XY1,10,66,38,43,0 2760 | 0,XY1,16,70,50,59,0 2761 | 0,XY1,23,74,61,77,2 2762 | 0,XY1,29,78,75,93,2 2763 | 0,XY1,35,86,87,111,2 2764 | 0,XY1,40,1,96,128,2 2765 | 0,XY1,46,6,106,147,2 2766 | 0,XY1,51,10,113,160,2 2767 | 0,XY1,57,15,118,170,2 2768 | 0,XY1,64,19,120,178,2 2769 | 0,XY1,69,23,119,180,2 2770 | 0,XY1,76,27,114,180,2 2771 | 0,XY1,81,32,108,175,2 2772 | 0,XY1,87,37,98,165,2 2773 | 0,XY1,92,41,86,154,2 2774 | 0,XY1,97,45,75,141,2 2775 | 0,XY1,105,48,62,125,2 2776 | 0,XY1,110,52,51,107,0 2777 | 0,XY1,116,57,39,91,0 2778 | 0,XY1,121,61,28,69,0 2779 | 0,XY1,125,65,19,57,0 2780 | 0,XY1,130,69,10,41,0 2781 | 0,XY1,137,73,5,29,0 2782 | 0,XY1,144,78,2,14,0 2783 | 0,XY1,150,82,2,6,0 2784 | 0,XY1,155,86,3,2,0 2785 | 0,XY1,161,1,7,2,0 2786 | 0,XY1,166,9,13,3,0 2787 | 0,XY1,172,13,21,6,0 2788 | 0,XY1,178,19,27,14,0 2789 | 0,XY1,5,23,35,24,0 2790 | 0,XY1,11,28,44,38,2 2791 | 0,XY1,17,32,52,52,2 2792 | 0,XY1,22,39,61,69,2 2793 | 0,XY1,28,44,70,89,2 2794 | 0,XY1,34,47,80,105,2 2795 | 0,XY1,41,52,91,123,2 2796 | 0,XY1,48,57,100,138,2 2797 | 0,XY1,53,61,112,152,2 2798 | 0,XY1,59,66,117,165,2 2799 | 0,XY1,65,71,120,173,2 2800 | 0,XY1,70,76,120,179,2 2801 | 0,XY1,76,81,118,181,2 2802 | 0,XY1,81,85,113,179,2 2803 | 0,XY1,89,89,106,174,2 2804 | 0,XY1,93,5,100,165,2 2805 | 0,XY1,99,11,94,150,0 2806 | 0,XY1,105,15,83,136,0 2807 | 0,XY1,111,21,75,117,0 2808 | 0,XY1,119,26,61,101,0 2809 | 0,XY1,126,30,38,84,0 2810 | 0,XY1,131,36,26,67,0 2811 | 0,XY1,136,40,16,50,0 2812 | 0,XY1,140,45,9,38,0 2813 | 0,XY1,145,48,4,29,0 2814 | 0,XY1,154,52,2,20,0 2815 | 0,XY1,165,59,2,13,0 2816 | 0,XY1,172,63,5,5,0 2817 | 0,XY1,177,67,10,2,0 2818 | 0,XY1,4,73,17,2,2 2819 | 0,XY1,9,77,26,4,2 2820 | 0,XY1,15,81,36,8,2 2821 | 0,XY1,21,85,51,14,2 2822 | 0,XY1,26,90,62,26,2 2823 | 0,XY1,33,4,72,40,2 2824 | 0,XY1,40,10,80,54,2 2825 | 0,XY1,46,14,89,72,2 2826 | 0,XY1,57,18,106,90,2 2827 | 0,XY1,64,23,114,107,2 2828 | 0,XY1,71,27,118,124,2 2829 | 0,XY1,77,31,120,143,2 2830 | 0,XY1,82,36,120,157,2 2831 | 0,XY1,89,41,113,168,2 2832 | 0,XY1,93,45,106,176,2 2833 | 0,XY1,98,48,98,180,0 2834 | 0,XY1,104,52,87,180,0 2835 | 0,XY1,110,56,73,176,0 2836 | 0,XY1,121,61,55,169,0 2837 | 0,XY1,127,69,43,158,0 2838 | 0,XY1,133,73,33,143,0 2839 | 0,XY1,139,78,20,126,0 2840 | 0,XY1,145,83,13,110,0 2841 | 0,XY1,148,89,7,92,0 2842 | 0,XY1,153,5,3,75,0 2843 | 0,XY1,163,9,2,58,0 2844 | 0,XY1,169,13,3,40,0 2845 | 0,XY1,175,21,7,27,0 2846 | 0,XY1,1,26,14,16,0 2847 | 0,XY1,8,34,27,8,0 2848 | 0,XY1,14,38,38,4,2 2849 | 0,XY1,19,43,58,2,2 2850 | 0,XY1,25,46,68,2,2 2851 | 0,XY1,31,50,83,4,2 2852 | 0,XY1,36,54,91,7,2 2853 | 0,XY1,42,58,101,14,2 2854 | 0,XY1,49,63,108,32,2 2855 | 0,XY1,61,67,112,46,2 2856 | 0,XY1,67,71,118,63,2 2857 | 0,XY1,75,76,120,80,2 2858 | 0,XY1,80,81,120,96,2 2859 | 0,XY1,87,89,116,116,2 2860 | 0,XY1,93,4,111,133,2 2861 | 0,XY1,98,8,104,148,2 2862 | 0,XY1,104,13,95,161,2 2863 | 0,XY1,110,17,84,170,0 2864 | 0,XY1,116,22,72,177,0 2865 | 0,XY1,123,28,61,180,0 2866 | 0,XY1,129,32,47,179,0 2867 | 0,XY1,136,36,36,175,0 2868 | 0,XY1,142,40,26,167,0 2869 | 0,XY1,147,44,17,156,0 2870 | 0,XY1,153,48,10,143,0 2871 | 0,XY1,158,52,5,128,0 2872 | 0,XY1,163,56,2,112,0 2873 | 0,XY1,173,60,4,90,0 2874 | 0,XY1,178,64,9,56,0 2875 | 0,XY1,5,69,15,39,0 2876 | 0,XY1,11,73,23,25,0 2877 | 0,XY1,17,77,33,15,2 2878 | 0,XY1,24,82,44,7,2 2879 | 0,XY1,30,86,58,3,2 2880 | 0,XY1,35,5,72,2,2 2881 | 0,XY1,41,10,83,2,2 2882 | 0,XY1,46,14,95,4,2 2883 | 0,XY1,52,19,105,8,2 2884 | 0,XY1,58,23,110,21,2 2885 | 0,XY1,63,27,114,34,2 2886 | 0,XY1,69,31,118,48,2 2887 | 0,XY1,75,37,120,67,2 2888 | 0,XY1,81,41,120,85,2 2889 | 0,XY1,92,45,117,102,2 2890 | 0,XY1,97,50,107,118,2 2891 | 0,XY1,103,56,99,139,2 2892 | 0,XY1,107,60,85,153,2 2893 | 0,XY1,111,65,70,166,0 2894 | 0,XY1,116,74,60,174,0 2895 | 0,XY1,120,78,48,179,0 2896 | 0,XY1,124,82,37,180,0 2897 | 0,XY1,130,86,23,177,0 2898 | 0,XY1,134,3,15,170,0 2899 | 0,XY1,139,7,9,161,0 2900 | 0,XY1,145,11,3,145,0 2901 | 0,XY1,149,16,2,127,0 2902 | 0,XY1,153,20,2,107,0 2903 | 0,XY1,158,24,5,90,0 2904 | 0,XY1,163,28,10,72,0 2905 | 0,XY1,168,33,17,52,0 2906 | 0,XY1,173,37,29,34,0 2907 | 0,XY1,177,41,44,21,0 2908 | 0,XY1,3,45,53,12,0 2909 | 0,XY1,7,50,66,5,2 2910 | 0,XY1,11,55,78,2,2 2911 | 0,XY1,16,59,95,2,2 2912 | 0,XY1,20,63,106,3,2 2913 | 0,XY1,24,68,114,8,2 2914 | 0,XY1,29,72,118,24,2 2915 | 0,XY1,34,76,120,37,2 2916 | 0,XY1,38,82,121,52,2 2917 | 0,XY1,44,86,118,69,2 2918 | 0,XY1,48,3,112,86,2 2919 | 0,XY1,53,7,97,105,2 2920 | 0,XY1,57,11,84,123,2 2921 | 0,XY1,61,16,73,138,2 2922 | 0,XY1,66,20,55,153,2 2923 | 0,XY1,71,24,44,165,2 2924 | 0,XY1,75,28,37,173,0 2925 | 0,XY1,79,33,29,178,0 2926 | 0,XY1,83,37,22,180,0 2927 | 0,XY1,86,41,10,178,0 2928 | 0,XY1,91,46,3,172,0 2929 | 0,XY1,96,50,2,164,0 2930 | 0,XY1,101,54,3,152,0 2931 | 0,XY1,106,58,8,138,0 2932 | 0,XY1,110,62,14,123,0 2933 | 0,XY1,114,66,24,101,0 2934 | 0,XY1,118,72,34,84,0 2935 | 0,XY1,125,76,45,64,0 2936 | 0,XY1,130,80,61,33,0 2937 | 0,XY1,136,84,72,21,0 2938 | 0,XY1,139,89,85,9,0 2939 | 0,XY1,143,4,96,4,2 2940 | 0,XY1,148,8,106,2,2 2941 | 0,XY1,151,13,113,3,2 2942 | 0,XY1,156,17,118,5,2 2943 | 0,XY1,161,22,120,9,2 2944 | 0,XY1,166,26,120,14,2 2945 | 0,XY1,172,30,119,25,2 2946 | 0,XY1,176,38,116,37,2 2947 | 0,XY1,3,43,107,47,2 2948 | 0,XY1,7,46,98,64,2 2949 | 0,XY1,11,50,88,80,2 2950 | 0,XY1,16,55,77,97,2 2951 | 0,XY1,20,59,66,115,2 2952 | 0,XY1,26,63,52,134,2 2953 | 0,XY1,29,67,37,148,0 2954 | 0,XY1,35,72,25,162,0 2955 | 0,XY1,39,76,16,174,0 2956 | 0,XY1,44,80,10,179,0 2957 | 0,XY1,48,85,5,180,0 2958 | 0,XY1,52,2,2,177,0 2959 | 0,XY1,56,6,2,170,0 2960 | 0,XY1,61,10,5,161,0 2961 | 0,XY1,66,14,11,149,0 2962 | 0,XY1,72,19,18,134,0 2963 | 0,XY1,80,23,27,117,0 2964 | 0,XY1,86,28,37,88,0 2965 | 0,XY1,91,34,51,70,0 2966 | 0,XY1,94,38,59,50,0 2967 | 0,XY1,98,42,68,35,0 2968 | 0,XY1,103,46,77,22,2 2969 | 0,XY1,106,49,89,13,2 2970 | 0,XY1,111,54,97,6,2 2971 | 0,XY1,116,58,106,2,2 2972 | 0,XY1,120,62,113,2,2 2973 | 0,XY1,125,66,118,4,2 2974 | 0,XY1,129,70,120,7,2 2975 | 0,XY1,133,75,118,12,2 2976 | 0,XY1,137,79,115,24,2 2977 | 0,XY1,142,84,109,38,2 2978 | 0,XY1,146,89,103,48,2 2979 | 0,XY1,151,4,96,63,2 2980 | 0,XY1,155,9,85,79,2 2981 | 0,XY1,160,14,66,90,2 2982 | 0,XY1,164,19,55,107,2 2983 | 0,XY1,169,24,46,125,0 2984 | 0,XY1,174,29,37,141,0 2985 | 0,XY1,179,33,27,154,0 2986 | 0,XY1,5,37,20,165,0 2987 | 0,XY1,10,41,10,175,0 2988 | 0,XY1,15,46,5,180,0 2989 | 0,XY1,19,49,2,180,0 2990 | 0,XY1,23,53,2,177,0 2991 | 0,XY1,29,57,4,171,0 2992 | 0,XY1,34,62,9,161,0 2993 | 0,XY1,38,66,17,149,0 2994 | 0,XY1,43,70,26,135,0 2995 | 0,XY1,47,75,36,114,0 2996 | 0,XY1,51,79,47,97,0 2997 | 0,XY1,55,84,58,80,0 2998 | 0,XY1,61,3,70,63,2 2999 | 0,XY1,65,11,83,47,2 3000 | 0,XY1,70,17,96,32,2 3001 | 0,XY1,74,21,105,21,2 --------------------------------------------------------------------------------