diff --git a/scr/FSI.BT.IR.Plc.TimeSync.sln b/scr/FSI.BT.IR.Plc.TimeSync.sln
new file mode 100644
index 0000000..5d5ab17
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync.sln
@@ -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
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/FSI.BT.IR.Plc.TimeSync.csproj b/scr/FSI.BT.IR.Plc.TimeSync/FSI.BT.IR.Plc.TimeSync.csproj
new file mode 100644
index 0000000..787de7b
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/FSI.BT.IR.Plc.TimeSync.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0
+ enable
+ enable
+ dotnet-FSI.BT.IR.Plc.TimeSync-965c3cf3-0f37-484a-865d-4762bb9fe30e
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Never
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Program.cs b/scr/FSI.BT.IR.Plc.TimeSync/Program.cs
new file mode 100644
index 0000000..e6b19f6
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Program.cs
@@ -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();
+builder.Services.AddSingleton();
+
+using IHost host = builder.Build();
+host.Run();
\ No newline at end of file
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Properties/launchSettings.json b/scr/FSI.BT.IR.Plc.TimeSync/Properties/launchSettings.json
new file mode 100644
index 0000000..8f4a42c
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Properties/launchSettings.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Settings/AppContext.cs b/scr/FSI.BT.IR.Plc.TimeSync/Settings/AppContext.cs
new file mode 100644
index 0000000..35dcf83
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Settings/AppContext.cs
@@ -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();
+ }
+
+ private void LoadCfg()
+ {
+ var values = new ConfigurationBuilder()
+ .AddJsonFile("config.json", optional: true, reloadOnChange: true)
+ .Build();
+
+ Cfg = values.Get();
+
+ Action onChange = () =>
+ {
+ Cfg = values.Get();
+ };
+
+ 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;
+ }
+}
+
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Settings/ConsoleArgs.cs b/scr/FSI.BT.IR.Plc.TimeSync/Settings/ConsoleArgs.cs
new file mode 100644
index 0000000..90f5616
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Settings/ConsoleArgs.cs
@@ -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; }
+
+
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Settings/Context.cs b/scr/FSI.BT.IR.Plc.TimeSync/Settings/Context.cs
new file mode 100644
index 0000000..9d8b96a
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Settings/Context.cs
@@ -0,0 +1,239 @@
+using System.ComponentModel;
+
+namespace FSI.BT.IR.Plc.TimeSync.Settings
+{
+ ///
+ /// Datenbasis von config.json und appsettings.json
+ /// Einstellungen und Konfigurationsdaten
+ ///
+ public class Context
+ {
+ public interface ISettings : INotifyPropertyChanged
+ {
+ ///
+ /// Daten von appsettings.json
+ ///
+ public Settings Settings { get; set; }
+
+ ///
+ /// Daten von config.json
+ ///
+ public Cfg Cfg { get; set; }
+ }
+
+ ///
+ /// Daten von appsettings.json
+ ///
+ public record Settings
+ {
+ ///
+ /// Versions-Informationen
+ ///
+ public Version Version { get; set; }
+
+ ///
+ /// Build-Informationen
+ ///
+ public Build Build { get; set; }
+
+ ///
+ /// Logging-Einstellungen
+ ///
+ public Logging Logging { get; set; }
+ }
+
+ ///
+ /// Versions-Informationen
+ ///
+ public record Version
+ {
+ ///
+ /// Haupt-Versionsnummer
+ ///
+ public uint Major { get; set; } = 0;
+
+ ///
+ /// Unter-Versionsnummer
+ ///
+ public uint Minor { get; set; } = 0;
+
+ ///
+ /// Patch/Hotfix
+ ///
+ public uint Patch { get; set; } = 0;
+
+ ///
+ /// optinoale Versionsinformationen
+ ///
+ public string? Optional { get; set; }
+
+ ///
+ /// gibt die Versions-Nummer zurück
+ ///
+ /// Versionsnummer
+ public override string ToString()
+ {
+ return Major.ToString() + "." + Minor.ToString() + "." + Patch.ToString() + ((Optional == string.Empty || Optional == null) ? "" : "-" + Optional);
+ }
+ }
+
+ ///
+ /// Build-Informationen
+ ///
+ public record Build
+ {
+ ///
+ /// Ersteller
+ ///
+ public string Creator { get; set; }
+
+ ///
+ /// Organisation
+ ///
+ public string Organization { get; set; }
+
+ ///
+ /// Erstellungsjahr
+ ///
+ public int CreationYear { get; set; }
+
+ ///
+ /// Beschreibung
+ ///
+ public string Description { get; set; }
+ }
+
+ public class Logging
+ {
+ public Loglevel LogLevel { get; set; }
+ }
+
+ ///
+ /// Logging-Einstellungen
+ ///
+ public class Loglevel
+ {
+ public string Default { get; set; }
+
+ [ConfigurationKeyName("Microsoft.Hosting.Lifetime")]
+ public string MicrosoftHostingLifetime { get; set; }
+ }
+
+ ///
+ /// Daten von config.json
+ ///
+ public record Cfg
+ {
+
+ ///
+ /// Spsen, deren Zeit mit NTP-Server syncronsiert werden sollen
+ ///
+ public List Plcs { get; set; }
+
+ ///
+ /// NTP-Server Adresse
+ ///
+ public string NtpServer { get; set; }
+ }
+
+ ///
+ /// SPS-Daten
+ ///
+ public record Plc : IPlc
+ {
+ ///
+ /// Name
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Beschreibung
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// IP-Adresse
+ ///
+ public string Adress { get; set; }
+
+ ///
+ /// Rack-Nummer
+ ///
+ public int Rack { get; set; }
+
+ ///
+ /// Slot-Nummer
+ ///
+ public int Slot { get; set; }
+
+ ///
+ /// Update-Intervall, in der die Zeit überprüft werden soll.
+ ///
+ public int UpdateIntervall { get; set; }
+
+ ///
+ /// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
+ ///
+ public int TimeDifference { get; set; }
+
+ ///
+ /// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
+ ///
+ public bool LocalTime { get; set; }
+
+ ///
+ /// Soll SPS-Zeit synchronisiert werden
+ ///
+ public bool Enable { get; set; }
+ }
+
+ public interface IPlc
+ {
+ ///
+ /// Name
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Beschreibung
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// IP-Adresse
+ ///
+ public string Adress { get; set; }
+
+ ///
+ /// Rack-Nummer
+ ///
+ public int Rack { get; set; }
+
+ ///
+ /// Slot-Nummer
+ ///
+ public int Slot { get; set; }
+
+ ///
+ /// Update-Intervall, in der die Zeit überprüft werden soll.
+ ///
+ public int UpdateIntervall { get; set; }
+
+ ///
+ /// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
+ ///
+ public int TimeDifference { get; set; }
+
+ ///
+ /// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
+ ///
+ public bool LocalTime { get; set; }
+
+ ///
+ /// Soll SPS-Zeit synchronisiert werden
+ ///
+ public bool Enable { get; set; }
+ }
+ }
+
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/SyncPlcTime.cs b/scr/FSI.BT.IR.Plc.TimeSync/SyncPlcTime.cs
new file mode 100644
index 0000000..37290a1
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/SyncPlcTime.cs
@@ -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;
+ }
+
+ ///
+ /// Name
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Beschreibung
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// IP-Adresse
+ ///
+ public string Adress { get; set; }
+
+ ///
+ /// Rack-Nummer
+ ///
+ public int Rack { get; set; }
+
+ ///
+ /// Slot-Nummer
+ ///
+ public int Slot { get; set; }
+
+ ///
+ /// Update-Intervall, in der die Zeit überprüft werden soll.
+ ///
+ public int UpdateIntervall { get; set; }
+
+ ///
+ /// Zeitdifferenz, ab der die SPS-Zeit angepasst werden soll.
+ ///
+ public int TimeDifference { get; set; }
+
+ ///
+ /// Locale Zeit wird an die SPS gesendet - nicht UTC-Zeit
+ ///
+ public bool LocalTime { get; set; }
+
+ ///
+ /// Soll SPS-Zeit synchronisiert werden
+ ///
+ public bool Enable { get; set; }
+
+ ///
+ /// NTP-Server Adresse
+ ///
+ 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);
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/Worker.cs b/scr/FSI.BT.IR.Plc.TimeSync/Worker.cs
new file mode 100644
index 0000000..ea93646
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/Worker.cs
@@ -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 _taskList;
+ private CancellationTokenSource _tokenSource;
+ private CancellationToken _stoppingToken;
+
+ ///
+ /// Standard Taks
+ ///
+ ///
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+
+ settings.PropertyChanged += Settings_PropertyChanged; // Event, bei Änderungen an der Config-Datei
+
+ StartTasks(); // Tasks, die beim Start ausgeführt werden
+
+ }
+
+ ///
+ /// Event, bei Änderungen an der config.json.
+ ///
+ ///
+ ///
+ private void Settings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ StopTasks();
+ StartTasks();
+ }
+
+ ///
+ /// Tasks, die beim Start ausgeführt werden sollen.
+ ///
+ private void StartTasks()
+ {
+
+ if (_taskList != null)
+ return;
+
+ _taskList = new List();
+ _tokenSource = new CancellationTokenSource();
+ _stoppingToken = _tokenSource.Token;
+
+ // Schleife ü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.");
+ }
+ }
+
+ }
+
+ ///
+ /// alle Taks werden gestoppt.
+ ///
+ private async void StopTasks()
+ {
+ _tokenSource.Cancel();
+ _stoppingToken = _tokenSource.Token;
+
+ // Schleife über alle Spsen
+ foreach (var task in _taskList)
+ {
+ task.Snyc(_stoppingToken);
+ _log.Info(task.Name + " Task beendet.");
+ }
+
+ _taskList?.Clear();
+ _taskList = null;
+ }
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/appsettings.Development.json b/scr/FSI.BT.IR.Plc.TimeSync/appsettings.Development.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/appsettings.json b/scr/FSI.BT.IR.Plc.TimeSync/appsettings.json
new file mode 100644
index 0000000..6884de1
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/appsettings.json
@@ -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"
+ }
+ }
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/config.json b/scr/FSI.BT.IR.Plc.TimeSync/config.json
new file mode 100644
index 0000000..040a7c8
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/config.json
@@ -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"
+}
diff --git a/scr/FSI.BT.IR.Plc.TimeSync/nlog.config b/scr/FSI.BT.IR.Plc.TimeSync/nlog.config
new file mode 100644
index 0000000..f0c9006
--- /dev/null
+++ b/scr/FSI.BT.IR.Plc.TimeSync/nlog.config
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file