From 38b83affd702884316cbe0d35d2487ed80298519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Thu, 27 Jul 2023 19:03:02 +0200 Subject: [PATCH] Fix multiple poller situational problems --- AutoTools.DocBuilder/Program.cs | 2 +- Examples.BasicEthernet/Program.cs | 3 - Examples.WPF/App.xaml | 9 + Examples.WPF/App.xaml.cs | 85 +++++++++ Examples.WPF/AssemblyInfo.cs | 10 ++ Examples.WPF/Examples.WPF.csproj | 14 ++ Examples.WPF/MainWindow.xaml | 162 ++++++++++++++++++ Examples.WPF/MainWindow.xaml.cs | 57 ++++++ Examples.WPF/ViewModels/AppViewModel.cs | 30 ++++ .../ViewModels/ConnectViewViewModel.cs | 117 +++++++++++++ .../ViewModels/PlcDataViewViewModel.cs | 14 ++ Examples.WPF/ViewModels/ViewModelBase.cs | 18 ++ Examples.WPF/Views/ConnectView.xaml | 129 ++++++++++++++ Examples.WPF/Views/ConnectView.xaml.cs | 78 +++++++++ Examples.WPF/Views/PlcDataView.xaml | 76 ++++++++ Examples.WPF/Views/PlcDataView.xaml.cs | 32 ++++ MewtocolNet.sln | 17 +- MewtocolNet/ComCassette/CassetteFinder.cs | 9 +- .../ComCassette/CassetteInformation.cs | 37 ++++ .../CodeDescriptions.cs | 2 +- MewtocolNet/Helpers/MewtocolHelpers.cs | 39 ++++- MewtocolNet/Helpers/ParsedPlcName.cs | 14 +- MewtocolNet/IPlc.cs | 10 ++ MewtocolNet/Logging/Logger.cs | 14 +- MewtocolNet/Mewtocol.cs | 120 +++++++------ MewtocolNet/MewtocolFrameResponse.cs | 5 +- MewtocolNet/MewtocolInterface.cs | 35 +++- .../MewtocolInterfaceRegisterHandling.cs | 69 ++++++-- MewtocolNet/PLCInfo.cs | 66 +++++-- MewtocolNet/PublicEnums/PlcType.cs | 5 + .../UnderlyingRegisters/MemoryAreaManager.cs | 89 ++++++---- MewtocolTests/TestLinkedLists.cs | 6 +- 32 files changed, 1243 insertions(+), 130 deletions(-) create mode 100644 Examples.WPF/App.xaml create mode 100644 Examples.WPF/App.xaml.cs create mode 100644 Examples.WPF/AssemblyInfo.cs create mode 100644 Examples.WPF/Examples.WPF.csproj create mode 100644 Examples.WPF/MainWindow.xaml create mode 100644 Examples.WPF/MainWindow.xaml.cs create mode 100644 Examples.WPF/ViewModels/AppViewModel.cs create mode 100644 Examples.WPF/ViewModels/ConnectViewViewModel.cs create mode 100644 Examples.WPF/ViewModels/PlcDataViewViewModel.cs create mode 100644 Examples.WPF/ViewModels/ViewModelBase.cs create mode 100644 Examples.WPF/Views/ConnectView.xaml create mode 100644 Examples.WPF/Views/ConnectView.xaml.cs create mode 100644 Examples.WPF/Views/PlcDataView.xaml create mode 100644 Examples.WPF/Views/PlcDataView.xaml.cs rename MewtocolNet/{Helpers => DataLists}/CodeDescriptions.cs (97%) diff --git a/AutoTools.DocBuilder/Program.cs b/AutoTools.DocBuilder/Program.cs index 0295877..583ab02 100644 --- a/AutoTools.DocBuilder/Program.cs +++ b/AutoTools.DocBuilder/Program.cs @@ -32,7 +32,7 @@ Console.WriteLine($"{filePath}"); StringBuilder markdownBuilder = new StringBuilder(); -var plcNames = Enum.GetNames().OrderBy(x => x).ToArray(); +var plcNames = Enum.GetNames().Where(x => x != PlcType.Unknown.ToString()).OrderBy(x => x).ToArray(); void WritePlcTypeTable(string[] names) { diff --git a/Examples.BasicEthernet/Program.cs b/Examples.BasicEthernet/Program.cs index 6c9862c..7ec537c 100644 --- a/Examples.BasicEthernet/Program.cs +++ b/Examples.BasicEthernet/Program.cs @@ -67,9 +67,6 @@ internal class Program { plc.ConfigureConnection("192.168.178.55", 9094); await plc.ConnectAsync(); - //await plc.SendCommandAsync($"%EE#RR0000100"); - //await plc.SendCommandAsync($"%EE#RCCR09030903"); - await plc.SendCommandAsync($"%EE#RP0000000067"); } diff --git a/Examples.WPF/App.xaml b/Examples.WPF/App.xaml new file mode 100644 index 0000000..a35b010 --- /dev/null +++ b/Examples.WPF/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Examples.WPF/App.xaml.cs b/Examples.WPF/App.xaml.cs new file mode 100644 index 0000000..6ef037a --- /dev/null +++ b/Examples.WPF/App.xaml.cs @@ -0,0 +1,85 @@ +using Examples.WPF.ViewModels; +using MewtocolNet; +using MewtocolNet.Logging; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Navigation; + +namespace Examples.WPF; + +public partial class App : Application { + + public static AppViewModel ViewModel { get; private set; } = null!; + + public static new App Current = null!; + + public static new MainWindow MainWindow = null!; + + public static ObservableCollection LoggerItems = null!; + + internal static event Action? LogEventProcessed; + + protected override void OnStartup(StartupEventArgs e) { + + ViewModel = new AppViewModel(); + + Current = this; + LoggerItems = new(); + + Logger.LogLevel = LogLevel.Verbose; + Logger.DefaultTargets = LoggerTargets.Trace; + + Logger.OnNewLogMessage((d, l, m) => { + + Application.Current.Dispatcher.BeginInvoke(() => { + + Brush msgColor = null!; + + switch (l) { + case LogLevel.Error: + msgColor = Brushes.Red; + break; + case LogLevel.Change: + msgColor = Brushes.Blue; + break; + case LogLevel.Verbose: + msgColor = Brushes.Gold; + break; + case LogLevel.Critical: + msgColor = Brushes.Gray; + break; + } + + if (LoggerItems.Count > 1000) LoggerItems.RemoveAt(0); + + var contRun = msgColor == null ? new Run(m) : new Run(m) { + Foreground = msgColor, + }; + + LoggerItems.Add(new TextBlock { + Inlines = { + new Run($"[{d:hh:mm:ss:ff}] ") { + Foreground = Brushes.LimeGreen, + }, + contRun + } + }); + + LogEventProcessed?.Invoke(); + + }, System.Windows.Threading.DispatcherPriority.Background); + + }); + + } + +} diff --git a/Examples.WPF/AssemblyInfo.cs b/Examples.WPF/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/Examples.WPF/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Examples.WPF/Examples.WPF.csproj b/Examples.WPF/Examples.WPF.csproj new file mode 100644 index 0000000..c9992de --- /dev/null +++ b/Examples.WPF/Examples.WPF.csproj @@ -0,0 +1,14 @@ + + + + WinExe + net7.0-windows + enable + true + + + + + + + diff --git a/Examples.WPF/MainWindow.xaml b/Examples.WPF/MainWindow.xaml new file mode 100644 index 0000000..895407b --- /dev/null +++ b/Examples.WPF/MainWindow.xaml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples.WPF/Views/ConnectView.xaml.cs b/Examples.WPF/Views/ConnectView.xaml.cs new file mode 100644 index 0000000..ceba6a7 --- /dev/null +++ b/Examples.WPF/Views/ConnectView.xaml.cs @@ -0,0 +1,78 @@ +using Examples.WPF.ViewModels; +using MewtocolNet; +using MewtocolNet.ComCassette; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Examples.WPF.Views; + +/// +/// Interaktionslogik für ConnectView.xaml +/// +public partial class ConnectView : UserControl { + + private ConnectViewViewModel viewModel; + + public ConnectView() { + + InitializeComponent(); + viewModel = new ConnectViewViewModel(); + this.DataContext = viewModel; + + Unloaded += (s, e) => viewModel.EndTimer(); + + } + + private void SelectedCassette(object sender, SelectionChangedEventArgs e) { + + var cassette = (CassetteInformation)((DataGrid)sender).SelectedItem; + if (cassette == null) return; + + viewModel.SelectedCassette(cassette); + + } + + private void ClickedConnectEth(object sender, RoutedEventArgs e) { + + Application.Current.Dispatcher.BeginInvoke(async () => { + + viewModel.IsConnecting = true; + + var parsedInt = int.Parse(viewModel.SelectedPort); + + App.ViewModel.Plc = Mewtocol.Ethernet(viewModel.SelectedIP, parsedInt) + .WithPoller() + .WithRegisters(b => { + b.Struct("DT0").Build(); + b.Struct("DT0").AsArray(30).Build(); + }) + .Build(); + + await App.ViewModel.Plc.ConnectAsync(); + + if (App.ViewModel.Plc.IsConnected) { + + App.MainWindow.mainContent.Content = new PlcDataView(); + + } + + viewModel.IsConnecting = false; + + }, System.Windows.Threading.DispatcherPriority.Send); + + } + +} diff --git a/Examples.WPF/Views/PlcDataView.xaml b/Examples.WPF/Views/PlcDataView.xaml new file mode 100644 index 0000000..4d84bda --- /dev/null +++ b/Examples.WPF/Views/PlcDataView.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples.WPF/Views/PlcDataView.xaml.cs b/Examples.WPF/Views/PlcDataView.xaml.cs new file mode 100644 index 0000000..ac9e834 --- /dev/null +++ b/Examples.WPF/Views/PlcDataView.xaml.cs @@ -0,0 +1,32 @@ +using Examples.WPF.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Examples.WPF.Views; + +public partial class PlcDataView : UserControl { + + private PlcDataViewViewModel viewModel; + + public PlcDataView() { + + InitializeComponent(); + + viewModel = new PlcDataViewViewModel(); + this.DataContext = viewModel; + + } + +} diff --git a/MewtocolNet.sln b/MewtocolNet.sln index 4f748d7..b1d0579 100644 --- a/MewtocolNet.sln +++ b/MewtocolNet.sln @@ -23,7 +23,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.ChmDataExtract", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.DocBuilder", "AutoTools.DocBuilder\AutoTools.DocBuilder.csproj", "{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.ProgramReadWrite", "Examples.ProgramReadWrite\Examples.ProgramReadWrite.csproj", "{51BDABAA-05B0-4802-AA37-243DAE22D5DC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.ProgramReadWrite", "Examples.ProgramReadWrite\Examples.ProgramReadWrite.csproj", "{51BDABAA-05B0-4802-AA37-243DAE22D5DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.WPF", "Examples.WPF\Examples.WPF.csproj", "{C8A486EA-6054-4B77-859E-BFEEA93658CF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -143,6 +145,18 @@ Global {51BDABAA-05B0-4802-AA37-243DAE22D5DC}.Release|x64.Build.0 = Release|Any CPU {51BDABAA-05B0-4802-AA37-243DAE22D5DC}.Release|x86.ActiveCfg = Release|Any CPU {51BDABAA-05B0-4802-AA37-243DAE22D5DC}.Release|x86.Build.0 = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|x64.Build.0 = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Debug|x86.Build.0 = Debug|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|Any CPU.Build.0 = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|x64.ActiveCfg = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|x64.Build.0 = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|x86.ActiveCfg = Release|Any CPU + {C8A486EA-6054-4B77-859E-BFEEA93658CF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,6 +168,7 @@ Global {5A9DE453-AD64-4F8D-8215-3BB26674D164} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D} {00ACA0AB-3988-4EF7-98A6-B39A36B136DA} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D} {51BDABAA-05B0-4802-AA37-243DAE22D5DC} = {323729B0-5FB2-4592-9FA6-220C46BBF84C} + {C8A486EA-6054-4B77-859E-BFEEA93658CF} = {323729B0-5FB2-4592-9FA6-220C46BBF84C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4ABB8137-CD8F-4691-9802-9ED371012F47} diff --git a/MewtocolNet/ComCassette/CassetteFinder.cs b/MewtocolNet/ComCassette/CassetteFinder.cs index 7079bab..809dbdd 100644 --- a/MewtocolNet/ComCassette/CassetteFinder.cs +++ b/MewtocolNet/ComCassette/CassetteFinder.cs @@ -1,4 +1,5 @@ -using System; +using MewtocolNet.Logging; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -16,6 +17,8 @@ namespace MewtocolNet.ComCassette { public static async Task> FindClientsAsync(string ipSource = null, int timeoutMs = 100) { + Logger.Log("Scanning for cassettes over UDP"); + var from = new IPEndPoint(IPAddress.Any, 0); var interfacesTasks = new List>>(); @@ -67,7 +70,9 @@ namespace MewtocolNet.ComCassette { } - return decomposed; + Logger.Log($"Found {decomposed.Count} cassettes"); + + return decomposed.OrderBy(x => x.IPAddress.ToString()); } diff --git a/MewtocolNet/ComCassette/CassetteInformation.cs b/MewtocolNet/ComCassette/CassetteInformation.cs index 008aeee..fec09d0 100644 --- a/MewtocolNet/ComCassette/CassetteInformation.cs +++ b/MewtocolNet/ComCassette/CassetteInformation.cs @@ -50,6 +50,11 @@ namespace MewtocolNet.ComCassette { /// public byte[] MacAddress { get; private set; } + /// + /// Mac address of the cassette formatted as a MAC string (XX:XX:XX:XX:XX) + /// + public string MacAddressStr => MacAddress.ToHexString(":"); + /// /// The source endpoint the cassette is reachable from /// @@ -177,6 +182,38 @@ namespace MewtocolNet.ComCassette { } + public static bool operator ==(CassetteInformation a, CassetteInformation b) => EqualProps(a, b); + + public static bool operator !=(CassetteInformation a, CassetteInformation b) => !EqualProps(a, b); + + private static bool EqualProps (CassetteInformation a, CassetteInformation b) { + + if (a is null && b is null) return true; + if (!(a is null) && b is null) return false; + if (!(b is null) && a is null) return false; + + return a.Name == b.Name && + a.UsesDHCP == b.UsesDHCP && + a.IPAddress.ToString() == b.IPAddress.ToString() && + a.SubnetMask.ToString() == b.SubnetMask.ToString() && + a.GatewayAddress.ToString() == b.GatewayAddress.ToString() && + a.MacAddressStr == b.MacAddressStr && + a.FirmwareVersion == b.FirmwareVersion && + a.Port == b.Port && + a.Status == b.Status; + + } + + public override bool Equals(object obj) { + + if ((obj == null) || !this.GetType().Equals(obj.GetType())) { + return false; + } else { + return (CassetteInformation)obj == this; + } + + } + } } diff --git a/MewtocolNet/Helpers/CodeDescriptions.cs b/MewtocolNet/DataLists/CodeDescriptions.cs similarity index 97% rename from MewtocolNet/Helpers/CodeDescriptions.cs rename to MewtocolNet/DataLists/CodeDescriptions.cs index 6b295cf..ecb4fc7 100644 --- a/MewtocolNet/Helpers/CodeDescriptions.cs +++ b/MewtocolNet/DataLists/CodeDescriptions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace MewtocolNet.Helpers { +namespace MewtocolNet.DataLists { internal class CodeDescriptions { diff --git a/MewtocolNet/Helpers/MewtocolHelpers.cs b/MewtocolNet/Helpers/MewtocolHelpers.cs index 2466c8c..ce8fabc 100644 --- a/MewtocolNet/Helpers/MewtocolHelpers.cs +++ b/MewtocolNet/Helpers/MewtocolHelpers.cs @@ -159,6 +159,40 @@ namespace MewtocolNet { } + /// + /// Splits a string by uppercase words + /// + internal static IEnumerable SplitByAlternatingCase(this string str) { + + var words = new List(); + var result = new StringBuilder(); + + for (int i = 0; i < str.Length; i++) { + + char lastCh = str[Math.Max(0, i - 1)]; + char ch = str[i]; + + if (char.IsUpper(ch) && char.IsLower(lastCh) && result.Length > 0) { + words.Add(result.ToString().Trim()); + result.Clear(); + } + + result.Append(ch); + + } + + if (!string.IsNullOrEmpty(result.ToString())) + words.Add(result.ToString().Trim()); + + return words; + + } + + /// + /// Splits a string by uppercase words and joins them with the given seperator + /// + internal static string JoinSplitByUpperCase(this string str, string seperator = " ") => string.Join(seperator, str.SplitByAlternatingCase()); + internal static string Ellipsis(this string str, int maxLength) { if (string.IsNullOrEmpty(str) || str.Length <= maxLength) @@ -366,7 +400,7 @@ namespace MewtocolNet { } -#if DEBUG + #if DEBUG internal static bool WasTestedLive(this PlcType plcT) { @@ -396,7 +430,7 @@ namespace MewtocolNet { } -#endif + #endif #endregion @@ -444,6 +478,7 @@ namespace MewtocolNet { #endregion + } } \ No newline at end of file diff --git a/MewtocolNet/Helpers/ParsedPlcName.cs b/MewtocolNet/Helpers/ParsedPlcName.cs index d24f395..d35c03d 100644 --- a/MewtocolNet/Helpers/ParsedPlcName.cs +++ b/MewtocolNet/Helpers/ParsedPlcName.cs @@ -14,12 +14,12 @@ namespace MewtocolNet { /// /// Whole name of the PLC /// - public string WholeName { get; internal set; } + public string WholeName { get; internal set; } = "Unknown PLC"; /// /// The family group of the PLC /// - public string Group { get; internal set; } + public string Group { get; internal set; } = "Unknown Group"; /// /// The Memory size of the PLC @@ -29,17 +29,17 @@ namespace MewtocolNet { /// /// The subtype strings of the plc /// - public string[] SubTypes { get; internal set; } + public string[] SubTypes { get; internal set; } = new string[] { "N/A" }; /// /// Typecode of the parsed string /// - public int TypeCode { get; internal set; } + public int TypeCode { get; internal set; } /// /// The encoded name, same as enum name /// - public string EncodedName { get; internal set; } + public string EncodedName { get; internal set; } = "Unknown"; /// /// True if the model is discontinued @@ -55,6 +55,10 @@ namespace MewtocolNet { internal static ParsedPlcName PlcDeconstruct(string wholeStr) { + if(wholeStr == "Unknown") { + return new ParsedPlcName(); + } + var reg = new Regex(@"(?[A-Za-z0-9]*)_(?[A-Za-z0-9]*)(?:__)?(?.*)"); var match = reg.Match(wholeStr); diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs index 2697ec4..22dd3e7 100644 --- a/MewtocolNet/IPlc.cs +++ b/MewtocolNet/IPlc.cs @@ -18,11 +18,21 @@ namespace MewtocolNet { /// bool IsConnected { get; } + /// + /// This device is sending a message to the plc + /// + bool IsSending { get; } + /// /// The current transmission speed in bytes per second /// int BytesPerSecondUpstream { get; } + /// + /// This device is receiving a message from the plc + /// + bool IsReceiving { get; } + /// /// The current transmission speed in bytes per second /// diff --git a/MewtocolNet/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs index 48f6509..6dbcc9b 100644 --- a/MewtocolNet/Logging/Logger.cs +++ b/MewtocolNet/Logging/Logger.cs @@ -22,9 +22,11 @@ namespace MewtocolNet.Logging { static Logger () { + var isConsoleApplication = Console.LargestWindowWidth != 0; + OnNewLogMessage((d, l, m) => { - if(DefaultTargets.HasFlag(LoggerTargets.Console)) { + if(isConsoleApplication && DefaultTargets.HasFlag(LoggerTargets.Console)) { switch (l) { case LogLevel.Error: @@ -88,5 +90,15 @@ namespace MewtocolNet.Logging { } + internal static void LogError (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Error, sender); + + internal static void Log (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Info, sender); + + internal static void LogChange (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Change, sender); + + internal static void LogVerbose (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Verbose, sender); + + internal static void LogCritical (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Critical, sender); + } } diff --git a/MewtocolNet/Mewtocol.cs b/MewtocolNet/Mewtocol.cs index dc3177b..4a3c7eb 100644 --- a/MewtocolNet/Mewtocol.cs +++ b/MewtocolNet/Mewtocol.cs @@ -19,7 +19,72 @@ namespace MewtocolNet /// public static class Mewtocol { - #region Build Order 1 + #region Data Access + + /// + /// Lists all usable COM port names + /// + /// + public static IEnumerable GetSerialPortNames () => SerialPort.GetPortNames(); + + /// + /// Lists all usable serial baud rates + /// + /// + public static IEnumerable GetUseableBaudRates() => Enum.GetValues(typeof(BaudRate)).Cast().Select(x => (int)x); + + /// + /// Lists all useable source endpoints of the device this is running on for usage with PLCs + /// + public static IEnumerable GetSourceEndpoints() { + + foreach (var netIf in GetUseableNetInterfaces()) { + + var addressInfo = netIf.GetIPProperties().UnicastAddresses + .FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + yield return new IPEndPoint(addressInfo.Address, 9094); + + } + + } + + /// + /// Lists all useable network interfaces of the device this is running on for usage with PLCs + /// + public static IEnumerable GetUseableNetInterfaces() { + + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { + + bool isEthernet = + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT || + netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet; + + bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + + bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up; + + if (!isUsable) continue; + if (!(isWlan || isEthernet)) continue; + + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + var hasUnicastInfo = ipProps.UnicastAddresses + .Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork); + + if (!hasUnicastInfo) continue; + + yield return netInterface; + + } + + } + + #endregion + + #region Interface building step 1 /// /// Builds a ethernet based Mewtocol Interface @@ -92,58 +157,9 @@ namespace MewtocolNet } - /// - /// Lists all useable source endpoints of the device this is running on for usage with PLCs - /// - public static IEnumerable GetSourceEndpoints () { - - foreach (var netIf in GetUseableNetInterfaces()) { - - var addressInfo = netIf.GetIPProperties().UnicastAddresses - .FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork); - - yield return new IPEndPoint(addressInfo.Address, 9094); - - } - - } - - /// - /// Lists all useable network interfaces of the device this is running on for usage with PLCs - /// - public static IEnumerable GetUseableNetInterfaces () { - - foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { - - bool isEthernet = - netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet || - netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || - netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || - netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT || - netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet; - - bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; - - bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up; - - if (!isUsable) continue; - if (!(isWlan || isEthernet)) continue; - - IPInterfaceProperties ipProps = netInterface.GetIPProperties(); - var hasUnicastInfo = ipProps.UnicastAddresses - .Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork); - - if (!hasUnicastInfo) continue; - - yield return netInterface; - - } - - } - #endregion - #region Build Order 2 + #region Interface building step 2 public class PollLevelConfigurator { @@ -389,7 +405,7 @@ namespace MewtocolNet #endregion - #region BuildLevel 3 + #region Interface building step 3 public class EndInit { diff --git a/MewtocolNet/MewtocolFrameResponse.cs b/MewtocolNet/MewtocolFrameResponse.cs index 31d65b1..db91232 100644 --- a/MewtocolNet/MewtocolFrameResponse.cs +++ b/MewtocolNet/MewtocolFrameResponse.cs @@ -1,6 +1,7 @@ -using MewtocolNet.Helpers; +using MewtocolNet.DataLists; -namespace MewtocolNet { +namespace MewtocolNet +{ public struct MewtocolFrameResponse { diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 914f4be..90323e8 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -69,6 +69,8 @@ namespace MewtocolNet { internal volatile bool pollerFirstCycle; internal bool usePoller = false; internal MemoryAreaManager memoryManager; + private volatile bool isReceiving; + private volatile bool isSending; #endregion @@ -101,6 +103,15 @@ namespace MewtocolNet { /// public int StationNumber => stationNumber; + /// + public bool IsSending { + get => isSending; + private set { + isSending = value; + OnPropChange(); + } + } + /// public int BytesPerSecondUpstream { get { return bytesPerSecondUpstream; } @@ -110,6 +121,15 @@ namespace MewtocolNet { } } + /// + public bool IsReceiving { + get => isReceiving; + private set { + isReceiving = value; + OnPropChange(); + } + } + /// public int BytesPerSecondDownstream { get { return bytesPerSecondDownstream; } @@ -148,14 +168,13 @@ namespace MewtocolNet { memoryManager = new MemoryAreaManager(this); + WatchPollerDemand(); + Connected += MewtocolInterface_Connected; RegisterChanged += OnRegisterChanged; void MewtocolInterface_Connected(object sender, PlcConnectionArgs args) { - if (usePoller) - AttachPoller(); - IsConnected = true; } @@ -290,10 +309,14 @@ namespace MewtocolNet { SetUpstreamStopWatchStart(); + IsSending = true; + //write inital command byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); stream.Write(writeBuffer, 0, writeBuffer.Length); + IsSending = false; + //calculate the expected number of frames from the message request int? wordsCountRequested = null; if (onReceiveProgress != null) { @@ -391,7 +414,9 @@ namespace MewtocolNet { SetDownstreamStopWatchStart(); byte[] buffer = new byte[RecBufferSize]; + IsReceiving = true; int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + IsReceiving = false; CalcDownstreamSpeed(bytesRead); @@ -411,7 +436,9 @@ namespace MewtocolNet { //request next frame var writeBuffer = Encoding.UTF8.GetBytes($"%{GetStationNumber()}**&\r"); + IsSending = true; await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + IsSending = false; Logger.Log($">> Requested next frame", LogLevel.Critical, this); wasMultiFramedResponse = true; @@ -527,6 +554,8 @@ namespace MewtocolNet { private protected virtual void OnDisconnect() { + IsReceiving = false; + IsSending = false; BytesPerSecondDownstream = 0; BytesPerSecondUpstream = 0; PollerCycleDurationMs = 0; diff --git a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs index 0840b71..6df6d2b 100644 --- a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs +++ b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs @@ -17,6 +17,8 @@ namespace MewtocolNet { /// public abstract partial class MewtocolInterface { + internal Task heartbeatTask = Task.CompletedTask; + internal Task pollCycleTask; private List registerCollections = new List(); @@ -39,15 +41,48 @@ namespace MewtocolNet { } } + private System.Timers.Timer heartBeatTimer = new System.Timers.Timer(); + #region Register Polling + internal void WatchPollerDemand() { + + memoryManager.MemoryLayoutChanged += () => TestPollerStartNeeded(); + + Connected += (s, e) => TestPollerStartNeeded(); + + Disconnected += (s, e) => { + + heartBeatTimer.Elapsed -= PollTimerTick; + heartBeatTimer.Stop(); + + }; + + } + + private void TestPollerStartNeeded () { + + if (!IsConnected) return; + + heartBeatTimer.Interval = 3000; + heartBeatTimer.Elapsed += PollTimerTick; + heartBeatTimer.Start(); + + if (!usePoller) return; + + bool hasCyclic = memoryManager.HasCyclicPollableRegisters(); + bool hasFirstCycle = memoryManager.HasSingleCyclePollableRegisters(); + + if (hasCyclic || hasFirstCycle) AttachPoller(); + + } + /// /// Kills the poller completely /// internal void KillPoller() { pollerTaskStopped = true; - ClearRegisterVals(); } @@ -56,8 +91,7 @@ namespace MewtocolNet { /// internal void AttachPoller() { - if (!pollerTaskStopped) - return; + if (!pollerTaskStopped) return; PollerCycleDurationMs = 0; pollerFirstCycle = true; @@ -66,6 +100,15 @@ namespace MewtocolNet { } + private void PollTimerTick(object sender, System.Timers.ElapsedEventArgs e) { + + heartbeatTask = Task.Run(async () => { + Logger.LogVerbose("Sending heartbeat", this); + await GetPLCInfoAsync(); + }); + + } + /// /// Runs a single poller cycle manually, /// useful if you want to use a custom update frequency @@ -86,10 +129,11 @@ namespace MewtocolNet { } - //polls all registers one by one (slow) + //performs one poll cycle, one cycle is defined as getting all regster values + //and (not every cycle) the status of the plc that is performed on a timer basis internal async Task Poll() { - Logger.Log("Poller is attaching", LogLevel.Info, this); + Logger.Log("Poller is attaching", this); pollerTaskStopped = false; @@ -100,37 +144,40 @@ namespace MewtocolNet { pollCycleTask = OnMultiFrameCycle(); await pollCycleTask; + InvokePolledCycleDone(); + if (!IsConnected) { pollerTaskStopped = true; return; } pollerFirstCycle = false; - InvokePolledCycleDone(); - + } } private async Task OnMultiFrameCycle() { + //await the timed task before starting a new poller cycle + if (!heartbeatTask.IsCompleted) await heartbeatTask; + var sw = Stopwatch.StartNew(); - //await UpdateRCPRegisters(); - - //await UpdateDTRegisters(); - await memoryManager.PollAllAreasAsync(); sw.Stop(); PollerCycleDurationMs = (int)sw.ElapsedMilliseconds; + if (!memoryManager.HasCyclicPollableRegisters()) KillPoller(); + } #endregion #region Smart register polling methods + [Obsolete] private async Task UpdateRCPRegisters() { //build booleans diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs index adbaf6c..b39f93a 100644 --- a/MewtocolNet/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; @@ -56,9 +57,17 @@ namespace MewtocolNet { operationMode = value; OnPropChange(); OnPropChange(nameof(IsRunMode)); + OnPropChange(nameof(OperationModeTags)); } } + /// + /// A list of operation mode tags, derived from the OPMode flags + /// + public IEnumerable OperationModeTags { + get => OperationMode.ToString().Split(',').Select(x => x.JoinSplitByUpperCase().Trim()); + } + /// /// Hardware information flags about the PLC /// @@ -67,10 +76,17 @@ namespace MewtocolNet { internal set { hardwareInformation = value; OnPropChange(); - + OnPropChange(nameof(HardwareInformationTags)); } } + /// + /// A list of hardware info tags, derived from the HardwareInformation flags + /// + public IEnumerable HardwareInformationTags { + get => HardwareInformation.ToString().Split(',').Select(x => x.JoinSplitByUpperCase().Trim()); + } + /// /// Current error code of the PLC /// @@ -91,15 +107,34 @@ namespace MewtocolNet { internal bool TryExtendFromEXRT(string msg) { - var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?....)(?....)(?....).*", RegexOptions.IgnoreCase); + var regexEXRT = new Regex(@"\%EE\$EX00RT00(?..)(?..)..(?..)(?..)..(?..)(?....)(?..)(?..)(?.)(?...)(?...).*", RegexOptions.IgnoreCase); var match = regexEXRT.Match(msg); if (match.Success) { + //overwrite the typecode byte typeCodeByte = byte.Parse(match.Groups["mc"].Value, NumberStyles.HexNumber); var overWriteBytes = BitConverter.GetBytes((int)this.TypeCode); overWriteBytes[0] = typeCodeByte; - this.TypeCode = (PlcType)BitConverter.ToInt32(overWriteBytes, 0); + //get the long (4 bytes) prog size + if (match.Groups["psize"]?.Value != null) { + + var padded = match.Groups["psize"].Value.PadLeft(4, '0'); + + overWriteBytes[1] = byte.Parse(padded.Substring(2, 2), NumberStyles.HexNumber); + overWriteBytes[2] = byte.Parse(padded.Substring(0, 2), NumberStyles.HexNumber); + + } + + var tempTypeCode = BitConverter.ToUInt32(overWriteBytes, 0); + + if (Enum.IsDefined(typeof(PlcType), tempTypeCode)) { + + this.TypeCode = (PlcType)tempTypeCode; + + } + + //overwrite the other vals that are also contained in EXRT this.CpuVersion = match.Groups["ver"].Value.Insert(1, "."); this.HardwareInformation = (HWInformation)byte.Parse(match.Groups["hwif"].Value, NumberStyles.HexNumber); @@ -119,20 +154,29 @@ namespace MewtocolNet { byte typeCodeByte = byte.Parse(match.Groups["cputype"].Value, NumberStyles.HexNumber); byte capacity = byte.Parse(match.Groups["cap"].Value, NumberStyles.Number); - var typeCodeFull = (PlcType)BitConverter.ToInt32(new byte[] { typeCodeByte, capacity, 0, 0}, 0); + var tempTypeCode = (PlcType)BitConverter.ToUInt32(new byte[] { typeCodeByte, capacity, 0, 0}, 0); float definedProgCapacity = 0; - var composedNow = typeCodeFull.ToNameDecompose(); + PlcType typeCodeFull; - if (composedNow != null) { + if (Enum.IsDefined(typeof(PlcType), tempTypeCode)) { - //already recognized the type code, use the capacity value encoded in the enum - definedProgCapacity = composedNow.Size; + typeCodeFull = (PlcType)tempTypeCode; + + var composedNow = typeCodeFull.ToNameDecompose(); + + if (composedNow != null) { + + //already recognized the type code, use the capacity value encoded in the enum + definedProgCapacity = composedNow.Size; + + } } else { + typeCodeFull = PlcType.Unknown; definedProgCapacity = int.Parse(match.Groups["cap"].Value); - + } inf = new PLCInfo { @@ -147,7 +191,7 @@ namespace MewtocolNet { } - inf = default(PLCInfo); + inf = default; return false; } @@ -192,7 +236,7 @@ namespace MewtocolNet { } - public override int GetHashCode() => GetHashCode(); + public override int GetHashCode() => base.GetHashCode(); private protected void OnPropChange([CallerMemberName] string propertyName = null) { diff --git a/MewtocolNet/PublicEnums/PlcType.cs b/MewtocolNet/PublicEnums/PlcType.cs index b2e767d..5bc9c9c 100644 --- a/MewtocolNet/PublicEnums/PlcType.cs +++ b/MewtocolNet/PublicEnums/PlcType.cs @@ -21,6 +21,11 @@ namespace MewtocolNet { /// public enum PlcType : uint { + /// + /// Fallback plc type + /// + Unknown = 0, + //NON SIMULATION TEST POSSIBLE #region FP5 Family (Legacy) diff --git a/MewtocolNet/UnderlyingRegisters/MemoryAreaManager.cs b/MewtocolNet/UnderlyingRegisters/MemoryAreaManager.cs index 600fc0a..af5be11 100644 --- a/MewtocolNet/UnderlyingRegisters/MemoryAreaManager.cs +++ b/MewtocolNet/UnderlyingRegisters/MemoryAreaManager.cs @@ -11,6 +11,8 @@ namespace MewtocolNet.UnderlyingRegisters { internal class MemoryAreaManager { + internal event Action MemoryLayoutChanged; + internal int maxOptimizationDistance = 8; internal int maxRegistersPerGroup = -1; internal PollLevelOverwriteMode pollLevelOrMode = PollLevelOverwriteMode.Highest; @@ -20,7 +22,27 @@ namespace MewtocolNet.UnderlyingRegisters { internal MewtocolInterface mewInterface; internal List pollLevels; - internal Dictionary pollLevelConfigs = new Dictionary(); + + internal Dictionary pollLevelConfigs = new Dictionary() { + { + MewtocolNet.PollLevel.Always, + new PollLevelConfig { + skipNth = 1, + } + }, + { + MewtocolNet.PollLevel.FirstIteration, + new PollLevelConfig { + skipAllButFirst = true + } + }, + { + MewtocolNet.PollLevel.Never, + new PollLevelConfig { + skipsAll = true, + } + } + }; private uint pollIteration = 0; @@ -36,11 +58,7 @@ namespace MewtocolNet.UnderlyingRegisters { wrAreaSize = wrSize; dtAreaSize = dtSize; - pollLevels = new List { - new PollLevel(wrSize, dtSize) { - level = 1, - } - }; + pollLevels = new List(); } @@ -89,7 +107,20 @@ namespace MewtocolNet.UnderlyingRegisters { } //order - foreach (var lvl in pollLevels) { + for (int i = 0; i < pollLevels.Count; i++) { + + PollLevel lvl = pollLevels[i]; + + //poll level has no areas + if(lvl.dataAreas.Count == 0 && + lvl.externalRelayInAreas.Count == 0 && + lvl.externalRelayOutAreas.Count == 0 && + lvl.internalRelayAreas.Count == 0) { + + pollLevels.Remove(lvl); + continue; + + } foreach (var area in lvl.dataAreas) { @@ -101,28 +132,12 @@ namespace MewtocolNet.UnderlyingRegisters { } + MemoryLayoutChanged?.Invoke(); + } private void TestPollLevelExistence(Register reg) { - if (!pollLevelConfigs.ContainsKey(MewtocolNet.PollLevel.Always)) { - pollLevelConfigs.Add(MewtocolNet.PollLevel.Always, new PollLevelConfig { - skipNth = 1, - }); - } - - if (!pollLevelConfigs.ContainsKey(MewtocolNet.PollLevel.FirstIteration)) { - pollLevelConfigs.Add(MewtocolNet.PollLevel.FirstIteration, new PollLevelConfig { - skipAllButFirst = true - }); - } - - if (!pollLevelConfigs.ContainsKey(MewtocolNet.PollLevel.Never)) { - pollLevelConfigs.Add(MewtocolNet.PollLevel.Never, new PollLevelConfig { - skipsAll = true, - }); - } - if (!pollLevels.Any(x => x.level == reg.pollLevel)) { pollLevels.Add(new PollLevel(wrAreaSize, dtAreaSize) { @@ -354,13 +369,6 @@ namespace MewtocolNet.UnderlyingRegisters { } - //get the plc status each n iterations - if (pollIteration % 5 == 0) { - - await mewInterface.GetPLCInfoAsync(); - - } - if (pollIteration == uint.MaxValue) { pollIteration = uint.MinValue; } else { @@ -494,6 +502,23 @@ namespace MewtocolNet.UnderlyingRegisters { } + internal bool HasSingleCyclePollableRegisters() { + + bool hasCyclicPollableLevels = pollLevels.Any(x => x.level != MewtocolNet.PollLevel.FirstIteration); + + return hasCyclicPollableLevels; + + } + + internal bool HasCyclicPollableRegisters () { + + bool hasCyclicPollableLevels = pollLevels + .Any(x => x.level != MewtocolNet.PollLevel.Never && x.level != MewtocolNet.PollLevel.FirstIteration && x.level != 0); + + return hasCyclicPollableLevels; + + } + } } diff --git a/MewtocolTests/TestLinkedLists.cs b/MewtocolTests/TestLinkedLists.cs index f4c1f75..a7f2a0d 100644 --- a/MewtocolTests/TestLinkedLists.cs +++ b/MewtocolTests/TestLinkedLists.cs @@ -1,10 +1,10 @@ using MewtocolNet; using Xunit; using Xunit.Abstractions; +using MewtocolNet.DataLists; -using MewtocolNet.Helpers; - -namespace MewtocolTests { +namespace MewtocolTests +{ public class TestLinkedLists {