Neuerstellung

This commit is contained in:
Stephan Maier
2024-08-27 07:48:29 +02:00
parent 7fb7fce332
commit 73dab93853
13 changed files with 746 additions and 0 deletions

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34723.18
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSI.BT.IR.Plc.TimeSync", "FSI.BT.IR.Plc.TimeSync\FSI.BT.IR.Plc.TimeSync.csproj", "{23DFF9D8-7A25-4465-865B-3D1834AF5725}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{23DFF9D8-7A25-4465-865B-3D1834AF5725}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23DFF9D8-7A25-4465-865B-3D1834AF5725}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23DFF9D8-7A25-4465-865B-3D1834AF5725}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23DFF9D8-7A25-4465-865B-3D1834AF5725}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {539E3901-0BD7-4435-8501-3C27CC52E614}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-FSI.BT.IR.Plc.TimeSync-965c3cf3-0f37-484a-865d-4762bb9fe30e</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Config.Net" Version="5.2.0" />
<PackageReference Include="GuerrillaNtp" Version="3.1.0" />
<PackageReference Include="Json.Net" Version="1.0.33" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" />
<PackageReference Include="Sharp7" Version="1.1.84" />
</ItemGroup>
<ItemGroup>
<Compile Update="Settings\AppContext.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="nlog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
using FSI.BT.IR.Plc.TimeSync;
using static FSI.BT.IR.Plc.TimeSync.Settings.Context;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<ISettings, FSI.BT.IR.Plc.TimeSync.Settings.AppContext>();
using IHost host = builder.Build();
host.Run();

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"FSI.BT.IR.Plc.TimeSync": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,71 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CommandLine;
using Microsoft.Extensions.Primitives;
using static FSI.BT.IR.Plc.TimeSync.Settings.Context;
namespace FSI.BT.IR.Plc.TimeSync.Settings
{
public class AppContext : ISettings
{
private Cfg _cfg;
private string[] _args;
public AppContext()
{
_args = new string[0];
LoadSettings();
LoadCfg();
}
private void LoadSettings()
{
var values = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.Build();
Settings = values.Get<Context.Settings>();
}
private void LoadCfg()
{
var values = new ConfigurationBuilder()
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();
Cfg = values.Get<Context.Cfg>();
Action onChange = () =>
{
Cfg = values.Get<Context.Cfg>();
};
ChangeToken.OnChange(() => values.GetReloadToken(), onChange);
}
public Context.Settings Settings { get; set; }
public Cfg Cfg
{
get { return _cfg; }
set
{
_cfg = value;
RaisePropertyChanged();
}
}
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
// Null means no subscribers to the event
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

View File

@@ -0,0 +1,12 @@
using CommandLine;
namespace FSI.BT.IR.Plc.TimeSync.Settings
{
public class ConsoleArgs
{
[Option('v', "version", Required = false, HelpText = "Version")]
public bool Version { get; set; }
}
}

View File

@@ -0,0 +1,239 @@
using System.ComponentModel;
namespace FSI.BT.IR.Plc.TimeSync.Settings
{
/// <summary>
/// Datenbasis von config.json und appsettings.json
/// Einstellungen und Konfigurationsdaten
/// </summary>
public class Context
{
public interface ISettings : INotifyPropertyChanged
{
/// <summary>
/// Daten von appsettings.json
/// </summary>
public Settings Settings { get; set; }
/// <summary>
/// Daten von config.json
/// </summary>
public Cfg Cfg { get; set; }
}
/// <summary>
/// Daten von appsettings.json
/// </summary>
public record Settings
{
/// <summary>
/// Versions-Informationen
/// </summary>
public Version Version { get; set; }
/// <summary>
/// Build-Informationen
/// </summary>
public Build Build { get; set; }
/// <summary>
/// Logging-Einstellungen
/// </summary>
public Logging Logging { get; set; }
}
/// <summary>
/// Versions-Informationen
/// </summary>
public record Version
{
/// <summary>
/// Haupt-Versionsnummer
/// </summary>
public uint Major { get; set; } = 0;
/// <summary>
/// Unter-Versionsnummer
/// </summary>
public uint Minor { get; set; } = 0;
/// <summary>
/// Patch/Hotfix
/// </summary>
public uint Patch { get; set; } = 0;
/// <summary>
/// optinoale Versionsinformationen
/// </summary>
public string? Optional { get; set; }
/// <summary>
/// gibt die Versions-Nummer zurück
/// </summary>
/// <returns>Versionsnummer</returns>
public override string ToString()
{
return Major.ToString() + "." + Minor.ToString() + "." + Patch.ToString() + ((Optional == string.Empty || Optional == null) ? "" : "-" + Optional);
}
}
/// <summary>
/// Build-Informationen
/// </summary>
public record Build
{
/// <summary>
/// Ersteller
/// </summary>
public string Creator { get; set; }
/// <summary>
/// Organisation
/// </summary>
public string Organization { get; set; }
/// <summary>
/// Erstellungsjahr
/// </summary>
public int CreationYear { get; set; }
/// <summary>
/// Beschreibung
/// </summary>
public string Description { get; set; }
}
public class Logging
{
public Loglevel LogLevel { get; set; }
}
/// <summary>
/// Logging-Einstellungen
/// </summary>
public class Loglevel
{
public string Default { get; set; }
[ConfigurationKeyName("Microsoft.Hosting.Lifetime")]
public string MicrosoftHostingLifetime { get; set; }
}
/// <summary>
/// Daten von config.json
/// </summary>
public record Cfg
{
/// <summary>
/// Spsen, deren Zeit mit NTP-Server syncronsiert werden sollen
/// </summary>
public List<Plc> Plcs { get; set; }
/// <summary>
/// NTP-Server Adresse
/// </summary>
public string NtpServer { get; set; }
}
/// <summary>
/// SPS-Daten
/// </summary>
public record Plc : IPlc
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Beschreibung
/// </summary>
public string Description { get; set; }
/// <summary>
/// IP-Adresse
/// </summary>
public string Adress { get; set; }
/// <summary>
/// Rack-Nummer
/// </summary>
public int Rack { get; set; }
/// <summary>
/// Slot-Nummer
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Update-Intervall, in der die Zeit überprüft werden soll.
/// </summary>
public int UpdateIntervall { get; set; }
/// <summary>
/// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
/// </summary>
public int TimeDifference { get; set; }
/// <summary>
/// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
/// </summary>
public bool LocalTime { get; set; }
/// <summary>
/// Soll SPS-Zeit synchronisiert werden
/// </summary>
public bool Enable { get; set; }
}
public interface IPlc
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Beschreibung
/// </summary>
public string Description { get; set; }
/// <summary>
/// IP-Adresse
/// </summary>
public string Adress { get; set; }
/// <summary>
/// Rack-Nummer
/// </summary>
public int Rack { get; set; }
/// <summary>
/// Slot-Nummer
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Update-Intervall, in der die Zeit überprüft werden soll.
/// </summary>
public int UpdateIntervall { get; set; }
/// <summary>
/// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
/// </summary>
public int TimeDifference { get; set; }
/// <summary>
/// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
/// </summary>
public bool LocalTime { get; set; }
/// <summary>
/// Soll SPS-Zeit synchronisiert werden
/// </summary>
public bool Enable { get; set; }
}
}
}

View File

@@ -0,0 +1,164 @@
using NLog;
using Sharp7;
using GuerrillaNtp;
using static FSI.BT.IR.Plc.TimeSync.Settings.Context;
namespace FSI.BT.IR.Plc.TimeSync
{
internal class SyncPlcTime : IPlc
{
#region Constants
private const string DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm:ss.fff"; // Zeitformat
#endregion
private Logger _log = LogManager.GetCurrentClassLogger(); // Nlog
public SyncPlcTime(IPlc plc)
{
Name = plc.Name;
Description = plc.Description;
Adress = plc.Adress;
Rack = plc.Rack;
Slot = plc.Slot;
UpdateIntervall = plc.UpdateIntervall;
TimeDifference = plc.TimeDifference;
LocalTime = plc.LocalTime;
Enable = plc.Enable;
}
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Beschreibung
/// </summary>
public string Description { get; set; }
/// <summary>
/// IP-Adresse
/// </summary>
public string Adress { get; set; }
/// <summary>
/// Rack-Nummer
/// </summary>
public int Rack { get; set; }
/// <summary>
/// Slot-Nummer
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Update-Intervall, in der die Zeit überprüft werden soll.
/// </summary>
public int UpdateIntervall { get; set; }
/// <summary>
/// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
/// </summary>
public int TimeDifference { get; set; }
/// <summary>
/// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
/// </summary>
public bool LocalTime { get; set; }
/// <summary>
/// Soll SPS-Zeit synchronisiert werden
/// </summary>
public bool Enable { get; set; }
/// <summary>
/// NTP-Server Adresse
/// </summary>
public string NtpServer { get; set; }
public async Task Snyc(CancellationToken cancellationToken) =>
await Task.Run(async () =>
{
var plc = new S7Client(); // SPS-Verbindung
var client = new NtpClient(NtpServer); // NTP-Client
NtpClock clock = client.Query(); // NTP-Client Uhrzeit
var plcDateTime = new DateTime(); // Uhrzeit SPS
DateTime ntpDateTime; // Uhrzeit NTP-Client
while (!cancellationToken.IsCancellationRequested)
{
try
{
// Verbindung mit SPS-Aufbauen
var connectionRslt = plc.ConnectTo(Adress, Rack, Slot);
// Verbindungsstatus überprüfen
if (connectionRslt == 0) // Verbindung i.O.
{
_log.Debug(Name + " Verbindung hergestellt.");
}
else // Verbindung n.i.O.
{
_log.Error(Name + " Verbindung nicht hergestellt.");
_log.Error(Name + " Fehler: " + plc.ErrorText(connectionRslt));
await Task.Delay(UpdateIntervall, cancellationToken); // Warten bis zum nächsten Verbindungsversuch (Zeiten aus config.json)
continue;
}
}
catch (Exception ex)
{
_log.Error(Name + " " + ex.Message);
}
plc.GetPlcDateTime(ref plcDateTime); // Uhrzeit aus SPS auslesen
_log.Debug(Name + " SPS Zeit (aktuell): " + plcDateTime.ToString(DATE_TIME_FORMAT));
if (LocalTime) // lokale Zeit/Ortszeit
{
_log.Debug(Name + " Ortszeit: " + clock.Now.ToString(DATE_TIME_FORMAT));
ntpDateTime = clock.Now.DateTime; // lokale Zeit/Ortszeit von NTP-Server
}
else // UTC - Zeit
{
_log.Debug(Name + " UTC-Zeit: " + clock.UtcNow.ToString(DATE_TIME_FORMAT));
ntpDateTime = clock.UtcNow.DateTime; // UTC-Zeit von NTP-Server
}
// Zeitdiffernz zwischen SPS-Zeit und Zeit von NTP-Server berechnen
var timeSpan = Math.Abs((plcDateTime - ntpDateTime).TotalMilliseconds);
if (
TimeDifference > 0 // Zeitdifferenz aus Einstellungen > 0 ms
&& timeSpan >= TimeDifference // Zeitdifferenz überprüfung
)
{
_log.Debug(Name + " Zeitdifferenz " + timeSpan + " ms überschritten");
var temp = plc.SetPlcDateTime(ntpDateTime); // Zeit an SPS senden
_log.Info(Name + " neue Zeit: " + ntpDateTime.ToString(DATE_TIME_FORMAT));
}
else if (TimeDifference == 0)
{
var temp = plc.SetPlcDateTime(ntpDateTime); // Zeit an SPS senden
_log.Info(Name + " neue Zeit: " + ntpDateTime.ToString(DATE_TIME_FORMAT));
}
plc.Disconnect(); // Verbindung zur SPS trennen
_log.Debug(Name + " Verbindung getrennt.");
_log.Debug(Name + " Start Task-Wartezeit");
try
{
await Task.Delay(UpdateIntervall, cancellationToken); // Warten bis zum nächsten Verbindungsversuch (Zeiten aus config.json)
}
catch (Exception ex)
{
_log.Error(Name + " " + ex.Message);
}
_log.Debug(Name + " Ende Task-Wartezeit");
}
}, cancellationToken);
}
}

View File

@@ -0,0 +1,85 @@
using NLog;
using static FSI.BT.IR.Plc.TimeSync.Settings.Context;
namespace FSI.BT.IR.Plc.TimeSync
{
public class Worker(ISettings settings) : BackgroundService
{
private Logger _log = LogManager.GetCurrentClassLogger();
private List<SyncPlcTime> _taskList;
private CancellationTokenSource _tokenSource;
private CancellationToken _stoppingToken;
/// <summary>
/// Standard Taks
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
settings.PropertyChanged += Settings_PropertyChanged; // Event, bei <20>nderungen an der Config-Datei
StartTasks(); // Tasks, die beim Start ausgef<65>hrt werden
}
/// <summary>
/// Event, bei <20>nderungen an der config.json.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Settings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
StopTasks();
StartTasks();
}
/// <summary>
/// Tasks, die beim Start ausgef<65>hrt werden sollen.
/// </summary>
private void StartTasks()
{
if (_taskList != null)
return;
_taskList = new List<SyncPlcTime>();
_tokenSource = new CancellationTokenSource();
_stoppingToken = _tokenSource.Token;
// Schleife <20>ber alle Spsen
foreach (var plc in settings.Cfg.Plcs)
{
if (plc.Enable)
{
var timeSync = new SyncPlcTime(plc);
timeSync.NtpServer = settings.Cfg.NtpServer;
timeSync.Snyc(_stoppingToken);
_taskList.Add(timeSync);
_log.Info(plc.Name + " Task gestartet.");
}
}
}
/// <summary>
/// alle Taks werden gestoppt.
/// </summary>
private async void StopTasks()
{
_tokenSource.Cancel();
_stoppingToken = _tokenSource.Token;
// Schleife <20>ber alle Spsen
foreach (var task in _taskList)
{
task.Snyc(_stoppingToken);
_log.Info(task.Name + " Task beendet.");
}
_taskList?.Clear();
_taskList = null;
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,20 @@
{
"Version": {
"Major": 0,
"Minor": 0,
"Patch": 0,
"Optional": "alpha"
},
"Build": {
"Creator": "Stephan Maier",
"Organization": "Fondium Singen GmbH",
"CreationYear": "2024",
"Description": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"Plcs": [
{
"Name": "PL1 FA",
"Description": "Beschreibung",
"Adress": "10.10.199.95",
"Rack": 0,
"Slot": 2,
"UpdateIntervall": 10000,
"TimeDifference": 10,
"LocalTime": false,
"Enable": true
},
{
"Name": "PL1 FA 123",
"Description": "PL1 Formanlage",
"Adress": "10.10.199.95",
"Rack": 0,
"Slot": 2,
"UpdateIntervall": 60000,
"TimeDifference": 0,
"LocalTime": true,
"Enable": false
}
],
"NtpServer": "10.10.199.41"
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwExceptions="false">
<variable name="appName" value="FSI.BT.IR.Plc.TimeSync" />
<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File"
name="logfile"
fileName="d:/logs/${appName}/${appName}.log"
archiveFileName ="d:/logs/${appName}/{#}_${appName}.log"
archiveNumbering ="Date" archiveEvery="Day"
archiveDateFormat="yyyyMMdd"/>
<target name="viewer"
xsi:type="NLogViewer"
includeSourceInfo="true"
address="udp://FDESINB0166:9999"/>
</targets>
<!-- rules to map from logger name to target -->
<rules>
<logger name="*"
minlevel="Trace"
writeTo="logfile,logconsole,console" />
<logger name="*"
minlevel="Debug"
writeTo="viewer" />
</rules>
</nlog>