Only update console, when content changes
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Sharp7.Monitor;
|
||||
|
||||
public static class CustomStyles
|
||||
{
|
||||
public static Style Error { get; } = new Style(foreground: Color.Red);
|
||||
|
||||
@@ -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<ReadPlcCommand.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
|
||||
{
|
||||
@@ -27,120 +27,121 @@ internal sealed class ReadPlcCommand : AsyncCommand<ReadPlcCommand.Settings>
|
||||
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, "<IP address>")]
|
||||
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, "<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")]
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Sharp7.Read;
|
||||
namespace Sharp7.Monitor;
|
||||
|
||||
public static class StringHelper
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user