From def185ec6c564bce9a294925e368a26ac4de1815 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Sun, 28 Apr 2024 20:59:34 +0200 Subject: [PATCH] Only update console, when content changes --- Sharp7.Monitor/CustomStyles.cs | 2 + Sharp7.Monitor/ReadPlcCommand.cs | 187 ++++++++++++++-------------- Sharp7.Monitor/StringHelper.cs | 2 +- Sharp7.Monitor/VariableContainer.cs | 2 + Sharp7.Monitor/VariableRecord.cs | 53 +++++++- 5 files changed, 150 insertions(+), 96 deletions(-) diff --git a/Sharp7.Monitor/CustomStyles.cs b/Sharp7.Monitor/CustomStyles.cs index 11b5813..eddd410 100644 --- a/Sharp7.Monitor/CustomStyles.cs +++ b/Sharp7.Monitor/CustomStyles.cs @@ -1,5 +1,7 @@ using Spectre.Console; +namespace Sharp7.Monitor; + public static class CustomStyles { public static Style Error { get; } = new Style(foreground: Color.Red); diff --git a/Sharp7.Monitor/ReadPlcCommand.cs b/Sharp7.Monitor/ReadPlcCommand.cs index be8719a..5f87aaa 100644 --- a/Sharp7.Monitor/ReadPlcCommand.cs +++ b/Sharp7.Monitor/ReadPlcCommand.cs @@ -1,20 +1,20 @@ -using System.Collections; -using System.ComponentModel; +using System.ComponentModel; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using JetBrains.Annotations; -using Sharp7.Read; using Sharp7.Rx; using Sharp7.Rx.Enums; using Spectre.Console; using Spectre.Console.Cli; using Spectre.Console.Rendering; +namespace Sharp7.Monitor; + internal sealed class ReadPlcCommand : AsyncCommand { public override async Task ExecuteAsync(CommandContext context, Settings settings) { - var token = (CancellationToken)(context.Data ?? CancellationToken.None); + var token = (CancellationToken) (context.Data ?? CancellationToken.None); try { @@ -27,120 +27,121 @@ internal sealed class ReadPlcCommand : AsyncCommand return 0; } - private static IRenderable FormatCellData(VariableRecord record) + private static IRenderable FormatCellData(object value) { - return record.Value switch + return value switch { IRenderable renderable => renderable, Exception ex => new Text(ex.Message, CustomStyles.Error), byte[] byteArray => new Text(string.Join(" ", byteArray.Select(b => $"0x{b:X2}")), CustomStyles.Hex), byte => FormatNo(), - short =>FormatNo(), - ushort =>FormatNo(), - int =>FormatNo(), - uint =>FormatNo(), - long =>FormatNo(), - ulong =>FormatNo(), + short => FormatNo(), + ushort => FormatNo(), + int => FormatNo(), + uint => FormatNo(), + long => FormatNo(), + ulong => FormatNo(), - _ => new Text(record.Value.ToString() ?? "") + _ => new Text(value.ToString() ?? "") }; - Markup FormatNo() => new($"[blue]0x{record.Value:X2}[/] {record.Value}"); + Markup FormatNo() => new($"[blue]0x{value:X2}[/] {value}"); } private static async Task RunProgram(Settings settings, CancellationToken token) -{ - AnsiConsole.MarkupLine($"Connecting to plc [green]{settings.PlcIp}[/], CPU [green]{settings.CpuMpiAddress}[/], rack [green]{settings.RackNumber}[/]. "); - AnsiConsole.MarkupLine("[gray]Press Ctrl + C to cancel.[/]"); - - using var plc = new Sharp7Plc(settings.PlcIp, settings.RackNumber, settings.CpuMpiAddress); - - await plc.TriggerConnection(token); - - // Connect - await AnsiConsole.Status() - .Spinner(Spinner.Known.BouncingBar) - .StartAsync("Connecting...", async ctx => - { - var lastState = ConnectionState.Initial; - ctx.Status(lastState.ToString()); - - while (!token.IsCancellationRequested) - { - var state = await plc.ConnectionState.FirstAsync(s => s != lastState).ToTask(token); - ctx.Status(state.ToString()); - - if (state == ConnectionState.Connected) - return; - } - }); - - token.ThrowIfCancellationRequested(); - - - using var variableContainer = VariableContainer.Initialize(plc, settings.Variables); - - // Create a table - var table = new Table { - Border = TableBorder.Rounded, - BorderStyle = new Style(foreground: Color.DarkGreen) - }; + AnsiConsole.MarkupLine($"Connecting to plc [green]{settings.PlcIp}[/], CPU [green]{settings.CpuMpiAddress}[/], rack [green]{settings.RackNumber}[/]. "); + AnsiConsole.MarkupLine("[gray]Press Ctrl + C to cancel.[/]"); - table.AddColumn("Variable"); - table.AddColumn("Value"); + using var plc = new Sharp7Plc(settings.PlcIp, settings.RackNumber, settings.CpuMpiAddress); - foreach (var record in variableContainer.VariableRecords) - table.AddRow(record.Address, "[gray]init[/]"); + await plc.TriggerConnection(token); - await AnsiConsole.Live(table) - .StartAsync(async ctx => - { - while (!token.IsCancellationRequested) + // Connect + await AnsiConsole.Status() + .Spinner(Spinner.Known.BouncingBar) + .StartAsync("Connecting...", async ctx => { - foreach (var record in variableContainer.VariableRecords) - table.Rows.Update( - record.RowIdx, 1, - FormatCellData(record) - ); + var lastState = ConnectionState.Initial; + ctx.Status(lastState.ToString()); - ctx.Refresh(); + while (!token.IsCancellationRequested) + { + var state = await plc.ConnectionState.FirstAsync(s => s != lastState).ToTask(token); + ctx.Status(state.ToString()); - await Task.Delay(100, token); - } - }); -} + if (state == ConnectionState.Connected) + return; + } + }); -[NoReorder] -public sealed class Settings : CommandSettings -{ - [Description("IP address of S7")] - [CommandArgument(0, "")] - public required string PlcIp { get; init; } + token.ThrowIfCancellationRequested(); - [CommandArgument(1, "[variables]")] - [Description("Variables to read from S7, like Db200.Int4.\r\nFor format description see https://github.com/evopro-ag/Sharp7Reactive.")] - public required string[] Variables { get; init; } - [CommandOption("-c|--cpu")] - [Description("CPU MPI address of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] - [DefaultValue(0)] - public int CpuMpiAddress { get; init; } + using var variableContainer = VariableContainer.Initialize(plc, settings.Variables); - [CommandOption("-r|--rack")] - [Description("Rack number of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] - [DefaultValue(0)] - public int RackNumber { get; init; } + // Create a table + var table = new Table + { + Border = TableBorder.Rounded, + BorderStyle = new Style(foreground: Color.DarkGreen) + }; - public override ValidationResult Validate() - { - if (!StringHelper.IsValidIp4(PlcIp)) - return ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); + table.AddColumn("Variable"); + table.AddColumn("Value"); - if (Variables == null || Variables.Length == 0) - return ValidationResult.Error("Please supply at least one variable to read"); + foreach (var record in variableContainer.VariableRecords) + table.AddRow(record.Address, "[gray]init[/]"); - return ValidationResult.Success(); + await AnsiConsole.Live(table) + .StartAsync(async ctx => + { + while (!token.IsCancellationRequested) + { + foreach (var record in variableContainer.VariableRecords) + if (record.HasUpdate(out var value)) + table.Rows.Update( + record.RowIdx, 1, + FormatCellData(value) + ); + + ctx.Refresh(); + + await Task.Delay(100, token); + } + }); + } + + [NoReorder] + public sealed class Settings : CommandSettings + { + [Description("IP address of S7")] + [CommandArgument(0, "")] + public required string PlcIp { get; init; } + + [CommandArgument(1, "[variables]")] + [Description("Variables to read from S7, like Db200.Int4.\r\nFor format description see https://github.com/evopro-ag/Sharp7Reactive.")] + public required string[] Variables { get; init; } + + [CommandOption("-c|--cpu")] + [Description("CPU MPI address of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] + [DefaultValue(0)] + public int CpuMpiAddress { get; init; } + + [CommandOption("-r|--rack")] + [Description("Rack number of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] + [DefaultValue(0)] + public int RackNumber { get; init; } + + public override ValidationResult Validate() + { + if (!StringHelper.IsValidIp4(PlcIp)) + return ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); + + if (Variables == null || Variables.Length == 0) + return ValidationResult.Error("Please supply at least one variable to read"); + + return ValidationResult.Success(); + } } -} } \ No newline at end of file diff --git a/Sharp7.Monitor/StringHelper.cs b/Sharp7.Monitor/StringHelper.cs index b161664..a47299e 100644 --- a/Sharp7.Monitor/StringHelper.cs +++ b/Sharp7.Monitor/StringHelper.cs @@ -1,4 +1,4 @@ -namespace Sharp7.Read; +namespace Sharp7.Monitor; public static class StringHelper { diff --git a/Sharp7.Monitor/VariableContainer.cs b/Sharp7.Monitor/VariableContainer.cs index 35a7f6d..7c4be13 100644 --- a/Sharp7.Monitor/VariableContainer.cs +++ b/Sharp7.Monitor/VariableContainer.cs @@ -3,6 +3,8 @@ using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; using Spectre.Console; +namespace Sharp7.Monitor; + public class VariableContainer : IDisposable { private readonly IDisposable subscriptions; diff --git a/Sharp7.Monitor/VariableRecord.cs b/Sharp7.Monitor/VariableRecord.cs index 70a8507..fbfeba5 100644 --- a/Sharp7.Monitor/VariableRecord.cs +++ b/Sharp7.Monitor/VariableRecord.cs @@ -1,6 +1,55 @@ -public class VariableRecord +using System.Diagnostics.CodeAnalysis; + +namespace Sharp7.Monitor; + +public class VariableRecord { + private object value = new(); + private int valueUpdated; public required string Address { get; init; } public required int RowIdx { get; init; } - public object Value { get; set; } + + public object Value + { + get => value; + set + { + if (!IsEquivalent(this.value, value)) + { + this.value = value; + valueUpdated = 1; + } + } + } + + private bool IsEquivalent(object oldValue, object newValue) + { + // Special treatmant for byte arrays + if (oldValue is byte[] oldArray && newValue is byte[] newArray) + { + if (oldArray.Length != newArray.Length) return false; + + for (var i = 0; i < oldArray.Length; i++) + if (oldArray[i] != newArray[i]) return false; + + return true; + } + + // all other types read from PLC have value compare semantics. + return oldValue == newValue; + } + + public bool HasUpdate([NotNullWhen(true)] out object? newValue) + { + if (Interlocked.Exchange(ref valueUpdated, 0) == 1) + { + newValue = value; + return true; + } + else + { + newValue = null; + return false; + } + } } \ No newline at end of file