Only update console, when content changes

This commit is contained in:
Peter Butzhammer
2024-04-28 20:59:34 +02:00
parent cd70cc4714
commit def185ec6c
5 changed files with 150 additions and 96 deletions

View File

@@ -1,5 +1,7 @@
using Spectre.Console; using Spectre.Console;
namespace Sharp7.Monitor;
public static class CustomStyles public static class CustomStyles
{ {
public static Style Error { get; } = new Style(foreground: Color.Red); public static Style Error { get; } = new Style(foreground: Color.Red);

View File

@@ -1,20 +1,20 @@
using System.Collections; using System.ComponentModel;
using System.ComponentModel;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks; using System.Reactive.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Sharp7.Read;
using Sharp7.Rx; using Sharp7.Rx;
using Sharp7.Rx.Enums; using Sharp7.Rx.Enums;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Cli; using Spectre.Console.Cli;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
namespace Sharp7.Monitor;
internal sealed class ReadPlcCommand : AsyncCommand<ReadPlcCommand.Settings> internal sealed class ReadPlcCommand : AsyncCommand<ReadPlcCommand.Settings>
{ {
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings) public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{ {
var token = (CancellationToken)(context.Data ?? CancellationToken.None); var token = (CancellationToken) (context.Data ?? CancellationToken.None);
try try
{ {
@@ -27,120 +27,121 @@ internal sealed class ReadPlcCommand : AsyncCommand<ReadPlcCommand.Settings>
return 0; return 0;
} }
private static IRenderable FormatCellData(VariableRecord record) private static IRenderable FormatCellData(object value)
{ {
return record.Value switch return value switch
{ {
IRenderable renderable => renderable, IRenderable renderable => renderable,
Exception ex => new Text(ex.Message, CustomStyles.Error), Exception ex => new Text(ex.Message, CustomStyles.Error),
byte[] byteArray => new Text(string.Join(" ", byteArray.Select(b => $"0x{b:X2}")), CustomStyles.Hex), byte[] byteArray => new Text(string.Join(" ", byteArray.Select(b => $"0x{b:X2}")), CustomStyles.Hex),
byte => FormatNo(), byte => FormatNo(),
short =>FormatNo(), short => FormatNo(),
ushort =>FormatNo(), ushort => FormatNo(),
int =>FormatNo(), int => FormatNo(),
uint =>FormatNo(), uint => FormatNo(),
long =>FormatNo(), long => FormatNo(),
ulong =>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) 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, AnsiConsole.MarkupLine($"Connecting to plc [green]{settings.PlcIp}[/], CPU [green]{settings.CpuMpiAddress}[/], rack [green]{settings.RackNumber}[/]. ");
BorderStyle = new Style(foreground: Color.DarkGreen) AnsiConsole.MarkupLine("[gray]Press Ctrl + C to cancel.[/]");
};
table.AddColumn("Variable"); using var plc = new Sharp7Plc(settings.PlcIp, settings.RackNumber, settings.CpuMpiAddress);
table.AddColumn("Value");
foreach (var record in variableContainer.VariableRecords) await plc.TriggerConnection(token);
table.AddRow(record.Address, "[gray]init[/]");
await AnsiConsole.Live(table) // Connect
.StartAsync(async ctx => await AnsiConsole.Status()
{ .Spinner(Spinner.Known.BouncingBar)
while (!token.IsCancellationRequested) .StartAsync("Connecting...", async ctx =>
{ {
foreach (var record in variableContainer.VariableRecords) var lastState = ConnectionState.Initial;
table.Rows.Update( ctx.Status(lastState.ToString());
record.RowIdx, 1,
FormatCellData(record)
);
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] token.ThrowIfCancellationRequested();
public sealed class Settings : CommandSettings
{
[Description("IP address of S7")]
[CommandArgument(0, "<IP address>")]
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")] using var variableContainer = VariableContainer.Initialize(plc, settings.Variables);
[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")] // Create a table
[Description("Rack number of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] var table = new Table
[DefaultValue(0)] {
public int RackNumber { get; init; } Border = TableBorder.Rounded,
BorderStyle = new Style(foreground: Color.DarkGreen)
};
public override ValidationResult Validate() table.AddColumn("Variable");
table.AddColumn("Value");
foreach (var record in variableContainer.VariableRecords)
table.AddRow(record.Address, "[gray]init[/]");
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
{ {
if (!StringHelper.IsValidIp4(PlcIp)) [Description("IP address of S7")]
return ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); [CommandArgument(0, "<IP address>")]
public required string PlcIp { get; init; }
if (Variables == null || Variables.Length == 0) [CommandArgument(1, "[variables]")]
return ValidationResult.Error("Please supply at least one variable to read"); [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; }
return ValidationResult.Success(); [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();
}
} }
} }
}

View File

@@ -1,4 +1,4 @@
namespace Sharp7.Read; namespace Sharp7.Monitor;
public static class StringHelper public static class StringHelper
{ {

View File

@@ -3,6 +3,8 @@ using Sharp7.Rx.Enums;
using Sharp7.Rx.Interfaces; using Sharp7.Rx.Interfaces;
using Spectre.Console; using Spectre.Console;
namespace Sharp7.Monitor;
public class VariableContainer : IDisposable public class VariableContainer : IDisposable
{ {
private readonly IDisposable subscriptions; private readonly IDisposable subscriptions;

View File

@@ -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 string Address { get; init; }
public required int RowIdx { 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;
}
}
} }